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

11 KiB
Raw Blame History

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:

    // 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:
    // 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.