docs: Plan Startseite & Community-Galerie
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
970e68e7fc
commit
23463f7d29
1 file changed
+171
@@ -0,0 +1,171 @@
|
|||||||
|
# AirCard-IOS Startseite & Community-Galerie – 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:** Kompakte Startseite mit Community-Galerie, Veröffentlichen/Melden/Admin, Spenden-Banner und Hell/Dunkel-Schalter für https://aircard-ios.org.
|
||||||
|
|
||||||
|
**Architecture:** Der bestehende Vite-+-TS-Client bekommt neue Routen und Module (`web/src/community/*`, `web/src/shell/*`). Lesen der Galerie direkt aus Appwrite (öffentliche Zeilen), alle Galerie-Schreibvorgänge über einen neuen kleinen Node-Dienst `aircard-api` (`deploy/api/`) mit Server-Key, erreichbar unter `/api` per Traefik. Schema-Erweiterung über das bestehende Setup-Skript.
|
||||||
|
|
||||||
|
**Tech Stack:** wie bisher (TypeScript 6.0.3, Vite 8.3.2, Vitest 5.0.3, happy-dom, appwrite 22.4.1), Node 22 für `aircard-api` (nur Built-ins), Appwrite 1.8.1 TablesDB, Traefik v3.6.
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-10-04-aircard-community-design.md` (Commit 970e68e). Basis: `docs/superpowers/specs/2026-10-04-aircard-website-design.md`.
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- Arbeitskopie `/home/webklar/apps/aircard-ios.org/dev`, Branch `main`, direkt committen, **nicht pushen** (Controller pusht).
|
||||||
|
- Versionen unverändert gepinnt; keine neuen npm-Abhängigkeiten im Client; `deploy/api/` ohne Abhängigkeiten.
|
||||||
|
- Alle sichtbaren Texte Deutsch, Wortlaut aus der Spec.
|
||||||
|
- Appwrite: Projekt `6ac2130a00289343d6d5`; Datenbank `aircard`; neue Tabellen `gallery`, `reports`; neuer Bucket `gallery-images`; bestehende `cards`/`card-images` unverändert.
|
||||||
|
- Galerie-Zeilenrechte: sichtbar genau `['read("any")']`; ausgeblendet genau `['read("user:<owner>")','read("label:admin")']`. Galerie-Dateirechte sichtbar `['read("any")','delete("user:<owner>")']`, ausgeblendet `['read("user:<owner>")','read("label:admin")','delete("user:<owner>")']`.
|
||||||
|
- Auto-Ausblenden ab **3** verschiedenen Meldern; Rate-Limit **10** Meldungen pro Stunde und IP; Seiten zu **24** Karten.
|
||||||
|
- Admin = Appwrite-Label `admin` (`account.get().labels`).
|
||||||
|
- `DONATE_URL` in `web/src/config.ts`; leer ⇒ Banner und Spenden-Knopf unsichtbar.
|
||||||
|
- Theme: `data-theme` auf `<html>`, `localStorage` `aircard.theme`; Banner `localStorage` `aircard.donateBannerClosed`. Jeder `localStorage`-Zugriff in try/catch.
|
||||||
|
- Hero: bei 1440 × 900 und 390 × 844 müssen „Community-Karten“-Überschrift und der obere Rand der ersten Kartenreihe im ersten Bildschirm sichtbar sein.
|
||||||
|
- Keine Secrets in Ausgaben, Logs, Commits; `.env` nie öffnen (Ausnahme: Controller-Ops).
|
||||||
|
- Tests: `cd web && npx vitest run`, `npx tsc --noEmit`, `npm run build`; `node --test deploy/api/*.test.js` (Glob in Anführungszeichen), bestehende node-Tests grün halten.
|
||||||
|
- Commits enden mit `Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>`.
|
||||||
|
- Subagents: `sonnet` (Implementierung/Review), `haiku` für kleine Nachprüfungen; nie Subagents aus Subagents.
|
||||||
|
|
||||||
|
## Review Focus
|
||||||
|
|
||||||
|
- **Fremdes Veröffentlichen**: Nutzer schickt `imageFileId` einer fremden Datei an `/api/veroeffentlichen` → 403, nichts angelegt. Test in Task 3.
|
||||||
|
- **Meldungs-Flut / Selbst-Ausblenden**: dieselbe IP meldet 5× → zählt 1; 11. Meldung in einer Stunde → 429. Test in Task 3.
|
||||||
|
- **Ausgeblendete Karte per Direkt-Link**: Gast öffnet `/community/<id>` einer ausgeblendeten Karte → „nicht (mehr) verfügbar“, kein Bild. Test in Task 6 (Client) und Task 3 (Rechte).
|
||||||
|
- **Leere Galerie / leeres `DONATE_URL`**: Startseite ohne Karten zeigt Beispielmotiv und Leertext; ohne Link kein toter Spenden-Knopf. Tests in Tasks 1 und 5.
|
||||||
|
- **Theme-Wechsel mit offenem Editor**: Umschalten ändert nur UI-Tokens, nicht die Kartenfarben/PNG. Test in Task 1 (render unabhängig von Theme).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Shell – Routen, Kopfzeile, Theme, Spenden-Banner
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `web/src/config.ts`, `web/src/shell/theme.ts`, `web/src/shell/donate-banner.ts`, `web/src/shell/header.ts`, `web/src/shell/shell.css`, `web/src/pages/home-page.ts` (Platzhalter mit Überschrift, Task 5 füllt), `web/src/pages/community-page.ts` (Platzhalter, Task 6 füllt)
|
||||||
|
- Modify: `web/index.html` (Kopfzeilen-Gerüst, Banner-Slot `#donate-banner`, Theme-Init-Skript gegen Aufblitzen), `web/src/pages/router.ts`, `web/src/main.ts`, `web/src/styles.css` (helle Tokens), `web/src/account/account-menu.ts` (Avatar-Kreis + Menü), `web/src/account/auth.ts` (`User.labels`, `isAdmin`), `web/src/editor/card-url.ts` und `web/src/pages/cards-page.ts` (Pfad `/gestalten?karte=`)
|
||||||
|
- Test: `web/src/pages/router.test.ts`, `web/src/shell/theme.test.ts`, `web/src/shell/donate-banner.test.ts`, `web/src/shell/header.test.ts`, `web/src/account/auth.test.ts` (labels)
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Produces:
|
||||||
|
```ts
|
||||||
|
// router.ts (ersetzt RouteId)
|
||||||
|
type RouteId = 'home' | 'gestalten' | 'cards' | 'community' | 'reset'
|
||||||
|
interface Route { id: RouteId; params: { id?: string } }
|
||||||
|
matchRoute(pathname: string): Route // '/' home, '/gestalten' gestalten, '/karten' cards, '/community/<id>' community {id}, '/passwort-neu' reset, sonst home
|
||||||
|
legacyRedirect(url: URL): string | null // '/?karte=X' → '/gestalten?karte=X', sonst null
|
||||||
|
navigate(path: string): void // pushState, render, scrollTo(0,0) außer bei Hash-Ziel (dann scrollIntoView des Ankers)
|
||||||
|
startRouter(render: (route: Route, root: HTMLElement) => void | (() => void)): void
|
||||||
|
ROUTE_TITLES / titleFor(route: Route, detailTitle?: string): string // Wortlaut Spec §2
|
||||||
|
// config.ts
|
||||||
|
export const DONATE_URL: string = '' // Kenso liefert den Link
|
||||||
|
// theme.ts
|
||||||
|
type Theme = 'light' | 'dark'
|
||||||
|
initialTheme(stored: string | null, prefersDark: boolean): Theme
|
||||||
|
applyTheme(t: Theme): void; toggleTheme(): Theme // speichert in localStorage (try/catch)
|
||||||
|
// donate-banner.ts
|
||||||
|
mountDonateBanner(slot: HTMLElement, url: string): void // nichts rendern wenn url leer oder geschlossen
|
||||||
|
// header.ts
|
||||||
|
mountHeader(el: HTMLElement, opts: { donateUrl: string }): () => void // Navigation, Suchfeld (Enter → navigate('/?q=…#community')), ☕-Knopf, Theme-Schalter, Menü-Icon < 900 px
|
||||||
|
// auth.ts
|
||||||
|
type User = { $id: string; email: string; labels: string[] }
|
||||||
|
isAdmin(user: User | null): boolean
|
||||||
|
```
|
||||||
|
- Kopfzeile: Logo `/favicon.svg?v=2` + „AirCard-IOS“ (→ `/`), Links „Community“ (`/#community`), „Gestalten“ (`/gestalten`), „Meine Karten“ (`/karten`), Suchfeld Platzhalter „Titel, Tags oder Herausgeber“, Knopf „☕ Unterstützen“, Theme-Knopf (`aria-label` „Hell/Dunkel umschalten“), Konto-Slot. Banner-Text exakt aus Spec §3.
|
||||||
|
- `index.html`: kleines Inline-Skript im `<head>` setzt `data-theme` aus `localStorage`/System vor dem ersten Paint (in try/catch).
|
||||||
|
|
||||||
|
- [ ] **Step 1: Tests schreiben** – router: alle Routen inkl. `/community/abc123` → `{id:'community', params:{id:'abc123'}}`, `/community/` → home, unbekannt → home; `legacyRedirect(new URL('https://x/?karte=K1'))` → `/gestalten?karte=K1`, ohne `karte` → null; `navigate('/karten')` ruft `scrollTo(0,0)`; Titel je Route. theme: `initialTheme(null,true)` → dark, `('light',true)` → light, ungültiger Wert → System; `toggleTheme` setzt `data-theme` und speichert; `localStorage` wirft → kein Fehler. donate-banner: leere URL → nichts gerendert; Schließen setzt Flag und entfernt; Flag gesetzt → nichts. header: Suche Enter mit „ visa “ → `navigate('/?q=visa#community')`; leere Eingabe → nichts; kein ☕-Knopf bei leerem `donateUrl`. auth: `getCurrentUser` übernimmt `labels` (fehlend → `[]`), `isAdmin` true nur mit `'admin'`.
|
||||||
|
- [ ] **Step 2: Tests laufen lassen** → FAIL.
|
||||||
|
- [ ] **Step 3: Implementieren**; `main.ts`: Legacy-Umleitung vor `startRouter` (replaceState), Routen auf die Mount-Funktionen; Editor jetzt unter `gestalten`; `document.title` per `titleFor`.
|
||||||
|
- [ ] **Step 4: Prüfen** – Suite grün, `tsc`, `build`.
|
||||||
|
- [ ] **Step 5: Commit** – `feat(shell): Routen, Kopfzeile, Hell/Dunkel, Spenden-Banner`
|
||||||
|
|
||||||
|
### Task 2: Schema-Erweiterung (Setup-Skript)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `web/scripts/appwrite-setup.mjs`, `web/scripts/appwrite-setup.test.mjs`, `docs/appwrite-console.md`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- `DESIRED` bekommt: Tabelle `gallery` (rowSecurity true, Tabellenrechte `[]`), Spalten gemäß Spec §7 (Typen string/boolean/integer, Standardwerte `hidden=false`, `reportCount=0`), Indizes `search_text` (fulltext, `[searchText]`), `owner` (key, `[ownerId]`), `hidden` (key, `[hidden]`); Tabelle `reports` (rowSecurity false, Tabellenrechte `['read("label:admin")']`), Spalten `galleryId` string 36 Pflicht, `reason` enum `[unangemessen, kartendaten, urheberrecht, spam, sonstiges]` Pflicht, `reporterHash` string 64 Pflicht, Index `gallery` (key, `[galleryId]`); Bucket `gallery-images` (Rechte `['create("users")']`, fileSecurity true, maximumFileSize 5242880, Endungen `[png, webp]`, compression none, antivirus false).
|
||||||
|
- REST: `POST …/columns/boolean {key, required:false, default:false}`, `…/columns/integer {key, required:false, default:0, min:0}`, `…/columns/enum {key, elements, required:true}`, Indizes `POST /tablesdb/{db}/tables/{t}/indexes {key, type: 'fulltext'|'key', columns}` erst nachdem alle Spalten `available`; Index-Status ebenfalls auf `available` warten. Vergleich (`--check`) deckt Spaltentyp, Standard, Enum-Elemente, Index-Typ/Spalten ab.
|
||||||
|
- `docs/appwrite-console.md`: Abschnitt „Community“: JWT muss **an** bleiben; Label `admin` setzen; Key `aircard-api` (Scopes `rows.read`, `rows.write`, `files.read`, `files.write`) + `read -s`-Befehl für `AIRCARD_API_KEY`; Hinweis, dass der Provisioning-Key für Indizes zusätzlich `indexes.read/write` braucht.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Tests** – Fake-Server um boolean/integer/enum/index-Routen erweitern; leerer Zustand → `--check` meldet alle neuen Objekte als FEHLT; Anlegen → alles da, Indizes erst nach `available`-Spalten; zweiter Lauf legt nichts an; abweichender Index-Typ → ABWEICHUNG.
|
||||||
|
- [ ] **Step 2: FAIL**, **Step 3: Implementieren**, **Step 4:** `node --test web/scripts/appwrite-setup.test.mjs` grün, **Step 5: Commit** `feat(appwrite): Schema für Galerie, Meldungen und Galerie-Bilder`.
|
||||||
|
|
||||||
|
### Task 3: API-Dienst `aircard-api`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `deploy/api/appwrite.js` (REST-Helfer mit Server-Key), `deploy/api/app.js` (Routen, Validierung, Logik), `deploy/api/server.js` (Start: Env lesen, `http.createServer`), `deploy/api/app.test.js`, `deploy/api/README.md`
|
||||||
|
- 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":[…]}`).
|
||||||
|
- `app.js`: `createApp({ appwrite, salt, now = Date.now, log })` → `(req, res) => Promise<void>`; 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)`.
|
||||||
|
- Compose-Service: `aircard-api` = `node:22-alpine`, `container_name webklar-aircard-api`, `init: true`, `mem_limit 128m`, `restart unless-stopped`, Volume `./aircard-ios.org/api:/srv:ro`, Command `node /srv/server.js`, Env wie oben aus `${…}`, Netz `webklar-network`, Labels Router `aircard-api` ``Host(`aircard-ios.org`) && PathPrefix(`/api`)`` websecure/letsencrypt Priorität 200, Port 8088.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Tests** (`node:test`, Fake-Appwrite im Speicher, echte HTTP-Requests auf Port 0): Veröffentlichen ok (Zeile mit sichtbaren Rechten, `searchText`, `ownerId`); ohne JWT 401; fremde Datei (Rechte ohne `delete("user:me")`) 403; Datei in falschem Bucket 403; doppelt 409; Validierung (Titel leer/zu lang, 6 Tags) 400. Melden: 3 verschiedene IPs → `hidden=true`, Zeilen- und Dateirechte = ausgeblendet; dieselbe IP 5× → `reportCount` 1, Antworten `schonGemeldet`; 11. Meldung derselben IP in 1 h → 429, nach `now`+1 h wieder erlaubt; ausgeblendete Karte melden → 404. Zurückziehen: Autor ok (Zeile, 2 Dateien, Meldungen weg), Admin ok, fremd 403. Admin: Nicht-Admin 403; Einblenden setzt sichtbare Rechte, `hidden=false`, `reportCount=0`, löscht Meldungen. Body > 8 KB → 413; Log enthält nie Key/Salt/JWT.
|
||||||
|
- [ ] **Step 2: FAIL** (`node --test "deploy/api/*.test.js"`), **Step 3: Implementieren**, **Step 4: PASS**, **Step 5: Commit** `feat(api): aircard-api für Veröffentlichen, Melden und Admin`.
|
||||||
|
|
||||||
|
### Task 4: Client-Datenschicht Community
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `web/src/community/gallery.ts`, `web/src/community/api.ts`, `web/src/community/publish.ts`
|
||||||
|
- Modify: `web/src/account/appwrite.ts` (`IDS.gallery='gallery'`, `IDS.reports='reports'`, `IDS.galleryBucket='gallery-images'`), `web/src/account/cards.ts` (`fetchFile(bucketId, fileId)` verallgemeinern, bestehende Aufrufer anpassen)
|
||||||
|
- Test: `web/src/community/gallery.test.ts`, `api.test.ts`, `publish.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- `gallery.ts`: `interface GalleryCard { id; title; issuer; tags: string[]; displayName; imageFileId; previewFileId; sourceCardId; ownerId; hidden: boolean; reportCount: number; createdAt: string }`; `toGalleryCard(row)`; `galleryQueries(opts: { q?: string; cursor?: string; filter?: 'all'|'reported'|'hidden' }): string[]` (öffentlich immer `equal('hidden', false)`, `orderDesc('$createdAt')`, `limit(24)`, Suche `search('searchText', q)`, Admin-Filter laut Spec §7); `listGallery(opts)` → `{ cards, nextCursor | null }`; `getGalleryCard(id)` → `GalleryCard | null` (404 → null); `myPublications(userId)` → `Map<sourceCardId, GalleryCard>`; `listReports(galleryId)` (Admin); `loadGalleryImage(card, 'full'|'preview')` → Blob (über `fetchFile`).
|
||||||
|
- `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 <jwt>`? → Ruling: Header `X-Aircard-Jwt: <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 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)
|
||||||
|
- Test: `web/src/pages/home-page.test.ts`, `web/src/community/gallery-grid.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- `mountHomePage(root, deps = defaultDeps)` – Hero laut Spec §4 (Texte exakt), Hero-Karte = Vollbild der neuesten sichtbaren Karte oder `/hero-beispiel.png`; Galerie-Abschnitt `id="community"`; liest `?q=` aus der URL; „Mehr laden“ hängt an; Leer-/Kein-Treffer-Texte exakt; Admin sieht Filter-Chips „Alle/Gemeldet/Ausgeblendet“.
|
||||||
|
- `mountGalleryGrid(el, { load, onTag })` – Kacheln (Vorschau per Object-URL, beim Cleanup freigeben), Titel, Herausgeber, ≤ 3 Tags als Knöpfe; Kachel verlinkt `/community/<id>` (`data-link`).
|
||||||
|
- 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=<tag>#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`.
|
||||||
|
|
||||||
|
### Task 6: Detailseite `/community/<id>` und „Als Vorlage“
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `web/src/pages/community-page.ts` (ersetzt Platzhalter), `web/src/editor/editor-page.ts` (`?vorlage=<id>`)
|
||||||
|
- 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`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Detailseite laut Spec §5 (Texte exakt): Herunterladen (Blob → `aircard-<slug>.png`), „Als Vorlage gestalten“ → `navigate('/gestalten?vorlage=<id>')`, Melden-Dialog (Gründe exakt, Ergebnis-Texte), Autor „Zurückziehen“ (Bestätigung via `confirmDialog`, danach `navigate('/')`), Admin „Ausblenden/Einblenden“, „Löschen“, „<n> Meldungen“ + Gründe (`listReports`). `document.title` = `titleFor(route, card.title)`.
|
||||||
|
- Editor `?vorlage=<id>`: Galerie-Vollbild laden → `setImage`, Vorlage `image`, kein `saved`; ungespeicherte Arbeit → bestehende Rückfrage; Fehler → Hinweis „Diese Vorlage ist nicht (mehr) verfügbar.“
|
||||||
|
|
||||||
|
- [ ] **Step 1: Tests** – nicht gefunden/ausgeblendet für Gast → Text + kein Bild; Autor sieht Zurückziehen, Fremder nicht, Admin sieht Admin-Knöpfe + Meldungszahl; Melden: doppelt → „Danke, du hast diese Karte schon gemeldet.“, 429 → Rate-Text; Vorlage setzt Template `image` und lässt `saved` null.
|
||||||
|
- [ ] **Step 2–5** wie üblich; Commit `feat(community): Detailseite, Melden, Admin-Aktionen, Vorlage`.
|
||||||
|
|
||||||
|
### Task 7: Veröffentlichen aus „Meine Karten“
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `web/src/community/publish-dialog.ts`
|
||||||
|
- Modify: `web/src/pages/cards-page.ts`, `web/src/pages/cards.css`
|
||||||
|
- Test: `web/src/community/publish-dialog.test.ts`, Ergänzung `web/src/pages/cards-page.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Dialog laut Spec §6 (Felder, Grenzen, Pflicht-Checkbox-Text exakt), Absenden über `publishSavedCard`; Kachel-Status aus `myPublications(uid)`: „Veröffentlicht“ + „Ansehen“ (`/community/<id>`) + „Zurückziehen“ bzw. „Ausgeblendet (gemeldet)“; nicht veröffentlicht → „Veröffentlichen“. Nicht angemeldet → bestehendes `withLogin`.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Tests** – Validierung im Dialog (Titel leer, > 60, 6 Tags, Checkbox fehlt); Erfolg → Badge + Ansehen-Link; 409 → Text; Zurückziehen → Status zurück.
|
||||||
|
- [ ] **Step 2–5**; Commit `feat(community): Veröffentlichen aus Meine Karten`.
|
||||||
|
|
||||||
|
### Task 8: Betrieb (Controller) und Abnahme
|
||||||
|
|
||||||
|
- [ ] Kenso: Label `admin`, Key `aircard-api` per `read -s` als `AIRCARD_API_KEY` in `.env` (Controller liefert den Befehl); Provisioning-Key um `indexes.read/write` ergänzen oder Controller legt Indizes nach Kensos OK an.
|
||||||
|
- [ ] Controller: `AIRCARD_API_SALT` (32 Byte hex) in `.env` (Backup), Setup-Skript `--check` → anlegen → `--check` OK; `/home/webklar/apps/aircard-ios.org/api/` mit `deploy/api/*.js` füllen (`cp`); Compose-Backup, Service einfügen, `docker compose config -q`, `docker compose up -d aircard-api`; `curl https://aircard-ios.org/api/gesundheit` → `{"ok":true}`; Push → Webhook-Build.
|
||||||
|
- [ ] Live-Abnahme (Claude in Chrome, Kensos Sitzung): Startseite 1440 × 900 Karten angeschnitten; Hell/Dunkel; Veröffentlichen einer Testkarte; Suche nach Tag; Detailseite, Herunterladen, Als Vorlage; Melden (3 verschiedene `X-Real-Ip` per `docker exec webklar-aircard-api` lokal gegen `127.0.0.1:8088`) → ausgeblendet, Gast sieht „nicht (mehr) verfügbar“; Admin Einblenden; Zurückziehen.
|
||||||
|
- [ ] Final-Review (Workflow, 3 Linsen) → eine Fix-Welle → Push; Memory aktualisieren; Bericht an Kenso.
|
||||||
Reference in new issue
Block a user