Files
AirCardIOS/docs/superpowers/specs/2026-10-04-aircard-community-design.md
T
2026-10-04 13:05:59 +00:00

132 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# AirCard-IOS – Startseite & Community-Galerie – Design
Stand: 2026-10-04 · Status: zur Freigabe durch Kenso · baut auf `2026-10-04-aircard-website-design.md` auf (live, main 562fac0)
## 1. Ziel und Rahmen
Die Website bekommt eine Startseite im Stil von Kensos Vorbild-Screenshot (bestehende „AirCards“-Seite), aber im eigenen AirCard-IOS-Look:
kompakter Hero, darunter sofort die **Community-Galerie**, Spenden-Banner und -Knopf, Hell/Dunkel-Schalter.
Nutzer können ihre gespeicherten Karten **sofort öffentlich veröffentlichen**; Besucher können melden; ab 3 Meldungen wird eine Karte automatisch ausgeblendet; Kenso moderiert als Admin.
| Punkt | Entscheidung (Kenso) |
|---|---|
| Umfang | Startseite + Community-Galerie; keine „Offiziellen Karten“ (echte Bank-Kartenmotive), keine Texte/Bilder vom Vorbild |
| Galerie-Inhalt | nur Community-Karten |
| Freigabe | sofort öffentlich + „Melden“; Auto-Ausblenden ab 3 verschiedenen Meldern |
| Extras | nur Hell/Dunkel-Schalter (kein Englisch, keine Anleitung-Seite, keine Highlights) |
| Hero | **nicht** Vollbild: erste Reihe Community-Karten muss im ersten Bildschirm anschneiden |
| Spenden | Buy Me a Coffee; öffentlicher Link kommt noch (Konfig-Wert) |
| Technik | Ansatz A: Appwrite + kleiner eigener API-Dienst `aircard-api` |
## 2. Seiten und Routen
| Route | Inhalt |
|---|---|
| `/` | Startseite: Hero + Community-Galerie (Suche über `?q=`, Anker `#community`) |
| `/gestalten` | der bisherige Editor (`?karte=<id>` öffnet eine gespeicherte Karte, `?vorlage=<galleryId>` übernimmt ein Community-Bild als „Nur Bild“) |
| `/karten` | Meine Karten (+ Veröffentlichen / Veröffentlicht-Status / Zurückziehen) |
| `/community/<id>` | Detailansicht einer Community-Karte |
| `/passwort-neu` | unverändert |
Alte Links `/?karte=<id>` leiten clientseitig auf `/gestalten?karte=<id>` um. `document.title` je Route („AirCard-IOS – Kartendesigns für Apple Wallet“, „Gestalten – AirCard-IOS“, „Meine Karten – AirCard-IOS“, „<Titel> – AirCard-IOS“).
## 3. Kopfzeile, Banner, Theme
- **Spenden-Banner** (ganz oben, schließbar, Zustand in `localStorage` `aircard.donateBannerClosed`): „☕ Gefällt dir AirCard-IOS? Spendier uns einen Kaffee – damit das Projekt weiterläuft.“ + Link „Unterstützen →“. Ziel = `DONATE_URL` aus `web/src/config.ts`. **Ist `DONATE_URL` leer, werden Banner und Spenden-Knopf nicht angezeigt** (bis Kenso den öffentlichen Link `buymeacoffee.com/<name>` liefert).
- **Kopfzeile**: Logo + „AirCard-IOS“ (→ `/`), Navigation „Community“ (→ `/#community`), „Gestalten“, „Meine Karten“; Suchfeld „Titel, Tags oder Herausgeber“ (Enter → `/?q=…#community`); Knopf „☕ Unterstützen“ (wenn `DONATE_URL`); Hell/Dunkel-Schalter (Sonne/Mond-Icon); Konto: abgemeldet „Anmelden“, angemeldet Avatar-Kreis mit Initiale (Menü: E-Mail, „Meine Karten“, „Abmelden“).
- Unter 900 px: Navigation in ein Menü-Icon, Suchfeld als eigene Zeile unter der Kopfzeile.
- **Theme**: `data-theme="light|dark"` auf `<html>`; Standard = System (`prefers-color-scheme`); Schalter speichert in `localStorage` `aircard.theme`. Helle Tokens: `--bg #F4F6FB; --panel #FFFFFF; --panel-2 #EEF1F7; --line #D9DFEA; --line-strong #B8C2D3; --sel-bg #E3ECFB; --sel-line #3A6FD8; --text #0F1828; --muted #5B6B82; --primary #2A4566; --primary-hover #34557C`. Die Kartenvorschau selbst bleibt unverändert (Kartenfarben hängen nicht vom Theme ab).
## 4. Startseite
- **Hero (kompakt)**: links Zeile „Ein bisschen Kunst für dein Wallet“, Überschrift „Kartendesigns für Apple Wallet“, ein Satz „Entdecke Motive der Community, gestalte dein eigenes und lade es als PNG herunter.“, Knöpfe **„Karten entdecken ↓“** (scrollt zu `#community`) und **„Gestalten“**. Rechts eine große Karte (neueste sichtbare Community-Karte; ist die Galerie leer: eigenes Beispielmotiv `web/public/hero-beispiel.png`, markenfrei, von Claude gestaltet) mit dezenter Schwebe-Animation (`prefers-reduced-motion` respektieren).
- **Höhe**: Hero höchstens 60 vh (Desktop) bzw. so, dass bei 1440 × 900 und 390 × 844 die Überschrift „Community-Karten“ **und** der obere Rand der ersten Kartenreihe im ersten Bildschirm sichtbar sind (Abnahmekriterium).
- **Community-Galerie** (`#community`): Überschrift „Community-Karten“, Untertitel „Entdecke Motive der Community – und veröffentliche deine eigenen.“, Knopf „Gestalten & veröffentlichen“. Suchzeile mit aktivem Suchbegriff + „ד. Raster aus Kacheln (Kartenformat 1536:969, abgerundet): Vorschau, Titel, Herausgeber, bis 3 Tag-Chips (Klick = Suche). Sortierung neueste zuerst, 24 pro Seite, „Mehr laden“ (Cursor). Leer: „Noch keine Community-Karten – sei die erste!“ + „Gestalten“. Keine Treffer: „Nichts gefunden für „…““.
## 5. Detailseite `/community/<id>`
Große Karte, Titel, Herausgeber, Tags, Datum (`dd.mm.yyyy`), Anzeigename („von <Name>“ bzw. „von Anonym“). Knöpfe: **Herunterladen** (PNG 1536 × 969, Dateiname `aircard-<titel-slug>.png`), **Als Vorlage gestalten** (→ `/gestalten?vorlage=<id>`), **Melden** (Dialog mit Grund: „Unangemessen“, „Echte Kartendaten“, „Urheberrecht/Marke“, „Spam“, „Sonstiges“).
Autor (angemeldet, `ownerId` = ich): zusätzlich **Zurückziehen**. Admin (Label `admin`): **Ausblenden/Einblenden**, **Löschen**, Anzeige „<n> Meldungen“ und Liste der Gründe.
Nicht vorhanden/ausgeblendet für mich: „Diese Karte ist nicht (mehr) verfügbar.“ + Link zur Galerie.
## 6. Veröffentlichen (aus „Meine Karten“)
Kachel-Aktion **„Veröffentlichen“** → Dialog: Titel (Pflicht, ≤ 60), Herausgeber/Bank (optional, ≤ 40), Tags (optional, bis 5, je ≤ 20, Komma-getrennt, kleingeschrieben gespeichert), Anzeigename (optional, ≤ 30, Standard leer → „Anonym“; **nie** die E-Mail), Pflicht-Checkbox „Das Bild gehört mir bzw. ich darf es verwenden, und es enthält keine echten Kartendaten.“
Ablauf: Karte rendern (PNG 1536 × 969 + WebP-Vorschau 480) → beide Dateien in Bucket `gallery-images` hochladen (Rechte `read("any")`, `delete("user:<ich>")`) → `POST /api/veroeffentlichen` → Zeile entsteht. Scheitert die API, löscht der Client die zwei Dateien wieder.
Danach zeigt die Kachel „Veröffentlicht“ + „Ansehen“ + „Zurückziehen“; ist die Karte ausgeblendet: „Ausgeblendet (gemeldet)“. Eine private Karte kann **einmal** veröffentlicht sein; „Erneut veröffentlichen“ = Zurückziehen + Veröffentlichen.
## 7. Daten (Appwrite, Projekt `6ac2130a00289343d6d5`, Setup-Skript erweitert)
**Tabelle `gallery`** (rowSecurity an, **keine** Tabellenrechte für Clients):
| Spalte | Typ | Inhalt |
|---|---|---|
| `title` | string 60, Pflicht | Titel |
| `issuer` | string 40 | Herausgeber/Bank |
| `tags` | string 120 | Tags, Komma-getrennt, klein |
| `displayName` | string 30 | Anzeigename oder leer |
| `imageFileId` | string 36, Pflicht | PNG im Bucket `gallery-images` |
| `previewFileId` | string 36, Pflicht | WebP-Vorschau |
| `sourceCardId` | string 36 | private Karte, aus der veröffentlicht wurde |
| `ownerId` | string 36, Pflicht | Nutzer-ID des Autors |
| `hidden` | boolean, Standard false | ausgeblendet |
| `reportCount` | integer, Standard 0 | Anzahl verschiedener Melder |
| `searchText` | string 300 | `title + issuer + tags`, klein; **Volltext-Index** |
Indizes: Volltext auf `searchText`; Key-Index auf `ownerId`; Key-Index auf `hidden`.
Zeilenrechte (setzt nur die API): sichtbar `["read(\"any\")"]`; ausgeblendet `["read(\"user:<owner>\")","read(\"label:admin\")"]`.
**Bucket `gallery-images`**: Bucket-Recht `create("users")`, fileSecurity an, max. 5 MiB, Endungen png/webp. Dateirechte beim Hochladen `read("any")`, `delete("user:<owner>")`; beim Ausblenden setzt die API `read("user:<owner>")`, `read("label:admin")`, `delete("user:<owner>")`.
**Tabelle `reports`** (rowSecurity aus, Tabellenrecht `read("label:admin")`, Schreiben nur API): `galleryId` string 36, `reason` enum (unangemessen, kartendaten, urheberrecht, spam, sonstiges), `reporterHash` string 64 (HMAC-SHA256 der IP mit Server-Salz), Index auf `galleryId`.
**Lesen im Client**: Galerie `listRows(gallery, [equal('hidden', false), orderDesc('$createdAt'), limit(24), cursorAfter])`, Suche zusätzlich `search('searchText', q)`; eigene Veröffentlichungen `equal('ownerId', me)`; Admin-Filter „Gemeldet/Ausgeblendet“ `greaterThan('reportCount', 0)` bzw. `equal('hidden', true)`.
## 8. API-Dienst `aircard-api`
Node 22, ohne Abhängigkeiten, Container `webklar-aircard-api` (`node:22-alpine`, `init: true`, `mem_limit 128m`), Traefik-Router ``Host(`aircard-ios.org`) && PathPrefix(`/api`)`` Priorität 200, Port 8088. Spricht Appwrite intern (`http://webklar-appwrite/v1`, Header `X-Appwrite-Project`, `X-Appwrite-Key` = `AIRCARD_API_KEY`). Secrets aus `.env`: `AIRCARD_API_KEY` (neuer Key `aircard-api`, Scopes `rows.read/write`, `files.read/write`), `AIRCARD_API_SALT` (zufällig, 32 Byte hex). Antworten JSON, Fehlertexte deutsch.
| Endpunkt | Auth | Wirkung |
|---|---|---|
| `POST /api/veroeffentlichen` `{title, issuer?, tags?, displayName?, imageFileId, previewFileId, sourceCardId}` | Nutzer-JWT | prüft Nutzer, Feldlängen, dass beide Dateien existieren, im Bucket `gallery-images` liegen und `delete("user:<ich>")` tragen, und dass für `sourceCardId` noch keine Veröffentlichung existiert → legt Zeile an (sichtbar) → `201 {id}` |
| `POST /api/zurueckziehen` `{id}` | Nutzer-JWT (Autor **oder** Admin) | löscht Zeile, beide Dateien und ihre Meldungen |
| `POST /api/melden` `{id, reason}` | keine | Rate-Limit 10/Stunde je IP (Speicher), ein Bericht je Melder-Hash und Karte (sonst `200 {schonGemeldet:true}`), speichert Meldung, setzt `reportCount` = Anzahl verschiedener Melder; ab **3** → Ausblenden (Zeilen- und Dateirechte, `hidden=true`) |
| `POST /api/admin/sichtbarkeit` `{id, sichtbar}` | Admin-JWT | Einblenden (öffentliche Rechte, `hidden=false`, `reportCount=0`, Meldungen löschen) oder Ausblenden |
| `GET /api/gesundheit` | keine | `{ok:true}` |
JWT-Prüfung: `GET /v1/account` mit `X-Appwrite-JWT` → Nutzer (`$id`, `labels`); Admin = `labels` enthält `admin`. Echte Client-IP aus `X-Real-Ip` (Traefik). Körper max. 8 KB. Alles andere 404.
**Wichtig**: JWT muss in den Auth-Methoden des Projekts **an** bleiben (die frühere Klickliste sagte „aus“ – wird korrigiert).
## 9. Admin (Kenso)
Kenso bekommt in der Appwrite-Console das Label `admin` (Auth → Users → Konto → Labels). Die Website erkennt es über `account.get().labels` und zeigt Admin-Knöpfe und den Galerie-Filter „Gemeldet“/„Ausgeblendet“.
## 10. Was Kenso selbst erledigt
1. Label `admin` am eigenen Konto setzen.
2. Neuen API-Key `aircard-api` (Scopes `rows.read`, `rows.write`, `files.read`, `files.write`) anlegen und mit dem verdeckten `read -s`-Befehl in `.env` eintragen (nicht im Chat).
3. Danach den alten, im Chat geposteten Key löschen (Setup-Skript nutzt ab dann `aircard-api` nicht – es bekommt bei Bedarf einen neuen Provisioning-Key).
4. Öffentlichen Buy-Me-a-Coffee-Link schicken.
## 11. Fehlerbehandlung
Veröffentlichen scheitert (Netz/API/Validierung) → hochgeladene Dateien löschen, Meldung im Dialog („Veröffentlichen fehlgeschlagen – …“). Doppelt veröffentlicht → „Diese Karte ist schon veröffentlicht.“ Melden doppelt → „Danke, du hast diese Karte schon gemeldet.“ Rate-Limit → „Zu viele Meldungen – bitte später nochmal.“ Nicht angemeldet bei Veröffentlichen → Login-Dialog (bestehendes `withLogin`). API nicht erreichbar → „Der Dienst ist gerade nicht erreichbar.“ Galerie-Ladefehler → Hinweis + „Nochmal versuchen“.
## 12. Tests
- Vitest: Router (neue Routen, Umleitung `/?karte=`), Theme (System/Speichern), Banner (ausblenden, leeres `DONATE_URL`), Galerie-Abfragen (Queries für Liste/Suche/Admin-Filter), Veröffentlichen-Ablauf mit Fake-API (Aufräumen bei Fehler), Such-Text-Bildung, Tags-Normalisierung.
- node:test für `aircard-api`: JWT-Prüfung (Fake-Appwrite), Validierung, Datei-Eigentum, doppelte Veröffentlichung, Melden (Dedup, Rate-Limit, Auto-Ausblenden bei 3, Rechte-Strings), Admin ein/aus, Zurückziehen (Autor/Admin/fremd → 403).
- Setup-Skript: `--check` deckt `gallery`, `reports`, `gallery-images` inkl. Indizes ab.
- Live: Veröffentlichen, Suche, Detailseite, Herunterladen, Als Vorlage, Melden aus 3 verschiedenen Netzen/Fake-IPs (über die API mit `X-Real-Ip`-Test im Container), Admin aus-/einblenden, Startseite bei 1440 × 900 und 390 × 844 (erste Kartenreihe angeschnitten), Hell/Dunkel.
## 13. Nicht im Umfang
Offizielle/Bank-Karten, Likes/„Beliebt“-Sortierung, Kommentare, Englisch, Anleitung-Seite, Highlights, öffentliche Profilseiten, Bearbeiten veröffentlichter Karten (nur zurückziehen + neu), E-Mail-Benachrichtigung an Kenso bei Meldungen.
## 14. Risiken
- **Öffentliche Inhalte ohne Vorab-Freigabe**: Kenso haftet ab Kenntnis; Melden + Auto-Ausblenden + Admin-Löschen mildern. Ohne Kontaktadresse/Impressum fehlt ein Weg für Beschwerden von außen (Kenso-Entscheidung, hier vermerkt).
- **Marken/Urheberrecht**: Nutzer können Bank-Kartenmotive hochladen; Pflicht-Checkbox + Melden-Grund „Urheberrecht/Marke“.
- **Speicher**: keine Kontingente je Nutzer; Bucket 5 MiB je Datei. Missbrauch → Admin löscht; Auth-Limits in der Console (Kensos Schritt 3) empfohlen.
- **Rate-Limit im Speicher** geht bei Neustart verloren (Dedup über `reports` bleibt).