Files
AirCardIOS/docs/superpowers/plans/2026-10-05-aircard-benutzernamen.md
T
2026-10-05 06:23:08 +00:00

114 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.