elpatronandCursor 6884b18899
CI/CD / check (push) Successful in 1m37s
CI/CD / Deploy to LXC (push) Successful in 4m29s
Point / at /de so Google indexes the German homepage as default.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-17 11:39:55 +02:00

Kugelstand

Liberapay receiving Live Next.js React TypeScript PostgreSQL Docker PWA i18n

Donate using Liberapay

Mobile-first PWA zum Aufzeichnen von Pétanque- und Boßeln-Scores. Spiele und Turniere bekommen eine Unique URL und können ohne Login geteilt und gemeinsam bearbeitet werden.

Features

  • Sportwahl: Pétanque oder Boßeln
  • Pétanque: Einzelwertung oder Teamwertung (14 Spielende), Zielpunktzahl (Presets 11/13, frei 199)
  • Boßeln (Standalone): zwei Mannschaften à max. 8 mit Nummern, Wertung einfach oder Punktspiel, laufende Schoet-Erfassung (+1), manuelles Beenden
  • Aufnahmen-/Schoet-Verlauf, Undo und Rematch (Standalone-Spiele)
  • Spielernamen nach Spielstart editierbar
  • Turniere (nur Pétanque) als Serie von Spielen mit kumuliertem Scoreboard und Schnell-Paarungen
  • Sprachen: Deutsch, Englisch, Französisch, Spanisch, Italienisch
  • Selfhosting mit Docker Compose
  • Live-Updates des Spielstands über Server-Sent Events (SSE)
  • Optionale Lage-Messung per Foto beim Punkteintrag (Pétanque; Tippen; mit OpenRouter auch Auto-Erkennung)
  • Ergebnis teilen (Text + Bild) für Messenger nach Spielende / Turnier-Scoreboard
  • Separater Zuschauer-Link (nur lesen) neben dem Schreib-Link; QR-Codes zum Teilen
  • Zuletzt geöffnete Spiele/Turniere lokal auf dem Gerät (ohne Account)
  • Liberapay-Spendenbutton auf der Startseite

Entwicklung

Voraussetzungen: Node.js 22+, Docker

# Postgres starten
docker compose -f docker-compose.dev.yml up -d

# Abhängigkeiten & Migrationen
cp .env.example .env
npm install
npx prisma migrate dev

# Dev-Server
npm run dev

App: http://localhost:3000

Tests der Domänenlogik (scoring, scoreboard, suggestPoints):

npm test

Produktion (Docker)

docker compose up -d --build

Die App lauscht auf Port 3000 (Host-Port über PORT in .env änderbar). Davor einen Reverse-Proxy (Caddy/Nginx/Traefik) mit TLS setzen. Postgres ist nur im Compose-Netz erreichbar (kein Host-Port).

Beim Container-Start führt der Entrypoint automatisch prisma migrate deploy aus und startet danach die App. Der App-Container prüft /api/health (inkl. DB).

Live-Updates laufen in-process (SSE). Deshalb einen App-Container betreiben (kein horizontales Scaling ohne zusätzlichen Pub/Sub). Proxy muss SSE/Streaming erlauben (kein Response-Buffering) und X-Forwarded-For setzen (für Rate-Limits).

Update auf dem Server

Im App-Verzeichnis (z.B. auf dem LXC):

./scripts/update.sh

Das Script macht git pull --ff-only, baut und startet Compose neu und räumt ungenutzte Docker-Images/Build-Caches auf. Voraussetzung: .env und docker-compose.yml liegen im Repo-Root.

Backup & Restore

./scripts/backup-db.sh

Erzeugt backups/kugelstand-<UTC-Zeitstempel>.sql.gz. Restore-Beispiel gibt das Script aus. Ideal täglich per Cron (z.B. 0 3 * * *).

CI/CD (Gitea Actions)

Push auf main/master läuft über .gitea/workflows/ci-cd.yml: Lint/Test/Build, danach Deploy per WireGuard → SSH → ./scripts/update.sh.

Runner auf Unraid (neben Gitea): Gitea führt Jobs nicht selbst aus act_runner als zusätzlicher Docker-Container auf dem gleichen Unraid.

Auf Unraid (Beispielpfad /mnt/user/appdata/gitea-runner): config.yaml mit privileged Job-Containern + TUN, Container Gitea-Runner mit --privileged und --device /dev/net/tun, Data-Mount inkl. .runner (Registrierung). Neu anlegen ggf. via unraid-run.sh im Appdata-Ordner.

Hinweise Unraid:

  • Job-Container nutzen Host-Netz, damit das LXC im LAN erreichbar ist (WireGuard im Job entfällt). config.yaml: network: host.
  • Nach Änderungen in der Unraid-Docker-UI Privileged + Device /dev/net/tun + CONFIG_FILE=/data/config.yaml beibehalten.
  • DEPLOY_SSH_KEY als Base64-One-Liner speichern (Gitea maskiert Multiline-Keys unzuverlässig).
  • Actions müssen in Gitea aktiv sein (ENABLED = true bzw. Admin-UI).

Repository-Secrets (Settings → Secrets):

Secret Inhalt
WG_CONF Optional: wg-quick-Config, nur wenn DEPLOY_HOST vom Runner aus nicht erreichbar ist
DEPLOY_SSH_KEY Privater SSH-Key als eine Zeile Base64 (base64 -w0 deploy_ed25519)
DEPLOY_HOST Erreichbare Adresse des LXC im WireGuard-Netz
DEPLOY_USER SSH-User
DEPLOY_PATH Absoluter Pfad zum App-Checkout auf dem LXC
DEPLOY_SSH_PORT Optional, Default 22
DEPLOY_HEALTH_URL Optional öffentlicher Check, z.B. https://kugelstand.de/api/health (Deploy prüft zuerst per SSH localhost:$PORT; öffentliche URL folgt Redirects, schlägt sie fehl ist das nur ein Warning)

Beispiel-WG_CONF (als Secret, eine Datei / Multiline):

[Interface]
PrivateKey = <CI-Peer-PrivateKey>
Address = 10.10.0.50/32

[Peer]
PublicKey = <Server-oder-LXC-PublicKey>
Endpoint = <öffentlicher-WG-Endpoint>:51820
AllowedIPs = 10.10.0.0/24
PersistentKeepalive = 25

Auf dem LXC muss der öffentliche Key dieses CI-Peers als WireGuard-Peer eingetragen sein; SSH-User braucht Rechte für git pull und docker compose im DEPLOY_PATH.

Teilen: Mitspielen vs. Zuschauen

Link Pfad Rechte
Schreib-Link /g/{id} bzw. /t/{id} Punkte eintragen, Setup, Rematch
Zuschauer-Link /vg/{viewId} bzw. /vt/{viewId} Nur lesen + Live-Updates

Der Zuschauer-Link enthält nicht die Schreib-ID. Über „Link teilen“ lassen sich beide Varianten (inkl. QR) erzeugen.

Umgebungsvariablen

Siehe .env.example. Docker Compose liest die Werte aus .env.

Variable Beschreibung
POSTGRES_USER Postgres-Benutzer
POSTGRES_PASSWORD Postgres-Passwort
POSTGRES_DB Datenbankname
DATABASE_URL Connection-String für lokales npm run dev
PORT Host-Port für Docker (Container intern immer 3000)
GIT_SHA Kurzer Git-SHA für Footer (update.sh schreibt .git-sha und --build-arg)
OPENROUTER_API_KEY Optional: Vision-API für Auto-Erkennung von Boules
OPENROUTER_MODEL Optional: Modell-ID (Default google/gemini-3.6-flash)
OPENROUTER_SITE_URL Optional: Referer für OpenRouter-Rankings

Foto-Messung

Beim Punkteintrag kann ein Vogelperspektiv-Foto (zentriert über dem Cochonnet) geladen werden. Die App markiert Kugeln nach aufsteigender Entfernung; Seiten lassen sich zuweisen und ein Punktvorschlag übernehmen. Ohne OPENROUTER_API_KEY funktioniert nur das manuelle Tippen. Mit Key wird das Bild zur Erkennung an OpenRouter gesendet (keine Speicherung in der App-Datenbank).

Analytics (Plausible)

Die App lädt das Tracker-Script von https://plausible.elpatron.me/js/script.js für die Domain kugelstand.de. Custom Events werden clientseitig über plausible() gesendet (src/lib/plausible.ts).

Damit die Goals im Dashboard erscheinen, müssen sie unter Website-Einstellungen → Goals als Custom Events angelegt werden (Namen exakt wie unten). Optional können Funnels z.B. Game CreatedGame StartedGame Finished gebaut werden.

Goal Wann Props
Game Created Neues Spiel von der Startseite angelegt sport: PETANQUE | BOSSELN
Tournament Created Neues Turnier von der Startseite angelegt
Game Started Spiel-Setup abgeschlossen (Modus + Spieler) mode: SOLO | TEAM, sport: PETANQUE | BOSSELN, optional bosselnVariant: SIMPLE | PUNKTSPIEL
Tournament Started Turnier-Setup abgeschlossen
Tournament Game Created Neues Spiel innerhalb eines Turniers mode: SOLO | TEAM
Share Clicked Link geteilt (Web Share API oder Clipboard) method: native | clipboard, context: game | tournament, kind: play | watch
Result Shared Spiel- oder Turnierergebnis geteilt method: native | clipboard, context: game | tournament, withImage: true | false
Game Finished Spiel beendet (Zielpunktzahl bzw. manuell bei Boßeln) mode: SOLO | TEAM, sport: PETANQUE | BOSSELN, optional bosselnVariant
Language Switched Sprache gewechselt from, to: de | en | fr | es | it
Outbound KnorrLabs Klick auf KnorrLabs im Footer

Hinweis: Events von localhost werden von Plausible oft verworfen (Bot-Filter). Zum Testen die Produktionsdomain oder eine in Plausible hinterlegte Staging-Domain nutzen.

S
Description
No description provided
Readme
606 KiB
Languages
TypeScript 94.7%
Shell 2.7%
CSS 1.7%
Dockerfile 0.7%
JavaScript 0.2%