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.4 KiB
Slice 1 — Backend: new service types + Jellyseerr absorption (worker output)
Files changed
| File | Status | Lines |
|---|---|---|
backend/src/media_library_viewer_api/integrations/backups.py |
new | 48 |
backend/src/media_library_viewer_api/integrations/authentik.py |
new | 35 |
backend/src/media_library_viewer_api/integrations/jellyfin.py |
modified | +11 / -3 |
backend/src/media_library_viewer_api/integrations/jellyseerr.py |
deleted | -33 |
backend/src/media_library_viewer_api/integrations/registry.py |
modified | +5 / -4 |
backend/src/media_library_viewer_api/services/settings_store.py |
modified | +74 / -0 |
backend/tests/test_services.py |
modified | +136 / -12 |
Total: ~343 changed lines (353 insertions, 52 deletions across tracked + new files). Under the 400-line budget.
What was implemented
1.1 — backups integration (integrations/backups.py)
BackupsConfig(ServiceConfigBase):ingestion_label: str = "default".- No secret fields.
- Widget kind
summary(declared on the service definition; the adapterBackupsWidgetSourcestays inwidgets/sources.pyfor now as instructed). - Registered as
BACKUPSinSERVICE_DEFINITIONS.
1.2 — authentik integration (integrations/authentik.py)
AuthentikConfig(ServiceConfigBase):base_url: ServiceBaseUrl,timeout_seconds: int = 10.- Secret field:
api_token(label "API token", required=True). - No widget kinds (empty list).
- Registered as
AUTHENTIKinSERVICE_DEFINITIONS.
1.3 — Jellyseerr absorbed into JellyfinConfig
- Added optional
jellyseerr_url: str = ""andjellyseerr_api_key: str = ""toJellyfinConfigwith a docstring noting they are the paired Jellyseerr companion config. - Deleted
integrations/jellyseerr.py. - Removed the
JELLYSEERRimport and registry entry fromregistry.py. integrations/__init__.pywas already clean (no jellyseerr reference).clients/jellyseerr.pywas left intact (JellyseerrClient stays for the existing enrichment flow).- Verified: no remaining references to
integrations.jellyseerranywhere insrc/.
1.4 — Jellyseerr migration (settings_store.py)
Added _migrate_jellyseerr_into_jellyfin() method, called from ensure_defaults() after the existing machine seeding. Policy:
- Query
services WHERE service_type = 'jellyseerr'. If none, return (idempotent). - For each jellyseerr row:
- Decrypt the
api_keyfrom the encrypted secrets blob (the secrets_json stores ciphertext; config stores plaintext). Thejellyseerr_api_keygoes into config as plaintext. - Exactly one Jellyfin: merge into it.
- Multiple Jellyfins: pick the first whose
jellyseerr_urlis empty. - No Jellyfin or all already paired: drop with a logged warning.
- Decrypt the
- Delete the jellyseerr row.
Migration is idempotent — running it twice is a no-op (no jellyseerr rows remain).
1.5 — Tests
test_registry_contains_eight_service_types: asserts the 8-type registry (alertmanager, authentik, backups, grafana, jellyfin, nextcloud, prometheus, ssh_tasks).test_jellyseerr_absorbed_into_jellyfin: asserts jellyseerr NOT in registry; JellyfinConfig hasjellyseerr_url/jellyseerr_api_keyin schema.test_backups_service_definition: asserts config fields, no secrets,summarywidget kind.test_authentik_service_definition: asserts config fields,api_tokensecret (required), no widgets.test_definitions_declare_widget_kinds: updated for backups + authentik.test_list_service_types: updated for the 8-type registry (API endpoint test).test_service_base_url_accepts_absolute_urls: parametrize updated (jellyseerr → authentik).- Migration tests:
test_jellyseerr_migrates_into_single_jellyfin,test_jellyseerr_dropped_when_no_jellyfin,test_jellyseerr_migration_is_idempotent.
Final registry type list
alertmanager, authentik, backups, grafana, jellyfin, nextcloud, prometheus, ssh_tasks
(8 types; jellyseerr removed)
Migration policy implemented
- Exactly one Jellyfin: merge unconditionally.
- Multiple Jellyfins: first Jellyfin whose
jellyseerr_urlis empty (first-unpaired). - No Jellyfin / all paired: drop with logged warning.
- Idempotent: no-op when no jellyseerr rows remain.
- Decryption: the jellyseerr api_key is decrypted before being placed into Jellyfin config (config_json is plaintext; secrets_json is encrypted).
Validation
cd backend && .venv/bin/python -m ruff check src/ tests/ → All checks passed!
cd backend && .venv/bin/python -m pytest tests/ → 256 passed, 2 warnings
Warnings are pre-existing (Starlette/httpx deprecation, pythonjsonlogger).
Deviations from design
-
jellyseerr_api_keystored in config as plaintext. The design said "encrypted at rest via the existing secrets mechanism if you prefer — design choice for tasks phase." I chose config (plaintext in config_json) for simplicity because: (a) the existing Jellyfin secret field isapi_keyonly — adding ajellyseerr_api_keysecret field would require adding it toSecretFieldon the Jellyfin DEFINITION, expanding scope; (b) the migration would then need to re-encrypt the decrypted value, adding complexity. The config_json column stores plaintext in SQLite regardless. If encryption is desired, a follow-up can add it as a Jellyfin secret field. -
No separate
BackupsSummaryWidgetConfigreuse ofBackupsWidgetSource. The design said "moveBackupsWidgetSourceadapter to bind the service_id." I declared the widget kindsummaryon the service definition, but left the adapter insources.pyunchanged (as instructed: "The adapter itself can stay in sources.py for now"). The built-inbackupswidget kind inbuiltin.pystill exists — this creates a temporary overlap (built-inbackupskind + servicesummarykind). This is intentional per the task instructions and will be resolved in Slice 3 (backups service attribution). -
_normalize_service_payloadis called indirectly viaupsert_serviceduring migration. The migration reads the current Jellyfin config vialist_services, merges fields, and callsupsert_serviceto persist. This is safe becauseupsert_servicehandles config as a raw dict and doesn't validate againstJellyfinConfig(validation happens at the API layer). Thejellyseerr_url/jellyseerr_api_keyfields are optional with defaults, so the config round-trips correctly.
skill_resolution
none — no project/user SKILL.md paths were injected by the parent, and no .atl/skill-registry.md was found. The task was self-contained against the OpenSpec design/tasks docs.
Residual risks
- Built-in
backupswidget still exists alongside the new servicesummarywidget kind. This temporary overlap is intentional and will be resolved in Slice 3 when backups gets service attribution. The built-inbackupskind keeps working; the servicesummarykind is declared but not yet wired to an adapter. jellyseerr_api_keyin config is plaintext (see deviation #1 above).- JellyseerrClient in
clients/jellyseerr.pyis still imported bydependencies.pyandrouters/users_impl.pyfor the existing enrichment flow. These references are valid (the client stays; only the integration definition was removed). They will be rewired in later slices.
Review findings
No blockers identified during self-review. All validation commands green. No staged files.