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 (1–4 Spielende), Zielpunktzahl (Presets 11/13, frei 1–99)
  • 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 Created → Game Started → Game 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
0 Stars 1 Watchers 0 Forks
Languages
TypeScript 94.7%
Shell 2.7%
CSS 1.7%
Dockerfile 0.7%
JavaScript 0.2%