No description
Find a file
2026-06-19 17:00:33 +02:00
.claude feat: Rallye-Template/Instanz-Trennung, WebDAV-Profile, Admin-UI-Refactoring 2026-05-16 00:57:16 +02:00
.roo/skills Archivseite und Reviews gemacht 2026-05-28 01:18:29 +02:00
backend feat(uc36): Gesamt-Rückblick + Foto-Galerie (lauf-weiter KI-Bericht) 2026-06-18 22:58:54 +02:00
doc feat(UC62): KI-Erstellungsbericht (Diagnose) + UC63 Thinking-Mode (WIP) 2026-06-15 00:43:37 +02:00
docs feat(uc31a): Foto-Review mit Referenzbild-Vergleich (Multi-Image-Vision) 2026-06-15 18:19:51 +02:00
frontend feat(uc36): Gesamt-Rückblick + Foto-Galerie (lauf-weiter KI-Bericht) 2026-06-18 22:58:54 +02:00
mapserver Add configurable map tile server and admin API 2026-05-24 18:08:34 +02:00
maptileserver Add configurable map tile server and admin API 2026-05-24 18:08:34 +02:00
plans fix: versehentlich entfernte Themen-Sortierung im Aufgaben-PDF wiederherstellen 2026-06-15 16:57:40 +02:00
uc51-frontend/frontend/src feat(rallye-planner): UC51 - Teamreihenfolge und Karten-Integration 2026-05-25 20:24:30 +02:00
.env.example feat(admin): UC41-UC43 — App-Admin & Spielleiter-Rollen umgesetzt 2026-05-23 18:59:51 +02:00
.gitignore Add configurable map tile server and admin API 2026-05-24 18:08:34 +02:00
CLAUDE.md Claude Gedächtnis 2026-05-17 23:48:14 +02:00
deploy.sh updates 2026-06-09 20:56:39 +02:00
docker-compose-omv.yml feat: KI-Setup vervollständigen + Team-Info-Dialog im Lauf 2026-05-18 22:16:20 +02:00
docker-compose-prod.yml Frontend-Komponenten, PWA und Dokumentation hinzufügen 2026-05-16 17:50:50 +02:00
docker-compose.yml Fix template placeholder rendering and HTML entity handling for legal pages 2026-05-21 01:12:29 +02:00
generate_keys.sh UI Update für den Chat 2026-05-17 23:47:10 +02:00
generate_vapid.sh feat(push): Web-Push für Chat-Nachrichten zuverlässig im PWA-Hintergrund 2026-05-17 20:27:30 +02:00
INSTALL.md feat: KI-Setup vervollständigen + Team-Info-Dialog im Lauf 2026-05-18 22:16:20 +02:00
README.md feat(phase6): GPS-Tracking + Karte — LocationPing, GpsHandler, Auto-Checker, Leaflet-Karten 2026-05-11 17:15:56 +02:00
Sozialtag_2026___Sozialtag_42_4__PROD__20260611_2043.json feat(UC62): KI-Erstellungsbericht (Diagnose) + UC63 Thinking-Mode (WIP) 2026-06-15 00:43:37 +02:00
test.sh feat(photo): KI-gestützte Foto-Vorprüfung (UC31) 2026-05-24 18:13:16 +02:00
uc51-todo.tar.gz feat(rallye-planner): UC51 - Teamreihenfolge und Karten-Integration 2026-05-25 20:24:30 +02:00
vorlage-3-slots-3-gruppen.json Test VOrlage 2026-06-10 00:32:41 +02:00

Stadtrallye — Phase 6 (GPS-Tracking + Karte)

Lauffähiges Backend, Frontend und PostGIS-Datenbank. Phase 6 ergänzt GPS-Tracking mit Live-Karte, automatische GPS-Task-Auswertung und Admin-Live-Dashboard.


Was ist neu in Phase 6

  • LocationPing Model (backend/app/models/location_ping.py):

    • Tabelle: location_pings mit PostGIS Geography(Point) für geom
    • Indizes: GIST auf geom, BRIN auf recorded_at, B-Tree auf (user_id, recorded_at desc)
    • User.tracking_consent — Master-Schalter für Standort-Erfassung
  • Migration 0006 (backend/alembic/versions/0006_location_pings.py):

    • Erstellt location_pings Tabelle mit PostGIS-Extension
    • GIST-Index für räumliche Queries, BRIN für Zeitreihen
  • Neue API-Endpoints (backend/app/api/location.py):

    • POST /api/v1/me/location — Location-Pings senden (Batch)
    • GET /api/v1/me/location/consent — Tracking-Consent lesen
    • PATCH /api/v1/me/location/consent — Tracking-Consent setzen
    • GET /api/v1/admin/rallyes/{id}/locations — Letzte Position pro Team
    • GET /api/v1/admin/rallyes/{id}/gps-tasks — GPS-Tasks mit Position
  • GpsHandler Erweiterung (backend/app/game/handlers.py):

    • evaluate_with_pings(): prüft LocationPings gegen Task-Position via ST_DWithin
    • min_dwell_seconds: User muss X Sekunden im Radius verweilen
  • GPS Auto-Checker Worker (backend/app/game/gps_checker.py):

    • APScheduler-Job alle 10 Sekunden
    • Prüft alle aktiven GPS-Tasks gegen LocationPings
    • Bei Treffer: auto_approved Submission + ScoreEvent
  • Frontend-Karten (react-leaflet + OSM):

    • MapPage (/map) — Teilnehmer-Karte mit Live-Position
    • AdminMapPage (/admin/rallyes/:id/map) — Live-Karte aller Teams
    • GpsInput — Distanz-Anzeige + "Hier abgeben"-Button für GPS-Tasks
    • AdminTaskEditor — MiniMapPicker + Radius/Dwell-Slider für GPS-Tasks
  • Demo-Daten: GPS-Bonus-Aufgabe "Hanos Versteck" am Marktplatz Hannover (50m Radius)

    • Erscheint nach Lösung von "Hello World" via Sichtbarkeitsregel

Backend

  • LocationPing Modellocation_pings Tabelle mit PostGIS Geography Point
  • Migration 0006 — GIST-Index auf geom, BRIN auf recorded_at
  • API Endpoints:
    • POST /api/v1/me/location — Location-Pings senden (Batch)
    • GET /api/v1/me/location/consent — Tracking-Consent lesen
    • PATCH /api/v1/me/location/consent — Tracking-Consent setzen
    • GET /api/v1/admin/rallyes/{id}/locations — Letzte Position pro Team
    • GET /api/v1/admin/rallyes/{id}/gps-tasks — GPS-Tasks mit Position
  • GpsHandler — ST_DWithin-Prüfung + min_dwell_seconds Support
  • Auto-Checker Worker — APScheduler prüft alle 10s auf GPS-Treffer
  • User.tracking_consent — Master-Schalter für Tracking

Frontend

  • react-leaflet + leaflet — OSM-Karten Integration
  • MapPage (/map) — Teilnehmer-Karte mit Live-Position
  • AdminMapPage (/admin/rallyes/:id/map) — Live-Karte aller Teams
  • GpsInput — Distanz-Anzeige + "Hier abgeben"-Button
  • AdminTaskEditor — MiniMapPicker + Radius/Dwell-Slider

Demo

  • GPS-Bonus-Aufgabe "Hanos Versteck" am Marktplatz (50m Radius)
  • Nach Lösung von "Hello World" erscheint auch diese Aufgabe

Lauffähiges Backend, Frontend und PostGIS-Datenbank. Phase 5 ergänzt eine Rule-Engine für Aufgaben-Sichtbarkeit: pro Aufgabe optional einen Regelbaum (all/any/not + Prädikate wie task_solved, team_score_gte, after, …) hinterlegen — die Aufgabe ist dann nur sichtbar, wenn die Regel für das jeweilige Team zutrifft. Admin-UI inklusive Editor und „Vorschau pro Team"-Modal.

Was ist neu in Phase 5

  • Rule Engine (backend/app/game/visibility.py):
    • VisibilityContext mit pre-loaded solved_task_ids, team_score, now
    • 6 Prädikate: task_solved, tasks_solved_count, team_score_gte, team_score_lt, after, before
    • Operatoren all / any / not als reine Komposition
    • evaluate_rule (raisable) + evaluate_rule_safe (broken rule ⇒ hidden)
    • validate_rule(rule, allowed_task_ids=…) mit Dangling-Reference-Erkennung
    • MAX_DEPTH=16 als DoS-Bremse, empty any ⇒ false, empty all ⇒ true
  • Backend-Integration:
    • participant.py: _is_visible(task, vctx) ruft die Engine; Kontext einmal pro Request, dann pro Aufgabe wiederverwendet
    • photos.py: identische Visibility-Prüfung beim Foto-Upload
    • Server-seitige Re-Prüfung bei Submission-Erstellung — kein URL-Tampering
  • 3 neue Admin-Endpoints (backend/app/api/admin_visibility.py):
    • GET /api/v1/admin/visibility/predicates — Editor-Katalog
    • POST /api/v1/admin/visibility/validate — strukturelle + Referenz-Prüfung
    • POST /api/v1/admin/rallyes/{r}/tasks/{t}/visibility/evaluate — Trockenlauf pro Team (Score, Solved-Count, sichtbar ja/nein)
  • Frontend:
    • VisibilityRuleEditor — rekursiver Tree-Builder mit kollabierbaren Knoten, Live-Validierung gegen /admin/visibility/validate (debounced)
    • Pro-Prädikat-Editoren (Task-Picker, Zahleneingabe, DateTime-Picker, „Mindestanzahl aus Auswahl"-Variante)
    • VisibilityPreviewDialog — Tabelle Team / Score / Solved / Sichtbar
    • Eingebaut in AdminTaskEditor als „Sichtbarkeit"-Card
    • 🔒-Chip in AdminRallyeDetail für Aufgaben mit Regel
  • Demo-Daten: neue Aufgabe „Versteckte Bonus-Aufgabe" mit visibility_rule = {"task_solved": <hello-world-id>} — erscheint im Teilnehmer-UI erst, nachdem Hello World gelöst wurde.

Keine DB-Migration nötig — die JSONB-Spalte tasks.visibility_rule existiert seit 0003.

Stack

  • Backend: FastAPI (Python 3.12) + SQLAlchemy 2 + Alembic + python-jose + Argon2
  • Datenbank: PostgreSQL 16 + PostGIS 3.4
  • Frontend: React 18 + Vite + MUI 5 + TanStack Query + React Router
  • Reverse-Proxy-bereit: alle Services hängen am Docker-Netz rallye-net

Schnellstart

cp .env.example .env
docker compose up --build

Dann öffnen:

Test-Logins (Seed beim ersten Start)

  • Admin: admin / admin → wählt nach Login die Rallye aus
  • Demo-Rallye: heißt „Demo Rallye", aktiv, mit verlinktem Team
  • Demo-Team: „Demo Team" — Code DEMO123 öffnet (Rallye, Team)
  • Demo-Aufgabe: „Hello World", Lösungswort hello world (case-insensitive, Levenshtein ≤1)

⚠️ Vor Produktion: JWT_SECRET setzen, Admin-Passwort ändern, Demo-Daten anpassen, CORS_ORIGINS einschränken.

📦 Beim Upgrade einer bestehenden Installation: Das Backend-Image muss neu gebaut werden (docker compose build --no-cache backend oder docker compose up --build), weil das Dockerfile die Python-Dependencies jetzt direkt aus pyproject.toml zieht (pip install . statt hartkodierter Liste). Vorher fehlte z.B. Pillow zur Laufzeit, obwohl es in pyproject.toml stand.

Datenmodell-Änderungen gegenüber Phase 1

Rallye:           id, name, description, status, starts_at, ends_at
RallyeTeam:       rallye_id, team_id, join_code (unique pro Tupel)
Task:             + rallye_id (NOT NULL), + source_task_id (Audit für Importe)
Submission:       + rallye_id (NOT NULL)
ScoreEvent:       + rallye_id (NOT NULL)
User:             + current_rallye_id (Session-Kontext)
Team:             - join_code (wandert auf RallyeTeam)

Score ist jetzt rallye-spezifisch: team_score(team_id, rallye_id) aggregiert ScoreEvents mit beiden Filtern. Ein Team kann an mehreren Rallyes teilnehmen und für jede einen eigenen Score haben.

Auth-Flow

Team-Code (für Teilnehmer)

Code öffnet (Rallye, Team)-Tupel — der User wird on-the-fly erzeugt, beidem zugeordnet, und ist sofort startklar. JWT enthält rid-Claim.

Username/Passwort (für Admins, feste Mitarbeiter)

  1. Login → POST /auth/login/password
  2. Verfügbare Rallyes → GET /auth/rallyes/available
  3. Auswahl → POST /auth/rallyes/select → neues JWT mit rid-Claim

Das Frontend führt Username-Logins automatisch zur Rallye-Auswahl, sofern keine current_rallye_id gesetzt ist. Admins können jederzeit über das Avatar-Menü „Rallye wechseln".

Aufgaben-Import

Aufgaben aus älteren Rallyes können in eine neue Rallye kopiert werden:

POST /api/v1/admin/rallyes/{id}/tasks/import
Body: {"source_task_ids": ["uuid1", "uuid2", ...]}

Was kopiert wird: alle Inhaltsfelder (Titel, Beschreibung, Storytext, Punkte, config, Position, evaluation_mode), source_task_id zeigt zur Original-ID.

Was nicht kopiert wird:

  • visibility_rule — Verweise zeigen sonst auf Tasks aus der Quell-Rallye, also ungültig
  • starts_at / ends_at — gehören zur konkreten Rallye, nicht zur Aufgabe

Der Admin baut Sichtbarkeitsregeln nach dem Import neu auf (Phase 3).

Für die Import-UI gibt es einen rallye-übergreifenden Lookup:

GET /api/v1/admin/tasks?exclude_rallye_id={current_id}

Was ist drin (Phase 0 / 1 / 2)

Bereich Phase 0 Phase 1 Phase 2
Docker-Compose-Stack mit Healthchecks
PostGIS-Extension
Auth: Username/Passwort + Team-Code
Datenmodell für Tasks/Submissions/ScoreEvents
Spiellogik: TaskHandler-Strategy + Lösungswort
ScoringService (Aggregation, kein Counter)
Admin-CRUD für Tasks/Teams/Users
Bewertungs-API für Submissions
Frontend: Dashboard + Tasks + Scoreboard
Rallye-Modell + CRUD
Rallye-Team-Verknüpfung mit Codes
Aufgaben pro Rallye + Bulk-Import
Score pro (Team, Rallye)
Rallye-Auswahl-UI für Username-Logins
Rallye-Name in AppBar, Wechseln-Menü

Roadmap

Phase Ziel Status
0 Skeleton
1 Datenmodell + CRUD + Lösungswort-Loop
2 Rallyes als eigene Entität, Aufgaben-Import
3 Sichtbarkeitsregeln (Rule Engine) offen
4 Foto-Aufgaben + Bewertungs-Queue offen
5 Karte + GPS-Tracking + GPS-Tasks offen
6 Kombinierte Aufgaben + manuelle Korrekturen offen
7 Chat (Team + Broadcast) offen
8 Polish: Service Worker, Audit-UI, Backups offen

Siehe ARCHITECTURE.md für das vollständige Konzept.

Hinter dem Reverse-Proxy

In Produktion ports: aus docker-compose.yml entfernen, bestehenden Reverse-Proxy ans externe Netz rallye-net hängen, dann routen:

  • /frontend:80
  • /api/backend:8000
  • /ws/backend:8000 (WebSocket-Upgrade, kommt in späteren Phasen)

Backups

docker compose exec -T db pg_dump -U rallye rallye | gzip > "backups/db-$(date +%F).sql.gz"
docker run --rm -v rallye_uploads:/data -v "$(pwd)/backups":/backup alpine \
  tar czf "/backup/uploads-$(date +%F).tar.gz" -C /data .

API-Endpunkte (Phase 2 — Auszug)

Auth

  • POST /api/v1/auth/login/password
  • POST /api/v1/auth/login/team-code
  • GET /api/v1/auth/rallyes/available — Liste auswählbarer Rallyes
  • POST /api/v1/auth/rallyes/select — Body {rallye_id} → neues JWT mit rid-Claim
  • GET /api/v1/auth/me

Teilnehmer (alle implizit auf aktive Rallye gefiltert)

  • GET /api/v1/me/rallye — Info zur aktuellen Rallye
  • GET /api/v1/tasks — sichtbare Aufgaben dieser Rallye
  • GET /api/v1/tasks/{id} — Detail
  • POST /api/v1/tasks/{id}/submissions — Lösung einreichen
  • GET /api/v1/me/submissions
  • GET /api/v1/me/team/score
  • GET /api/v1/scoreboard

Admin: Rallyes

  • GET/POST /api/v1/admin/rallyes
  • GET/PATCH/DELETE /api/v1/admin/rallyes/{id}
  • GET/POST /api/v1/admin/rallyes/{id}/teams — Team verknüpfen, optional Code
  • PATCH/DELETE /api/v1/admin/rallyes/{id}/teams/{team_id}
  • GET/POST /api/v1/admin/rallyes/{id}/tasks — Aufgaben dieser Rallye
  • PATCH/DELETE /api/v1/admin/rallyes/{id}/tasks/{task_id}
  • POST /api/v1/admin/rallyes/{id}/tasks/import — Body {source_task_ids}

Admin: Stammdaten

  • GET /api/v1/admin/tasks?rallye_id=&exclude_rallye_id= — rallye-übergreifender Lookup
  • GET/POST /api/v1/admin/teams
  • GET/PATCH/DELETE /api/v1/admin/teams/{id}
  • GET/POST /api/v1/admin/users
  • PATCH/DELETE /api/v1/admin/users/{id}
  • GET /api/v1/admin/submissions?status=
  • POST /api/v1/admin/submissions/{id}/review

Beispiel-Workflow per API

# 1. Admin login
TOKEN=$(curl -s -X POST http://localhost:8000/api/v1/auth/login/password \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"admin"}' | jq -r .access_token)

# 2. Neue Rallye anlegen
RALLYE_ID=$(curl -s -X POST http://localhost:8000/api/v1/admin/rallyes \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"Sommerrallye 2026","status":"active"}' | jq -r .id)

# 3. Aktive Rallye auswählen (Admin-Session-Kontext)
TOKEN=$(curl -s -X POST http://localhost:8000/api/v1/auth/rallyes/select \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"rallye_id\":\"$RALLYE_ID\"}" | jq -r .access_token)

# 4. Bestehende Tasks aus anderen Rallyes anschauen
curl -s "http://localhost:8000/api/v1/admin/tasks?exclude_rallye_id=$RALLYE_ID" \
  -H "Authorization: Bearer $TOKEN" | jq '.[].title'

# 5. Aufgaben in die neue Rallye importieren
curl -X POST http://localhost:8000/api/v1/admin/rallyes/$RALLYE_ID/tasks/import \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"source_task_ids":["<uuid-1>","<uuid-2>"]}'

# 6. Team mit Code verknüpfen
curl -X POST http://localhost:8000/api/v1/admin/rallyes/$RALLYE_ID/teams \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"team_id":"<team-uuid>","join_code":"SUMMER1"}'

Migrieren auf Phase 2 (über Phase 0/1 hinweg)

Migration 0003_rallyes ist robust: sie legt eine Default-Rallye an, backfillt alle bestehenden Tasks/Submissions/ScoreEvents, schiebt Team.join_code in rallye_teams, droppt dann die alte Spalte. Bestehende Demo-Daten landen in der Default-Rallye, der DEMO123-Code funktioniert weiter.

docker compose up --build -d
docker compose logs -f backend

Projektstruktur (Phase 2 ergänzt)

backend/app/
├── main.py                     # Seed: Admin, Demo Rallye, Demo Team mit Code, Demo Task
├── core/                       # config, security
├── db/
├── game/
│   ├── handlers.py             # Strategy: SolutionWord/Gps/Photo/Combined
│   └── scoring.py              # rallye-aware: emit / team_score / scoreboard
├── models/
│   ├── rallye.py               (NEU)
│   ├── rallye_team.py          (NEU)
│   ├── task.py                 (+ rallye_id, source_task_id)
│   ├── submission.py           (+ rallye_id)
│   ├── score_event.py          (+ rallye_id)
│   ├── user.py                 (+ current_rallye_id)
│   └── team.py                 ( join_code)
├── schemas/
│   ├── rallye.py               (NEU)
│   ├── auth.py                 (+ RallyeSelectRequest, current_rallye_id)
│   └── ...
└── api/
    ├── auth.py                 (+ Rallye-Auswahl-Endpoints, rid-Claim)
    ├── deps.py                 (+ get_current_rallye_id, require_active_rallye)
    ├── admin_rallyes.py        (NEU: CRUD, Team-Linking, Task-Import)
    ├── admin_tasks.py          (Lookup-only über Rallyes hinweg)
    ├── participant.py          (rallye-gefiltert)
    └── ...

frontend/src/
├── App.tsx                     # Routing: Login → Rallye-Auswahl → Hauptbereich
├── lib/
│   ├── api.ts                  (+ availableRallyes, selectRallye, current rallye)
│   └── auth.tsx                (+ selectRallye)
├── pages/
│   ├── RallyeSelectPage.tsx    (NEU)
│   ├── DashboardPage.tsx
│   ├── TasksPage.tsx
│   ├── TaskDetailPage.tsx
│   └── LoginPage.tsx
└── components/
    ├── AppShell.tsx            (Rallye-Name in AppBar, „Rallye wechseln" für Admins)
    ├── StatusBadge.tsx
    └── PlaceholderPage.tsx