diff --git a/AGENTS.md b/AGENTS.md index 1c0433b..0c8faed 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -43,8 +43,8 @@ Agents mogen NOOIT: - code buiten deze repository aanpassen - andere Podman containers aanpassen - systeemconfiguratie wijzigen -- Agents mogen geen wijzigingen maken die het contract in contracts/API_GOLDEN.md breken. -- Agents mogen geen refactors of verbeteringen uitvoeren die niet expliciet gevraagd zijn. +- wijzigingen maken die het contract in contracts/API_GOLDEN.md breken +- refactors of verbeteringen uitvoeren die niet expliciet gevraagd zijn Agents mogen alleen werken **binnen deze repository**. @@ -56,14 +56,14 @@ Agents mogen alleen werken **binnen deze repository**. De volgende bestanden zijn kritisch en mogen alleen aangepast worden na expliciete bevestiging van de gebruiker: Deze bestanden bevatten infrastructuur en API-contracten. -app/services/tvdb_auth_service.py -container/Containerfile -requirements.txt -contracts/API_GOLDEN.md -AGENTS.md -CHANGE_POLICY.md -SAFE_FILES.md -docs/ARCHITECTURE.md +- app/services/tvdb_auth_service.py +- container/Containerfile +- requirements.txt +- contracts/API_GOLDEN.md +- AGENTS.md +- CHANGE_POLICY.md +- SAFE_FILES.md +- docs/ARCHITECTURE.md --- @@ -118,6 +118,13 @@ Dit script valideert: Als een regressietest faalt moet de wijziging worden teruggedraaid. +Test-URL policy: + +- Op de host moet standaard getest worden via `http://127.0.0.1:8085` +- In een sandbox of containerized AI-omgeving moet getest worden via `http://host.containers.internal:8085` +- Testscripts moeten automatisch één van beide kunnen detecteren +- Agents mogen niet aannemen dat `127.0.0.1:8085` vanuit de sandbox naar de host verwijst + --- # Feature Test Policy @@ -145,9 +152,7 @@ Regressietests mogen **geen nieuw TVDB-token forceren**. De volgende tests moeten altijd slagen: -curl 127.0.0.1:8085/api/health -curl 127.0.0.1:8085/api/tvdb/auth-status -curl "127.0.0.1:8085/api/tvdb/search?q=elsbeth" +./regression_tests.sh Als een van deze tests faalt moet de wijziging worden teruggedraaid. diff --git a/app/api/tvdb.py b/app/api/tvdb.py index 72ebf21..dd67e89 100644 --- a/app/api/tvdb.py +++ b/app/api/tvdb.py @@ -55,3 +55,123 @@ def search_series(q: str = Query(..., min_length=1)): return {"items": normalized} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) + + +def _series_display_name(name: str | None, year: int | str | None) -> str | None: + if name and year: + return f"{name} ({year})" + return name + + +def _extract_episodes(payload: dict) -> list[dict]: + data = payload.get("data") + if isinstance(data, list): + return data + if isinstance(data, dict): + episodes = data.get("episodes") + if isinstance(episodes, list): + return episodes + return [] + + +def _fetch_aired_episodes(client: TvdbClient, series_id: int) -> list[dict]: + attempts = [ + (f"/series/{series_id}/episodes/default", {"seasonType": "aired"}), + (f"/series/{series_id}/episodes/official", None), + ] + errors = [] + + for path, params in attempts: + try: + payload = client.get(path, params=params) + items = _extract_episodes(payload) + if items: + return items + except Exception as exc: + errors.append(f"{path}: {exc}") + + raise RuntimeError( + "Unable to load aired episodes from TVDB; tried known episode endpoints" + if not errors + else f"Unable to load aired episodes from TVDB ({'; '.join(errors)})" + ) + + +@router.get("/series/{series_id}/episodes") +def get_series_episodes( + series_id: int, + order_type: str = Query("aired", min_length=1), +): + if order_type.lower() != "aired": + raise HTTPException( + status_code=400, + detail="Unsupported order_type. Only 'aired' is supported in this MVP.", + ) + + client = TvdbClient() + + try: + series_payload = client.get(f"/series/{series_id}") + series_data = series_payload.get("data", {}) if isinstance(series_payload, dict) else {} + series_name = ( + series_data.get("name") + or series_data.get("seriesName") + or series_data.get("slug") + ) + series_year = series_data.get("year") + + episodes = _fetch_aired_episodes(client, series_id) + normalized = [] + + for item in episodes: + season_number = ( + item.get("seasonNumber") + or item.get("airedSeason") + or item.get("season") + or 0 + ) + episode_number = ( + item.get("number") + or item.get("airedEpisodeNumber") + or item.get("episodeNumber") + or 0 + ) + title = item.get("name") or item.get("episodeName") or "" + aired_on = item.get("aired") or item.get("firstAired") + + label = None + try: + season_num = int(season_number) + episode_num = int(episode_number) + label = f"S{season_num:02}E{episode_num:02} - {title}" + if aired_on: + label = f"{label} - {aired_on}" + except (TypeError, ValueError): + pass + + normalized.append( + { + "id": item.get("id"), + "season_number": season_number, + "episode_number": episode_number, + "title": title, + "aired": aired_on, + "label": label, + "raw": item, + } + ) + + return { + "series": { + "id": series_data.get("id") or series_id, + "name": series_name, + "year": series_year, + "display_name": _series_display_name(series_name, series_year), + }, + "order_type": "aired", + "items": normalized, + } + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) diff --git a/container/Containerfile b/container/Containerfile index 3cd2bd4..fef304b 100644 --- a/container/Containerfile +++ b/container/Containerfile @@ -1,12 +1,26 @@ -Ik heb een paar opmerkingen +FROM python:3.12-slim -AGENTS.md -Regression testst -curl -X POST 127.0.0.1:8085/api/tvdb/login -zorgt deze curl er niet continue voor dat er een nieuw token wordt aangevraagd en dat thetvdb mij dan gaat blokkeren? +WORKDIR /app -ARCHITECTURE.md -Ik wil de volumes voor de videobestanden graag als volgt. Dat komt overeen met de host en maakt het herkenbaarder +# Install minimal system dependencies +RUN apt-get update && apt-get install -y \ + curl \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* -/Volumes/8TB/Shared_Folders/TV_Shows → /Volumes/8TB/Shared_Folders/TV_Shows -/Volumes/8TB_RAID1/Shared_Folders/Library/TV_Shows → /Volumes/8TB_RAID1/Shared_Folders/Library/TV_Shows \ No newline at end of file +# Copy requirements +COPY requirements.txt . + +RUN pip install --no-cache-dir -r requirements.txt + +# Copy app +COPY app /app/app + +# Create runtime dirs +RUN mkdir -p /app/data + +EXPOSE 8080 + +ENV PYTHONUNBUFFERED=1 + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"] diff --git a/feature_test_template.sh b/feature_test_template.sh index f1be18a..b55c0ec 100755 --- a/feature_test_template.sh +++ b/feature_test_template.sh @@ -1,7 +1,16 @@ #!/usr/bin/env bash set -euo pipefail -BASE_URL="${BASE_URL:-http://127.0.0.1:8085}" +if [ -z "${BASE_URL:-}" ]; then + if curl --silent --fail http://127.0.0.1:8085/api/health >/dev/null 2>&1; then + BASE_URL="http://127.0.0.1:8085" + elif curl --silent --fail http://host.containers.internal:8085/api/health >/dev/null 2>&1; then + BASE_URL="http://host.containers.internal:8085" + else + echo "ERROR: could not determine BASE_URL. Tried 127.0.0.1 and host.containers.internal." >&2 + exit 1 + fi +fi TMP_DIR="$(mktemp -d)" trap 'rm -rf "$TMP_DIR"' EXIT diff --git a/feature_tests_episodes.sh b/feature_tests_episodes.sh new file mode 100755 index 0000000..dc81c28 --- /dev/null +++ b/feature_tests_episodes.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [ -z "${BASE_URL:-}" ]; then + if curl --silent --fail http://127.0.0.1:8085/api/health >/dev/null 2>&1; then + BASE_URL="http://127.0.0.1:8085" + elif curl --silent --fail http://host.containers.internal:8085/api/health >/dev/null 2>&1; then + BASE_URL="http://host.containers.internal:8085" + else + echo "ERROR: could not determine BASE_URL. Tried 127.0.0.1 and host.containers.internal." >&2 + exit 1 + fi +fi +SERIES_ID="${SERIES_ID:-430543}" +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TMP_DIR"' EXIT + +echo "== Feature test 1: episodes endpoint returns series + items ==" +curl --fail --silent --show-error \ + "${BASE_URL}/api/tvdb/series/${SERIES_ID}/episodes?order_type=aired" \ + -o "${TMP_DIR}/episodes.json" + +cat "${TMP_DIR}/episodes.json" + +python3 - "${TMP_DIR}/episodes.json" <<'PY' +import json +import sys +from pathlib import Path + +path = Path(sys.argv[1]) +data = json.loads(path.read_text(encoding="utf-8")) + +assert isinstance(data, dict), "episodes response must be an object" +assert "series" in data and isinstance(data["series"], dict), "episodes response missing series object" +assert data.get("order_type") == "aired", "order_type must be aired" +assert "items" in data and isinstance(data["items"], list), "episodes response missing items list" + +series = data["series"] +for key in ["id", "name", "year", "display_name"]: + assert key in series, f"series missing key: {key}" + +print("episodes endpoint JSON validation passed") +PY + +echo +echo "== Feature test 2: unsupported order_type is rejected ==" +curl --silent --show-error \ + -o "${TMP_DIR}/episodes_bad_order.json" \ + -w "%{http_code}" \ + "${BASE_URL}/api/tvdb/series/${SERIES_ID}/episodes?order_type=absolute" \ + > "${TMP_DIR}/episodes_bad_order.status" + +cat "${TMP_DIR}/episodes_bad_order.json" + +python3 - "${TMP_DIR}/episodes_bad_order.status" "${TMP_DIR}/episodes_bad_order.json" <<'PY' +import json +import sys +from pathlib import Path + +status = Path(sys.argv[1]).read_text(encoding="utf-8").strip() +data = json.loads(Path(sys.argv[2]).read_text(encoding="utf-8")) + +assert status == "400", f"expected HTTP 400, got {status}" +assert isinstance(data, dict), "error response must be an object" +assert "detail" in data, "error response missing detail" + +print("unsupported order_type validation passed") +PY + +echo +echo "== Feature test 3: search output keeps year/display_name contract ==" +curl --fail --silent --show-error \ + "${BASE_URL}/api/tvdb/search?q=elsbeth" \ + -o "${TMP_DIR}/search.json" + +cat "${TMP_DIR}/search.json" + +python3 - "${TMP_DIR}/search.json" <<'PY' +import json +import sys +from pathlib import Path + +path = Path(sys.argv[1]) +data = json.loads(path.read_text(encoding="utf-8")) + +assert isinstance(data, dict), "search response must be an object" +assert "items" in data and isinstance(data["items"], list), "search response missing items list" +assert len(data["items"]) > 0, "search items list must not be empty" + +first = data["items"][0] +for key in ["id", "name", "year", "display_name"]: + assert key in first, f"search item missing key: {key}" + +print("search contract validation passed") +PY + +echo +echo "All episodes feature tests passed." diff --git a/regression_tests.sh b/regression_tests.sh index 4b575ef..19514bb 100755 --- a/regression_tests.sh +++ b/regression_tests.sh @@ -1,7 +1,16 @@ #!/usr/bin/env bash set -euo pipefail -BASE_URL="${BASE_URL:-http://127.0.0.1:8085}" +if [ -z "${BASE_URL:-}" ]; then + if curl --silent --fail http://127.0.0.1:8085/api/health >/dev/null 2>&1; then + BASE_URL="http://127.0.0.1:8085" + elif curl --silent --fail http://host.containers.internal:8085/api/health >/dev/null 2>&1; then + BASE_URL="http://host.containers.internal:8085" + else + echo "ERROR: could not determine BASE_URL. Tried 127.0.0.1 and host.containers.internal." >&2 + exit 1 + fi +fi TMP_DIR="$(mktemp -d)" trap 'rm -rf "$TMP_DIR"' EXIT diff --git a/rename-mvp-codex-definitieve-instructie.md b/rename-mvp-codex-definitieve-instructie.md new file mode 100644 index 0000000..6639e31 --- /dev/null +++ b/rename-mvp-codex-definitieve-instructie.md @@ -0,0 +1,639 @@ +# Rename-MVP — Definitieve Codex instructie en technisch ontwerp + +## Doel + +Bouw verder aan een minimalistische webapplicatie als uitgeklede webversie van **Rename My TV Series**. + +De applicatie moet: + +- draaien in een **rootless Podman container** +- lokale mediabestanden kunnen hernoemen binnen expliciet toegestane videomappen +- metadata ophalen via **TheTVDB v4 API** +- een **4-panel interface** hebben +- altijd eerst een **preview** tonen voordat bestanden echt worden hernoemd +- veilig omgaan met paden, tokens en bestandsoperaties +- de **serienaam inclusief jaartal** gebruiken in de UI en in de bestandsnaam + +Dit project heeft al een **werkende baseline**: +- container build werkt +- FastAPI health endpoint werkt +- TVDB login werkt +- JWT parsing werkt +- token persistence werkt +- auth-status endpoint werkt +- TVDB search werkt +- `token_source` werkt + +Belangrijk: dit project is nu **geen greenfield ontwerp meer**, maar een **bestaand werkend project** dat gecontroleerd verder moet worden uitgebouwd. + +--- + +# 1. Technische context + +## Hostomgeving + +- Host OS: Debian Trixie +- Podman version: 5.4.2 +- Project root: `/home/kodi/.config/rename-mvp` + +## Runtime + +- Rootless Podman +- FastAPI backend +- SQLite voor app-state en logging +- JSON voor TVDB auth state +- TheTVDB API v4 +- Lokale tests via `127.0.0.1:8085` + +## Bestaande paden + +### Auth state JSON +- Host: `/home/kodi/.config/rename-mvp/data/tvdb_auth.json` +- Container: `/app/data/tvdb_auth.json` + +### Werkende testscripts +- `/home/kodi/.config/rename-mvp/regression_tests.sh` +- `/home/kodi/.config/rename-mvp/feature_test_template.sh` + +### Governance-bestanden +- `/home/kodi/.config/rename-mvp/AGENTS.md` +- `/home/kodi/.config/rename-mvp/CHANGE_POLICY.md` +- `/home/kodi/.config/rename-mvp/SAFE_FILES.md` +- `/home/kodi/.config/rename-mvp/docs/ARCHITECTURE.md` +- `/home/kodi/.config/rename-mvp/contracts/API_GOLDEN.md` + +--- + +# 2. Niet-onderhandelbare regels + +## 2.1 Geen contractbreuk + +Je mag geen bestaande API endpoints of bestaande response-structuren breken. + +Je mag: +- nieuwe velden toevoegen +- nieuwe endpoints toevoegen + +Je mag niet: +- bestaande veldnamen wijzigen +- bestaande velden verwijderen +- response-structuren wijzigen +- bestaande endpoints hernoemen +- bestaande endpoints verwijderen + +`contracts/API_GOLDEN.md` is leidend. + +## 2.2 Geen speculative improvements + +Voer **geen refactors, herstructureringen of “verbeteringen” uit die niet expliciet nodig zijn voor de gevraagde wijziging**. + +Als een wijziging niet strikt nodig is om het gevraagde doel te bereiken, doe die wijziging niet. + +## 2.3 Safe files + +De volgende bestanden zijn beschermd en mogen alleen gewijzigd worden als dat expliciet noodzakelijk is en als je het duidelijk benoemt: + +- `app/services/tvdb_auth_service.py` +- `container/Containerfile` +- `requirements.txt` +- `contracts/API_GOLDEN.md` +- `AGENTS.md` +- `CHANGE_POLICY.md` +- `SAFE_FILES.md` +- `docs/ARCHITECTURE.md` + +## 2.4 Scopebeperking + +Werk alleen binnen deze repository: + +Pad op de host: `/home/kodi/.config/rename-mvp` +Pad in deze codex omgeving: `/workspace/rename-mvp` + + +Je mag niets aanpassen buiten deze projectscope. + +## 2.5 Escalated execution + +Escalated execution is toegestaan binnen deze projectscope voor: + +- `podman build` +- `podman run` +- curl tests +- lokale build- en testacties +- bestanden aanmaken of aanpassen binnen dit project +- dependency install binnen dit project + +Niet toegestaan: + +- systeembrede wijzigingen +- firewallwijzigingen +- system upgrades +- andere repositories +- andere stacks of containers aanpassen +- destructieve cleanup acties buiten deze projectscope + +--- + +# 3. Bestaande baseline die behouden moet blijven + +Deze functionaliteit werkt al en mag niet stuk: + +## 3.1 Health endpoint +`GET /api/health` + +Expected: +```json +{"status":"ok"} +``` + +## 3.2 TVDB login endpoint +`POST /api/tvdb/login` + +Doet een expliciete forced login en vraagt een nieuw token aan. + +Deze endpoint is **alleen voor debugging en handmatige auth-tests**, niet voor regressietests. + +## 3.3 TVDB auth-status endpoint +`GET /api/tvdb/auth-status` + +Deze endpoint moet de actuele auth-status tonen inclusief: +- configured +- has_token +- issued_at +- expires_at +- expires_at_unix +- renew_after +- renew_after_unix +- is_expired +- is_renewal_due +- last_login_attempt_at +- last_login_success_at +- last_login_status +- last_login_error +- jwt_payload +- token_source + +## 3.4 TVDB search endpoint +`GET /api/tvdb/search?q=elsbeth` + +Deze endpoint werkt al en moet series teruggeven met ten minste: +- id +- name +- year +- display_name + +De UI en bestandsnaamopbouw moeten het **jaartal van de serie** gebruiken. + +## 3.5 Token lifecycle +De TVDB bearer token is een JWT. +Gebruik: +- `exp` als primaire bron voor expiry +- `iat` indien aanwezig +- renew alleen wanneer nodig +- niet onnodig opnieuw `/login` doen + +De token mag **niet continu vernieuwd worden**. + +## 3.6 token_source +De auth-state gebruikt: +- `none` +- `login` +- `cached` +- `renewed` + +Dit gedrag moet behouden blijven. + +--- + +# 4. Functioneel doel van de applicatie + +De uiteindelijke webapp moet een minimalistische 4-panel interface hebben. + +## Paneel 1 — TVDB Search +Functie: +- zoekterm invoeren +- zoeken via backend naar TheTVDB +- resultaten tonen +- gekozen serie details tonen + +Zoekresultaten moeten minimaal tonen: +- serienaam +- jaar +- first aired indien beschikbaar +- network indien beschikbaar +- poster indien beschikbaar + +De series moeten in de UI worden weergegeven als bijvoorbeeld: + +- `Elsbeth (2024)` +- `The Office (2005)` + +## Paneel 2 — Episodes +Functie: +- afleveringen van gekozen serie tonen +- minimaal `Aired Order` ondersteunen +- afleveringen per seizoen tonen +- gebruiker kan willekeurige afleveringen selecteren + +Afleveringen tonen bijvoorbeeld: +- `S02E03 - Devil's Night - 2025-03-13` + +## Paneel 3 — Selected Episodes +Functie: +- lijst van gekozen afleveringen voor de huidige sessie + +Functionaliteit: +- toevoegen +- verwijderen +- clear +- reorder +- move up / move down + +## Paneel 4 — Selected Files +Functie: +- lijst van gekozen lokale videobestanden + +Functionaliteit: +- Add Files +- Add Directory +- Sort +- Remove +- Clear +- Move Up / Move Down + +## Workflow +1. user zoekt serie +2. user kiest serie +3. backend haalt afleveringen op +4. user voegt afleveringen toe aan selected episodes +5. user voegt bestanden toe aan selected files +6. backend maakt een 1-op-1 mapping op basis van volgorde +7. backend maakt preview +8. user bevestigt +9. backend voert rename uit + +--- + +# 5. Bestandsnaamopbouw + +De standaard bestandsnaam moet zijn: + +```text +{series} ({year}) - S{season:02}E{episode:02} - {title}{ext} +``` + +Voorbeeld: + +```text +Elsbeth (2024) - S02E03 - Devil's Night.mkv +``` + +Belangrijk: +- het **serienaam-jaartal** is verplicht onderdeel van de naam +- het jaartal komt uit TheTVDB serie metadata +- de UI moet dit jaartal ook tonen + +De eerste MVP hoeft **airdate niet per se in de filename** te zetten zolang het afgesproken template hierboven intact blijft. + +--- + +# 6. TVDB authenticatie en tokenbeheer + +## 6.1 Authflow +Gebruik de officiële TVDB v4 flow: +- `POST /login` +- body bevat `apikey` +- `pin` alleen meesturen als aanwezig +- response bevat bearer token + +## 6.2 JWT verwerking +De token is een JWT. + +Verplicht: +- decodeer payload +- lees `exp` +- lees `iat` indien aanwezig +- gebruik `exp` als primaire expiry-bron + +## 6.3 Opslag +TVDB auth state wordt persistent opgeslagen in: + +`/app/data/tvdb_auth.json` + +Hostpad: + +`/home/kodi/.config/rename-mvp/data/tvdb_auth.json` + +In JSON mogen staan: +- token +- issued_at +- expires_at +- expires_at_unix +- renew_after +- renew_after_unix +- last_login_attempt_at +- last_login_success_at +- last_login_status +- last_login_error +- jwt_payload +- token_source + +Niet in JSON: +- `TVDB_API_KEY` +- `TVDB_PIN` + +Die moeten uit environment variables komen. + +## 6.4 Renew-logica +Renew alleen wanneer: +- `now >= renew_after` +- of TVDB `401` teruggeeft + +Niet continu nieuwe tokens opvragen. + +`POST /api/tvdb/login` is geen regressietest en geen normale flow; het is een debug endpoint. + +--- + +# 7. Toegestane videopaden + +De container moet dezelfde videopaden gebruiken als de host. + +## Allowed media roots + +- `/Volumes/8TB/Shared_Folders/TV_Shows` +- `/Volumes/8TB_RAID1/Shared_Folders/Library/TV_Shows` + +## Volume mappings + +- `/Volumes/8TB/Shared_Folders/TV_Shows -> /Volumes/8TB/Shared_Folders/TV_Shows` +- `/Volumes/8TB_RAID1/Shared_Folders/Library/TV_Shows -> /Volumes/8TB_RAID1/Shared_Folders/Library/TV_Shows` +- `/home/kodi/.config/rename-mvp/data -> /app/data` + +De app mag alleen lezen/schrijven binnen een van de toegestane media roots. + +--- + +# 8. Securitymodel + +## Bestandspaden +Nooit: +- absolute clientpaden blind vertrouwen +- `..` toestaan +- buiten allowed media roots resolven + +Altijd: +- path validation doen +- binnen allowlist van media roots blijven +- alleen toegestane extensies accepteren +- preview afdwingen vóór rename + +## Allowed extensions MVP +- `.mkv` +- `.mp4` +- `.avi` +- `.m4v` +- `.srt` + +## Logging +Nooit loggen: +- API keys +- bearer tokens +- env secrets + +--- + +# 9. Container en runtime + +## Containerfile +De bestaande werkende runtime moet behouden blijven. + +## Rootless Podman +De applicatie draait rootless. + +## Lokale test-URL +Gebruik voor runtime-tests de BASE_URL die bereikbaar is vanuit de huidige omgeving: + +- op de host meestal: http://127.0.0.1:8085 +- in sandbox/container-omgevingen meestal: http://host.containers.internal:8085 + +Gebruik niet automatisch localhost. + +De testscripts moeten BASE_URL automatisch detecteren of expliciet meegegeven krijgen. + +--- + +# 10. Verwachte repositorystructuur + +Houd deze structuur aan. Geen onnodige nieuwe mappen toevoegen. + +```text +/home/kodi/.config/rename-mvp +├── AGENTS.md +├── CHANGE_POLICY.md +├── SAFE_FILES.md +├── regression_tests.sh +├── feature_test_template.sh +├── requirements.txt +├── .env +├── .env.example +├── README.md +├── app/ +├── container/ +├── data/ +├── contracts/ +│ └── API_GOLDEN.md +└── docs/ + └── ARCHITECTURE.md +``` + +--- + +# 11. API-contract dat behouden moet blijven + +## 11.1 Health +`GET /api/health` + +Response: +```json +{"status":"ok"} +``` + +## 11.2 TVDB Login +`POST /api/tvdb/login` + +Response bevat ten minste: +```json +{ + "status": "ok", + "issued_at": "...", + "expires_at": "...", + "renew_after": "..." +} +``` + +## 11.3 TVDB Auth Status +`GET /api/tvdb/auth-status` + +Response bevat ten minste: +```json +{ + "configured": true, + "has_token": true, + "expires_at": "...", + "renew_after": "...", + "token_source": "cached" +} +``` + +Meer velden zijn toegestaan, minder niet. + +## 11.4 TVDB Search +`GET /api/tvdb/search?q=query` + +Response bevat ten minste: +```json +{ + "items": [ + { + "id": "...", + "name": "...", + "year": "...", + "display_name": "..." + } + ] +} +``` + +--- + +# 12. Testbeleid + +## 12.1 Verplichte regressietests na elke wijziging +Na **iedere wijziging** moet dit script worden uitgevoerd: + +```bash +./regression_tests.sh +``` + +Dit script is verplicht. + +Het valideert de bestaande baseline en mag geen nieuw TVDB-token forceren. + +## 12.2 Verplichte feature tests +Na iedere wijziging moet je daarnaast **drie tests uitvoeren voor de nieuwe of gewijzigde functionaliteit**. + +Gebruik hiervoor als basis: + +```bash +./feature_test_template.sh +``` + +Deze template moet per feature worden aangepast zodat de nieuwe functionaliteit echt en mechanisch getest wordt. + +## 12.3 Geen forced login in regressie +Verboden als standaard regressietest: + +```bash +curl -X POST 127.0.0.1:8085/api/tvdb/login +``` + +Deze endpoint is alleen voor debugging. + +--- + +# 13. Ontwikkelworkflow voor Codex + +Bij iedere opdracht moet je dit proces volgen: + +1. lees eerst de bestaande governance-bestanden: + - `AGENTS.md` + - `CHANGE_POLICY.md` + - `SAFE_FILES.md` + - `docs/ARCHITECTURE.md` + - `contracts/API_GOLDEN.md` + +2. vat kort samen wat je gaat wijzigen + +3. benoem welke bestanden je wilt aanpassen + +4. voer alleen de strikt noodzakelijke wijziging uit + +5. draai verplichte regressietests: + - `./regression_tests.sh` + +6. draai drie feature tests voor de nieuwe functionaliteit + +7. rapporteer: + - welke bestanden zijn aangepast + - welke regressietests zijn uitgevoerd + - welke feature tests zijn uitgevoerd + - welke risico’s of beperkingen er nog zijn + +--- + +# 14. Eerste concrete opdracht aan Codex + +Begin niet meteen met een brede refactor. + +Begin met een gecontroleerde volgende stap. + +## Eerste aanbevolen fase +Bouw de volgende functionaliteit uit: + +### Doel +- endpoint om afleveringen van een gekozen TVDB serie op te halen +- minimaal ondersteuning voor `aired order` +- seriegegevens inclusief `year` behouden +- output geschikt maken voor paneel 2 van de UI + +### Verwachte richting +Bijvoorbeeld iets als: + +`GET /api/tvdb/series/{series_id}/episodes?order_type=aired` + +Maar: +- breek geen bestaande endpoints +- wijzig geen bestaande auth-logica +- wijzig geen container setup +- gebruik bestaande TVDB token lifecycle +- voeg tests toe + +### Voor deze wijziging verplicht +1. regressietests blijven slagen +2. drie feature tests toevoegen/uitvoeren voor het nieuwe episodes endpoint +3. output JSON valideren +4. zoekresultaten en year-handling niet breken + +--- + +# 15. Wat je uitdrukkelijk niet moet doen + +- geen speculative refactors +- geen auth-systeem herschrijven +- geen token lifecycle vereenvoudigen +- geen nieuwe frameworkkeuzes introduceren +- geen projectstructuur wijzigen +- geen governance-bestanden wijzigen tenzij expliciet nodig en gemeld +- geen forced login gebruiken als regressietest +- geen container- of pathmodel veranderen + +--- + +# 16. Samenvatting voor Codex + +Dit project heeft al een werkende baseline. + +Jouw taak is niet om het project opnieuw te ontwerpen, maar om het gecontroleerd uit te bouwen. + +Belangrijkste regels: +- behoud werkende baseline +- breek geen API contract +- gebruik JWT `exp` als expiry-bron +- renew token alleen wanneer nodig +- gebruik seriejaar in UI en filename +- werk alleen binnen allowed media roots +- draai altijd `./regression_tests.sh` +- draai altijd drie feature tests voor nieuwe functionaliteit +- geen speculative improvements +- escalated execution is toegestaan binnen alleen deze projectscope