a8dfbd5dc6
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).
113 lines
7.4 KiB
Markdown
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.
|