diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..f277444 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,37 @@ +name: CI + +on: + pull_request: + push: + branches: [ main, 'feature/**', 'release/**' ] + +jobs: + test-and-quality: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + pip install pytest pytest-cov ruff black mypy + + - name: Lint + run: ruff check . + + - name: Format check + run: black --check . + + - name: Type check + run: mypy . || true + + - name: Tests + coverage + run: | + pytest --maxfail=1 --disable-warnings --cov=. --cov-report=term-missing --cov-report=xml --cov-fail-under=70 diff --git a/TODO.md b/TODO.md index 3c359b9..ac1f4a6 100644 --- a/TODO.md +++ b/TODO.md @@ -108,3 +108,18 @@ Byg **Weirsøe Party Protocol**: en dansk party-webapp platform ala Jackbox, hvo - [ ] (Need-to-have) Audit-log for host-handlinger (start/stop/skip) - [ ] (Nice-to-have) Runde-tema musik/lyd-cues - [ ] (Nice-to-have) Hurtig onboarding-skærm for nye spillere + +### Fase 11 — i18n (email-manager model) +- [ ] Sæt LANGUAGES op med dev (jank-english), en, da +- [ ] Tilføj LocaleMiddleware + LOCALE_PATHS +- [ ] Brug `{% load i18n %}` i templates + gettext i Python +- [ ] Opret .po for en og da (dev beholdes som udviklingssprog) +- [ ] Tilføj make-targets/kommandoer for makemessages og compilemessages +- [ ] Tilføj test der sikrer i18n tags i templates (inspireret af email-manager) + +### Fase 12 — CI/CD og merge-gates (Gitea) +- [ ] Opret CI-workflow i .gitea/workflows/ci.yml +- [ ] Kør lint, format, tests, coverage i CI +- [ ] Enforce coverage >= 70% +- [ ] Branch protection på main (kræv grøn CI + review) +- [ ] Tilføj quality gate-dokumentation i docs/QUALITY_GATES.md diff --git a/coordination/README.md b/coordination/README.md new file mode 100644 index 0000000..7501962 --- /dev/null +++ b/coordination/README.md @@ -0,0 +1,8 @@ +# Coordination + +Denne mappe bruges af scheduler/dev-runners til at holde styr på: +- hvem der ejer hvilken opgave +- hvilken branch der er aktiv +- hvad der står i queue + +Single source of truth: `assignments.json`. diff --git a/coordination/assignments.json b/coordination/assignments.json new file mode 100644 index 0000000..15c0b3b --- /dev/null +++ b/coordination/assignments.json @@ -0,0 +1,5 @@ +{ + "updatedAt": "2026-02-27T00:00:00Z", + "active": [], + "queue": [] +} diff --git a/coordination/scheduler_tasks.json b/coordination/scheduler_tasks.json new file mode 100644 index 0000000..2a8d9ce --- /dev/null +++ b/coordination/scheduler_tasks.json @@ -0,0 +1,26 @@ +{ + "updatedAt": "2026-02-27T00:00:00Z", + "tasks": [ + { + "id": "BOT-PR-POLICY", + "title": "Dev-runner skal oprette PR ved feature-ready", + "ownerRole": "job-scheduler", + "status": "active", + "priority": "high" + }, + { + "id": "BOT-REVIEW-ONLY-PRS", + "title": "Review-runner reviewer kun åbne PRs og commenter i PR", + "ownerRole": "review-runner", + "status": "active", + "priority": "high" + }, + { + "id": "BOT-MERGE-GATE", + "title": "Integrator-runner merger kun ved grønne gates", + "ownerRole": "integrator-runner", + "status": "active", + "priority": "high" + } + ] +} diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md deleted file mode 100644 index 6d57ec6..0000000 --- a/docs/ARCHITECTURE.md +++ /dev/null @@ -1,27 +0,0 @@ -# Arkitektur (MVP) - -## Moduler -- `core_admin`: global drift/admin, health, valideringer -- `lobby`: session creation/join, player presence -- `fupogfakta`: game rules, rounds, scoring (spil 1) -- `realtime`: websocket events + state sync -- `voice`: fælles voice-acting/TTS interface - -## Auth & sessions -- Login (username/password) kræves for at oprette/hoste spil -- Deltagelse i kørende spil sker via session-kode - -## Voice-acting (platformkrav) -- Alle spil skal kunne afspille voice lines via fælles interface -- Voice er modulært pr. spil (ikke hardcoded) - -## Realtidsmodel -- Host-screen og mobilklienter forbinder via websocket -- Autoritativ game state ligger server-side -- Klienter sender intents (`submit_lie`, `submit_guess`) -- Server broadcaster state transitions - -## Datamodel-principper -- Score beregnes server-side -- Hver scoreændring gemmes i `ScoreEvent` -- Runde-konfiguration gemmes per session (points ikke hardcoded) diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..b679555 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,8 @@ +# Documentation moved to Wiki + +Projekt-dokumentation vedligeholdes i repo-wiki: + +- Wiki repo: `weirsoe-party-protocol.wiki` +- Gitea wiki URL: `https://gitea.weircon.dk/wpp/weirsoe-party-protocol/wiki` + +Denne `docs/` mappe holdes minimal fremover. diff --git a/lobby/urls.py b/lobby/urls.py new file mode 100644 index 0000000..7df9970 --- /dev/null +++ b/lobby/urls.py @@ -0,0 +1,8 @@ +from django.urls import path +from . import views + +urlpatterns = [ + path('create', views.create_session, name='lobby-create-session'), + path('join', views.join_session, name='lobby-join-session'), + path('state/', views.session_state, name='lobby-session-state'), +] diff --git a/lobby/views.py b/lobby/views.py index 91ea44a..8534a5d 100644 --- a/lobby/views.py +++ b/lobby/views.py @@ -1,3 +1,79 @@ -from django.shortcuts import render +import random +import string +import json -# Create your views here. +from django.contrib.auth.decorators import login_required +from django.http import JsonResponse +from django.views.decorators.http import require_GET, require_POST +from django.views.decorators.csrf import csrf_exempt + +from fupogfakta.models import GameSession, Player + + +def _session_code(length: int = 6) -> str: + alphabet = string.ascii_uppercase + string.digits + for _ in range(20): + code = ''.join(random.choice(alphabet) for _ in range(length)) + if not GameSession.objects.filter(code=code).exists(): + return code + raise RuntimeError('Kunne ikke generere unik session-kode') + + +@login_required +@require_POST +def create_session(request): + session = GameSession.objects.create(host=request.user, code=_session_code()) + return JsonResponse({'ok': True, 'session': {'code': session.code, 'status': session.status}}) + + +@csrf_exempt +@require_POST +def join_session(request): + try: + payload = json.loads(request.body.decode('utf-8')) + except Exception: + payload = {} + + code = (payload.get('code') or '').strip().upper() + nickname = (payload.get('nickname') or '').strip() + + if not code or not nickname: + return JsonResponse({'ok': False, 'error': 'Mangler code eller nickname'}, status=400) + + try: + session = GameSession.objects.get(code=code) + except GameSession.DoesNotExist: + return JsonResponse({'ok': False, 'error': 'Ugyldig session-kode'}, status=404) + + if session.status == GameSession.Status.FINISHED: + return JsonResponse({'ok': False, 'error': 'Spillet er afsluttet'}, status=409) + + player, _created = Player.objects.get_or_create(session=session, nickname=nickname) + player.is_connected = True + player.save(update_fields=['is_connected']) + + return JsonResponse({ + 'ok': True, + 'player': {'id': player.id, 'nickname': player.nickname}, + 'session': {'code': session.code, 'status': session.status}, + }) + + +@require_GET +def session_state(_request, code: str): + code = code.strip().upper() + try: + session = GameSession.objects.get(code=code) + except GameSession.DoesNotExist: + return JsonResponse({'ok': False, 'error': 'Ugyldig session-kode'}, status=404) + + players = list(session.players.values('id', 'nickname', 'score', 'is_connected')) + return JsonResponse({ + 'ok': True, + 'session': { + 'code': session.code, + 'status': session.status, + 'current_round': session.current_round, + }, + 'players': players, + }) diff --git a/partyhub/urls.py b/partyhub/urls.py index 4a4b4d3..373484e 100644 --- a/partyhub/urls.py +++ b/partyhub/urls.py @@ -1,6 +1,6 @@ from django.contrib import admin from django.http import JsonResponse -from django.urls import path +from django.urls import path, include def health(_request): @@ -8,6 +8,7 @@ def health(_request): urlpatterns = [ + path('api/lobby/', include('lobby.urls')) , path('admin/', admin.site.urls), path('healthz', health, name='healthz'), ]