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

144 lines
7.1 KiB
Markdown

# 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
```acceptance-report
{
"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."
}
```