Files
AirCardIOS/docs/superpowers/specs/2026-10-04-aircard-website-design.md
T

238 lines
19 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-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` schaltet die Route nur für SDK-Aufrufe frei (das Web-SDK sendet den Header bei jedem Aufruf). Er isoliert **nicht**: Appwrite wertet `?project=` vor dem Header aus – wie unter appwrite.webklar.com ohnehin öffentlich. Dateien werden deshalb nie über `getFileView`/`getFilePreview`-URLs (Projekt als Query, ohne Header) geladen, sondern per `fetch` mit Header. Alle anderen `/v1`-Pfade beantwortet nginx mit 404, damit ein Routingfehler nicht als Seite durchgeht.
**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** = Nummer, PLATINUM, Kontaktlos-Symbol und einfarbige Logos in Weiß (#FFFFFF); **Dunkel** = in #111111. Der Chip ist eine helle bzw. dunkle Metallfläche (Verlauf) mit Kontaktlinien in Dunkelgrau (#7A7A7A) bzw. fast Schwarz (#0A0A0A), unabhängig von der Details-Farbe.
### 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 | JCB |
| Discover | Discover |
Dazu bei *Logo*: AirCards · Symbol, AirCards · Text, Apple, Kein Logo (Wortlaut wie im Screenshot).
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/<id>.svg`, Register in `card/marks.ts`.
### Download
`canvas.toBlob('image/png')` mit 1536 × 969, sRGB, ohne Alphakanal, ohne Ecken-Rundung.
Dateiname `aircard-<name oder datum>.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 <noreply@webklar.com>“ 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:<id>")`.
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` | nach dem Vorbild ticket: Repo `knso/AirCardIOS`, Branch `main`, Unterordner `web/`, `npm ci`, `vite build`, `dist/` → `out/`; merkt sich den zuletzt **versuchten** Commit, damit ein kaputter Build nicht alle 15 Minuten wiederholt wird |
| `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, `^~ /v1/` → 404, 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:22-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 → 404 von nginx.
- 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.