# 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](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) ```sh 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: ```sh 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](../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 ```sh 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](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 nicht `aircard-api`: so heißt schon die Appwrite-Route (`/v1`) in `traefik/dynamic/aircard.yml`. - **Prüfen** nach dem Start: `curl -s https://aircard-ios.org/api/gesundheit` liefert `{"ok":true}`; `docker logs webklar-aircard-api` zeigt `listening on :8088`. Läuft der Dienst nicht, fehlt auch sein Traefik-Router: nginx antwortet dann auf `/api/*` mit `503` und `{"error":"Der Dienst ist gerade nicht erreichbar."}` (Content-Type `application/json`), nie mit der App. - **Ändern**: Dateien mit `cp` nach `aircard-ios.org/api/` ersetzen (alle fünf: `appwrite.js`, `common.js`, `app.js`, `logos.js`, `server.js`; `app.js` lädt `common.js` und `logos.js`, ohne sie startet der Dienst nicht), dann `cd /home/webklar/apps && docker compose restart aircard-api`. Nach einer Änderung der `.env` (Key, Salz) reicht `restart` nicht: `docker compose up -d aircard-api`. - **Logos brauchen das Schema**: Die Logo-Endpunkte schreiben in die Tabellen `logos`, `logo_likes`, `logo_reports` und den Bucket `logo-images`. Das Schema-Skript `web/scripts/appwrite-setup.mjs` legt sie an (zuerst mit `--check` ansehen, was fehlt, siehe [../README.md](../README.md)); das gehört vor den Neustart des API-Dienstes mit den neuen Dateien. Ohne die Tabellen antworten die Logo-Endpunkte mit `500` „Interner Fehler.“, die Karten-Endpunkte laufen weiter. - **Log-Grenze**: Der Docker-Daemon des Servers hat keine eigenen Log-Optionen, deshalb steht im Service `logging: json-file` mit `max-size: 5m` und `max-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 nie `ports:` ergänzen. Im Docker-Netz `webklar-network` erreichen ihn andere Container trotzdem und könnten `X-Real-Ip` setzen: akzeptiertes Risiko, siehe [api/README.md](api/README.md) (Client-IP). Vor einem AAAA-Eintrag für aircard-ios.org die Client-IP-Ermittlung prüfen. ## Manuell deployen ```sh 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 Header `x-gitea-signature` (HMAC-SHA256 über den Rohbody). `401` bei falscher oder fehlender Signatur, `413` bei Body über 1 MB, `200` bei Push auf einen anderen Branch, `202` bei Push auf `main`; der Build läuft danach im Hintergrund. Alles andere antwortet `404`. - **Warteschlange**: Builds laufen nacheinander, höchstens einer läuft und einer wartet. Weitere Pushes während ein Lauf wartet, werden mit `202` beantwortet und fügen keinen Lauf hinzu (der wartende Lauf holt ohnehin den neuesten Stand). - **Trigger**: `webhook` und manuelle Aufrufe bauen immer. `initial` (Container-Start) und `poll` (alle 15 Minuten) bauen nur, wenn es einen neuen Commit gibt oder `out/index.html` fehlt. - **Gemerkter Commit**: `build/.git/aircard-last-deployed-commit` enthä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 failed` protokolliert und beendet den Server nicht. Fehlerzeilen des Skripts beginnen mit `ERROR:`. - **Build-Variablen**: `VITE_APPWRITE_ENDPOINT` und `VITE_APPWRITE_PROJECT_ID` kommen aus der Container-Umgebung (compose); `AIRCARD_APPWRITE_PROJECT_ID` wird aus `/home/webklar/apps/.env` gelesen. Vite liest sie direkt. - **.env-Variablen**: `AIRCARD_APPWRITE_PROJECT_ID` (Projekt-ID `6ac2130a00289343d6d5`) und `AIRCARD_APPWRITE_API_KEY` (für das Setup-Skript) müssen in `/home/webklar/apps/.env` eingetragen sein; für `aircard-api` zusätzlich `AIRCARD_API_KEY` (Key `aircard-api`) und `AIRCARD_API_SALT`. ## Änderungen später - `deploy.sh` ändern: Datei auf dem Server mit `cp` überschreiben (nicht `mv`, sonst zeigt der Datei-Mount auf die alte Datei). Gilt beim nächsten Lauf. - `webhook-server.js` ändern: ebenso mit `cp` ersetzen, dann `cd /home/webklar/apps && docker compose restart aircard-build`. - `deploy/api/*.js` ändern (die fünf Dateien des Dienstes, nicht die Tests und nicht `fake-appwrite.js`): ebenso mit `cp` ersetzen, dann `cd /home/webklar/apps && docker compose restart aircard-api`. - nginx-Konfiguration ändern: mit `cp` ersetzen, dann `docker 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 ```sh 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.