diff --git a/docs/superpowers/plans/2026-10-04-aircard-community.md b/docs/superpowers/plans/2026-10-04-aircard-community.md index 266dd94..e0aad86 100644 --- a/docs/superpowers/plans/2026-10-04-aircard-community.md +++ b/docs/superpowers/plans/2026-10-04-aircard-community.md @@ -98,7 +98,7 @@ - Modify: `deploy/compose-snippet.yml` (Service `aircard-api`), `deploy/README.md` **Interfaces:** -- `appwrite.js`: `createAppwrite({ endpoint, project, key, fetchImpl })` → `{ getRow, listRows(table, queries), createRow, updateRow(table, id, data, permissions?), deleteRow, getFile(bucket, id), updateFilePermissions(bucket, id, permissions), deleteFile, accountFromJwt(jwt) }` (letzteres `GET /account` mit `X-Appwrite-JWT`, ohne Key). Queries als Appwrite-JSON-Strings (`{"method":"equal","attribute":"…","values":[…]}`). +- `appwrite.js`: `createAppwrite({ endpoint, project, key, fetchImpl })` → `{ getRow, listRows(table, queries), createRow, updateRow(table, id, data, permissions?), deleteRow, getFile(bucket, id), updateFilePermissions(bucket, id, permissions), deleteFile, accountFromJwt(jwt) }` (letzteres `GET /account` mit `X-Appwrite-JWT`, ohne Key; der Client schickt das JWT im Header `X-Aircard-Jwt`). Queries als Appwrite-JSON-Strings (`{"method":"equal","attribute":"…","values":[…]}`). - `app.js`: `createApp({ appwrite, salt, now = Date.now, log })` → `(req, res) => Promise`; reine Helfer exportiert: `visibleRowPermissions()`, `hiddenRowPermissions(owner)`, `visibleFilePermissions(owner)`, `hiddenFilePermissions(owner)`, `reporterHash(salt, ip)` (HMAC-SHA256 hex), `clientIp(req)` (`x-real-ip`, sonst erstes `x-forwarded-for`, sonst Socket), `validatePublish(body)` → `{ok, data|message}` (Längen Spec §6, Tags normalisiert: trim, klein, leer raus, max. 5, je ≤ 20), `buildSearchText({title, issuer, tags})` (klein, ≤ 300). - Endpunkte, Statuscodes und Texte exakt Spec §8/§11: `POST /api/veroeffentlichen` (401 ohne/ungültiges JWT, 400 Validierung, 403 Datei nicht meine/falscher Bucket, 409 „Diese Karte ist schon veröffentlicht.“, 201 `{id}`), `POST /api/zurueckziehen` (403 fremd, 404 fehlt, 200), `POST /api/melden` (400 Grund, 404 fehlt/ausgeblendet, 200 `{ok:true}` bzw. `{schonGemeldet:true}`, 429 „Zu viele Meldungen – bitte später nochmal.“), `POST /api/admin/sichtbarkeit` (403 kein Admin, 200), `GET /api/gesundheit`. Body max. 8 KB (413), nur `application/json` (415), sonst 404. - `server.js`: Env `AIRCARD_API_PORT` (8088), `AIRCARD_APPWRITE_ENDPOINT` (`http://webklar-appwrite/v1`), `AIRCARD_APPWRITE_PROJECT_ID`, `AIRCARD_API_KEY`, `AIRCARD_API_SALT`; fehlt Key oder Salt → Fehlermeldung ohne Wert, `exit(1)`. @@ -119,13 +119,14 @@ - `api.ts`: `apiPost(path, body, { auth: boolean })` → JSON; mit `auth` holt `account.createJWT()` (Cache 10 min); Fehler als `ApiError { status, message }` mit Server-Text; Netzfehler → „Der Dienst ist gerade nicht erreichbar.“; `publishCard(input)`, `withdrawCard(id)`, `reportCard(id, reason)`, `setVisibility(id, visible)`. - `publish.ts`: `publishSavedCard(card: SavedCard, form: { title; issuer; tags; displayName }, deps)` → `{ id }`: Bild laden, rendern (PNG 1536 + WebP 480 via `exportCard`), zwei Uploads in `gallery-images` mit `[Permission.read(Role.any()), Permission.delete(Role.user(uid))]`, dann `publishCard`; bei Fehler beide Dateien löschen (best effort) und Fehler weiterwerfen. `normalizeTags(input: string): string[]` (wie Server). -- [ ] **Step 1: Tests** – Queries exakt (inkl. Suche + Cursor + Admin-Filter); `toGalleryCard` mit fehlenden Feldern; `getGalleryCard` 404 → null; `apiPost` hängt `X-Appwrite-JWT`… nein: `Authorization: Bearer `? → Ruling: Header `X-Aircard-Jwt: ` (API liest ihn); JWT-Cache; Fehlertext aus JSON `message`; Netzfehler-Text. `publishSavedCard`: Reihenfolge Upload, Upload, API; API-Fehler → 2× `deleteFile`, Fehler kommt an; zweiter Upload scheitert → erster gelöscht. +- [ ] **Step 1: Tests** – Queries exakt (inkl. Suche + Cursor + Admin-Filter); `toGalleryCard` mit fehlenden Feldern; `getGalleryCard` 404 → null; `apiPost` mit `auth` sendet Header `X-Aircard-Jwt: `; JWT-Cache (zweiter Aufruf innerhalb 10 min ohne neues `createJWT`); Fehlertext aus JSON `message`; Netzfehler-Text. `publishSavedCard`: Reihenfolge Upload, Upload, API; API-Fehler → 2× `deleteFile`, Fehler kommt an; zweiter Upload scheitert → erster gelöscht. - [ ] **Step 2: FAIL**, **Step 3: Implementieren**, **Step 4: PASS + tsc + build**, **Step 5: Commit** `feat(community): Datenschicht für Galerie, API und Veröffentlichen`. ### Task 5: Startseite (Hero + Galerie) **Files:** -- Create: `web/src/pages/home-page.ts` (ersetzt Platzhalter), `web/src/community/gallery-grid.ts`, `web/src/community/community.css`, `web/public/hero-beispiel.png` (1536 × 969, eigenes markenfreies Motiv: lila/blauer Verlauf mit abstrakten Wellen + AirCard-IOS-Symbol, erzeugt mit einem Skript im Scratchpad) +- Modify: `web/src/pages/home-page.ts` (Platzhalter aus Task 1 ersetzen) +- Create: `web/src/community/gallery-grid.ts`, `web/src/community/community.css`, `web/public/hero-beispiel.png` (1536 × 969, eigenes markenfreies Motiv: lila/blauer Verlauf mit abstrakten Wellen + AirCard-IOS-Symbol, erzeugt mit einem Skript im Scratchpad) - Test: `web/src/pages/home-page.test.ts`, `web/src/community/gallery-grid.test.ts` **Interfaces:** @@ -134,12 +135,12 @@ - CSS: Hero `min-height: auto; max-height: 60vh`, Raster `repeat(auto-fill, minmax(260px, 1fr))`; Hero-Karte schwebt (`@media (prefers-reduced-motion: no-preference)`). - [ ] **Step 1: Tests** – leere Galerie → Beispielbild + Leertext; Suche aus `?q=` landet in Queries und zeigt aktiven Begriff mit „ד; „Mehr laden“ nur bei `nextCursor`; Tag-Klick navigiert zu `/?q=#community`; Object-URLs werden beim Cleanup freigegeben. -- [ ] **Step 2: FAIL**, **Step 3: Implementieren** (inkl. Hero-Bild), **Step 4: PASS + build**; Sichtprüfung headless Chromium (`npx vite preview`) bei 1440 × 900 und 390 × 844 mit gemockter Galerie (z. B. per `?demo=1`-freiem Weg: Test-Stub über `page.route` auf `/v1/tablesdb/…`) – Überschrift „Community-Karten“ und Oberkante der ersten Reihe im Viewport; Screenshots im Scratchpad. **Step 5: Commit** `feat(home): kompakter Hero und Community-Galerie`. +- [ ] **Step 2: FAIL**, **Step 3: Implementieren** (inkl. Hero-Bild), **Step 4: PASS + build**; Sichtprüfung headless Chromium (`npx vite preview`) bei 1440 × 900 und 390 × 844 mit gemockter Galerie (Playwright `page.route` beantwortet `/v1/tablesdb/…/rows` mit 30 Fake-Zeilen und `/v1/storage/…/view` mit einem PNG) – Überschrift „Community-Karten“ und Oberkante der ersten Reihe im Viewport; Screenshots im Scratchpad. **Step 5: Commit** `feat(home): kompakter Hero und Community-Galerie`. ### Task 6: Detailseite `/community/` und „Als Vorlage“ **Files:** -- Modify: `web/src/pages/community-page.ts` (ersetzt Platzhalter), `web/src/editor/editor-page.ts` (`?vorlage=`) +- Modify: `web/src/pages/community-page.ts` (Platzhalter aus Task 1 ersetzen), `web/src/editor/editor-page.ts` (`?vorlage=`) - Create: `web/src/community/report-dialog.ts` - Test: `web/src/pages/community-page.test.ts`, `web/src/community/report-dialog.test.ts`, Ergänzung `web/src/editor/editor-page.test.ts`