19 KiB
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
- Startseite = Editor. Ohne Bild zeigt die Mitte die Ablagefläche „Ziehe deine Idee hierher“ mit „Bild auswählen“ (PNG/JPG/WebP, max. 5 MiB).
- Nach dem Hochladen erscheint die Karte. Das Bild füllt die Karte („cover“), lässt sich per Ziehen verschieben und per Regler zoomen.
- Links Vorlage wählen, rechts Netzwerk-Logo, Details (Hell/Dunkel) und Logo. Die Vorschau aktualisiert sofort.
- Herunterladen erzeugt ein PNG mit 1536 × 969 px, eckig (Wallet rundet selbst).
- Speichern: nicht angemeldet → Dialog Anmelden/Registrieren, danach wird automatisch gespeichert. Der Editor-Zustand bleibt im Speicher erhalten.
- Meine Karten (
/karten): Kacheln mit Vorschaubild und Name. Aktionen: Öffnen (zurück in den Editor), Herunterladen, Umbenennen, Löschen. - 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.loadgeladen, 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),localhostfü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(reinesfetch, Projekt-API-Key aus.env, idempotent,--check-Modus, wartet aufavailablevor Indizes) – nach dem Vorbild/home/webklar/apps/videogen/scripts/setup-appwrite.mjs, das auf diesem Server mit TablesDB läuft. - SDK:
appwriteWeb-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-VariablenVITE_APPWRITE_ENDPOINT=https://aircard-ios.org/v1undVITE_APPWRITE_PROJECT_ID=aircard, Webhook-RouterHost(aircard-ios.org) && Path(/webhook/deploy)mit Priorität 200.aircard:nginx:alpine,mem_limit: 64m, RouterHost(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 nacktesup -dwü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:
--checkgegen 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_aircardmit Domain.aircard-ios.org, keincookieFallbackim 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
- Appwrite-Console (ca. 3 Minuten): Projekt „AirCard“ mit Projekt-ID
aircardanlegen; Web-Plattformaircard-ios.orgundlocalhost; API-Keyaircard-provisionmit den Scopesdatabases,tables,columns,indexes,buckets(jeweils read/write); Key alsAIRCARD_APPWRITE_API_KEYin/home/webklar/apps/.enveintragen. Auth-Einstellungen aus Abschnitt 5 setzen (ich liefere eine Klick-Liste). - Push nach
mainfreigeben (mein Push auf main ist im Auto-Modus gesperrt). - 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.