docs: Spec und Plan Benutzernamen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-05 06:23:08 +00:00
1 parent 13b46da2e0
commit a65c075004
2 files changed
+222

No files matched your search

@@ -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 <noreply@anthropic.com>`.
- 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<string | null>; // Profil lesen, 404 → null
export function usernamesFor(userIds: readonly string[]): Promise<Map<string, string>>; // gesammelt, zwischengespeichert, Fehler → leere Map
export function isUsernameTaken(name: string): Promise<boolean>; // Abfrage equal('usernameKey', key)
export function saveUsername(name: string): Promise<string>; // POST /api/profil, aktualisiert Cache + meldet Änderung
export function myUsername(): string | null; // Cache des angemeldeten Kontos (null = keiner/unbekannt)
export function loadMyUsername(): Promise<string | null>; // 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<string | null>; // null = Später/Abbrechen
export function requireUsername(): Promise<boolean>; // 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.