commit 0cd12c1ed31d6752ef6a20fc5a7bfbf29d0cef01 Author: Claude Date: Sun Oct 4 06:44:46 2026 +0000 docs: AirCard-Website Design-Spec Co-Authored-By: Claude Opus 5.5 diff --git a/docs/superpowers/specs/2026-10-04-aircard-website-design.md b/docs/superpowers/specs/2026-10-04-aircard-website-design.md new file mode 100644 index 0000000..fc74026 --- /dev/null +++ b/docs/superpowers/specs/2026-10-04-aircard-website-design.md @@ -0,0 +1,237 @@ +# AirCard-Website – Design + +Stand: 2026-10-04 · Status: zur Freigabe durch Kenso + +## 1. Ziel und Rahmen + +Eine Website unter **https://aircard-ios.org**, auf der man eigene Kartenmotive (Apple-Wallet-Card-Art) gestaltet: +Bild hochladen, Vorlage wählen, Kartennetzwerk-Logo, Hell/Dunkel und Logo einstellen, als PNG herunterladen. +Wer will, legt ein Konto an und speichert seine Karten. + +Festgelegt im Brainstorming (vom Nutzer bestätigt): + +| Punkt | Entscheidung | +|---|---| +| Charakter | Spaßprojekt, kein WEBklar- oder Kundenprojekt | +| Repo | `knso/AirCardIOS`, Website im Unterordner `web/` (Wurzel bleibt frei für eine spätere iOS-App) | +| Domain | `aircard-ios.org` (DNS bei Porkbun, A-Record zeigt schon auf 91.99.156.85) | +| Cloudflare | **nicht** verwendet | +| Impressum/Datenschutz | **nicht** gewünscht | +| Ansatz | A: statischer Editor (Vite + TypeScript, Canvas) + neues Appwrite-Projekt „AirCard“ auf unserem Appwrite 1.8.1 | +| Login | Gestalten und Herunterladen **ohne** Login; **Speichern** verlangt Login | +| Vorlage für das Aussehen | Screenshot des (nicht mehr auffindbaren) Claude-Design-Entwurfs | + +## 2. Nutzerablauf + +1. Startseite = Editor. Ohne Bild zeigt die Mitte die Ablagefläche „Ziehe deine Idee hierher“ mit „Bild auswählen“ (PNG/JPG/WebP, max. 5 MiB). +2. Nach dem Hochladen erscheint die Karte. Das Bild füllt die Karte („cover“), lässt sich per Ziehen verschieben und per Regler zoomen. +3. Links Vorlage wählen, rechts Netzwerk-Logo, Details (Hell/Dunkel) und Logo. Die Vorschau aktualisiert sofort. +4. **Herunterladen** erzeugt ein PNG mit 1536 × 969 px, eckig (Wallet rundet selbst). +5. **Speichern**: nicht angemeldet → Dialog Anmelden/Registrieren, danach wird automatisch gespeichert. Der Editor-Zustand bleibt im Speicher erhalten. +6. **Meine Karten** (`/karten`): Kacheln mit Vorschaubild und Name. Aktionen: Öffnen (zurück in den Editor), Herunterladen, Umbenennen, Löschen. +7. **Passwort vergessen**: Mail mit Link auf `https://aircard-ios.org/passwort-neu?userId=…&secret=…`, dort neues Passwort setzen. + +## 3. Architektur + +``` +Browser (aircard-ios.org) + │ statische Dateien │ Appwrite-API (same origin) + ▼ ▼ +Traefik ── Host(aircard-ios.org) ─────────────► nginx (webklar-aircard, out/) + └─ Host(aircard-ios.org) && PathPrefix(/v1) + && Header(X-Appwrite-Project, aircard) ─► appwrite@docker (Projekt "aircard") + └─ Host(aircard-ios.org) && Path(/webhook/deploy) ─► webklar-aircard-build (Port 8087) + +Gitea knso/AirCardIOS ── Push main ── Webhook ──► aircard-build: git pull, npm ci, vite build (web/) → out/ +``` + +**Warum `/v1` über die eigene Domain:** Appwrite setzt das Session-Cookie auf die Domain des Requests (`'.' + Host`). +Läuft `/v1` über `aircard-ios.org`, ist das Cookie first-party (`.aircard-ios.org`, Secure, HttpOnly), +Safari blockiert es nicht, und das SDK braucht keinen localStorage-Fallback. Geprüft im Quellcode von Appwrite 1.8.1 und per Probe-Request mit Host `aircard-ios.org` (200 auf `/v1/health/version`). +Der Header-Matcher `X-Appwrite-Project: aircard` sorgt dafür, dass über diese Domain nur das AirCard-Projekt erreichbar ist (keine Console, kein Portal). Das Web-SDK sendet den Header bei jedem Aufruf. + +**Fallback**, falls die Route im Live-Test scheitert: Endpoint `https://appwrite.webklar.com/v1` wie bei eship (Web-Plattform `aircard-ios.org` eintragen, SDK nutzt dann den Cookie-Fallback). + +### Module im Frontend (`web/src/`) + +| Datei/Ordner | Aufgabe | hängt ab von | +|---|---|---| +| `card/geometry.ts` | Koordinaten aller Elemente je Vorlage (reine Daten/Funktionen) | – | +| `card/crop.ts` | Cover-Fit, Verschieben, Zoom, Begrenzung | – | +| `card/config.ts` | Typ `CardConfig`, Standardwerte, `parseConfig()` (unbekannt/kaputt → Standard), `serializeConfig()` | – | +| `card/marks.ts` | Register aller Logo-Varianten (ID, Label, SVG, Größe, Färbung) | `assets/marks/*.svg` | +| `card/render.ts` | Zeichnet `CardConfig` + Bild auf ein Canvas (Vorschau und Export) | geometry, crop, marks | +| `editor/*.ts` | UI: Vorlagenleiste, Mitte (Ablage, Vorschau, Ziehen/Zoom), rechte Optionen, Download | card/* | +| `account/appwrite.ts` | Client, Account, TablesDB, Storage; Endpoint `location.origin + '/v1'` | `appwrite` (Web-SDK) | +| `account/auth.ts` | Registrieren, Anmelden, Abmelden, Passwort vergessen/neu | appwrite.ts | +| `account/cards.ts` | Karte speichern/laden/umbenennen/löschen, Bilder per `fetch` → Blob-URL | appwrite.ts, config.ts | +| `pages/*.ts` | Routen `/`, `/karten`, `/passwort-neu` (History-API, kein Framework) | editor, account | + +`card/*` kennt weder DOM-Events noch Appwrite und ist deshalb einzeln testbar. + +## 4. Editor + +### Layout (wie Screenshot, dunkles Navy-Thema) + +- **Links „Vorlage“**: vier Kacheln mit Live-Miniatur, Titel und Untertitel: + *Nur Bild* („Bild unverändert lassen“), *Minimalistisch* („Dein Bild steht im Mittelpunkt“), + *Klassisch* („Die vertrauten Kartendetails“), *Signature* („Ein bisschen mehr Persönlichkeit“). Auswahl mit Rahmen + Häkchen. +- **Mitte**: Ablagefläche bzw. Kartenvorschau (abgerundet mit 57 px Radius, nur in der Vorschau), darunter „1536 × 969 PNG“, „Bild ersetzen“, **Herunterladen**, **Speichern**. Zoom-Regler unter der Karte, sobald ein Bild da ist. +- **Rechts „Individuell gestalten“**: Abschnitte *Kartennetzwerk* (3-spaltiges Raster), *Details* (Hell/Dunkel), *Logo*. +- **Kopfzeile**: AirCard-Logo, „Meine Karten“, Anmelden/Konto-Menü (E-Mail, Abmelden). +- **Fußzeile** (Hinweis): „Inoffizielles Hobbyprojekt. Keine Verbindung zu Apple, Visa, Mastercard, UnionPay, American Express, JCB oder Discover. Alle Marken gehören ihren Inhabern.“ +- Hinweis unter der Mitte (statt „bleibt bis zum Einreichen auf deinem Gerät“): „Dein Bild bleibt auf deinem Gerät, bis du speicherst.“ +- **Handy** (< 900 px): Vorschau oben, Vorlagen als waagerechte Reihe, Optionen darunter. +- Schrift: **Nunito Sans** (OFL), selbst gehostet als woff2 – für UI und Kartentexte. Vor dem Zeichnen per `document.fonts.load` geladen, damit das PNG immer gleich aussieht. + +### Kartengeometrie (Canvas 1536 × 969, Rand M = 56 px) + +| Element | Vorlagen | Position | +|---|---|---| +| Hintergrundbild | alle | ganze Fläche, Cover-Fit + Verschiebung/Zoom des Nutzers | +| Netzwerk-Logo | Minimalistisch, Klassisch, Signature | unten rechts, rechte Kante x = 1480, Unterkante y = 913, Box max. 360 × 109 px (reine Symbole wie Mastercard-Kreise bis 125 px hoch) | +| AirCard-/Apple-Logo | Klassisch, Signature | oben links, Box x 56–416, y 56–156 | +| „PLATINUM“ | Klassisch | rechtsbündig x = 1480, Grundlinie y = 123, fett 46 px, Laufweite 0,2 em | +| „PLATINUM“ | Signature | unter dem Logo, x = 60, Grundlinie y = 226, fett 44 px, Laufweite 0,18 em | +| Chip | Klassisch | abgerundetes Rechteck x 157, y 326, 226 × 205, r 32; Kontaktlinien 3 px (waagerecht y 383/429/474, senkrecht x 234/307) | +| Kontaktlos-Symbol | Klassisch | vier Bögen, Box x 480–568, y 374–484 | +| Kartennummer | Klassisch | `•••• •••• •••• 8888`, Start x = 157, Grundlinie y = 650, 72 px, feste Zeichenbreite 45,6 px | + +Unten links (x 0–620, y 779–969) bleibt frei, weil Wallet dort die letzten vier Ziffern einblendet. +*Nur Bild* zeichnet nur das Hintergrundbild. +Details **Hell** = Chip-Linien, Nummer, PLATINUM, Kontaktlos-Symbol und einfarbige Logos in Weiß (#FFFFFF); **Dunkel** = in #111111. Der Chip selbst ist eine helle bzw. dunkle Metallfläche (Verlauf). + +### Logo-Varianten (Kartennetzwerk, 18 Stück) + +| Marke | Varianten (Label wie Entwurf) | +|---|---| +| Visa | Automatisch (in Details-Farbe: Hell = weiß, Dunkel = schwarz), Blau (#1434CB), Marineblau (#1A1F71), Metallisches Silber (Verlauf) | +| Mastercard | Farbe (Symbol), Metallisches Silber (Symbol als Verlauf), Wortmarke (Symbol + „mastercard“ darunter), Silber + Text, Klassisch (altes Logo mit „MasterCard“ in den Kreisen) | +| UnionPay | Farbe, Metallisches Silber, Weißer Rahmen (Farblogo mit weißer Kontur), Englisch (ohne chinesische Zeichen) | +| American Express | Blau (Blue Box), Umriss (weiß/transparent), Select (Blue Box, kleiner) | +| JCB | Farbe | +| Discover | Wortmarke | + +Dazu bei *Logo*: AirCards Symbol, AirCards Text, Apple, Kein Logo. +Das AirCards-Symbol und der AirCards-Schriftzug werden nach dem Screenshot neu gezeichnet (eigenes SVG). + +Quellen: bereinigte SVGs aus `/root/reports/aircard-marks-research/` (Wikimedia Commons bzw. offizielle Dateien, mit svgo bereinigt). +Silber-, Marineblau- und Rahmen-Varianten sind eigene Abwandlungen und entsprechen nicht den Markenrichtlinien. Das ist bewusst so (Spaßprojekt), der Footer-Hinweis deckt es ab. +Ablage: `web/src/assets/marks/.svg`, Register in `card/marks.ts`. + +### Download + +`canvas.toBlob('image/png')` mit 1536 × 969, sRGB, ohne Alphakanal, ohne Ecken-Rundung. +Dateiname `aircard-.png`. Ziel: unter 4 MB (Wallet-Grenze). Liegt das PNG darüber, Hinweis anzeigen (das Bild wird trotzdem geladen). + +## 5. Konten und Speichern (Appwrite) + +### Projekt `aircard` auf appwrite.webklar.com + +- **Auth**: nur E-Mail + Passwort (min. 8 Zeichen). Keine E-Mail-Bestätigung. Passwort-Wörterbuch an, Sitzungsdauer 30 Tage, max. 5 Sitzungen, Nutzerlimit 2000. +- **Mails** (Passwort vergessen): globaler SMTP, also Absender „WEBklar “ mit deutschem Standardtext, der den Projektnamen „AirCard“ nennt. Eigener Absendername wäre nur mit Projekt-SMTP möglich – optional, nicht im Umfang. +- **Web-Plattform**: `aircard-ios.org` (für Passwort-Link-Prüfung und den Fallback), `localhost` für Entwicklung. +- Nicht benötigte Dienste (Functions, Sites, Messaging, Teams, GraphQL) werden abgeschaltet. + +### Datenmodell (TablesDB) + +Datenbank `aircard`, Tabelle `cards`: + +| Spalte | Typ | Pflicht | Inhalt | +|---|---|---|---| +| `name` | string(80) | ja | Name der Karte (Standard: „Meine Karte“) | +| `config` | string(2000) | ja | `CardConfig` als JSON: Vorlage, Netzwerk-Variante, Details, Logo, Bildausschnitt (x, y, zoom), Version | +| `imageFileId` | string(36) | ja | Originalbild im Bucket | +| `previewFileId` | string(36) | ja | Vorschaubild 480 × 303 WebP im Bucket | + +Sortierung „zuletzt geändert“ über das eingebaute `$updatedAt`. +Tabelle: Rechte `create("users")`, **rowSecurity an**, keine Tabellen-Leserechte. Jede Zeile bekommt `read/update/delete("user:")`. + +Bucket `card-images`: Rechte `create("users")`, **fileSecurity an**, max. 5 MiB (5 242 880 Byte), Endungen png/jpg/jpeg/webp, keine Virenprüfung. Dateien bekommen dieselben Nutzerrechte. + +So sieht jeder Nutzer nur seine eigenen Karten und Bilder – erzwungen durch Appwrite, nicht nur durch die Oberfläche. + +### Abläufe + +- **Speichern (neu)**: Original hochladen → Vorschau (aus dem Canvas) hochladen → Zeile anlegen. Scheitert ein Schritt, werden schon hochgeladene Dateien wieder gelöscht. +- **Speichern (bestehend)**: Zeile aktualisieren; Original nur neu hochladen, wenn das Bild ersetzt wurde (alte Datei danach löschen); Vorschau immer neu. +- **Laden**: Zeile lesen → Original per `fetch` (mit Projekt-Header und Cookie) → Blob-URL → Canvas. Blob-URLs sind same-origin, das Canvas bleibt exportierbar. +- **Löschen**: Zeile löschen, dann beide Dateien. +- **Schema anlegen**: Skript `web/scripts/appwrite-setup.mjs` (reines `fetch`, Projekt-API-Key aus `.env`, idempotent, `--check`-Modus, wartet auf `available` vor Indizes) – nach dem Vorbild `/home/webklar/apps/videogen/scripts/setup-appwrite.mjs`, das auf diesem Server mit TablesDB läuft. +- SDK: `appwrite` Web-SDK **exakt 22.4.1** gepinnt (laut README „compatible with Appwrite server version 1.8.x“; ab 23.0.0 „latest“). + +## 6. Deployment + +Vorbild ist **ticket.webklar.com** (Commit-Marker, Branch-Filter, sofortige 202-Antwort), **nicht** eship (das baute alle 15 Minuten neu und ist deshalb abgeschaltet). + +Dateien unter `/home/webklar/apps/aircard-ios.org/`: + +| Pfad | Inhalt | +|---|---| +| `build/` | Klon des Repos (Arbeitsordner des Build-Containers) | +| `out/` | gebaute Seite, von nginx ausgeliefert | +| `deploy.sh` | aus ticket abgeleitet: Repo `knso/AirCardIOS`, Branch `main`, Unterordner `web/`, `npm ci`, `vite build`, `dist/` → `out/` | +| `webhook-server.js` | Kopie von ticket mit eigenen Variablen, Port 8087, **schlägt fehl, wenn das Secret fehlt** (statt durchzulassen) | +| `nginx/aircard-ios.org.conf` | SPA-Fallback auf `index.html`, `^~ /assets/` 1 Jahr Cache, `index.html` ohne Cache, Sicherheits-Header, gzip | +| `dev/` | Arbeitskopie für die Entwicklung (nicht gemountet) | + +Compose (`/home/webklar/apps/docker-compose.yml`, vorher Backup `docker-compose.yml.bak-2026-10-04-aircard`): + +- `aircard-build`: `node:20-alpine`, `init: true`, `mem_limit: 768m`, `NODE_OPTIONS=--max-old-space-size=512`, Build-Variablen `VITE_APPWRITE_ENDPOINT=https://aircard-ios.org/v1` und `VITE_APPWRITE_PROJECT_ID=aircard`, Webhook-Router `Host(aircard-ios.org) && Path(/webhook/deploy)` mit Priorität 200. +- `aircard`: `nginx:alpine`, `mem_limit: 64m`, Router `Host(aircard-ios.org)` (websecure, Let's Encrypt) + HTTP→HTTPS-Weiterleitung. +- Appwrite-Route als Datei `/home/webklar/apps/traefik/dynamic/aircard.yml` (Hot-Reload, kein Eingriff am Appwrite-Container): `Host(aircard-ios.org) && PathPrefix(/v1) && Header(X-Appwrite-Project, aircard)` → `appwrite@docker`, Priorität 150. +- Gestartet wird **nur** `docker compose up -d aircard-build aircard` (ein nacktes `up -d` würde eship-build und lernen-build wieder anlegen). + +Gitea-Webhook an `knso/AirCardIOS`: Push, Branch-Filter `main`, URL `https://aircard-ios.org/webhook/deploy`, Secret = `GITEA_DEPLOY_WEBHOOK_SECRET` aus `.env`. + +**www**: `www.aircard-ios.org` hat noch keinen DNS-Eintrag. Ohne Eintrag gibt es nur die Hauptdomain. Legt Kenso bei Porkbun einen A-Record (91.99.156.85) an, kommen eigene Router dazu, die per 301 auf `https://aircard-ios.org` weiterleiten (eigene Router, damit ein www-Fehler das Hauptzertifikat nicht blockiert). + +Last auf dem Server: nginx ca. 7 MB, Build-Container im Leerlauf 20–50 MB, beim Build kurz einige 100 MB. + +## 7. Fehlerbehandlung + +| Fall | Verhalten | +|---|---| +| Falscher Dateityp / > 5 MiB / Bild nicht lesbar | Meldung an der Ablagefläche, bisheriger Zustand bleibt | +| Speichern ohne Login | Login-Dialog; nach Erfolg wird gespeichert | +| Sitzung abgelaufen (401) beim Speichern/Laden | Login-Dialog, danach Aktion wiederholen | +| Netzwerkfehler / 5xx beim Speichern | Meldung „Speichern fehlgeschlagen – nochmal versuchen“, Editor-Zustand bleibt, halbe Uploads werden aufgeräumt | +| Zu viele Versuche (429) | Meldung „Zu viele Versuche, bitte kurz warten“ | +| Registrierung mit vorhandener E-Mail (409) | Meldung mit Link „Anmelden“ | +| Passwort-Link ungültig/abgelaufen (1 h) | Meldung + Knopf „Neuen Link anfordern“ | +| `config` einer Karte unlesbar / ältere Version | `parseConfig()` füllt Standardwerte, Karte öffnet trotzdem | +| Bild einer gespeicherten Karte fehlt | Karte öffnet mit leerer Ablagefläche und Hinweis | +| Download | funktioniert immer lokal, auch ohne Netz/Appwrite | + +## 8. Tests + +- **Unit-Tests (Vitest)** für die reinen Module: `geometry` (alle Boxen im Canvas, Netzwerk-Logo in der Box rechts unten, nichts im Wallet-Freibereich), `crop` (Cover-Fit für Hoch-/Querformat, Zoom-Grenzen, Verschieben bleibt in der Fläche), `config` (Rundlauf serialize/parse, kaputtes JSON → Standard, unbekannte Variante → Standard), Dateiprüfung (Typ/Größe), `marks` (alle 18 Netzwerk- und 4 Logo-Varianten registriert, jede SVG-Datei vorhanden und ladbar). +- **Setup-Skript**: `--check` gegen das echte Projekt muss „alles vorhanden“ melden. +- **Abnahme nach Deploy** (im Browser über Claude in Chrome): + - `curl -H 'X-Appwrite-Project: aircard' https://aircard-ios.org/v1/health/version` → `1.8.1`; ohne Header → Seite, nicht Appwrite. + - HTTP → 301 HTTPS, Deep-Link `/karten` → 200, unsignierter POST auf `/webhook/deploy` → 401. + - Bild hochladen, jede Vorlage, Stichprobe Netzwerk-Varianten, Hell/Dunkel, Download → PNG 1536 × 969. + - Registrieren, Speichern, Neuladen, Karte öffnen, Umbenennen, Löschen. Cookie `a_session_aircard` mit Domain `.aircard-ios.org`, kein `cookieFallback` im localStorage. + - Zweites Testkonto sieht die Karten des ersten nicht (Abruf per ID → 404/401). + - Passwort vergessen mit eigener Adresse → Mail kommt an, Link setzt neues Passwort. + - Handy-Breite (390 px) ohne waagerechtes Scrollen. + +## 9. Was Kenso selbst erledigt + +1. **Appwrite-Console** (ca. 3 Minuten): Projekt „AirCard“ mit Projekt-ID `aircard` anlegen; Web-Plattform `aircard-ios.org` und `localhost`; API-Key `aircard-provision` mit den Scopes `databases`, `tables`, `columns`, `indexes`, `buckets` (jeweils read/write); Key als `AIRCARD_APPWRITE_API_KEY` in `/home/webklar/apps/.env` eintragen. Auth-Einstellungen aus Abschnitt 5 setzen (ich liefere eine Klick-Liste). +2. **Push nach `main`** freigeben (mein Push auf main ist im Auto-Modus gesperrt). +3. Optional: **www-A-Record** bei Porkbun. + +## 10. Nicht im Umfang + +Impressum/Datenschutz, Cloudflare, E-Mail-Bestätigung, eigene Absenderadresse @aircard-ios.org, Teilen/Galerie öffentlicher Karten, mehrere Sprachen (nur Deutsch), Konto löschen im UI, iOS-App, Wallet-Import direkt aus der Website, Admin-Bereich. + +## 11. Risiken und Hinweise + +- **Marken**: Netzwerk-Logos und Apple-Logo sind Marken; die Silber-/Marine-/Rahmen-Varianten verändern sie. Bewusst so gewählt, Footer-Hinweis vorhanden. Kann bei öffentlicher Nutzung Abmahnungen/Takedowns auslösen. +- **Datenschutz**: Mit Konten gilt die DSGVO auch für Spaßprojekte; auf Wunsch von Kenso ohne Datenschutzseite. +- **Same-Origin-Route** ist aus Quellcode und Probe-Requests abgeleitet; der echte Cookie-Test steht bei der Abnahme an (Fallback siehe Abschnitt 3). +- **Backups**: Appwrite-Datenbank wird zweimal täglich gesichert (3 Stände). Hochgeladene Bilder (`apps_appwrite_uploads`) sichert derzeit kein Job – separat gemeldet, betrifft alle Appwrite-Projekte. +- **Anmelde-Limits** von Appwrite (z. B. 10 Konten/Stunde pro IP) gelten pro echter Client-IP, weil Traefik direkt an Appwrite weiterreicht. +- **Klassisch** mit Chip und Nummer ist nur Optik; echte Herausgeber lehnen solche Motive ab. Es wird nie eine echte Nummer verwendet (immer `8888`). +- Name: Es gibt bereits Tools namens „AirCard“ (aircards.org u. a.); Verwechslungsgefahr ist Kenso bekannt bzw. hier vermerkt.