docs: Spec und Plan Benutzernamen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
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.
|
||||
Reference in new issue
Block a user