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

19 KiB
Raw Blame History

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.