Files
manage/.pi-tmp/svc-hub-slice2-out.md
T
Developer a8dfbd5dc6 Cleanup: delete dead top-level pages + update docs (Slice 11)
Delete the old top-level page files whose content was migrated into
service-page tabs in slices 5-9:
- pages/Media.tsx, Applications.tsx (-> MediaTab)
- pages/FileBrowser.tsx, FileBrowser.impl.tsx (-> FilesTab)
- pages/Actions.tsx (-> ActionsTab)
- pages/Users.tsx, UsersPage.impl.tsx (replaced by Authentik tabs)
- components/BackupsPage.tsx (-> JobsTab)
- components/ObservabilityPage.tsx (split into Alerts/Links/Metrics tabs)
- hooks/useUsers.ts (orphaned after Users page deletion)
- the corresponding page test files (Media, FileBrowser, Applications,
  Actions, UsersPage) that tested the deleted pages directly.

The service-tab components are the live implementations; ServicePage
renders them. No live code references the deleted files.

Docs: append an Information Architecture section to REQUIREMENTS.md
documenting the services-as-hub model (nav shape, service-page tabs,
service type registry, Users->Authentik, Observability split, legacy
route 404s, empty state). Add a CHANGELOG entry under [Unreleased].

92 frontend tests pass (was 112; -20 deleted page tests); 271 backend
tests pass; lint/build green.

Refs openspec/changes/services-as-hub-ia/ (tasks slice 11).
2026-06-26 20:11:02 +00:00

7.1 KiB

Slice 2 — Authentik directory client + endpoint (worker output)

Files changed

File Status Lines
backend/src/media_library_viewer_api/clients/authentik.py new 133
backend/src/media_library_viewer_api/routers/authentik_users.py new 88
backend/src/media_library_viewer_api/main.py modified +4 / -1
backend/tests/test_authentik_client.py new 175

Total: ~400 changed lines (400 insertions, 1 deletion). At the 400-line budget.

What was implemented

2.1 — AuthentikClient (clients/authentik.py)

  • AuthentikClient(base_url, api_token, timeout=10.0) — mirrors the JellyseerrClient pattern.
  • requests.Session() with Authorization: Bearer <token> header + Accept: application/json.
  • base_url normalization: rstrip "/" and strip trailing /api/v3 suffix.
  • get(path, **params) helper — same error-logging pattern as JellyseerrClient (raise_for_status with detail text on HTTPError).
  • users(search, page, page_size) — calls GET /api/v3/core/users/ with query params search, page, page_size. Normalizes the Authentik {pagination: {count}, results: [...]} response shape into {items, total, page, page_size}. Handles empty results and non-dict payloads defensively.
  • ValueError on empty base_url or api_token.
  • Module-level logger.

2.2 — Directory endpoint (routers/authentik_users.py)

  • GET /api/services/authentik/{service_id}/users — resolves the service record, builds an AuthentikClient from config + decrypted api_token secret, calls users().
  • Query params: search: str | None = None, page: int = 1, page_size: int = 50.
  • Graceful error handling matching monitoring.py's pattern:
    • Service not configured → {"items": [], "total": 0, ..., "error": "Authentik service not configured"} with 200.
    • Request failure → {"items": [], ..., "error": "Authentik is unreachable"} with 200, logs the exception.
  • _resolve_service_record helper copied into the new router (type-specific to authentik; the monitoring.py one is generic but takes service_type as a param — copying keeps the new router self-contained without restructuring monitoring.py).
  • Router registered in main.py.

Authentik API endpoint shape

GET /api/services/authentik/{service_id}/users?search=ali&page=1&page_size=50

Response (success):
{
  "items": [{"pk": 1, "username": "alice", "email": "...", "avatar": "...", ...}],
  "total": 42,
  "page": 1,
  "page_size": 50
}

Response (not configured / unreachable):
{
  "items": [],
  "total": 0,
  "page": 1,
  "page_size": 50,
  "error": "Authentik service not configured" | "Authentik is unreachable"
}

Validation

cd backend && .venv/bin/ruff check src/ tests/    → All checks passed!
cd backend && .venv/bin/python -m pytest tests/   → 268 passed, 2 warnings (pre-existing)

New tests: 12 (8 client unit tests + 3 endpoint integration tests + 1 get URL/params assertion).

Deviations from design

  1. _resolve_service_record copied rather than imported. The monitoring.py helper takes (store, service_type, service_id) and is tightly coupled to monitoring's imports. Copying the ~15 lines into the new router (hardcoding service_type="authentik") keeps the new router self-contained. A follow-up refactor could extract a shared resolve_service_record utility.

  2. timeout config parsing is guarded. Added a try/except (TypeError, ValueError) around float(config.get("timeout_seconds") or 10) to handle a malformed config value gracefully (falls back to 10.0). Minor defensive addition not named in the design.

skill_resolution

none — no project/user SKILL.md paths were injected, and no .atl/skill-registry.md was found.

Residual risks

  • The Authentik directory API field coverage (avatar, is_active, attributes, groups, etc.) is not pinned — the client returns raw user dicts and the frontend (Slice 8 UsersTab) will pick fields. Some fields the old compose flow used (Jellyfin activity state, Jellyseerr enrichment) will not be available from Authentik.
  • _resolve_service_record is duplicated across monitoring.py and the new authentik_users.py. A shared utility extraction is a follow-up.

Acceptance

{
  "criteriaSatisfied": [
    {
      "id": "criterion-1",
      "status": "satisfied",
      "evidence": "Slice 2 implements AuthentikClient + directory endpoint + tests without widening scope (only authentik.py, authentik_users.py, main.py, test file). Mirrors JellyseerrClient + monitoring.py patterns. 268 backend tests pass; ruff clean."
    }
  ],
  "changedFiles": [
    "backend/src/media_library_viewer_api/clients/authentik.py",
    "backend/src/media_library_viewer_api/routers/authentik_users.py",
    "backend/src/media_library_viewer_api/main.py",
    "backend/tests/test_authentik_client.py"
  ],
  "testsAddedOrUpdated": [
    "backend/tests/test_authentik_client.py"
  ],
  "commandsRun": [
    {
      "command": "cd backend && .venv/bin/ruff check src/ tests/",
      "result": "passed",
      "summary": "All checks passed (after --fix import sorting)"
    },
    {
      "command": "cd backend && .venv/bin/python -m pytest tests/test_authentik_client.py -v",
      "result": "passed",
      "summary": "12 passed (8 client + 4 endpoint)"
    },
    {
      "command": "cd backend && .venv/bin/python -m pytest tests/ -q",
      "result": "passed",
      "summary": "268 passed, 2 warnings (pre-existing deprecation warnings)"
    },
    {
      "command": "git diff --cached --stat",
      "result": "passed",
      "summary": "Empty — no staged files"
    }
  ],
  "validationOutput": [
    "AuthentikClient mirrors JellyseerrClient: Session, Bearer header, base_url normalization, get() helper with raise_for_status + detail logging.",
    "users() normalizes Authentik {pagination, results} into {items, total, page, page_size}; handles empty + non-dict payloads.",
    "GET /api/services/authentik/{id}/users resolves service record, builds client from decrypted secret, returns graceful error dict on not-configured/unreachable (200, matching monitoring.py).",
    "Router registered in main.py alongside existing routers.",
    "268 backend tests pass (+12 new); ruff clean."
  ],
  "residualRisks": [
    "Authentik directory API field coverage not pinned (frontend UsersTab will pick fields in Slice 8).",
    "_resolve_service_record duplicated across monitoring.py and authentik_users.py (shared utility extraction is a follow-up)."
  ],
  "noStagedFiles": true,
  "diffSummary": "Adds AuthentikClient (clients/authentik.py, 133 lines) with Bearer-auth session + users() pagination normalization, a directory endpoint (routers/authentik_users.py, 88 lines) at GET /api/services/authentik/{id}/users with graceful error handling, main.py router registration (+4 lines), and 12 new tests (175 lines). 400 lines total, at budget.",
  "reviewFindings": [
    "no blockers"
  ],
  "manualNotes": "git status confirms nothing is staged; all changes are unstaged/untracked, ready for the parent to review and commit. The _resolve_service_record helper was copied (not imported) to keep the new router self-contained; monitoring.py was not modified."
}