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

56 lines
4.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.py``ServiceDataHarness`: 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.py``QbittorrentSampleStore`: `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.py``QbittorrentClient`: 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_items`** — `DELETE 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).