Gitea Actions runs `run:` steps with `sh` by default, which doesn't
support bash's `set -o pipefail` ("Illegal option -o pipefail",
exit 2). node:22-bookworm-slim (Debian) includes bash, so declaring
shell: bash for this step fixes it without changing the base image.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Anouma
Die Website von Anouma — Next.js (App Router) mit einem eingebauten Payload CMS (PostgreSQL) für Termine, Beiträge, Angebote und Seiteninhalte, plus Online-Termine mit P2P-Video-Call (WebRTC), Anmeldungen und E-Mail-Erinnerungen.
Projektstruktur
app/(frontend)/— die öffentliche Website (bestehendes ANOUMA-Design, eigener Root-Layout)app/(call)/— die Video-Call-Oberfläche (/termine/[slug]/call), eigenes minimalistisches dunkles Layout ohne Navbar/Footerapp/(payload)/— der Admin-Bereich unter/admin, Payloads REST/GraphQL-API sowie die Meeting-/Cron-API-Routen (api/meetings/...,api/cron/...)app/global-not-found.tsx— statische 404-Seite (siehe unten, warum sie nötig ist)collections/,globals/,access/,fields/,payload.config.ts— die CMS-Konfigurationlib/payload/— Local-API-Zugriffe für die öffentliche Website (mitReact.cachepro Request memoisiert)lib/meeting/— Meeting-Passwort, Zeitfenster/Status, signierte Beitritts-Tokens, WebSocket-Signaling, Reminder-Logiklib/email/— SMTP-Versand (nodemailer) und das E-Mail-Template für Reminderlib/texte.ts— die ursprünglichen Anouma-Texte, nur noch als Seed-Quelle verwendetscripts/seed.ts— überträgt die vorhandenen Inhalte ins CMSserver.ts— eigener Node-Server (stattnext start), weil daran der WebRTC-Signaling-WebSocket hängt
Zwei bzw. drei Root-Layouts ((frontend), (payload), (call)) bedeuten: Next.js kann daraus keine einzelne 404-Seite komponieren, deshalb gibt es app/global-not-found.tsx (siehe experimental.globalNotFound in next.config.ts).
Online-Termine & Video-Call
- Ein Termin wird per Häkchen „Online-Termin“ zu einem Video-Call-Termin. Payload generiert dabei automatisch ein zufälliges Meeting-Passwort (nie aus der Meeting-ID abgeleitet); die Admin kann jederzeit ein neues erzeugen lassen.
- Öffentliche Beitrittsseite:
/termine/[slug]/beitreten(Name + Passwort, beide Pflicht) → bei Erfolg/termine/[slug]/call. - Ist die aufrufende Person im selben Browser als Admin eingeloggt, wird sie automatisch als Host erkannt (kein Passwort nötig, größeres Beitritts-Zeitfenster, exklusive Steuerung: Bildschirmfreigabe, Teilnehmer entfernen).
- Die Zeitfenster (wie früh Host/Teilnehmer beitreten dürfen, wie lange das Meeting nach Ende offen bleibt) sind zentral im CMS unter Video-Termin-Einstellungen konfigurierbar.
- Server-seitige Validierung:
/api/meetings/[slug]/joinprüft Termin, Zeitfenster und Passwort und stellt danach erst ein kurzlebiges, signiertes Sitzungs-Token aus (MEETING_SESSION_SECRET). Das WebSocket-Signaling (/ws/signaling, sieheserver.ts) prüft dieses Token erneut, bevor jemand einem Raum beitreten darf. - Reines P2P-WebRTC: Der Server relayt nur kleine Signaling-Nachrichten (Angebot/Antwort/ICE), niemals Audio/Video/Bildschirmfreigabe. Es wird nichts aufgezeichnet oder gespeichert.
- Teilnehmer-Anmeldungen laufen über die Collection Anmeldungen (
event-registrations), öffentlich erreichbar über/api/meetings/[slug]/register.
E-Mail-Erinnerungen
Erinnerungs-Mails (60/30 Minuten vorher, pro Termin im CMS einzeln an-/abschaltbar) werden nicht automatisch im Hintergrund verschickt, sondern müssen von einem externen Cron-Job ausgelöst werden, z. B. alle 5 Minuten:
curl -H "Authorization: Bearer $CRON_SECRET" https://anouma.org/api/cron/event-reminders
Der Versand ist idempotent (reminder60Sent/reminder30Sent je Anmeldung, hostReminder60Sent/hostReminder30Sent je Termin) — ein häufiger laufender Cron verschickt also nie doppelt.
Setup (Produktion / Deployment)
Produktion läuft komplett über Gitea Actions CI/CD — der Server baut nichts selbst, er zieht ein fertiges, getestetes Image aus der Registry. Einmalig auf dem Server:
git clone https://git.maro.run/maro/anouma.git
cd anouma
chmod +x setup.sh
./setup.sh
setup.sh installiert bei Bedarf Docker, legt /opt/anouma an, generiert alle Secrets automatisch (nie erneut, wenn schon vorhanden), richtet einen eingeschränkten Deployment-User samt SSH-Key für Gitea Actions ein und zeigt am Ende genau die Werte an, die als Gitea Secrets hinterlegt werden müssen. ./setup.sh --non-interactive läuft ohne Rückfragen.
Jedes weitere Deployment läuft danach ausschließlich über einen Git-Tag:
git tag v1.2.0 && git push origin v1.2.0
Gitea Actions testet, baut das Image, pusht es in die Registry und deployt es per SSH auf den Server — inklusive Backup, Healthcheck und automatischem Rollback bei Fehlschlag. Es gibt kein separates Update-Script.
Für alle Details (Architektur, Gitea Secrets, CI/Release-Workflows, NPM-Reverse-Proxy-Einrichtung, Environment-Variablen, WebRTC/STUN/TURN, Backups, Rollback, Security) siehe DEPLOYMENT.md.
Lokale Entwicklung (ohne Docker für die App)
npm install
cp .env.example .env
# .env ausfüllen: DATABASE_URI auf 127.0.0.1 statt "postgres" setzen, PAYLOAD_SECRET (z. B. mit `openssl rand -base64 48`)
Datenbank
Nur Postgres per Docker, die App läuft direkt auf dem Host:
docker compose up -d postgres
(oder eine gehostete Postgres-Instanz, z. B. Neon/Supabase — einfach DATABASE_URI in .env entsprechend setzen).
Entwicklung
npm run dev
- Website: http://localhost:3000
- Admin: http://localhost:3000/admin — beim ersten Aufruf legst du dort direkt deinen eigenen Admin-Zugang (E-Mail + Passwort) an. Es gibt kein Standardpasswort im Code.
Vorhandene Inhalte ins CMS übertragen
npm run seed
Überträgt die 7 Angebote sowie die Texte für Startseite, Über mich, Angebote-Einleitung, Aktuelles-Einleitung, Kontakt und Termin buchen aus lib/texte.ts ins CMS. Kann gefahrlos mehrfach ausgeführt werden. Kontaktdaten (E-Mail/Telefon/Region) werden zunächst als Platzhalter gesetzt — bitte im Admin unter „Kontakt“ durch die echten Angaben ersetzen.
Produktions-Build ohne Docker
npm run build
npm run migrate # wendet Datenbank-Migrationen an
npm run start
Für einen vollständig dockerisierten Produktionsbetrieb (App + Datenbank, Healthchecks, Reverse-Proxy-Integration) siehe oben bzw. DEPLOYMENT.md.
Weitere Skripte
| Skript | Zweck |
|---|---|
npm run generate:types |
payload-types.ts aus der Config neu erzeugen |
npm run generate:importmap |
Admin-Importmap neu erzeugen (nach neuen Feldtypen) |
npm run migrate:create |
Neue Datenbank-Migration aus Config-Änderungen bauen |
npm run lint |
ESLint |
npm test |
Test-Suite (aktuell ein Platzhalter — noch keine Tests vorhanden) |
npm run seo:check |
Optionale SEO-Prüfung (doppelte Slugs, fehlende Alt-Texte) — nicht produktionskritisch |
Design
Siehe app/(frontend)/globals.css für das Farbsystem und AGENTS.md für die Next.js-Version-16-Hinweise, die für jede Code-Änderung in app/ gelten.