# 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.