Slice 3 (final) of jellyfin-service-registry. Documents the completed
migration and archives the SDD change.
- docs/REQUIREMENTS.md: marked the machine-level Jellyfin follow-up
resolved; added a decision-log entry (Jellyfin no longer a machine
service, dead media_root/path_prefix removed; global config +
path_utils retained for Jellyfin->SSH path resolution).
- CHANGELOG.md: struck through the old follow-up note; added a
Follow-up #2 section describing the machine field + service removal.
- Archived openspec/changes/jellyfin-service-registry (no active SDD
changes remain).
Backend ruff clean / 239 tests pass; frontend 0 lint errors / build
clean / 72 tests pass.
- Archive the completed observability-service-registry SDD change into
openspec/changes/archive/ (delivered across 5 slices; only
jellyfin-service-registry remains active).
- Stop ignoring .pi-map.md / .pi-map.index.md so the navigation maps are
versioned alongside the code, and add the regenerated map pairs repo-wide.
Rename grafana-prometheus-polish -> observability-service-registry and
rewrite proposal/design/tasks for the approved vision: all observability
integration (alertmanager, grafana, prometheus) configured as service-
registry instances in the UI, surfaced on a dedicated page, with widgets
per service definition -- nothing in the env.
Key scope decisions captured:
- Add alertmanager as a 6th service type + active_alerts widget.
- Rewire /alerts + /alertmanager-status to resolve from service records
(first-enabled-instance default; no primary flag in v1).
- Add /grafana-status + /prometheus-status health endpoints.
- Observability page discovers services; kill VITE_GRAFANA_URL /
VITE_PROMETHEUS_URL deep-links.
- Webhook receiver stays log-only (drop the outbound forward).
- Remove PROMETHEUS_FILE_SD_DIR + the file-writer; external Prometheus
uses http_sd_configs against GET /api/monitoring/prometheus-targets.
build_node_exporter_targets + that endpoint stay.
- PROMETHEUS_ENABLED stays (Manage's own /metrics toggle).
- End state: zero observability *service* env vars.
Plan = 5 slices, each <=400 changed lines, green tests/lint/build,
commit per slice.
- jellyfin-service-registry: proposal, design, and tasks for completing
the Jellyfin migration off machine-level config.
- grafana-prometheus-polish: proposal, design, and tasks for improving
the Grafana/Prometheus observability integration.
Both are planning-only artifacts; implementation not started.
Move finished change directories to openspec/changes/archive/:
- configurable-dashboard-widgets
- decommission-monitoring-poller
- service-registry
- unify-tasks-on-services
All associated implementation has been merged to main.
- Add shared task_runner.run_saved_task helper used by routers/tasks.py and
widgets/sources.py SshTaskWidgetSource.
- Saved tasks now target ssh_tasks service instances via default_service_id;
the legacy default_machine_id and saved_task_runs are removed.
- Actions page lists ssh_tasks services for default and run-time selection.
- Update types, API client, hooks, tests, docs, and changelog.
Backend tests: 222 passed. Frontend lint/build/test: clean (71 passed).
Slice 4b frontend half. Jellyfin-touching pages now select a Jellyfin service
instance instead of a machine.
- api/client.ts: Jellyfin-backed calls (counts/libraries/activity/users, media
status/build/stop/force-stop, queryMedia) send jellyfin_service_id.
- hooks/useDashboard, useUsers, useMedia: selector param renamed to
jellyfinServiceId.
- pages/Media + Applications: list jellyfin service instances and persist
jellyfin_service_id in the URL.
- Dashboard (widgets) and Users (default instance) need no selector change.
- Update Applications + Media tests for the new hook/param.
Files/SSH transport keeps machine_id. Verification: frontend lint 0 errors,
build success, 70 tests; backend ruff clean, 222 tests.
Slice 4b backend half. Jellyfin and Jellyseerr clients are now resolved from
service instances instead of machine-level app config.
- Add jellyseerr service definition (6 service types total); add user_id to
the Jellyfin service config.
- dependencies.py: jellyfin_service_id query param + _service_record
(decrypt-on-read); get_jellyfin_client / get_jellyseerr_client / get_user_id
resolve against the service registry (first enabled instance as fallback).
- SSH/Files transport (get_ssh_client) unchanged; still uses machine_id.
- Update service-registry tests for 6 types.
Selection model: split params — ?jellyfin_service_id= for Jellyfin/Jellyseerr,
?machine_id= for SSH/Files. Frontend threading follows in the next PR.
Verification: backend ruff clean, pytest 222 passed; frontend green (unchanged).
PR 4a of the runtime service registry change.
- Remove addon pages (/addons/:addonId, AddonPage, addons/*) superseded by
service pages.
- Remove grafana_url/prometheus_url from backend config, compose, .env.example,
and README (URLs now live on service records; VITE_ frontend deep-link vars
retained).
- Add Services page (/services) with create/list/delete + sidebar nav, so
services are configurable in the tool itself and service pages are reachable.
- Update docs/REQUIREMENTS.md service-registry section; add CHANGELOG.md with
the breaking-upgrade note (MANAGE_ENCRYPTION_KEY required; grafana/prometheus
env vars removed; default widget seeding removed).
Verification: backend ruff clean, pytest 222 passed; frontend lint 0 errors,
build success, 70 tests passed.
PR 3 of 4 for the runtime service registry change.
- Add service + new-shape widget TypeScript types; widgets carry service_id
+ widget_kind (service-bound) or null (built-in).
- Add services API client + TanStack Query hooks; reconcile the widget API
client/hooks to the new endpoints (remove sources/types; add builtin kinds).
- Add closed frontend service registry (integrations/registry.ts) mirroring the
backend, with resolveWidget(widget, services) mapping a widget to its
component + refresh interval.
- Add ServicePage at /services/:serviceType/:serviceId with config view,
empty-on-edit secret inputs + 'set' badges, enable toggle, delete, and the
service's widget-kind list.
- Register /services/:serviceType/:serviceId in App.tsx.
- Reconcile the six widget components to refreshIntervalMs + description props;
rewrite WidgetConfigDialog around a service -> widget-kind picker.
- Update Dashboard test; add integrations/registry.test.ts.
Verification: frontend lint 0 errors, build success, 70 tests passed; backend
ruff clean, 222 tests passed.
PR 2 of 4 for the runtime service registry change.
- dashboard_widgets gains service_id + widget_kind columns (legacy
addon_id/widget_type kept but unused).
- Source adapters take (service: ServiceRecord | None, widget_kind, config).
SERVICE_ADAPTERS keyed by service_type; BUILTIN_ADAPTERS for backups/static.
- Backups and static stay as service-less built-ins (service_id nullable),
exposed via GET /api/widgets/builtin.
- SSH task adapter resolves the task + instance, runs over SSH, and appends a
service_task_runs history row on success/failure/timeout/error.
- Retire widgets/registry.py; widget metadata now comes from the integrations
registry + widgets/builtin. Remove /api/widgets/types and /api/widgets/sources.
- Stop default widget seeding (fresh install = empty dashboard).
- Rewrite widget tests around the service-bound + built-in model (26 tests).
Backend-only breaking change; frontend is reconciled in Slice 3. Build/lint
stay green; pytest 222 passed.
PR 4 of 4 for configurable dashboard widgets.
- Replace hard-coded Jellyfin/Backups dashboard sections with a loop that
renders enabled widget instances by sort_order.
- Add WidgetInstance renderer and WidgetConfigDialog for adding, editing,
enabling/disabling, deleting, and reordering widgets.
- Add addon pages for grafana, prometheus, and ssh-tasks at /addons/:addonId.
- Register /addons/:addonId route in App.tsx.
- Update docs/REQUIREMENTS.md with the widget system design and API.
Verification:
- backend ruff clean; pytest 200 passed
- frontend npm run lint: 0 errors
- frontend npm run build: success
- frontend npm run test -- src/widgets/registry.test.ts: 3 passed
The 2026-06-16/17 observability update externalised metrics to
Prometheus + node_exporter + Grafana, but the legacy Manage-side
SSH-scraping monitor was never removed. It duplicated the new stack,
ran SSH df on every machine every 300s, and fed nothing (its UI was
deleted in e2ad731). This slice decommissions the duplication.
Removed (backend):
- services/monitoring_poller.py (MonitoringPoller) — entire file
- services/monitoring_actions.py (disk_space, run_machine_operation,
poll_machine_snapshot, build_machine_client) — entire file;
run_machine_operation had only 2 callers (the poller + /disk), both gone
- tests/test_monitoring_actions.py
- endpoints: POST /api/monitoring/poller, GET /machines/{id}/actions,
GET /disk (and the now-dead _resolve_machine helper)
- lifespan wiring (main.py), dependency wrapper (dependencies.py),
poller.start()/kick() from machine save (routers/settings.py)
- SettingsStore: monitoring_machine_actions table CREATE + 2 indexes +
record/list/prune_machine_actions methods; DROP TABLE IF EXISTS on
startup cleans existing DBs (user-approved)
- config knobs: monitoring_poll_interval_seconds,
monitoring_poll_initial_delay_seconds, monitoring_action_retention_days
- test_api.py: TestMonitoring._ensure_machine + test_disk
Kept (fits the new model): /machines, /prometheus-targets, /alerts,
/alertmanager-status, /alertmanager-webhook; the disk_usage JOB template
(manual on-demand, not monitoring); node_exporter_* machine fields
(they point Prometheus at the right host).
Gate: backend pytest 173 passed; ruff clean.
Move the completed, verified, synced change folder to the dated archive:
openspec/changes/web-ui-rework/ -> openspec/changes/archive/2026-06-17-web-ui-rework/
Canonical spec stays in place at openspec/specs/web-ui/spec.md (not moved).
This is the physical archive step deferred (documented-pending-manual) in
archive-report.md ec46e26; user-approved.
Web UI rework. Completes the DataGrid migration (7a + 7b):
- pages/Media.tsx off @mui/x-data-grid + @mui/material onto DataTable:
15 locked columns (title/series/season/episode/type/year/runtime_min/
size/bitrate/hdr/video/resolution/date_added/library/path);
enablePagination + manualPagination + rowCount from queryResult.total;
page state (pageIndex/pageSize) -> offset/limit into useMediaQuery;
onRowClick -> navigate('/files?path=...') preserved; stable path-derived
getRowId so selection survives server paging; column-visibility toggle.
Hard rule honored: NO sorting, NO resizing (visibility-only).
- Migrate Media shell (Select/Input/Progress/Card/grid/Typography/Tabs).
- Media component tests (column set + row-click nav).
- Harness fix: polyfill ResizeObserver in test/setup.ts — jsdom lacks it
and Radix primitives (Select/ScrollArea/etc.) reference it; was causing
cross-test failures once Media pulled shadcn Select into the pool.
Gate: build + lint + test green (23 files / 64 tests).
- Add get_api_key() and require_api_key() to auth.py
- Add generic key-value settings storage to SettingsStore
- Add test_api_key_auth to verify the implementation
- Uses secrets.compare_digest for timing-safe comparison
- Auto-generates API key on first use and stores in settings DB