Die Zeilen-ID eines gelöschten Logos steht öffentlich in der Liste und in gespeicherten Karten. War sie die Datei-ID, konnte jeder eine Datei
mit genau dieser ID anlegen, als Logo hochladen und so die alte ID neu belegen: gespeicherte Karten anderer zeigten dann sein Bild.
- logos.js: Zeilen-ID = hash32("<ownerId>|<fileId>") (logoRowId); die Antwort 201 {id} trägt sie. Dieselbe Datei desselben Kontos ergibt dieselbe ID
(409 logoTaken, bei einem Altlogo über die fileId der Zeile); alle anderen Regeln bleiben. Die Logzeilen nennen die Zeilen-ID, die Fehlerzeilen
zusätzlich die Datei-ID (sie folgt nicht mehr aus der Zeilen-ID, und die Zeile ist beim Aufräumen vielleicht schon weg).
- Altlogos (Zeilen-ID = Datei-ID, eines ist live) gehen weiter: Herz, Melden, Löschen und Sichtbarkeit adressieren nur die Zeile und lesen die Datei aus fileId.
- Tests: seedLogo legt Zeile und Datei mit verschiedenen IDs an; neu: logoRowId, die ID eines gelöschten Logos lässt sich nicht neu belegen, anderes Konto
bekommt eine verwendete Datei nicht (403), Altlogo (Herz, Meldung, Ausblenden/Einblenden, Löschen, erneutes Hochladen 409).
- README und Spec: ID-Regel. Der Client (upload-logo.ts) folgt im nächsten Commit; beide gehen zusammen live.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Deployment aircard-ios.org
Die Seite wird auf dem Webklar-Server aus dem Gitea-Repo knso/AirCardIOS gebaut und von nginx ausgeliefert. Traefik stellt TLS und Weiterleitungen. Dieser Ordner enthält alles, was dafür im Repo liegt; die Dateien auf dem Server sind Kopien davon.
Bestandteile
| Datei im Repo | Ziel auf dem Server (/home/webklar/apps/) |
Aufgabe |
|---|---|---|
deploy/deploy.sh |
aircard-ios.org/deploy.sh (ausführbar) |
klont/aktualisiert das Repo, baut web/ und kopiert dist/ nach out/ |
deploy/webhook-server.js |
aircard-ios.org/webhook-server.js |
nimmt den Gitea-Webhook an, startet deploy.sh nacheinander, pollt alle 15 Minuten |
deploy/nginx/aircard-ios.org.conf |
aircard-ios.org/nginx/aircard-ios.org.conf |
nginx-Konfiguration der Seite |
deploy/traefik/aircard.yml |
traefik/dynamic/aircard.yml |
Route /v1 mit Header X-Appwrite-Project: 6ac2130a00289343d6d5 an Appwrite |
deploy/api/appwrite.js, common.js, app.js, logos.js, server.js |
aircard-ios.org/api/ (nur diese fünf Dateien) |
API-Dienst aircard-api der Community-Galerie und der Community-Logos, siehe api/README.md |
deploy/compose-snippet.yml |
docker-compose.yml (nach dem eship-Block) |
Dienste aircard-build, aircard und aircard-api |
Weitere Ordner unter aircard-ios.org/ (nicht im Repo): build/ (Klon des Repos, Arbeitsordner des Build-Containers), out/ (gebaute Seite, von nginx ausgeliefert), dev/ (Arbeitskopie für die Entwicklung, nicht gemountet).
Einrichten (einmalig)
APP=/home/webklar/apps
SRC=$APP/aircard-ios.org/dev/deploy
mkdir -p $APP/aircard-ios.org/build $APP/aircard-ios.org/out $APP/aircard-ios.org/nginx $APP/aircard-ios.org/api
cp $SRC/deploy.sh $APP/aircard-ios.org/deploy.sh && chmod +x $APP/aircard-ios.org/deploy.sh
cp $SRC/webhook-server.js $APP/aircard-ios.org/webhook-server.js
cp $SRC/nginx/aircard-ios.org.conf $APP/aircard-ios.org/nginx/aircard-ios.org.conf
cp $SRC/api/appwrite.js $SRC/api/common.js $SRC/api/app.js $SRC/api/logos.js $SRC/api/server.js $APP/aircard-ios.org/api/
# Compose: Backup, dann den Inhalt von compose-snippet.yml unter `services:` direkt nach dem eship-Block einfügen
cp $APP/docker-compose.yml $APP/docker-compose.yml.bak-2026-10-04-aircard
cd $APP && docker compose config -q; echo $? # 0; einzige erlaubte Ausgabe: Warnung "attribute `version` is obsolete"
# Traefik-Route atomar ablegen (Datei-Provider lädt sie ohne Neustart)
cp $SRC/traefik/aircard.yml $APP/traefik/dynamic/aircard.yml.tmp && mv $APP/traefik/dynamic/aircard.yml.tmp $APP/traefik/dynamic/aircard.yml
Die .env neben der docker-compose.yml muss GITEA_DEPLOY_TOKEN (Lesezugriff auf knso/AirCardIOS) und GITEA_DEPLOY_WEBHOOK_SECRET enthalten, für den API-Dienst außerdem AIRCARD_APPWRITE_PROJECT_ID, AIRCARD_API_KEY und AIRCARD_API_SALT. Diese Werte gehören nie in Logs, Chats oder Commits. Das Salz entsteht, ohne dass der Wert auf dem Bildschirm erscheint:
printf 'AIRCARD_API_SALT=%s\n' "$(openssl rand -hex 32)" >> /home/webklar/apps/.env
Den Key AIRCARD_API_KEY legt Kenso nach docs/appwrite-console.md (Abschnitt 7.3) an (Scopes documents.read, documents.write, files.read, files.write, so beschriftet die Console die Zeilen-Scopes) und trägt ihn mit dem verdeckten read -rs-Befehl ein (leere Eingabe schreibt nichts).
Starten
cd /home/webklar/apps && docker compose up -d aircard-build aircard aircard-api
Nur diese drei Dienste starten, nie ein nacktes docker compose up -d (das würde nicht laufende Dienste wie eship-build wieder anlegen). Der Build-Container installiert beim Start git, startet den Webhook-Server und baut sofort einmal (initial). Den Fortschritt zeigt docker logs -f webklar-aircard-build; am Ende steht Deployment abgeschlossen (oder eine Zeile mit ERROR:).
Ohne AIRCARD_WEBHOOK_TOKEN (also ohne GITEA_DEPLOY_WEBHOOK_SECRET in der .env) beendet sich der Webhook-Server mit Exit-Code 1, statt Anfragen ungeprüft durchzulassen. Ebenso beendet sich aircard-api mit Exit-Code 1, wenn AIRCARD_API_KEY, AIRCARD_API_SALT oder AIRCARD_APPWRITE_PROJECT_ID fehlen (die Meldung nennt nur den Namen der Variable).
API-Dienst aircard-api
Kleiner Node-Dienst ohne Abhängigkeiten (deploy/api/), Container webklar-aircard-api. Er führt alle Schreibzugriffe der Community-Galerie und der Community-Logos mit dem Appwrite-Server-Key aus (Veröffentlichen mit Bildmaß-Prüfung und höchstens 20 Karten je Konto, Zurückziehen, Melden mit Auto-Ausblenden ab 3 Meldern aus verschiedenen Netzen, Admin-Sichtbarkeit; bei den Logos Hochladen mit höchstens 30 Logos je Konto, Herzen, Melden, Löschen, Admin-Sichtbarkeit). Endpunkte, Statuscodes, Fehlercodes und Betrieb stehen in api/README.md.
- Route: Traefik-Label am Dienst, Router
aircard-gallery-api(Host(`aircard-ios.org`) && PathPrefix(`/api`), Priorität 200, Port 8088). Der Name ist absichtlich nichtaircard-api: so heißt schon die Appwrite-Route (/v1) intraefik/dynamic/aircard.yml. - Prüfen nach dem Start:
curl -s https://aircard-ios.org/api/gesundheitliefert{"ok":true};docker logs webklar-aircard-apizeigtlistening on :8088. Läuft der Dienst nicht, fehlt auch sein Traefik-Router: nginx antwortet dann auf/api/*mit503und{"error":"Der Dienst ist gerade nicht erreichbar."}(Content-Typeapplication/json), nie mit der App. - Ändern: Dateien mit
cpnachaircard-ios.org/api/ersetzen (alle fünf:appwrite.js,common.js,app.js,logos.js,server.js;app.jslädtcommon.jsundlogos.js, ohne sie startet der Dienst nicht), danncd /home/webklar/apps && docker compose restart aircard-api. Nach einer Änderung der.env(Key, Salz) reichtrestartnicht:docker compose up -d aircard-api. - Logos brauchen das Schema: Die Logo-Endpunkte schreiben in die Tabellen
logos,logo_likes,logo_reportsund den Bucketlogo-images. Das Schema-Skriptweb/scripts/appwrite-setup.mjslegt sie an (zuerst mit--checkansehen, was fehlt, siehe ../README.md); das gehört vor den Neustart des API-Dienstes mit den neuen Dateien. Ohne die Tabellen antworten die Logo-Endpunkte mit500„Interner Fehler.“, die Karten-Endpunkte laufen weiter. Die ganze Reihenfolge steht im nächsten Abschnitt. - Log-Grenze: Der Docker-Daemon des Servers hat keine eigenen Log-Optionen, deshalb steht im Service
logging: json-filemitmax-size: 5mundmax-file: 2(höchstens 10 MB). Der Dienst selbst schreibt kein Zugriffsprotokoll und höchstens 120 Zeilen pro Minute. - Kein Port nach außen: der Dienst vertraut dem Header
X-Real-Ip(setzt Traefik) für Rate-Limit und Melder-Hash, deshalb nieports:ergänzen. Im Docker-Netzwebklar-networkerreichen ihn andere Container trotzdem und könntenX-Real-Ipsetzen: akzeptiertes Risiko, siehe api/README.md (Client-IP). Vor einem AAAA-Eintrag für aircard-ios.org die Client-IP-Ermittlung prüfen.
Reihenfolge bei Änderungen an Schema, API und Seite
Bringt eine Änderung neue Tabellen, neue Endpunkte und eine Seite mit, die beides braucht (wie die Community-Logos), gilt immer diese Reihenfolge. Die Seite geht zuletzt live: sonst ruft sie Endpunkte auf, die es noch nicht gibt, und ein Push von main baut sie sofort.
-
Schema:
node --env-file=/home/webklar/apps/.env web/scripts/appwrite-setup.mjs --checkzeigt, was fehlt (FEHLT: …). Ohne--checklegt das Skript es an. Danach noch einmal mit--check: die Ausgabe muss mitOK: Schema vollständigenden (Exit-Code 0). Für die Logos sind das die Tabellenlogos,logo_likes,logo_reportsund der Bucketlogo-images. -
API: alle fünf Dateien (
appwrite.js,common.js,app.js,logos.js,server.js) mitcpnachaircard-ios.org/api/kopieren, danncd /home/webklar/apps && docker compose restart aircard-api.docker logs webklar-aircard-apizeigtlistening on :8088. Prüfen, dass die Logo-Endpunkte da sind: einPOSTauf/api/logos/herzohne JWT muss mit401und"code":"unauthorized"antworten (der HeaderContent-Type: application/jsongehört dazu, sonst kommt415):curl -s -i -X POST -H 'Content-Type: application/json' -d '{}' https://aircard-ios.org/api/logos/herzAntwortet er mit
404, läuft noch die alte API (Dateien oder Neustart prüfen). Das Schema prüft dieser Aufruf nicht, denn die Anmeldung kommt vor jedem Zugriff auf Appwrite: dafür ist Schritt 1 da. -
Seite: erst jetzt
mainpushen (Gitea-Webhook, siehe unten). Der Build führtnpm ciundnpm run buildaus: Typprüfung ohne Testdateien (web/tsconfig.build.json), dannvite build.
Manuell deployen
docker exec webklar-aircard-build sh /deploy.sh
Ohne DEPLOY_TRIGGER baut das Skript immer neu, auch wenn sich der Commit nicht geändert hat.
Es läuft immer nur ein Deployment: Ist schon eines aktiv (Sperrverzeichnis build/.git/aircard-deploy.lock), schreibt das Skript „Deployment läuft bereits“ und endet mit Exit-Code 0. Eine verwaiste Sperre (Prozess weg oder älter als 60 Minuten) übernimmt der nächste Lauf selbst. npm ci und der Build brechen nach 15 Minuten ab (timeout 900).
Gitea-Webhook
Repo knso/AirCardIOS → Einstellungen → Webhooks → Gitea:
| Feld | Wert |
|---|---|
| Ziel-URL | https://aircard-ios.org/webhook/deploy |
| HTTP-Methode | POST |
| Content-Type | application/json |
| Secret | Wert von GITEA_DEPLOY_WEBHOOK_SECRET aus /home/webklar/apps/.env (volle Länge, nur dort nachsehen) |
| Auslöser | nur Push-Ereignisse |
| Branch-Filter | main |
| Aktiv | ja |
„Test delivery“ muss 202 ergeben. 401 heißt: Secret stimmt nicht. 200 mit {"ignored":true} heißt: Push auf einen anderen Branch als main.
Verhalten
- Webhook (
POST /webhook/deploy): prüft die Signatur im Headerx-gitea-signature(HMAC-SHA256 über den Rohbody).401bei falscher oder fehlender Signatur,413bei Body über 1 MB,200bei Push auf einen anderen Branch,202bei Push aufmain; der Build läuft danach im Hintergrund. Alles andere antwortet404. - Warteschlange: Builds laufen nacheinander, höchstens einer läuft und einer wartet. Weitere Pushes während ein Lauf wartet, werden mit
202beantwortet und fügen keinen Lauf hinzu (der wartende Lauf holt ohnehin den neuesten Stand). - Trigger:
webhookund manuelle Aufrufe bauen immer.initial(Container-Start) undpoll(alle 15 Minuten) bauen nur, wenn es einen neuen Commit gibt oderout/index.htmlfehlt. - Gemerkter Commit:
build/.git/aircard-last-deployed-commitenthält den zuletzt versuchten Commit, geschrieben vor dem Build. Ein kaputter Build wird deshalb nicht alle 15 Minuten wiederholt; die alte Seite bleibt online, bis ein neuer Commit oder ein Webhook-/manueller Lauf kommt. - Fehler: Ein fehlgeschlagener Lauf wird mit
[webhook] … deployment failedprotokolliert und beendet den Server nicht. Fehlerzeilen des Skripts beginnen mitERROR:. - Build-Variablen:
VITE_APPWRITE_ENDPOINTundVITE_APPWRITE_PROJECT_IDkommen aus der Container-Umgebung (compose);AIRCARD_APPWRITE_PROJECT_IDwird aus/home/webklar/apps/.envgelesen. Vite liest sie direkt. - .env-Variablen:
AIRCARD_APPWRITE_PROJECT_ID(Projekt-ID6ac2130a00289343d6d5) undAIRCARD_APPWRITE_API_KEY(für das Setup-Skript) müssen in/home/webklar/apps/.enveingetragen sein; füraircard-apizusätzlichAIRCARD_API_KEY(Keyaircard-api) undAIRCARD_API_SALT.
Änderungen später
deploy.shändern: Datei auf dem Server mitcpüberschreiben (nichtmv, sonst zeigt der Datei-Mount auf die alte Datei). Gilt beim nächsten Lauf.webhook-server.jsändern: ebenso mitcpersetzen, danncd /home/webklar/apps && docker compose restart aircard-build.deploy/api/*.jsändern (die fünf Dateien des Dienstes, nicht die Tests und nichtfake-appwrite.js): ebenso mitcpersetzen, danncd /home/webklar/apps && docker compose restart aircard-api.- nginx-Konfiguration ändern: mit
cpersetzen, danndocker exec webklar-aircard nginx -t && docker exec webklar-aircard nginx -s reload. - Traefik-Route ändern: Datei atomar ersetzen wie oben; Löschen der Datei entfernt die Route wieder.
www.aircard-ios.org
Solange dig +short www.aircard-ios.org @8.8.8.8 nichts liefert, gibt es nur die Hauptdomain. Zeigt der A-Record auf 91.99.156.85, werden die auskommentierten www-Labels am Dienst aircard in docker-compose.yml aktiviert (Backup vorher, dann docker compose config -q und docker compose up -d aircard). Sie leiten per 301 auf https://aircard-ios.org weiter und haben eigene Router, damit ein www-Fehler das Zertifikat der Hauptdomain nicht blockiert.
Tests
node --test deploy/webhook-server.test.js deploy/deploy.test.js
node --test "deploy/api/*.test.js"
Die Tests brauchen nur Node und starten die Server auf Port 0. Die API-Tests laufen gegen einen Fake-Appwrite im Speicher (deploy/api/fake-appwrite.js); das echte Appwrite wird nie angesprochen. Das Glob muss in Anführungszeichen stehen.