From a65c075004928df27d8b73144a5556a7b63fefe0 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 06:23:08 +0000 Subject: [PATCH] docs: Spec und Plan Benutzernamen Co-Authored-By: Claude Opus 5.5 --- .../plans/2026-10-05-aircard-benutzernamen.md | 113 ++++++++++++++++++ ...2026-10-05-aircard-benutzernamen-design.md | 109 +++++++++++++++++ 2 files changed, 222 insertions(+) create mode 100644 docs/superpowers/plans/2026-10-05-aircard-benutzernamen.md create mode 100644 docs/superpowers/specs/2026-10-05-aircard-benutzernamen-design.md diff --git a/docs/superpowers/plans/2026-10-05-aircard-benutzernamen.md b/docs/superpowers/plans/2026-10-05-aircard-benutzernamen.md new file mode 100644 index 0000000..11c1cee --- /dev/null +++ b/docs/superpowers/plans/2026-10-05-aircard-benutzernamen.md @@ -0,0 +1,113 @@ +# AirCard-IOS Benutzernamen – Implementierungsplan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Eindeutige Benutzernamen pro Konto (Pflicht, änderbar), angezeigt als „von @name“ an Logos und Community-Karten. + +**Architecture:** Neue öffentliche Tabelle `profiles` (Zeile = Konto, Unique-Index auf dem kleingeschriebenen Namen), geschrieben nur über `aircard-api` (`POST /api/profil`, neues Modul `deploy/api/profiles.js`); Hochladen/Veröffentlichen verlangen serverseitig ein Profil. Der Client liest Profile direkt aus Appwrite, fragt den Namen beim Registrieren bzw. nach dem Anmelden ab und zeigt ihn statt des alten Anzeigenamens. + +**Tech Stack:** wie bisher (TypeScript, Vite, Vitest + happy-dom, appwrite 22.4.1; Node 22 ohne Abhängigkeiten; Appwrite 1.8.1 TablesDB). + +**Spec:** `docs/superpowers/specs/2026-10-05-aircard-benutzernamen-design.md` + +## Global Constraints + +- Arbeitskopie `/home/webklar/apps/aircard-ios.org/dev`, Branch `main`, direkt committen, **nicht pushen** (Controller pusht). +- Keine neuen Abhängigkeiten; Versionen unverändert; deutsche Kommentare im Stil des Bestands. +- Alle neuen sichtbaren Texte über `t()`/`msg()` mit Eintrag in `web/src/i18n/en.ts` (Schlüssel eindeutig, Vorhandenes wiederverwenden); Tests laufen auf Deutsch; `coverage.test.ts` (Abdeckung, Heuristik, AST-Wache) bleibt grün. +- Namensmuster genau `^[A-Za-z0-9][A-Za-z0-9._-]{2,19}$`; Schlüssel = `toLowerCase()`; Sperrliste genau wie Spec § 2. +- Tabelle `profiles`: Zeilen-ID = Nutzer-ID, Spalten `username` (string 20, Pflicht), `usernameKey` (string 20, Pflicht), Index `username_key` unique auf `usernameKey`, rowSecurity an, keine Tabellenrechte, Zeilenrecht genau `['read("any")']`. +- API-Codes und Texte genau wie Spec § 4 (`usernameInvalid`, `usernameReserved`, `usernameTaken`, `usernameRequired`); Client-Tabelle `API_MESSAGES` bleibt im Gleichlauf mit `deploy/api/common.js` `MSG`. +- `sessionStorage`-Schlüssel `aircard.usernamePrompted`; jeder Storage-Zugriff in try/catch. +- Tests: `cd web && npx vitest run && npx tsc --noEmit && npm run build`; `node --test "deploy/api/*.test.js"`; `node --test web/scripts/appwrite-setup.test.mjs`. +- Commits enden mit `Co-Authored-By: Claude Opus 5.5 `. +- Subagents: `sonnet` (Umsetzung/Review), `haiku` für Kleinstes; nie Subagents aus Subagents; keine Browser-/Headless-Prüfungen (macht der Controller live). + +## Review Focus + +- **Zwei Konten wollen gleichzeitig denselben Namen**: genau eines bekommt ihn, das andere 409 `usernameTaken` (Unique-Index, nicht nur Vorabprüfung). Test in Task 1. +- **Registrierung mit vergebenem Namen**: Fehler am Feld, **bevor** das Konto angelegt wird (kein halbes Konto). Test in Task 2. +- **Altes Konto ohne Namen lädt hoch / veröffentlicht**: erst der Namensdialog; „Abbrechen“ → nichts passiert; Server würde ohne Profil 403 liefern. Tests in Tasks 1 und 3. +- **Umbenennen**: alter Name sofort für andere frei, eigene Kacheln/Infozeile/Detailseite zeigen sofort den neuen. Tests in Tasks 1 und 3. +- **Profil nicht lesbar / gelöscht / Netzfehler bei der Anzeige**: Fallback auf alten Anzeigenamen bzw. „Anonym“, keine Fehlermeldung, kein Absturz. Test in Task 3. + +--- + +### Task 1: Server – Schema `profiles`, `POST /api/profil`, Pflicht beim Hochladen/Veröffentlichen + +**Files:** +- Modify: `web/scripts/appwrite-setup.mjs` (+ Test): Tabelle `profiles` nach `logo_reports` mit Spalten und Unique-Index aus den Global Constraints. +- Create: `deploy/api/profiles.js`, `deploy/api/profiles.test.js` +- Modify: `deploy/api/common.js` (`MSG` + 4 Texte, Muster/Sperrliste/Schlüssel-Helfer), `deploy/api/app.js` (Routen einhängen; `publish` prüft Profil), `deploy/api/logos.js` (`hochladen` prüft Profil), `deploy/api/fake-appwrite.js` (Tabelle `profiles`, Unique-Index → 409 bei doppeltem `usernameKey`), `deploy/api/app.test.js`/`logos.test.js` (Profil in bestehenden Hochladen-/Veröffentlichen-Tests anlegen; neue 403-Tests), `deploy/api/README.md`, `deploy/README.md` (sechs Dateien kopieren) + +**Interfaces:** +- Produces: + ```js + // common.js + const USERNAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{2,19}$/; + function usernameKey(name) // name.toLowerCase() + function isReservedUsername(name) // Schlüssel in der Sperrliste + // profiles.js + function createProfileRoutes({ appwrite, say, withLock, requireUser }) // → [['POST /api/profil', {name:'profil', json:true, run}]] + async function hasProfile(appwrite, userId) // getRow('profiles', userId) !== null — von logos.js und app.js genutzt + ``` + +- [ ] **Step 1: Tests schreiben:** anlegen → 200 `{username}`, Zeile ID = Nutzer-ID, Rechte genau `['read("any")']`, `usernameKey` klein; ändern → alter Schlüssel frei (anderes Konto kann ihn danach nehmen); „kenso“ → „Kenso“ beim selben Konto erlaubt; vergeben → 409 `usernameTaken`; Rennen zweier Konten (Verzögerung im Fake) → genau ein 200, ein 409; gesperrt (`Admin`, `AirCard-IOS`) → 400 `usernameReserved`; Muster (`ab`, `_abc`, 21 Zeichen, `a b`, Umlaut) → 400 `usernameInvalid`; ohne JWT 401; `hochladen` und `veroeffentlichen` ohne Profil → 403 `usernameRequired` (vor allen anderen Prüfungen, nichts angelegt); Setup-Skript: `profiles` samt Unique-Index wird angelegt/geprüft. +- [ ] **Step 2:** `node --test "deploy/api/*.test.js"` und `node --test web/scripts/appwrite-setup.test.mjs` → FAIL. +- [ ] **Step 3:** Umsetzen. +- [ ] **Step 4:** beide Suiten grün; `cd web && npx vitest run` grün (Gleichlauftest `API_MESSAGES` erst in Task 2 — falls er hier rot wird, die vier Texte in `web/src/i18n/api-messages.ts` + `en.ts` gleich mit ergänzen). +- [ ] **Step 5:** Commit `feat(api): Benutzernamen (Profil-Tabelle, /api/profil, Pflicht beim Hochladen und Veröffentlichen)`. + +### Task 2: Client – Profil-Daten, Dialog „Benutzername wählen“, Registrieren, Konto-Menü + +**Files:** +- Create: `web/src/account/profile.ts` (+ Test), `web/src/account/username-dialog.ts` (+ Test) +- Modify: `web/src/account/appwrite.ts` (`IDS.profiles = 'profiles'`), `web/src/account/login-dialog.ts` (+ Test), `web/src/account/account-menu.ts` (+ Test), `web/src/main.ts` (+ Test: Abfrage einmal pro Sitzung nach Start/Anmeldung), `web/src/i18n/api-messages.ts`, `web/src/i18n/en.ts`, `web/src/account/account.css` + +**Interfaces:** +- Produces: + ```ts + // account/profile.ts + export const USERNAME_PATTERN: RegExp; + export function usernameKey(name: string): string; + export function validateUsername(name: string): string | null; // übersetzter Text oder null (Muster, Sperrliste) + export function getUsername(userId: string): Promise; // Profil lesen, 404 → null + export function usernamesFor(userIds: readonly string[]): Promise>; // gesammelt, zwischengespeichert, Fehler → leere Map + export function isUsernameTaken(name: string): Promise; // Abfrage equal('usernameKey', key) + export function saveUsername(name: string): Promise; // POST /api/profil, aktualisiert Cache + meldet Änderung + export function myUsername(): string | null; // Cache des angemeldeten Kontos (null = keiner/unbekannt) + export function loadMyUsername(): Promise; // nach Anmeldung/Start + export function onUsernameChange(fn: (userId: string, username: string) => void): () => void; + // account/username-dialog.ts + export function usernameDialog(opts: { mode: 'prompt' | 'required' | 'change'; signal?: AbortSignal }): Promise; // null = Später/Abbrechen + export function requireUsername(): Promise; // hat Namen → true; sonst Dialog 'required' + ``` +- [ ] **Step 1: Tests:** `validateUsername` (Grenzen 3/20, erstes Zeichen, erlaubte Zeichen, Sperrliste case-insensitiv); `usernamesFor` sammelt IDs, eine Abfrage pro Aufruf, Cache, Fehler → leere Map; Registrieren: Feld „Benutzername“ Pflicht, Vorabprüfung (Muster, Sperre, `isUsernameTaken`) **vor** `register()`, Fehler am Feld; nach erfolgreichem `register` → `saveUsername`; scheitert das, schließt der Login-Dialog trotzdem erfolgreich und der Namensdialog erscheint; Dialog: Modi prompt („Später“), required („Abbrechen“), change (vorbelegt), Speichern ruft `saveUsername`, 409 → Text am Feld, Busy-Sperre; Konto-Menü: „@name“ + E-Mail klein, Eintrag „Benutzername ändern“ bzw. „Benutzername wählen“; `main.ts`: nach Start mit Sitzung bzw. nach Anmeldung ohne Namen genau einmal pro Sitzung Dialog `prompt`. +- [ ] **Step 2:** `cd web && npx vitest run src/account src/main.test.ts` → FAIL. +- [ ] **Step 3:** Umsetzen (Dialog nach dem Muster von `logo-dialog.ts`/`publish-dialog.ts`, Texte nur als Text). +- [ ] **Step 4:** `cd web && npx vitest run && npx tsc --noEmit && npm run build` grün. +- [ ] **Step 5:** Commit `feat(account): Benutzername beim Registrieren, im Konto-Menü und als einmalige Abfrage`. + +### Task 3: Client – Anzeige „von @name“, Anzeigename-Felder weg, Pflicht vor Hochladen/Veröffentlichen + +**Files:** +- Modify: `web/src/logos/logo-picker.ts`, `web/src/logos/logo-info.ts`, `web/src/pages/community-page.ts`, `web/src/logos/logo-dialog.ts`, `web/src/logos/upload-logo.ts`, `web/src/community/publish-dialog.ts`, `web/src/community/publish.ts`, `web/src/pages/cards-page.ts` (Veröffentlichen-Aufruf), Tests dazu; `web/src/i18n/en.ts` +- Delete: `web/src/account/display-name.ts` (+ Test) + +**Interfaces:** +- Consumes: Task 2 (`usernamesFor`, `myUsername`, `onUsernameChange`, `requireUsername`, `currentUserId`). +- Byline-Regel (eine Hilfsfunktion, z. B. in `profile.ts`): `byline(ownerId, legacyDisplayName, names)` → `t('von @{name}', {name})` wenn Profil, sonst `t('von {name}', {name: legacy})` wenn Altname, sonst `t('von {name}', {name: t('Anonym')})`. + +- [ ] **Step 1: Tests:** Kacheln/Infozeile/Detailseite zeigen „von @name“ (eine `usernamesFor`-Abfrage pro geladener Seite), Fallback Altname bzw. „Anonym“, Fehler beim Nachschlagen → Fallback ohne Fehlertext; eigene Umbenennung (`onUsernameChange`) aktualisiert eigene Einträge sofort; Dialoge „Logo hochladen“ und „Veröffentlichen“ ohne Anzeigename-Feld, senden `displayName: ''`; vor Hochladen bzw. Veröffentlichen `requireUsername()` → false = nichts passiert; Serverantwort 403 `usernameRequired` → Namensdialog `required`, danach erneuter Versuch möglich; `display-name.ts` und `aircard.displayName` nirgends mehr genutzt. +- [ ] **Step 2:** `cd web && npx vitest run src/logos src/community src/pages` → FAIL. +- [ ] **Step 3:** Umsetzen. +- [ ] **Step 4:** `cd web && npx vitest run && npx tsc --noEmit && npm run build` grün. +- [ ] **Step 5:** Commit `feat(community): „von @name“ statt Anzeigename, Benutzername vor dem Hochladen und Veröffentlichen`. + +### Task 4: Betrieb und Abnahme (Controller) + +- [ ] Schema: Setup-Skript `--check` → anlegen → `--check` Exit 0 (Unique-Index `username_key`). +- [ ] API: Backup, sechs Dateien kopieren, `docker compose restart aircard-api`, Gesundheit + `POST /api/profil` ohne JWT = 401 `unauthorized`. +- [ ] Push `main` → Build → neues Bundle live. +- [ ] Live als Gast: Kacheln/Detailseite zeigen „von …“ (Altdaten „Anonym“). Kenso: registrieren bzw. Abfrage nach Anmeldung, Name ändern, Name am eigenen Logo sehen. +- [ ] Review (ein `sonnet`-Reviewer über alles), Fix-Welle nur für Critical/Important, Memory, Bericht. diff --git a/docs/superpowers/specs/2026-10-05-aircard-benutzernamen-design.md b/docs/superpowers/specs/2026-10-05-aircard-benutzernamen-design.md new file mode 100644 index 0000000..b018201 --- /dev/null +++ b/docs/superpowers/specs/2026-10-05-aircard-benutzernamen-design.md @@ -0,0 +1,109 @@ +# AirCard-IOS – Benutzernamen – Design + +Stand: 2026-10-05 · Status: Entwurf im Chat von Kenso freigegeben („ja passt, mach weiter“), Spec + Plan + Umsetzung ohne weitere Rückfrage · baut auf `2026-10-04-aircard-logos-sprache-design.md` auf (live, main 13b46da) + +## 1. Ziel und Rahmen + +Jedes Konto bekommt einen eindeutigen Benutzernamen. Logos und Community-Karten zeigen „von @name“ statt „Anonym“, damit man Leute wiedererkennt. + +| Punkt | Entscheidung (Kenso) | +|---|---| +| Eindeutig | Ja, jeder Name nur einmal; Groß-/Kleinschreibung zählt beim Vergleich nicht. | +| Pflicht | Ja, auch für bestehende Konten. Registrieren nur mit Name; alte Konten werden nach dem Anmelden einmal gefragt; Hochladen und Veröffentlichen gehen erst mit Name. | +| Anzeige | Immer der Benutzername; das Feld „Anzeigename“ fällt weg; gilt auch für vorhandene Einträge des Kontos. | +| Ändern | Ja, im Konto-Menü; danach überall der neue Name, der alte wird frei. | + +## 2. Regeln für den Namen + +- Muster: `^[A-Za-z0-9][A-Za-z0-9._-]{2,19}$`. Das sind 3–20 Zeichen, das erste ein Buchstabe oder eine Zahl, danach Buchstaben, Zahlen, `.`, `_`, `-`. +- Schlüssel für die Eindeutigkeit: der Name in Kleinbuchstaben (`usernameKey`). Gespeichert und angezeigt wird der Name, wie er eingegeben wurde. +- Gesperrte Schlüssel: `admin`, `administrator`, `root`, `system`, `support`, `moderator`, `mod`, `aircard`, `aircardios`, `aircard-ios`, `aircard.ios`, `aircard_ios`, `webklar`, `anonym`, `anonymous`, `null`, `undefined`, `team`, `staff`, `official`, `offiziell`, `hilfe`, `help`, `kontakt`, `contact`, `info`. +- Anzeige mit `@` davor: „von @name“ bzw. „by @name“. + +## 3. Daten (Appwrite, Datenbank `aircard`, Setup-Skript erweitert) + +**Tabelle `profiles`** (rowSecurity an, keine Tabellenrechte für Clients): + +| Spalte | Typ | Inhalt | +|---|---|---| +| `username` | string 20, Pflicht | Name wie eingegeben | +| `usernameKey` | string 20, Pflicht | Name klein geschrieben | + +- Zeilen-ID = Nutzer-ID (eine Zeile pro Konto). +- Index `username_key`: **unique** auf `usernameKey`. Damit kann auch bei Gleichzeitigkeit kein Name doppelt vergeben werden. +- Zeilenrecht genau `['read("any")']`, öffentlich lesbar. Schreiben nur über die API. + +## 4. API-Dienst `aircard-api` + +Eigenes Modul `deploy/api/profiles.js`, eingebunden wie `logos.js`. Der Betrieb kopiert künftig sechs Dateien: `appwrite.js`, `common.js`, `app.js`, `logos.js`, `profiles.js`, `server.js`. + +`POST /api/profil` `{username}`, Nutzer-JWT: + +- 401 ohne Anmeldung. +- 400 `usernameInvalid` bei falschem Muster. +- 400 `usernameReserved` bei gesperrtem Schlüssel. +- 409 `usernameTaken`, wenn ein anderes Konto den Schlüssel hat. Das wird vorab per Abfrage geprüft; bei einem Rennen meldet Appwrite 409 auf dem Unique-Index, das wird ebenfalls zu `usernameTaken`. +- Sonst: Hat das Konto schon eine Zeile, wird sie aktualisiert (der alte Name ist damit frei); sonst wird sie mit ID = Nutzer-ID und `read("any")` angelegt. Antwort `200 {username}`. +- Groß-/Kleinschreibung des eigenen Namens ändern („kenso“ → „Kenso“) ist erlaubt. +- Sperre je Konto (`profile:`). + +**Pflicht serverseitig:** `POST /api/logos/hochladen` und `POST /api/veroeffentlichen` antworten **403 `usernameRequired`**, wenn das Konto keine Zeile in `profiles` hat. Die Prüfung läuft nach der Anmeldung und vor allen anderen Prüfungen. Bestehende Logos und Karten bleiben unberührt. + +Neue `MSG`-Texte: + +- `usernameInvalid`: „Der Benutzername muss 3–20 Zeichen lang sein, mit einem Buchstaben oder einer Zahl beginnen und darf nur Buchstaben, Zahlen, Punkt, Unterstrich und Bindestrich enthalten.“ +- `usernameReserved`: „Dieser Benutzername ist nicht erlaubt.“ +- `usernameTaken`: „Dieser Benutzername ist schon vergeben.“ +- `usernameRequired`: „Bitte wähle zuerst einen Benutzernamen.“ + +## 5. Oberfläche + +- **Registrieren** (Login-Dialog, Reiter „Registrieren“): neues Pflichtfeld „Benutzername“ vor der E-Mail, `autocomplete="username"`, Hinweis „3–20 Zeichen: Buchstaben, Zahlen, . _ -“. + - Vor dem Anlegen des Kontos werden Muster, Sperrliste und Verfügbarkeit geprüft (Abfrage auf `profiles`). Ein Fehler steht am Feld. + - Danach: Konto anlegen → anmelden → `POST /api/profil`. + - Scheitert dieser letzte Schritt (Rennen, Netz), ist man trotzdem angemeldet, und der Dialog „Benutzername wählen“ erscheint. +- **Dialog „Benutzername wählen“** mit Feld, „Speichern“ und „Später“ bzw. „Abbrechen“: + - Nach dem Anmelden oder beim Seitenstart mit Sitzung, wenn das Konto keinen Namen hat. Einmal pro Sitzung (Merker in `sessionStorage` `aircard.usernamePrompted`, Zugriffe in try/catch), mit „Später“. + - Vor dem Hochladen eines Logos und vor dem Veröffentlichen, wenn kein Name da ist. Hier gibt es „Abbrechen“ statt „Später“; ohne Namen geht es nicht weiter. + - Antwortet der Server mit `usernameRequired`, erscheint der Dialog ebenfalls. +- **Konto-Menü**: Statt der E-Mail steht „@name“ oben (die E-Mail darunter klein). Neuer Eintrag „Benutzername ändern“, der den Dialog vorbelegt öffnet. Ohne Namen heißt der Eintrag „Benutzername wählen“. +- **Anzeige „von @name“**: Logo-Kacheln, Logo-Infozeile, Detailseite einer Community-Karte. + - Der Name wird über `ownerId` in `profiles` nachgeschlagen, gesammelt je Seite (`equal('$id', ids)`) und zwischengespeichert. + - Ohne Profil: der früher gespeicherte `displayName` (ohne `@`), sonst „Anonym“. + - Nach einer eigenen Namensänderung aktualisieren Kacheln, Infozeile und Detailseite die eigenen Einträge sofort. +- **Weg fällt** das Feld „Anzeigename“ in den Dialogen „Logo hochladen“ und „Veröffentlichen“ samt `account/display-name.ts` und dem `localStorage`-Schlüssel `aircard.displayName`. Der Client schickt `displayName: ''`; die API nimmt das Feld weiter an (Altbestand). + +Alle Texte gibt es auf Deutsch und Englisch. + +## 6. Fehlerbehandlung + +| Fall | Meldung | +|---|---| +| Muster falsch / gesperrt / vergeben | die drei `MSG`-Texte aus § 4, am Feld | +| Server meldet `usernameRequired` | Dialog „Benutzername wählen“ | +| Netz/API weg beim Speichern | „Der Dienst ist gerade nicht erreichbar.“, Dialog bleibt offen | +| Profil lässt sich nicht lesen (Anzeige) | Fallback `displayName` bzw. „Anonym“, kein Fehlertext | + +## 7. Tests + +- **node:test:** + - `/api/profil`: anlegen, ändern, Groß/klein am eigenen Namen, vergeben 409, gesperrt 400, Muster 400, ohne JWT 401. + - Rennen mit zwei Konten: genau eines bekommt den Namen. + - Zeilenrechte genau `read("any")`. + - Hochladen und Veröffentlichen ohne Profil → 403 `usernameRequired`. + - Setup-Skript deckt `profiles` mit Unique-Index ab. +- **Vitest:** + - Prüfung des Namens (Muster, Sperrliste, Schlüssel). + - Registrieren mit Vorabprüfung und Fehlern am Feld. + - Dialog: Später, Abbrechen, Speichern, Fehler. + - Einmal-pro-Sitzung-Abfrage. + - Konto-Menü mit „@name“ und Ändern. + - Anzeige „von @name“ mit Fallbacks; sofortige Aktualisierung nach Umbenennen. + - Dialoge ohne „Anzeigename“. + - Hochladen und Veröffentlichen fragen vorher nach dem Namen. + - Gleichlauf der API-Codes, i18n-Abdeckung. +- **Live:** Gast sieht „von …“-Zeilen. Kenso registriert bzw. wird gefragt, ändert den Namen und sieht ihn an seinem Logo. + +## 8. Nicht im Umfang + +Profilseiten, Suche nach Benutzernamen, Moderation von Namen, Konto löschen, Avatar-Bilder, Benutzername als Anmeldename (angemeldet wird weiter per E-Mail).