Files
manage/openspec/changes/service-storage-harness/apply-progress.md
T
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

4.8 KiB
Raw Blame History

Apply Progress: Service Storage Harness

Change: service-storage-harness Phase: apply-progress Date: 2026-07-09 Status: complete — all 35 tasks done, all gates green, verified (see verify-report.md)

Slices delivered

Four slices, each its own commit, each leaving pytest / npm run build / npm run lint / ruff green.

Slice 1 — Harness + qBit store + client + integration (commit e7bd0af, amended)

  • services/service_data.pyServiceDataHarness: lifecycle-only (concern registration via dataclass, idempotent run_migrations catching "duplicate column name" per-statement, cascade_delete(service_id) iterating owned tables). No generic data ops (SS-101..104).
  • services/qbittorrent_store.pyQbittorrentSampleStore: qbittorrent_speed_samples(service_id, ts, dl_speed, up_speed) + index in dedicated qbittorrent.db; append/window/prune, MAX_SAMPLES=120; registered as a harness concern (SS-105..107).
  • clients/qbittorrent.pyQbittorrentClient: cookie login via /api/v2/auth/login, 403 re-login+retry, maindata() via /api/v2/sync/maindata (SS-108..110).
  • integrations/qbittorrent.py + registry entry — config (base_url, timeout_seconds) + secret (username, password) schema; 3 widget kinds declared.
  • Initialized in main.py lifespan.
  • Tests: test_service_data.py (harness lifecycle), test_qbittorrent_store.py, test_qbittorrent_client.py.

Slice 2 — qBit widgets + LineSeriesChart extract (commit 8b0e7ea, amended)

  • QbittorrentWidgetSource in widgets/sources.py — 3 branches: totals (item count from maindata), active (filter state ∈ {downloading, uploading}), speed (append sample + return {series} from .window(), ts×1000 for JS ms). Errors → {error}. Registered in SERVICE_ADAPTERS (SS-111..115).
  • frontend/src/components/LineSeriesChart.tsx — shared recharts renderer extracted from PrometheusChartWidget (~40 lines, props {series, height?}); PrometheusChartWidget becomes a thin wrapper. Extraction non-regressive: 4 PrometheusChartWidget tests stay green (SS-118).
  • QbittorrentTotalsWidget (count), QbittorrentActiveTorrentsWidget (list), QbittorrentSpeedWidget (uses LineSeriesChart). Registry binding + barrel + tests (SS-116..117).

Slice 3 — MediaIndex migration (commit c87f398) — LOAD-BEARING

  • Idempotent harness migration: ALTER TABLE media_items ADD COLUMN service_id TEXT NOT NULL DEFAULT ''. media_index.db file location UNCHANGED. Existing rows backfill to service_id='' via DEFAULT (SS-119, SS-120, SS-124).
  • Scoped replace_itemsDELETE FROM media_items WHERE service_id = ? replaces the prior global DELETE FROM media_items. FIXES the latent global-clear bug where rebuilding for one Jellyfin wiped another's rows. service_id stamped into inserted rows. Regression test test_replace_scoped_by_service_id_preserves_other_services proves svc-A survives svc-B's rebuild (SS-121).
  • query(service_id="") shows all rows (backward-compat); query(service_id="X") scopes. Existing MediaIndex + API tests pass unchanged (62 passed) (SS-122).
  • Worker threads the real service_id (already plumbed via --service-id) into replace_items so new rows are stamped (SS-123).

Slice 4 — Cascade-delete wiring (commit 75c949a)

  • settings_store.delete_service calls ServiceDataHarness.cascade_delete(service_id) after existing cleanup, best-effort try/except (failure logs, doesn't crash the delete) (SS-125).
  • Integration test proves end-to-end cascade across BOTH concerns (qBit samples + media items) with multi-instance preservation (SS-126).

Deviations from tasks.md

  • Slice 2 over the 400-line review budget (verify-report flagged +644 source lines). The slice is additive (3 new widgets + extraction + tests), no scope creep, but the per-slice budget from openspec/config.yaml was exceeded. Retrospectively this could have been split (extraction in one slice, qBit widgets in another). No code defect; recorded here as a process note for future slicing. The verify agent flagged it WARNING, not blocking.

Final gate results

Gate Result
backend && PYTHONPATH=src python3 -m pytest -q 322 passed, 2 warnings (pre-existing pythonjsonlogger DeprecationWarning)
backend && PYTHONPATH=src python3 -m ruff check src tests All checks passed
frontend && npm run build exit 0 (pre-existing chunk-size warning)
frontend && npm run lint 0 errors, 1 pre-existing warning (WidgetConfigDialog.tsx, untouched)
frontend && npx vitest run PrometheusChartWidget.test.tsx 4 passed (extraction non-regression confirmed)

Verification

See verify-report.md — adversarial fresh-context review: 28/28 PASS. No blocking code findings. Archive blocker is doc-only (this file + the ticked tasks.md clear it).