feedc0de e59fde7bde
Publish and deploy birthday invitation / Check JavaScript (push) Successful in 11s
Publish and deploy birthday invitation / Build and push (push) Successful in 25s
Publish and deploy birthday invitation / Deploy to Kubernetes (push) Successful in 51s
add install script
2026-09-08 18:44:51 +02:00
2026-09-08 18:30:41 +02:00
2026-09-08 18:30:41 +02:00
2026-09-08 18:44:51 +02:00
2026-09-08 17:20:12 +02:00
2026-09-08 18:44:51 +02:00
2026-09-08 18:30:41 +02:00

Einladung & RSVP

Eine responsive Einladung für Vicky und Daniels 30. Geburtstag. Jeder Gast erhält einen persönlichen Link, kann die Originaleinladung als PDF öffnen und eine von drei Antworten abgeben:

  • Ich komme nicht
  • Ich komme alleine
  • Ich komme mit jemandem (inklusive Name der Begleitung)

Der Server benötigt nur Node.js 22.5 oder neuer und keine zusätzlichen Pakete. Als Datenbank dient SQLite über Nodes eingebautes node:sqlite-Modul.

Lokal starten

ADMIN_KEY='ein-langes-geheimes-passwort' \
ORIGIN='http://localhost:3000' \
npm start

Danach läuft die Seite unter http://localhost:3000. Die Verwaltung ist unter http://localhost:3000/admin erreichbar. Ohne ADMIN_KEY oder vorgeschaltetes Authentik bleiben die Admin-Endpunkte aus Sicherheitsgründen deaktiviert.

Öffne /admin, melde dich mit dem ADMIN_KEY an und füge bis zu 200 Namen auf einmal ein einen Namen pro Zeile. Die erzeugten Links können einzeln oder gesammelt kopiert werden.

Die Übersicht zeigt:

  • ob und wann ein Link erstmals geöffnet wurde
  • wann er zuletzt geöffnet wurde und wie oft
  • die aktuelle Zu- oder Absage
  • den Namen einer Begleitperson
  • wann die Antwort zuletzt gespeichert beziehungsweise geändert wurde
  • die erwartete Gesamtzahl der kommenden Gäste

Über „CSV herunterladen“ lässt sich die vollständige Liste für Excel, Numbers oder LibreOffice exportieren. Eine Öffnung wird gezählt, sobald das JavaScript der RSVP-Seite die Einladung abruft. Manche besonders aktive Link-Vorschau-Bots können theoretisch ebenfalls eine Öffnung erzeugen.

Am einfachsten direkt auf dem Server:

ORIGIN='https://30.brunner.ninja' npm run invite -- "Max Mustermann"

Die Ausgabe enthält den fertigen Link, zum Beispiel:

Max Mustermann: https://30.brunner.ninja/i/zufaelliger-sicherer-token

Alternativ können mehrere Links über die Admin-API erzeugt werden:

curl -X POST 'https://30.brunner.ninja/api/admin/invites' \
  -H 'Authorization: Bearer DEIN_ADMIN_KEY' \
  -H 'Content-Type: application/json' \
  --data '{"names":["Max Mustermann","Erika Musterfrau"]}'

Antworten exportieren

curl 'https://30.brunner.ninja/api/admin/responses.csv' \
  -H 'Authorization: Bearer DEIN_ADMIN_KEY' \
  -o zusagen.csv

Die CSV-Datei lässt sich direkt mit Excel, Numbers oder LibreOffice öffnen.

Daten und Betrieb

Antworten werden in data/invites.db gespeichert. SQLite läuft im WAL-Modus; das Verzeichnis muss im Produktionsbetrieb beschreibbar und persistent sein. Regelmäßige Backups der Datenbank werden empfohlen.

Für die öffentliche Domain sollte die App hinter einem HTTPS-Reverse-Proxy (beispielsweise Caddy oder nginx) laufen. Benötigte Umgebungsvariablen:

Variable Zweck Standard
PORT Interner HTTP-Port 3000
HOST Listen-Adresse 0.0.0.0
ORIGIN Öffentliche Basis-URL für erzeugte Links http://localhost:3000
DATABASE_FILE Pfad zur SQLite-Datenbank data/invites.db
ADMIN_KEY Geheimer Schlüssel für lokalen Admin-Zugriff keiner
TRUST_AUTHENTIK Akzeptiert Authentiks verifiziertes Benutzer-Header (true) false

Die RSVP-Seiten liegen unter /i/<token>; von dort kann die persönliche PDF-Einladung geöffnet werden.

Die Einladungsgrafik und PDF sind bewusst nicht im Git-Repository oder Container-Image enthalten. In Kubernetes werden sie aus dem Secret 30-birthday-invitation schreibgeschützt eingebunden und ausschließlich über einen gültigen Gast-Token ausgeliefert. Direkte öffentliche Asset-URLs existieren nicht.

Kubernetes, Traefik und Authentik

Ein vollständiges Beispiel liegt in k8s/invitation.yaml. Es verwendet:

  • genau einen Pod (SQLite ist hier bewusst nicht horizontal skaliert)
  • einen 128Mi-rook-ceph-block-PVC für /data/invites.db
  • die Domain 30.brunner.ninja
  • ausschließlich Traefiks EntryPoint websecure
  • das bestehende default-authentik@kubernetescrd-Middleware nur für /admin* und /api/admin/*
  • öffentliche Gast- und Asset-Routen ohne Authentik
  • Recreate-Updates mit einer kurzen Unterbrechung, damit nur ein SQLite-Prozess den Block-PVC verwendet

Zuerst das Image bauen und in die eigene Registry pushen:

docker build -t registry.brunner.ninja/feedc0de/30-birthday:latest .
docker push registry.brunner.ninja/feedc0de/30-birthday:latest

Die Gitea Action in .gitea/workflows/container.yml übernimmt Build, Push und Deployment automatisch. Sie benötigt diese Repository-Secrets:

  • QUAY_USERNAME: feedc0de+birthday30
  • QUAY_TOKEN: Token dieses Robot-Accounts
  • KUBE_CONFIG_BASE64: Kubeconfig des eingeschränkten 30-birthday-deployer-ServiceAccounts

Workload und CI-Berechtigungen werden einmalig mit einem Cluster-Administrator angelegt:

./install.sh
./k8s/create-ci-kubeconfig.sh

Die Action baut registry.brunner.ninja/feedc0de/30-birthday, versieht jedes Image mit dem unveränderlichen Commit-SHA und aktualisiert auf main ausschließlich das bestehende default/30-birthday-Deployment. Ihre Kubernetes-Rolle kann keine Secrets oder anderen Deployments lesen oder verändern.

Der Cluster stellt Authentiks /outpost.goauthentik.io/* bereits zentral bereit. Der App-Service ist nur als ClusterIP erreichbar; daher kann ein externer Client Authentiks Identitätsheader nicht direkt am App-Port einschleusen.

S
Description
Geburtstagskarte zum 30. Geburtstag mit Zu- oder Absagendatenbank
Readme GPL-3.0
818 KiB
Languages
JavaScript 52.8%
CSS 25.2%
HTML 17.7%
Shell 3.3%
Dockerfile 1%