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.
Graph widgets need consistent value scaling (kB/MB/GB, kbps/Mbps, …). Add
shared `unit` (none/bytes/bytes_per_sec/bits_per_sec/bits/percent/seconds) and
`scale` (auto/k/m/g/t) enum fields to:
- PrometheusChartWidgetConfig (alongside promql/window)
- new QbittorrentSpeedWidgetConfig — the speed widget previously had NO config
options at all; totals/active keep their empty config.
Declared as Pydantic Literal enums so the widget-kind JSON schema exposes
`enum`, which the frontend config dialog renders as a dropdown. The data
sources are unchanged (raw values); scaling is a display concern handled
client-side. qBittorrent speed defaults to bytes/sec.
Test: chart widget kinds expose the shared unit/scale enums; speed defaults to
bytes_per_sec; totals/active stay option-less. 389/389 backend tests pass.
The credential tester always reported "Authentication failed — qBittorrent
rejected the credentials" for the qBittorrent service, even when credentials
were correct. test_connection classified any RuntimeError whose message
contained "login failed" as an auth failure — and the gateway-timeout error
(502/503/504 from the reverse proxy) and the wrong-URL diagnostic both started
with "qBittorrent login failed:", so a proxy timeout was reported as a
credentials rejection. That sent users down the wrong path (re-entering correct
passwords to fix a 504).
- QbittorrentClient._login: gateway and URL/routing errors no longer contain
"login failed"; only a genuine "Fails." body carries the
"invalid username or password" signal.
- integrations/qbittorrent.test_connection: key the auth message off
"invalid username or password" specifically; all other login errors flow
through translate_connection_error so the real reason (proxy timeout, wrong
URL, empty body) is surfaced.
After this, a failing test reports the actual cause (e.g. "qBittorrent is
unreachable: reverse proxy returned HTTP 504 ...") instead of accusing the
credentials. New regression test asserts a gateway error is NOT reported as
"Authentication failed". 386/386 backend tests pass; ruff clean.
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.
- 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.