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

113 lines
7.4 KiB
Markdown

# 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.