Commit Graph

264 Commits

Author SHA1 Message Date
Developer 50c0c9b548 fix(gauge): render single value arc with correct Tailwind v4 colors
The gauge rendered as multiple black rings. Two causes:

1. recharts RadialBarChart draws each data entry as a CONCENTRIC RING, not an
   arc segment, so the 3 "track band" entries + value produced 4 nested rings.
   Render a single value arc over a neutral background track instead, colored
   by status, with the readout absolutely centered (replacing the -mt-12 hack).

2. The fills used hsl(var(--primary)) / hsl(var(--chart-1)) etc., but this
   project's Tailwind v4 theme (index.css) defines colors as --color-* holding
   full hex values (--color-primary: #4f8cff). So the references were doubly
   invalid (wrong name + hsl() wrapping a hex) -> invalid SVG fill defaults to
   black. Use var(--color-*) directly, with the semantically correct chart
   colors: ok=--color-chart-2 (green), warn=--color-chart-3 (amber),
   crit=--color-chart-4 (red).

Also fix the same hsl(var(--x)) -> var(--color-x) bug in LineSeriesChart's
tooltip contentStyle (popover/border/popover-foreground). The line stroke
palette already used the correct var(--color-chart-N) form.

166/166 frontend tests pass; typecheck + ESLint clean.
2026-07-11 13:05:42 +00:00
Developer b011d2421b fix(qbittorrent): reuse authenticated client across widget fetches
QbittorrentWidgetSource built a brand-new QbittorrentClient on every fetch,
logging in each time. With three qBittorrent widgets polling every 5-30s and
qBittorrent verifying passwords with slow PBKDF2 hashing, the concurrent login
load saturates its web thread pool and the reverse proxy returns 504 gateway
timeouts on /api/v2/auth/login. The client was already designed for reuse
(login once, SID cookie reuse, 403 re-login) — the source just wasn't using it.

Cache one QbittorrentClient per service (lru_cache keyed by service id, URL,
credentials, timeout) so the SID cookie persists across fetches and login
happens once. Mirrors dependencies._jellyfin_client_for. A credentials/URL
change produces a new cache key, so stale clients aren't reused after
reconfiguration.

Also surface 502/503/504 from the login as a clear "reverse proxy returned
HTTP <code> ... qBittorrent may be down/starting/overloaded" RuntimeError
instead of a bare HTTPError, so future gateway issues read as infrastructure,
not auth.

Tests: autouse fixture clears the client cache between tests; new gateway-error
login test. 385/385 backend tests pass; ruff clean.
2026-07-11 12:19:19 +00:00
Developer dad2202756 fix: reset service editor on switch + add service-page settings shortcut
Settings.tsx: ServiceConfigEditor derived editable state (name, config,
secrets) from the instance prop via useState, but the parent rendered it
without a key. Switching services in the rail reused the same component, so
name/config stayed pinned to the previously selected service while
instance.id/service_type (read live from props) pointed at the new one —
saving then wrote the stale values onto the wrong row (e.g. saving qBittorrent
renamed it "Jellyfin" with Jellyfin's URL). Add key={selectedService.id} so
the editor remounts and resets on switch.

ServicePage: add a Settings shortcut in the header that deep-links to
/settings?tab=services&service=<id>. Settings now reads tab + service query
params (useSearchParams) to open the Services tab with that service
pre-selected, via a new initialServiceId prop on ServicesAdminCard.

Tests: new Settings.services.test.tsx regression test (fails without the key,
passes with it); wrap existing Settings tests in MemoryRouter since Settings
now uses useSearchParams. 166/166 frontend tests pass; typecheck + ESLint clean.
2026-07-11 11:54:18 +00:00
Developer 84dcf9e010 fix: resolve Jellyfin usernames to internal Id and harden qBittorrent login
Jellyfin: get_user_id() returned the configured user_id verbatim, so a
username like "admin" hit /Users/admin/Views and got HTTP 400 ("The value
'admin' is not valid."). The index worker already had username->Id
resolution, but the live API paths (dashboard counts, media query) did not.
Route all user-scoped paths through the new JellyfinClient.resolve_user_id()
(exact Id match -> Name match -> first user), cached per service/credentials
in get_user_id() so repeated requests don't re-list users. The worker is
simplified to call the same method.

qBittorrent: _login() raised "qBittorrent login failed: " (empty) on a 200
with an empty body, which happens when base_url doesn't reach the qBittorrent
login handler (wrong URL/path or a reverse proxy misroute) — not a credentials
issue. Now accepts the SID cookie as a success signal (reverse proxies that
mangle the body), returns a clear "invalid username or password" for "Fails.",
and surfaces a diagnostic error (HTTP status + body + base_url/proxy hint) for
any other/empty body.

Tests: new tests/test_jellyfin_client.py (5) + 3 qBittorrent login tests.
Full backend suite (384) passes; ruff clean.
2026-07-11 11:21:31 +00:00
Developer ecabc65dd4 fix: widget edit crash (#185) + resizable textarea for complex fields
WidgetConfigDialog crashed on edit with React error #185 (Maximum update
depth exceeded) when the references/instances query returned undefined and
the inline '= []' fallback created a new array ref every render, looping the
auto-edit useEffect. Stabilize via useMemo(data ?? []). Also moved the
referencedWidgetIds Set inside the availableWidgets useMemo (clears the
pre-existing exhaustive-deps warning).

Complex config fields (promql, query, text, command, notes, or opt-in via
format: 'textarea') now render as a taller resizable Textarea (rows=6,
min-h-120px, font-mono, resize) in both WidgetConfigFields and
ServiceConfigFields, instead of a single-line Input.

Build + lint clean (referencedWidgetIds warning gone), 165 vitest pass.
2026-07-11 10:31:57 +00:00
Developer 044d386ac7 fix: split HTTP connect/read timeouts (Jellyfin build + qBit stats)
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+.
2026-07-10 11:43:07 +00:00
Developer 9bc8fab971 chore: fold pre-existing ServicesPage.tsx formatter stray
Whitespace-only JSX reflow (prettier) from earlier #1 validation-surfacing fix;
folding so the working tree goes pristine before the final push.
2026-07-10 00:17:51 +00:00
Developer 29650ca512 chore(per-instance-hook-scoping): archive verified+synced change
Move to openspec/changes/archive/2026-07-09-per-instance-hook-scoping/
(R100 renames preserved). 9 artifacts. Canonical openspec/specs/
service-instance-scoping/ remains. Resolves multi-instance wrong-data bug
(hooks now scope by instance.id; instance switcher re-scopes).
Carry-overs: fetchBackupDashboard untouched (design decision 5); subquery
scoping for runs/alerts (schema asymmetry).
2026-07-10 00:17:51 +00:00
Developer f921524d37 spec(per-instance-hook-scoping): sync into new canonical domain
New canonical openspec/specs/service-instance-scoping/spec.md (21 reqs
PI-101..121). Change-side delta + sync-report. web-ui/prometheus-charting/
service-storage/service-credential-testing canonicals untouched.
2026-07-10 00:12:54 +00:00
Developer 8d3c44d87f spec(per-instance-hook-scoping): verify + reconcile tracking
Write apply-progress.md, tick all 17 tasks, add verify-report.md (21/21 PI-101..121
PASS). Gates green: 368 pytest, ruff clean, npm build+lint 0 errors, 165 vitest.
fetchBackupDashboard/useBackupDashboard/get_backup_dashboard confirmed untouched
(design decision 5). No blocking code findings.
2026-07-10 00:05:58 +00:00
Developer 3bc7ce5269 feat(per-instance-hook-scoping): scope observability + backup hooks by instance 2026-07-09 23:54:31 +00:00
Developer ad61d92b32 spec(per-instance-hook-scoping): add tasks (single slice, ~257 lines)
Backend backup endpoint+store filter (subquery for runs/alerts); frontend 6
hooks + 7 API fns + 3 tabs. fetchBackupDashboard excluded (widget path).
Each gate green.
2026-07-09 23:40:39 +00:00
Developer dbc332d1b6 spec(per-instance-hook-scoping): add design
6 decisions: queryKey appends serviceId??''; API client reuses get(path,params);
service_id: str|None=None on backup endpoints; store filter via subquery for
runs/alerts (schema asymmetry — only backup_jobs has service_id column);
fetchBackupDashboard excluded (widget path, PI-117 risk). Single slice ~257
lines. 3 source findings: Alertmanager/Prom endpoints confirmed already take
service_id; backup runs/alerts attributed via FK chain (subquery needed).
2026-07-09 23:37:35 +00:00
Developer 87f42b4ec3 spec(per-instance-hook-scoping): add spec (21 reqs PI-101..121)
Correctness fix for multi-instance: 6 hooks gain optional serviceId in queryKey;
7 API fns append ?service_id; 4 backup endpoints gain service_id filter
(Alertmanager/Prometheus status already take it — zero backend change there);
3 tabs pass instance.id; dashboard widgets unaffected; all params optional
(backward-compat). usePrometheusTargets + useMonitoringMachines stay global.
2026-07-09 23:32:40 +00:00
Developer 5addc9dae9 chore(service-credential-tester): archive verified+synced change
Move to openspec/changes/archive/2026-07-09-service-credential-tester/
(R100 renames preserved). 9 artifacts. Canonical openspec/specs/
service-credential-testing/ remains. Resolves qBit 'login failed' #3 pain
at the UI layer (auth failure surfaced in result pill, no log-digging).
Carry-overs in archive-report incl N-2 strengthened, N-6 presentational panel,
edit-surface-is-Settings.tsx source-finding.
2026-07-09 23:29:19 +00:00
Developer 6bcb60a74d spec(service-credential-tester): sync into new canonical domain
New canonical openspec/specs/service-credential-testing/spec.md (21 reqs
CT-101..121). Change-side delta + sync-report. web-ui/prometheus-charting/
service-storage canonicals untouched.
2026-07-09 23:22:53 +00:00
Developer 98bf496a98 spec(service-credential-tester): verify + strengthen no-secret-logs test + reconcile
Strengthen test_secrets_not_logged (N-2): now sends real-looking secrets
through prometheus + qbittorrent test_callables (mocked at network boundary),
asserts no fragments leak into caplog, verified non-vacuous. Write
apply-progress.md, tick all 29 tasks, add verify-report.md (21/21 PASS).
Gates green: 362+ pytest, ruff clean, npm build+lint 0 errors, 158 vitest.
2026-07-09 23:15:47 +00:00
Developer f6c67bd3ff feat(service-credential-tester): slice 2 — Test button + gating (shared ServiceTestPanel)
Presentational ServiceTestPanel (props-driven, no internal hooks) wired into
both CreateServiceDialog (ServicesPage.tsx) and ServiceConfigEditor
(Settings.tsx). Parent owns testResult + saveAnyway state; store-previous
pattern resets on input change (avoids setState-in-effect). Create/Save
button gated on testPassed || saveAnyway. 7 panel tests (button states,
success/failure pills, checkbox toggle). All gates: 158 vitest, build exit 0,
lint 0 errors, 362 backend pytest (regression).
2026-07-09 22:57:02 +00:00
Developer 3391fbc85d feat(service-credential-tester): slice 1 — backend test endpoint + per-type routines
TestResult dataclass + translate_connection_error shared helper in base.py.
test_callable field on ServiceDefinition (default None). 7 per-type
test_connection routines (qbittorrent, prometheus via Grafana gateway,
alertmanager, jellyfin, authentik, ssh_tasks via build_ssh_client, nextcloud).
POST /api/services/test endpoint: validation-first (422 on malformed config),
dispatch, no-persistence, no-secret-logs. backups has test_callable=None.
qBit 'Fails.' → specific auth message (resolves #3 at API layer).
Backend: 362 pytest pass (+31 new), ruff clean. Frontend: build green.
2026-07-09 22:41:06 +00:00
Developer c4f68b4938 spec(service-credential-tester): add tasks (2 slices, each <=400 lines)
S1 backend: TestResult + test_callable + translate_connection_error + POST
/api/services/test + 7 per-type routines + tests. S2 frontend: type + API fn +
hook + shared ServiceTestPanel wired into CreateServiceDialog + Settings.tsx
ServiceConfigEditor (per design source-finding). Each slice leaves pytest/npm
build/npm lint green.
2026-07-09 22:25:27 +00:00
Developer 1fc3127b58 spec(service-credential-tester): add design
8 decisions: TestResult dataclass, test_callable(store, config, secrets),
translate_connection_error shared helper (extracts test_machine_ssh patterns),
POST /api/services/test validation-first, shared ServiceTestPanel component,
field-edit-clears-result, Prom test via Grafana gateway, ssh_tasks reuses
build_ssh_client. 2-slice plan. Source findings: edit dialog is in Settings.tsx
ServiceConfigEditor (not ServicePage.tsx — spec drift); test_machine_ssh is
inlined in router (not reusable as-is).
2026-07-09 22:21:03 +00:00
Developer ce5ee4f0a0 spec(service-credential-tester): add spec (21 reqs CT-101..121)
Per-type test routines for 7 remote types (qbittorrent, prometheus via Grafana
gateway, alertmanager, jellyfin, authentik, ssh_tasks, nextcloud) + backups
(no test). Endpoint POST /api/services/test, no persistence, friendly error
translation (resolves qBit login #3 at UI layer). Frontend Test button + gate
Create/Save on pass with Save-anyway override. Stale-proposal correction:
jellyseerr is not in the registry (merged into jellyfin).
2026-07-09 22:12:32 +00:00
Developer a5ca1521fe spec(service-credential-tester): update prom test to gateway path
Dependency on grafana-metric-gateway: the prometheus service now sources via
Grafana /api/ds/query (no direct Prom endpoint). Test routine changes from
GET /api/v1/query?query=up to POST {grafana_url}/api/ds/query with api_key +
datasource_uid, expr 'up'.
2026-07-09 22:07:37 +00:00
Developer 9236fd8ac2 chore(grafana-metric-gateway): archive verified+synced change
Move to openspec/changes/archive/2026-07-09-grafana-metric-gateway/
(R100 renames preserved). 9 artifacts. Canonical openspec/specs/
prometheus-charting/ (30 reqs, first non-additive sync) remains. Carry-overs
in archive-report: SC-106 stale component name (cosmetic); partial revert of
prometheus-direct-charting per new network constraint.
2026-07-09 22:06:43 +00:00
Developer cb8dd13514 spec(grafana-metric-gateway): sync — FIRST non-additive (MODIFIED) canonical
16 MODIFIED prometheus-charting requirements (transport: direct Prom -> Grafana
gateway; intent preserved where applicable), 3 ADDED (SC-128 gateway status,
SC-129 startup validation, SC-130 sanctioned transport), 11 PRESERVED, 0 REMOVED.
All 16 MODIFIED headers matched canonical exactly. Post-sync: 30 requirements.
web-ui + service-storage canonicals untouched.
2026-07-09 22:01:55 +00:00
Developer c886fcdf09 spec(grafana-metric-gateway): verify + close GM-115 + reconcile tracking
Add 3 test cases (startup old-config validation warning; status auth_failed
for 401/403) closing the GM-115 PARTIAL. Write apply-progress.md, tick all 33
tasks, add verify-report.md (15/16 PASS, 1 PARTIAL->PASS). Gates green: 331+
pytest, ruff clean, npm build+lint 0 errors, 151 vitest.
2026-07-09 21:53:18 +00:00
Developer 7e91e7f931 feat(grafana-metric-gateway): slice 2 — rename widgets to Metric*
git mv PrometheusChartWidget→MetricChartWidget, PrometheusGaugeWidget→
MetricGaugeWidget, PrometheusMeanWidget→MetricMeanWidget (+ 3 test files,
R100 history preserved). Update registry imports/refs + barrel exports.
Adapt PrometheusMetricWidget for §3.4 Option A: read normalized {result:
[{label,points}]} series shape (last-point extraction) instead of old Prom
{resultType,result} vector. PrometheusMetricWidget NOT renamed (design §3.1).
GM-111/112/116 satisfied. All gates: 151 vitest, build exit 0, lint 0 errors.
2026-07-09 21:34:26 +00:00
Developer df80c68f89 feat(grafana-metric-gateway): slice 1 — backend gateway transport
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).
2026-07-09 21:26:05 +00:00
Developer 798196ffc7 spec(grafana-metric-gateway): add tasks (2 slices, each <=400 lines)
S1 backend transport: gateway config + normalize_grafana_frames (from 65bae95)
+ MetricSource adapter + status + validation + CHANGELOG + tests. S2 frontend:
git mv widget renames to Metric* + registry/barrel updates. Each slice leaves
pytest/npm build/npm lint green. 5 risk flags incl first non-additive sync.
2026-07-09 21:01:38 +00:00
Developer 872e95f8f7 spec(grafana-metric-gateway): add design
8 design decisions: PrometheusConfig (grafana_url/datasource_uid + grafana_api_key
secret), /api/ds/query body per widget kind (window presets -> intervalMs/
maxDataPoints), normalize_grafana_frames refactored from 65bae95 into
prometheus_range.py (shares label-dedup with matrix normalizer), MetricSource
adapter, gateway-path status check, git mv widget renames, startup old-config
validation. 2-slice plan. 3 source findings flagged.
2026-07-09 20:57:07 +00:00
Developer bf8de32815 spec(grafana-metric-gateway): add spec (16 reqs GM-101..116)
Gateway transport: prometheus service config gains grafana_url/api_key/
datasource_uid; all queries via POST /api/ds/query; frames->series restored.
First non-additive canonical sync: MODIFIES 17 SC- requirements (transport
changes, intent preserved), PRESERVES 11, ADDS 3 (gateway status, startup
validation, sanctioned-transport statement). 3 spec assumptions settled where
proposal was silent.
2026-07-09 20:49:45 +00:00
Developer 8afdd9c2bc spec(grafana-metric-gateway): add proposal
Route metric queries through Grafana /api/ds/query instead of direct Prom
(Prom is firewalled / unreachable from Manage; Grafana is the only path).
Partial revert of prometheus-direct-charting: keep the gauge/mean/
LineSeriesChart rendering, restore the frames->series normalizer (recovered
from git 65bae95), change prometheus service config to hold Grafana gateway
fields (url + api_key + datasource_uid). Rename widgets to neutral Metric*.
First non-additive canonical sync (prometheus-charting MODIFIED). Updates the
pending service-credential-tester proposal dependency.
2026-07-09 20:41:59 +00:00
Developer 3e3e69b0be spec(service-credential-tester): add proposal
On-demand per-service-type credential tester for the add/edit dialog. Model on
test_machine_ssh. POST /api/services/test dispatching to per-type routines
(qBit login+maindata, Prom instant query, Jellyfin /Users, etc.) returning
{ok, detail, evidence}. Frontend Test button + gate Create/Save on pass with
'Save anyway' override. Resolves #3 (qBit login failures visible in UI not
logs) and complements #1 (validation surfacing, already shipped).
2026-07-09 20:13:57 +00:00
Developer cadb6d0991 fix(nav): add qBittorrent to per-service-type navbar entries
qBittorrent was missing from SERVICE_TYPE_NAV_ENTRIES, so configured qBit
instances never appeared in the left nav (unlike jellyfin/prometheus/etc.).
Add entry with Magnet icon. navEntries.test.ts filters by configured types
(no fixed-count assertion) so it stays green.
2026-07-09 20:11:12 +00:00
Developer 493c0e1aeb fix(services): surface validation errors in add-service dialog
CreateServiceDialog.save() awaited mutateAsync without a try/catch, so a
backend 422 (e.g. base_url missing http:// schema) threw uncaught and the
dialog sat silent with no feedback. Wrap in try/catch, hold the error in
local state, render a destructive Alert above the footer. Reset/onClose
only on success; on error the user can fix and retry.
2026-07-09 20:10:46 +00:00
Developer a9381e2471 spec(per-instance-hook-scoping): add proposal
Correctness fix: observability + backup hooks query globally, so multi-instance
service pages show data for the wrong instance. Tabs already accept instance
prop with TODO comments; backend mostly supports service_id already. Scope:
add serviceId to 6 hooks + fetch fns + 3 tabs; add service_id to backup
endpoints. Backward-compatible (optional params). ~250-350 lines, single slice.
2026-07-09 14:35:14 +00:00
Developer e461279566 chore(services-as-hub-ia): archive verified change (paperwork-only)
Code merged to main on 2026-06-26 (01527ae) + fix passes. All 10 ACs PASS.
~8600 ins / ~3900 del across 103 files. Only SDD artifacts were untracked.
No code changes, no canonical sync. NOTE: verify-report describes a Grafana
LinksTab later removed by prometheus-direct-charting (2026-07-08); historically
accurate point-in-time record — see archive-report.md footnote.
2026-07-09 14:11:15 +00:00
Developer 1fe7c06083 chore(mobile-responsive-parity): archive verified change (paperwork-only)
Code merged to main on 2026-06-26 via rebase (01527ae) + bug-fix passes
(5a43894/04871bd/f7f590f). All 8 ACs PASS. Only SDD artifacts were untracked;
this archive closes the paperwork gap. No code changes, no canonical sync.
See archive-report.md for carry-over notes (iOS real-device test residual).
2026-07-09 14:11:05 +00:00
Developer ec7ebd7013 chore: regenerate stale .pi-map.md / .pi-map.index.md project maps
Project map had drifted: claimed recharts unused (was used), ObservabilityPage.tsx
exists (refactored to service-tabs/), missed JellyfinNowPlayingWidget +
authentik/backups service types. Regenerate to reflect post-change reality:
new Qbittorrent/LineSeriesChart/Prometheus{Gauge,Mean}Widget files,
service_data.py/qbittorrent_store.py, canonical prometheus-charting +
service-storage specs, archived changes.
2026-07-09 14:05:56 +00:00
Developer 98f17f6e5d chore: apply pre-existing formatter reformat to MediaTab.tsx
Whitespace-only line reflow (formatter-on-save artifact from a prior session,
not logic). Committing to pristine the working tree.
2026-07-09 13:50:36 +00:00
Developer 5cb5e79032 chore(service-storage-harness): archive verified+synced change
Move to openspec/changes/archive/2026-07-09-service-storage-harness/
(history preserved via rename detection). 9 artifacts: proposal/spec/design/
tasks/apply-progress/verify-report/sync-report/archive-report + delta spec.
Canonical openspec/specs/service-storage/ remains. Native status engine
discrepancy (ambiguous change selection) disregarded per parent verification.
2026-07-09 09:47:05 +00:00
Developer c9201a004c spec(service-storage-harness): sync into canonical service-storage domain
New canonical openspec/specs/service-storage/spec.md (28 reqs SS-101..128).
Change-side delta + sync-report. web-ui + prometheus-charting canonicals
untouched.
2026-07-09 09:37:22 +00:00
Developer 40a7ac80d3 spec(service-storage-harness): verify + reconcile tracking
Write apply-progress.md, tick all 35 tasks, add verify-report.md (28/28
SS-101..128 PASS). Gates green: 322 pytest, ruff clean, FE build+lint 0
errors, PrometheusChartWidget extraction 4/4 non-regressive. No blocking
code findings. Process note: slice 2 was +644 lines over 400-line budget
(additive, no scope creep; retrospectively extract+widgets could split).
2026-07-09 09:30:52 +00:00
Developer c9404f0794 spec(service-storage-harness): add spec (28 requirements SS-101..128)
Covers harness lifecycle, QbittorrentSampleStore, QbittorrentClient,
3 widget kinds (totals=item count, active=DL/UL, speed=LineSeriesChart
reuse), MediaIndex migration (+service_id, scoped replace_items bug fix,
backward-compat query), cascade-delete wiring, test/build greenness.
2026-07-09 09:15:32 +00:00
Developer 75c949ad25 feat(service-storage-harness): slice 4 — cascade-delete wiring + integration test
Wire ServiceDataHarness.cascade_delete into SettingsStore.delete_service
(best-effort try/except, logs on failure). Fix migration runner to also
catch 'no such table' on fresh DBs (ALTER TABLE before init_schema).
Integration test proves end-to-end cascade across both concerns (qBit
samples + media items) with multi-instance preservation.

Backend: 322 pytest pass, ruff clean.
2026-07-09 09:11:42 +00:00
Developer c87f398e37 feat(service-storage-harness): slice 3 — migrate MediaIndex onto harness (scoped replace_items, +service_id)
Register MediaIndex as a harness concern with ALTER TABLE migration to add
service_id column (idempotent). Scope replace_items by service_id (FIXES latent
global-clear bug where building for one Jellyfin wiped another's rows). Scope
query by service_id (empty-string = all rows, backward-compat). Thread
service_id through build_media_index + worker + query_media router. New
regression test proves scoped replace preserves other services' rows.

Backend: 321 pytest pass, ruff clean. Frontend: build green.
2026-07-09 09:00:31 +00:00
Developer 1fb12b8a0a feat(service-storage-harness): slice 2 — qbit widgets + LineSeriesChart extract 2026-07-09 08:49:20 +00:00
Developer e7bd0afdd1 feat(service-storage-harness): slice 1 — harness + qbit store + client + integration
ServiceDataHarness (services/service_data.py): lifecycle-only registry of
per-concern storage — DB provisioning, idempotent migrations (ALTER TABLE
duplicate-column-name caught per-statement), cascade_delete(service_id).
QbittorrentSampleStore: append/window/prune (MAX_SAMPLES=120) in
qbittorrent.db. QbittorrentClient: cookie-login Web API client (403 re-login,
/sync/maindata). Integration registered with 3 widget kinds (totals/active/
speed). Harness initialized in main.py lifespan.

Backend: 314 pytest pass (21 new), ruff clean.
2026-07-09 08:24:08 +00:00
Developer 9a251db23c spec(service-storage-harness): add tasks (4 slices, each <=400 lines)
S1 harness+store+client+integration; S2 widget adapter+3 FE widgets+
LineSeriesChart extract; S3 MediaIndex +service_id migration (fixes latent
global-clear bug); S4 cascade-delete wiring. Each slice leaves pytest/npm
build/npm lint green. Risk flags on LineSeriesChart extract + MediaIndex migration.
2026-07-09 08:02:31 +00:00
Developer cff082c7a1 spec(service-storage-harness): add design
10 design decisions incl: lifecycle-only harness, per-concern DB files,
QbittorrentSampleStore (120-sample cap), qBit cookie client, 3 widget kinds
(totals=item count, active=DL/UL state, speed=reuses Change A renderer via
shared LineSeriesChart extract), MediaIndex +service_id backfill, scoped
replace_items (fixes latent global-clear bug), best-effort cascade-delete.
4-slice plan. 3 source findings (worker already threads service_id).
2026-07-09 07:55:59 +00:00