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).
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()withAuthorization: Bearer <token>header +Accept: application/json.- base_url normalization: rstrip "/" and strip trailing
/api/v3suffix. get(path, **params)helper — same error-logging pattern as JellyseerrClient (raise_for_status with detail text on HTTPError).users(search, page, page_size)— callsGET /api/v3/core/users/with query paramssearch,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.ValueErroron 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 + decryptedapi_tokensecret, callsusers().- 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.
- Service not configured →
_resolve_service_recordhelper copied into the new router (type-specific toauthentik; the monitoring.py one is generic but takesservice_typeas 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
-
_resolve_service_recordcopied 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 (hardcodingservice_type="authentik") keeps the new router self-contained. A follow-up refactor could extract a sharedresolve_service_recordutility. -
timeoutconfig parsing is guarded. Added atry/except (TypeError, ValueError)aroundfloat(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_recordis duplicated acrossmonitoring.pyand the newauthentik_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."
}