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