Every HTTP client passed an integer timeout to requests, applying the same
value to BOTH connect and read phases. A slow Jellyfin /Items page or qBit
/sync/maindata blew through the 10s read budget → ReadTimeoutError. Split
into a (connect=5s, read=60s default) tuple via shared http_timeout() helper.
The media index build worker uses a 180s read floor. Existing services with
low timeout_seconds benefit from bumping to 60+.
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).
Add gauge widget (recharts RadialBarChart with configurable threshold
bands, scalar-only per SC-111) and mean widget (client-side average over
range-query window, scalar-only per SC-114). Extract shared _instant_query
helper from the metric path; _fetch_gauge and _fetch_mean dispatch in
PrometheusWidgetSource.fetch(). Both new widget kinds declared in
integrations/prometheus.py and frontend registry.
Backend: 305 pytest pass, ruff clean. Frontend: 136 vitest pass, build+lint green.
The iframe-based 'panel' widget didn't work: the browser couldn't
authenticate against the OIDC-protected Grafana (Authentik), and
iframes can't carry Bearer tokens or share cross-origin session
cookies. Result: blank iframe or login redirect.
Replace it with a 'chart' widget that queries Grafana's datasource
API server-side:
Backend (GrafanaWidgetSource): POSTs to /api/ds/query with the stored
api_key (which bypasses OIDC), using the widget's configured PromQL
query, datasource_uid, time range, and resolution. Normalizes Grafana's
frame-based response into a simple {series: [{label, points: [{t, v}]}]}
shape. The api_key is never exposed to the browser.
Frontend (GrafanaChartWidget): renders the series data as a recharts
LineChart with dark-mode-aware colors (Tailwind --chart-* tokens),
responsive container, custom tooltip, and per-series lines. Loading
skeleton, error Alert, and empty state. recharts ^3.9.2 added.
The 'link' widget kind (deep-link URL) is unchanged. The 'panel' kind
and GrafanaPanelWidget are fully removed.
Backend: 279 tests pass (+1 net: -2 panel + 3 chart). Frontend: 127
tests pass (net 0: -3 panel + 3 chart). Lint/build green both sides.
Two new additive widget kinds:
Jellyfin 'now_playing': like the existing 'activity' widget but filters
to only sessions with active playback (NowPlayingItem present + not
paused). Shows who's actually watching right now. The 'activity' kind
is unchanged (shows all sessions including idle).
Grafana 'panel': embeds a single Grafana panel directly in the app via
an iframe, using Grafana's /d-solo/ endpoint (renders one panel without
dashboard chrome, kiosk=tv). Configurable dashboard_uid, panel_id, and
time range (from/to, defaults now-1h/now). Includes a fallback 'Open in
Grafana' link for when embedding is blocked by X-Frame-Options/CSP. The
'link' kind is unchanged (still builds a deep-link URL).
Backend: new widget configs + definitions on jellyfin/grafana; source
adapter logic (session filter for now_playing; d-solo embed URL for
panel); 6 new tests.
Frontend: JellyfinNowPlayingWidget + GrafanaPanelWidget components;
registry bindings; 6 new tests.
278 backend tests pass (+6); 127 frontend tests pass (+6); lint/build
green both sides.
The service detail page showed non-secret connection config (base_url,
user_id, username, timeout_seconds) as read-only. Render schema-driven
editable inputs (reusing the create-dialog pattern) with a draftConfig
state hydrated from the instance, and unify the save button to persist
both config and secrets. Number fields render as type=number; the base_url
schema description surfaces as helper text.
Add a shared ServiceBaseUrl type (BeforeValidator + Field description) in
integrations/base.py and apply it to base_url across all six service configs
(grafana, prometheus, alertmanager, jellyfin, jellyseerr, nextcloud). Missing
http:// or https:// schema now fails fast with a clear 422 instead of breaking
HTTP clients silently. Tests cover reject/accept cases; REQUIREMENTS updated.
- 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.
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 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.