feedc0de 79e27c0c7b
Test, publish birthday app and chart / validate (push) Successful in 7s
Test, publish birthday app and chart / publish-image (push) Successful in 5s
Test, publish birthday app and chart / publish-chart (push) Successful in 3s
Remove legacy Kubernetes CD bootstrap
2026-10-19 08:50:16 +02:00
2026-10-19 08:43:31 +02:00
2026-09-08 18:30:41 +02:00
2026-10-19 08:50:16 +02:00
2026-10-14 21:19:04 +02:00
2026-10-14 21:19:04 +02:00
2026-10-19 08:43:31 +02:00
2026-09-08 17:20:12 +02:00
2026-10-19 08:43:31 +02:00
2026-10-14 21:19:04 +02:00

Einladung & RSVP

Eine responsive Einladung von Vicky und Daniel zu 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 „Antwort setzen“ beziehungsweise „Antwort ändern“ kann die Zu- oder Absage im Adminbereich stellvertretend für einen Gast gespeichert werden. Bei einer Zusage mit Begleitung wird auch deren Name erfasst.

Ü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

Das veröffentlichte Helm Chart befindet sich in chart/. Es owns the Deployment, Service, probes, SQLite persistence contract, and the public/admin Ingresses. SQLite must stay at one replica and uses a Recreate strategy.

For this homelab, Argo CD is the sole workload writer. Its cluster values and existing-claim reference are in the separate homelab-gitops application. The live 30-birthday-data PVC is referenced as existingClaim; the chart does not render or replace it. Invitation assets stay in the existing 30-birthday-invitation Secret, outside public Git. Authentik protects only admin paths; guest and invitation asset routes remain public.

For another cluster, create the invitation Secret first (the local helper expects private assets excluded from Git), then install the chart:

./k8s/create-invitation-secret.sh
helm repo add brunner.ninja https://brunner.ninja/charts
helm repo update brunner.ninja
helm upgrade --install 30-birthday brunner.ninja/30-birthday \
  --namespace default --wait --timeout 5m

Main-branch CI tests the app, publishes an immutable image, packages a chart with that exact image as appVersion, and promotes only the chart version in homelab-gitops. CI uses QUAY_USERNAME, QUAY_TOKEN, PACKAGE_USERNAME, PACKAGE_TOKEN, and GITOPS_TOKEN; it has no Kubernetes credentials.

The cluster exposes Authentik's /outpost.goauthentik.io/* centrally. The app Service remains ClusterIP, so external clients cannot inject Authentik identity headers through the app port.

S
Description
Geburtstagskarte zum 30. Geburtstag mit Zu- oder Absagendatenbank
Readme GPL-3.0
873 KiB
Languages
JavaScript 54.6%
CSS 23.1%
HTML 17.8%
Shell 3.4%
Dockerfile 0.8%
Other 0.3%