Files
manage/.pi-tmp/svc-hub-slice1-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.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 adapter BackupsWidgetSource stays in widgets/sources.py for now as instructed).
  • Registered as BACKUPS in SERVICE_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 AUTHENTIK in SERVICE_DEFINITIONS.

1.3 — Jellyseerr absorbed into JellyfinConfig

  • Added optional jellyseerr_url: str = "" and jellyseerr_api_key: str = "" to JellyfinConfig with a docstring noting they are the paired Jellyseerr companion config.
  • Deleted integrations/jellyseerr.py.
  • Removed the JELLYSEERR import and registry entry from registry.py.
  • integrations/__init__.py was already clean (no jellyseerr reference).
  • clients/jellyseerr.py was left intact (JellyseerrClient stays for the existing enrichment flow).
  • Verified: no remaining references to integrations.jellyseerr anywhere in src/.

1.4 — Jellyseerr migration (settings_store.py)

Added _migrate_jellyseerr_into_jellyfin() method, called from ensure_defaults() after the existing machine seeding. Policy:

  1. Query services WHERE service_type = 'jellyseerr'. If none, return (idempotent).
  2. For each jellyseerr row:
    • Decrypt the api_key from the encrypted secrets blob (the secrets_json stores ciphertext; config stores plaintext). The jellyseerr_api_key goes into config as plaintext.
    • Exactly one Jellyfin: merge into it.
    • Multiple Jellyfins: pick the first whose jellyseerr_url is empty.
    • No Jellyfin or all already paired: drop with a logged warning.
  3. 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 has jellyseerr_url/jellyseerr_api_key in schema.
  • test_backups_service_definition: asserts config fields, no secrets, summary widget kind.
  • test_authentik_service_definition: asserts config fields, api_token secret (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_url is 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

  1. jellyseerr_api_key stored 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 is api_key only — adding a jellyseerr_api_key secret field would require adding it to SecretField on 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.

  2. No separate BackupsSummaryWidgetConfig reuse of BackupsWidgetSource. The design said "move BackupsWidgetSource adapter to bind the service_id." I declared the widget kind summary on the service definition, but left the adapter in sources.py unchanged (as instructed: "The adapter itself can stay in sources.py for now"). The built-in backups widget kind in builtin.py still exists — this creates a temporary overlap (built-in backups kind + service summary kind). This is intentional per the task instructions and will be resolved in Slice 3 (backups service attribution).

  3. _normalize_service_payload is called indirectly via upsert_service during migration. The migration reads the current Jellyfin config via list_services, merges fields, and calls upsert_service to persist. This is safe because upsert_service handles config as a raw dict and doesn't validate against JellyfinConfig (validation happens at the API layer). The jellyseerr_url/jellyseerr_api_key fields 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 backups widget still exists alongside the new service summary widget kind. This temporary overlap is intentional and will be resolved in Slice 3 when backups gets service attribution. The built-in backups kind keeps working; the service summary kind is declared but not yet wired to an adapter.
  • jellyseerr_api_key in config is plaintext (see deviation #1 above).
  • JellyseerrClient in clients/jellyseerr.py is still imported by dependencies.py and routers/users_impl.py for 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.