11 KiB
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, Branchmain, 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 inweb/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, Spaltenusername(string 20, Pflicht),usernameKey(string 20, Pflicht), Indexusername_keyunique aufusernameKey, rowSecurity an, keine Tabellenrechte, Zeilenrecht genau['read("any")']. - API-Codes und Texte genau wie Spec § 4 (
usernameInvalid,usernameReserved,usernameTaken,usernameRequired); Client-TabelleAPI_MESSAGESbleibt im Gleichlauf mitdeploy/api/common.jsMSG. sessionStorage-Schlüsselaircard.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),haikufü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): Tabelleprofilesnachlogo_reportsmit 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;publishprüft Profil),deploy/api/logos.js(hochladenprüft Profil),deploy/api/fake-appwrite.js(Tabelleprofiles, Unique-Index → 409 bei doppeltemusernameKey),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:
// 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")'],usernameKeyklein; ändern → alter Schlüssel frei (anderes Konto kann ihn danach nehmen); „kenso“ → „Kenso“ beim selben Konto erlaubt; vergeben → 409usernameTaken; Rennen zweier Konten (Verzögerung im Fake) → genau ein 200, ein 409; gesperrt (Admin,AirCard-IOS) → 400usernameReserved; Muster (ab,_abc, 21 Zeichen,a b, Umlaut) → 400usernameInvalid; ohne JWT 401;hochladenundveroeffentlichenohne Profil → 403usernameRequired(vor allen anderen Prüfungen, nichts angelegt); Setup-Skript:profilessamt Unique-Index wird angelegt/geprüft. -
Step 2:
node --test "deploy/api/*.test.js"undnode --test web/scripts/appwrite-setup.test.mjs→ FAIL. -
Step 3: Umsetzen.
-
Step 4: beide Suiten grün;
cd web && npx vitest rungrün (GleichlauftestAPI_MESSAGESerst in Task 2 — falls er hier rot wird, die vier Texte inweb/src/i18n/api-messages.ts+en.tsgleich 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:
// 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);usernamesForsammelt IDs, eine Abfrage pro Aufruf, Cache, Fehler → leere Map; Registrieren: Feld „Benutzername“ Pflicht, Vorabprüfung (Muster, Sperre,isUsernameTaken) vorregister(), Fehler am Feld; nach erfolgreichemregister→saveUsername; scheitert das, schließt der Login-Dialog trotzdem erfolgreich und der Namensdialog erscheint; Dialog: Modi prompt („Später“), required („Abbrechen“), change (vorbelegt), Speichern ruftsaveUsername, 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 Dialogprompt. - 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 buildgrü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, sonstt('von {name}', {name: legacy})wenn Altname, sonstt('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, sendendisplayName: ''; vor Hochladen bzw. VeröffentlichenrequireUsername()→ false = nichts passiert; Serverantwort 403usernameRequired→ Namensdialogrequired, danach erneuter Versuch möglich;display-name.tsundaircard.displayNamenirgends 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 buildgrü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 →--checkExit 0 (Unique-Indexusername_key). - API: Backup, sechs Dateien kopieren,
docker compose restart aircard-api, Gesundheit +POST /api/profilohne JWT = 401unauthorized. - 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.