Groundwork for Jellyseerr request stats in the Jellyfin service, behind a
small reusable abstraction so future stats services (Sonarr/Radarr) reuse it.
Backend:
- JellyseerrClient.request_count() -> /api/v1/request/count (normalized
total/pending/approved/declined/processing/available) and recent_requests()
-> /api/v1/request mapped to {name,type,status,media_status,created_at}
with numeric status enums labelled.
- widgets/stats_provider.py: StatsProvider protocol + registry keyed by
service_type (StatValue/StatsResult). A thin generic interface.
- widgets/jellyseerr_stats.py: JellyseerrStatsProvider registered for the
Jellyfin service; reuses one authenticated client per service (lru_cache) and
caches the StatsResult for ~10s under a lock, so multiple widgets + the tab
collapse onto one Jellyseerr fetch (same lesson as the qBittorrent client).
Accepts jellyseerr_api_key from secrets OR config during the upcoming
config->secret migration.
- Jellyfin service gains two widget kinds: `stat` (a Literal selector over the
six stats — the "extract one value into a widget" affordance) and
`stats_overview` (all stats + recent list).
- widgets router routes widget_kind in {stat, stats_overview} to a generic
StatsWidgetSource (dispatches to the service type's provider), independent of
service type.
- new /api/jellyseerr/stats router endpoint for the Requests tab (resolves the
Jellyfin service by id or first-enabled; shares the provider cache).
Tests: provider normalization, not-configured, TTL caching; stat selector +
overview + unknown-stat widget dispatch; 7 new tests. 400/400 backend pass;
ruff clean.
Route all prometheus widget queries through Grafana /api/ds/query instead of
direct Prom HTTP. PrometheusConfig: drop base_url, add grafana_url +
datasource_uid; secret grafana_api_key (required). PrometheusWidgetSource →
MetricSource with _gateway_query POST method. normalize_grafana_frames
recovered from 65bae95 + shared _dedup_label helper. Gateway-path status
check. Startup old-config validation. CHANGELOG migration note. All adapter
tests rewritten for POST /api/ds/query + Grafana frames mock. Backend: 331
pytest pass, ruff clean. Frontend: build green (unchanged in S1).
Slice 4 of observability-service-registry. Removes the shared-file
Prometheus bridge; external Prometheus now consumes node-exporter targets
via http_sd_configs against GET /api/monitoring/prometheus-targets.
- services/targets.py: removed write_prometheus_targets() (the file
writer) and its json/Path/get_settings imports; updated module docstring.
build_node_exporter_targets() is unchanged and still powers the HTTP
endpoint.
- main.py: removed the startup write_prometheus_targets call.
- routers/settings.py: removed the _write_prometheus_targets helper and
its three post machine create/update/delete call sites + the now-unused
targets import.
- config.py: removed the prometheus_file_sd_dir field.
- docker-compose.yml / docker-compose.dev.yml: removed the
PROMETHEUS_FILE_SD_DIR backend env var.
- tests: removed TestWritePrometheusTargets + the write_prometheus_targets
import in test_targets.py; rewrote the two TestSettingsMachines tests to
assert machines appear/disappear from /api/monitoring/prometheus-targets
(the surviving HTTP path) instead of the removed file-writer side effect.
ruff clean; 239 backend tests pass.
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.
- backend/ uses proper Python src layout (src/media_library_viewer_api/)
with pyproject.toml, hatchling build, and PYTHONPATH=src convention
- frontend/ is a Vite + React + TypeScript SPA
- archive/ preserves the original Streamlit prototype for reference
- Cleaned up root to only contain docs, license, and subproject dirs
- Updated README for the new dual-subproject architecture