diff --git a/TODO.md b/TODO.md
index 37687b8..cb9931e 100644
--- a/TODO.md
+++ b/TODO.md
@@ -5,36 +5,6 @@ es eine Entscheidung braucht oder weil es außerhalb des Repos passiert.
## Entscheidungen
-### Rollenmodell: Superadmin appweit
-
-Geplant sind drei Rollen: **Superadmin** (appweit, über alle Teams),
-**Team-Admin** (Owner/Admins eines Teams, gibt es schon) und **Mitglied**.
-
-Umsetzung, sobald die zwei Fragen unten geklärt sind:
-
-- `users.superadmin` als Bool-Feld.
-- Die `updateRule` der users-Collection muss dabei
- `@request.body.superadmin:isset = false` ergänzen — sonst kann sich jeder
- Nutzer selbst befördern, denn er darf seinen eigenen Datensatz ändern.
-
-Offen:
-
-1. **Vorhandene Team-Flags.** Flag-Typen sollen appweit gelten (siehe unten).
- Heute hat jedes Team seinen eigenen Satz. Zusammenführen und Duplikate von
- Hand aufräumen, oder alle behalten und ab sofort global sichtbar machen?
-2. **Erster Superadmin.** Den kann nur jemand direkt im PocketBase-Admin
- setzen. Reicht ein Hinweis im Migrationskommentar, oder soll es eine
- Superadmin-Verwaltung in den Einstellungen geben?
-
-### Flag-Typen appweit statt pro Team
-
-Hängt am Rollenmodell. Heute ist `trail_flags` team-gebunden: Das Feld `team`
-existiert, der Store filtert danach, lesen darf das Team, schreiben Owner und
-Admins.
-
-Ziel: `team` entfällt, lesen für alle Eingeloggten, schreiben nur Superadmin.
-Die Typen werden damit App-weite Vorschläge, aus denen jedes Team auswählt.
-
### Trail-Status: dürfen Mitglieder melden?
Anzeige und Herkunft (`status_changed`, `status_by`) sind fertig. Ändern darf
@@ -80,12 +50,13 @@ Kacheln ohne Schlüssel weiter — die Karte bleibt also sichtbar.
### Migrationen greifen beim nächsten Start
-Vier neue Migrationen liegen in `backend/pb_migrations/`:
+Fünf neue Migrationen liegen in `backend/pb_migrations/`:
- `1754500500_riders_user_and_admin_rules.js`
- `1754500600_events_dates_instead_of_status.js`
- `1754500700_trail_status_meta.js`
- `1754500800_trail_markers_custom.js`
+- `1754500900_users_superadmin.js`
Solange sie nicht gelaufen sind, scheitert alles, was auf den neuen Regeln
aufbaut. Am deutlichsten beim Anlegen eines Mitglieds oder eines Fahrer-Logins:
@@ -118,4 +89,12 @@ des Generators.
- Der Melder eines Trail-Markers („wer") zeigt bis zum Deploy „Unbekannt". Die
gelockerte `users.viewRule` kommt mit `1754500500`.
+- Die Rolle **Superadmin** existiert seit `1754500900` und ist für
+ stammtisch@dne.name gesetzt, gibt in der App aber noch keine besonderen
+ Rechte. Sie ist bewusst nur in PocketBase vergebbar: Die `updateRule` der
+ users-Collection schließt `superadmin` vom Selbstsetzen aus. Sobald klar ist,
+ was sie dürfen soll, kommen die Regeln dazu.
+- Flag-Typen bleiben **teamspezifisch**. Ein neues Team bekommt beim Anlegen
+ den hartkodierten Standardsatz aus `trailFlags.svelte.ts`, den es danach frei
+ ändern kann. Eine appweite Verwaltung gibt es damit bewusst nicht.
- `NAMING.md` und `frontend/static/pedaler.svg` liegen bewusst nur lokal.
diff --git a/backend/pb_migrations/1754400000_init_schema.js b/backend/pb_migrations/1754400000_init_schema.js
index bcfcf21..6848b17 100644
--- a/backend/pb_migrations/1754400000_init_schema.js
+++ b/backend/pb_migrations/1754400000_init_schema.js
@@ -14,7 +14,7 @@ migrate((app) => {
"listRule": "id = @request.auth.id",
"viewRule": "@request.auth.id != \"\"",
"createRule": "@request.auth.id != \"\"",
- "updateRule": "id = @request.auth.id",
+ "updateRule": "id = @request.auth.id && @request.body.superadmin:isset = false",
"deleteRule": "id = @request.auth.id",
"name": "users",
"type": "auth",
@@ -125,6 +125,15 @@ migrate((app) => {
"system": false,
"type": "text"
},
+ {
+ "hidden": false,
+ "id": "bool_user_superadmin",
+ "name": "superadmin",
+ "presentable": false,
+ "required": false,
+ "system": false,
+ "type": "bool"
+ },
{
"help": "",
"hidden": false,
diff --git a/backend/pb_migrations/1754500900_users_superadmin.js b/backend/pb_migrations/1754500900_users_superadmin.js
new file mode 100644
index 0000000..ce5d6bf
--- /dev/null
+++ b/backend/pb_migrations/1754500900_users_superadmin.js
@@ -0,0 +1,47 @@
+///
+
+// Superadmin als appweite Rolle, über allen Teams.
+//
+// Wichtig ist die Ergänzung der updateRule: Jeder darf seinen eigenen
+// Datensatz ändern — ohne `@request.body.superadmin:isset = false` könnte
+// sich damit jeder selbst befördern. Vergeben wird die Rolle deshalb nur
+// direkt in PocketBase oder durch eine Migration wie diese.
+//
+// Der erste Superadmin ist stammtisch@dne.name. Existiert das Konto (noch)
+// nicht, passiert hier nichts weiter — die Rolle lässt sich jederzeit im
+// Admin-UI nachziehen.
+//
+// Flag-Typen bleiben ausdrücklich Sache der Teams: Jedes Team verwaltet
+// seine eigenen und bekommt beim Anlegen den Standardsatz aus der App.
+// Superadmin hat damit im Alltag nichts zu tun.
+
+const FIRST_SUPERADMIN = 'stammtisch@dne.name'
+
+migrate((app) => {
+ const users = app.findCollectionByNameOrId('users')
+
+ if (!users.fields.getByName('superadmin')) {
+ users.fields.add(new Field({
+ id: 'bool_user_superadmin',
+ name: 'superadmin',
+ type: 'bool',
+ }))
+ }
+
+ users.updateRule = 'id = @request.auth.id && @request.body.superadmin:isset = false'
+ app.save(users)
+
+ try {
+ const first = app.findAuthRecordByEmail('users', FIRST_SUPERADMIN)
+ first.set('superadmin', true)
+ app.save(first)
+ } catch {
+ // Konto noch nicht angelegt — die Rolle wird dann von Hand vergeben.
+ }
+}, (app) => {
+ const users = app.findCollectionByNameOrId('users')
+
+ users.fields.removeByName('superadmin')
+ users.updateRule = 'id = @request.auth.id'
+ app.save(users)
+})
diff --git a/frontend/src/lib/stores/trailFlags.svelte.ts b/frontend/src/lib/stores/trailFlags.svelte.ts
index 3a3cb82..916ad9b 100644
--- a/frontend/src/lib/stores/trailFlags.svelte.ts
+++ b/frontend/src/lib/stores/trailFlags.svelte.ts
@@ -105,14 +105,19 @@ export class TrailFlagStore {
* unangetastet — der Knopf lässt sich also gefahrlos mehrfach drücken.
* Gibt die Anzahl neu angelegter Flags zurück.
*/
- async seedDefaults(): Promise {
- const teamId = this.teams.activeId
+ async seedDefaults(teamId = this.teams.activeId): Promise {
if (!teamId) throw new Error('Kein aktives Team')
- if (!this.canManage) {
+ // Beim Anlegen eines Teams ist man dessen Owner, aber der Datensatz
+ // steckt womöglich noch nicht im Store — dann greift die Prüfung ins
+ // Leere. Für das eigene, gerade erzeugte Team wird sie übersprungen;
+ // die createRule der Collection setzt sie ohnehin serverseitig durch.
+ if (teamId === this.teams.activeId && !this.canManage) {
throw new Error('Dafür brauchst du Administratorrechte im Team.')
}
- const existing = new Set(this.scoped.map((f) => f.label))
+ const existing = new Set(
+ this.records.filter((f) => f.team === teamId).map((f) => f.label),
+ )
let created = 0
for (const flag of DEFAULT_FLAGS) {
diff --git a/frontend/src/lib/types.d.ts b/frontend/src/lib/types.d.ts
index 847c28e..f5f5bd9 100644
--- a/frontend/src/lib/types.d.ts
+++ b/frontend/src/lib/types.d.ts
@@ -202,6 +202,7 @@ export type UsersRecord = {
id: string
name?: string
password: string
+ superadmin?: boolean
tokenKey: string
updated?: IsoDateString
username: string
diff --git a/frontend/src/routes/dashboard/+layout.svelte b/frontend/src/routes/dashboard/+layout.svelte
index 20cf648..848c16e 100644
--- a/frontend/src/routes/dashboard/+layout.svelte
+++ b/frontend/src/routes/dashboard/+layout.svelte
@@ -123,38 +123,49 @@