docs: AirCard-Website Design-Spec

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-04 06:44:46 +00:00
commit 0cd12c1ed3
1 file changed
+237
@@ -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/<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` | 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.