Frontend for Jellyseerr request stats, reusing the generic stat abstraction.
- api/jellyseerr.ts + hooks/useJellyseer.ts: fetchJellyseerrStats +
useJellyseerrStats (polls /api/jellyseerr/stats, no-retry; shares the backend
cache with the widgets).
- Two reusable widgets backed by the stat/stats_overview kinds:
- RequestStatWidget: a single selected stat (big value + label).
- RequestsOverviewWidget: a MetricCard grid of all stats + a recent-requests
list with status/media-status badges.
- registry.ts: Jellyfin gains `stat` (a dropdown over
total/pending/approved/declined/processing/available — the "extract one stat
into a widget" affordance, rendered as a Select via the existing enum UI) and
`stats_overview` widget kinds, wired to the new components.
- RequestsTab rewritten: live stats grid (6 counts) + recent-requests list +
a hint to pin individual stats via the Request stat widget. Reads
jellyseerr_url from config and the now-secret jellyseerr_api_key from
secrets_set.
Tests: RequestStatWidget + RequestsOverviewWidget rendering/error; RequestsTab
not-configured CTA, configured stats grid, and error states. 184/184 frontend
tests pass; tsc + ESLint clean.
The app was saturating qBittorrent's single-threaded web server and causing
its own Web UI (and the reverse proxy) to hang/504: each of the 3 qBittorrent
widgets fetched /sync/maindata independently, every call was a FULL snapshot
(no rid), and polling was aggressive (5s for speed). For large torrent lists
each snapshot is heavy, so the server queued and Traefik timed out.
QbittorrentClient.maindata now:
- Uses the incremental rid protocol: the first call is a full_update;
subsequent calls send the last rid and get a small diff that is merged into
a cached snapshot (full_update replaces; partial_update merges server_state,
torrents {added/None-removed/..._removed}, categories, tags, trackers).
Payloads shrink dramatically for large libraries.
- Serves a short-TTL (3s) cached snapshot under a lock, so concurrent widget
polls collapse onto a single HTTP fetch instead of N.
- Backs off exponentially (capped 30s) on repeated failure, serving the last
good snapshot when available, so a struggling qBittorrent isn't hammered
further. Returns a shallow race-safe copy of the snapshot per call.
Also slow the speed widget poll from 5s -> 15s (backend widget-kind +
frontend registry) for ~3x fewer calls.
Tests: rid full+partial merge, cache collapses within-TTL calls, backoff
skips the network after failure and serves stale. 393/393 backend + 180/180
frontend tests pass; ruff + tsc + ESLint clean.
The config dialog reads each widget kind's schema from the static frontend
SERVICE_REGISTRY (registry.ts), not the backend pydantic schema. The previous
commit added unit/scale to the backend configs but not to the frontend mirror,
so the options never appeared in the dialog — the Prometheus "chart" binding
still listed only promql/window and the qBittorrent "speed" binding had an
empty configSchema.
Add a shared AXIS_FORMAT_PROPERTIES fragment (unit + scale enums) and spread
it into the prometheus chart and qbittorrent speed bindings, with matching
defaultConfig (chart: none/auto; speed: bytes_per_sec/auto). Combined with the
enum <Select> rendering already added to WidgetConfigDialog, the options now
show up as dropdowns when editing those widgets.
Test: registry exposes unit/scale enums on chart + speed; speed defaults to
bytes_per_sec. 180/180 frontend tests pass; tsc + ESLint clean.
- 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.