|
|
||
|---|---|---|
| .claude | ||
| .roo/skills | ||
| backend | ||
| doc | ||
| docs | ||
| frontend | ||
| mapserver | ||
| maptileserver | ||
| plans | ||
| uc51-frontend/frontend/src | ||
| .env.example | ||
| .gitignore | ||
| CLAUDE.md | ||
| deploy.sh | ||
| docker-compose-omv.yml | ||
| docker-compose-prod.yml | ||
| docker-compose.yml | ||
| generate_keys.sh | ||
| generate_vapid.sh | ||
| INSTALL.md | ||
| README.md | ||
| Sozialtag_2026___Sozialtag_42_4__PROD__20260611_2043.json | ||
| test.sh | ||
| uc51-todo.tar.gz | ||
| vorlage-3-slots-3-gruppen.json | ||
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_pingsmit PostGIS Geography(Point) fürgeom - Indizes: GIST auf
geom, BRIN aufrecorded_at, B-Tree auf(user_id, recorded_at desc) User.tracking_consent— Master-Schalter für Standort-Erfassung
- Tabelle:
-
Migration 0006 (
backend/alembic/versions/0006_location_pings.py):- Erstellt
location_pingsTabelle mit PostGIS-Extension - GIST-Index für räumliche Queries, BRIN für Zeitreihen
- Erstellt
-
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 lesenPATCH /api/v1/me/location/consent— Tracking-Consent setzenGET /api/v1/admin/rallyes/{id}/locations— Letzte Position pro TeamGET /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_DWithinmin_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-PositionAdminMapPage(/admin/rallyes/:id/map) — Live-Karte aller TeamsGpsInput— Distanz-Anzeige + "Hier abgeben"-Button für GPS-TasksAdminTaskEditor— 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 Model —
location_pingsTabelle 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 lesenPATCH /api/v1/me/location/consent— Tracking-Consent setzenGET /api/v1/admin/rallyes/{id}/locations— Letzte Position pro TeamGET /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):VisibilityContextmit pre-loadedsolved_task_ids,team_score,now- 6 Prädikate:
task_solved,tasks_solved_count,team_score_gte,team_score_lt,after,before - Operatoren
all/any/notals reine Komposition evaluate_rule(raisable) +evaluate_rule_safe(broken rule ⇒ hidden)validate_rule(rule, allowed_task_ids=…)mit Dangling-Reference-ErkennungMAX_DEPTH=16als DoS-Bremse, emptyany⇒ false, emptyall⇒ true
- Backend-Integration:
participant.py:_is_visible(task, vctx)ruft die Engine; Kontext einmal pro Request, dann pro Aufgabe wiederverwendetphotos.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-KatalogPOST /api/v1/admin/visibility/validate— strukturelle + Referenz-PrüfungPOST /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
AdminTaskEditorals „Sichtbarkeit"-Card - 🔒-Chip in
AdminRallyeDetailfü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:
- Frontend: http://localhost:8080
- Backend Health: http://localhost:8000/api/v1/health
- OpenAPI / Swagger: http://localhost:8000/api/v1/docs
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_SECRETsetzen, Admin-Passwort ändern, Demo-Daten anpassen,CORS_ORIGINSeinschränken.
📦 Beim Upgrade einer bestehenden Installation: Das Backend-Image muss neu gebaut werden (
docker compose build --no-cache backendoderdocker compose up --build), weil das Dockerfile die Python-Dependencies jetzt direkt auspyproject.tomlzieht (pip install .statt hartkodierter Liste). Vorher fehlte z.B. Pillow zur Laufzeit, obwohl es inpyproject.tomlstand.
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)
- Login →
POST /auth/login/password - Verfügbare Rallyes →
GET /auth/rallyes/available - Auswahl →
POST /auth/rallyes/select→ neues JWT mitrid-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ültigstarts_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/passwordPOST /api/v1/auth/login/team-codeGET /api/v1/auth/rallyes/available— Liste auswählbarer RallyesPOST /api/v1/auth/rallyes/select— Body{rallye_id}→ neues JWT mit rid-ClaimGET /api/v1/auth/me
Teilnehmer (alle implizit auf aktive Rallye gefiltert)
GET /api/v1/me/rallye— Info zur aktuellen RallyeGET /api/v1/tasks— sichtbare Aufgaben dieser RallyeGET /api/v1/tasks/{id}— DetailPOST /api/v1/tasks/{id}/submissions— Lösung einreichenGET /api/v1/me/submissionsGET /api/v1/me/team/scoreGET /api/v1/scoreboard
Admin: Rallyes
GET/POST /api/v1/admin/rallyesGET/PATCH/DELETE /api/v1/admin/rallyes/{id}GET/POST /api/v1/admin/rallyes/{id}/teams— Team verknüpfen, optional CodePATCH/DELETE /api/v1/admin/rallyes/{id}/teams/{team_id}GET/POST /api/v1/admin/rallyes/{id}/tasks— Aufgaben dieser RallyePATCH/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 LookupGET/POST /api/v1/admin/teamsGET/PATCH/DELETE /api/v1/admin/teams/{id}GET/POST /api/v1/admin/usersPATCH/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