diff --git a/openspec/changes/service-storage-harness/specs/service-storage/spec.md b/openspec/changes/service-storage-harness/specs/service-storage/spec.md
new file mode 100644
index 0000000..5993fab
--- /dev/null
+++ b/openspec/changes/service-storage-harness/specs/service-storage/spec.md
@@ -0,0 +1,139 @@
+# Service Storage — Delta (`service-storage-harness`)
+
+> Change: `service-storage-harness` · Domain: `service-storage` · Phase: **spec** (reconciled during `sdd-sync`).
+> Distilled verbatim from the verified flat `spec.md` (28 requirements, SS-101 … SS-128) of change
+> `service-storage-harness`, cross-referenced against `design.md` and `verify-report.md`. Captures
+> the **durable, post-change end-state contracts** for the lifecycle-only `ServiceDataHarness`, the
+> qBittorrent store/client/widget stack built on it, the MediaIndex migration onto the harness, and
+> the cross-concern cascade-delete wiring.
+
+## ADDED Requirements
+
+> The canonical `openspec/specs/service-storage/spec.md` did not exist before this change. All
+> requirements below are therefore **ADDED** to a new `service-storage` domain; `sdd-sync` copies
+> them into the canonical spec (native helper rule: when the canonical spec does not exist, the
+> change spec becomes the new canonical spec).
+>
+> Requirement IDs (SS-101 … SS-128) and body text are preserved **exactly** from the verified flat
+> `spec.md`. Requirements are grouped logically and listed in the following group order:
+>
+> - **ServiceDataHarness (lifecycle layer)** — SS-101 … SS-104
+> - **QbittorrentSampleStore** — SS-105 … SS-107
+> - **QbittorrentClient** — SS-108 … SS-110
+> - **qBittorrent widget source adapter** — SS-111 … SS-115
+> - **qBittorrent frontend widgets** — SS-116 … SS-118
+> - **MediaIndex migration onto harness** — SS-119 … SS-124
+> - **Cascade-delete wiring** — SS-125 … SS-126
+> - **Test and build greenness** — SS-127 … SS-128
+
+### Requirement: SS-101 — Harness is lifecycle-only
+
+A `ServiceDataHarness` class exists in `backend/src/media_library_viewer_api/services/service_data.py` that owns ONLY lifecycle concerns: per-concern DB provisioning, per-integration migrations, `service_id` cascade-delete. It MUST NOT provide generic data operations (no generic value table, no generic CRUD).
+
+### Requirement: SS-102 — Concern registration
+
+An integration/concern registers via a dataclass carrying: DB filename, ordered migration SQL list, owned-tables list, and `service_id` column name. The harness stores registered concerns.
+
+### Requirement: SS-103 — Idempotent migrations
+
+`run_migrations` runs each concern's migration statements and MUST be idempotent — specifically, re-running `ALTER TABLE ... ADD COLUMN` on an already-migrated DB MUST NOT raise (the harness catches "duplicate column name" per-statement).
+
+### Requirement: SS-104 — Cascade-delete across concerns
+
+`cascade_delete(service_id)` iterates every registered concern and, for each owned table, executes `DELETE FROM
WHERE = ?`. It MUST cover every registered concern (qBittorrent samples + media items after this change).
+
+### Requirement: SS-105 — Schema
+
+`QbittorrentSampleStore` owns a `qbittorrent_speed_samples` table with columns `(service_id, ts, dl_speed, up_speed)` and an index on `(service_id, ts)`, in a dedicated `qbittorrent.db` file (per-concern topology).
+
+### Requirement: SS-106 — append/window/prune operations
+
+The store exposes `append(service_id, ts, dl_speed, up_speed)`, `window(service_id, since_ts)` returning ordered rows, and prunes per-append to `MAX_SAMPLES = 120`.
+
+### Requirement: SS-107 — Registered as a harness concern
+
+The qBittorrent sample store is registered with the harness so its table participates in cascade-delete (SS-104).
+
+### Requirement: SS-108 — Cookie login
+
+`QbittorrentClient` authenticates via `POST /api/v2/auth/login` with username/password, stores the resulting cookie, and reuses it for subsequent requests. Credentials are resolved from the `ServiceRecord` secrets (Fernet-encrypted at rest).
+
+### Requirement: SS-109 — 403 re-login
+
+On HTTP 403 the client MUST transparently re-login once and retry the request.
+
+### Requirement: SS-110 — maindata fetch
+
+The client exposes `maindata()` calling `/api/v2/sync/maindata` and returning its dict. Errors (timeout, connection, non-2xx) propagate as exceptions for the adapter to catch.
+
+### Requirement: SS-111 — Three widget kinds dispatched
+
+`QbittorrentWidgetSource.fetch(service, widget_kind, config)` dispatches on `widget_kind` ∈ {`totals`, `active`, `speed`}, resolving the client inline from `ServiceRecord` (same pattern as `PrometheusWidgetSource`).
+
+### Requirement: SS-112 — totals = item count
+
+`totals` returns the count of currently-listed torrents from `maindata()` as `{total: int}`. It is NOT cumulative transfer bytes.
+
+### Requirement: SS-113 — active = downloading/uploading filter
+
+`active` returns the subset of torrents whose `state` is `downloading` or `uploading` as `{torrents: [...]}`.
+
+### Requirement: SS-114 — speed appends sample + returns series
+
+`speed` reads the current dl/up speeds from `maindata()`, appends a sample via `QbittorrentSampleStore.append`, and returns `{series: [{label: "download", points: [...]}, {label: "upload", points: [...]}]}` from `.window()` — the exact shape `LineSeriesChart` consumes (timestamps in JS milliseconds).
+
+### Requirement: SS-115 — Errors degrade gracefully
+
+Adapter errors (auth failure, timeout, connection) return `{error: str}` and MUST NOT raise.
+
+### Requirement: SS-116 — Three widget components
+
+`QbittorrentTotalsWidget`, `QbittorrentActiveTorrentsWidget`, `QbittorrentSpeedWidget` exist under `frontend/src/widgets/` and are bound to the `qbittorrent` service binding in `integrations/registry.ts` with their respective kinds.
+
+### Requirement: SS-117 — Speed widget reuses shared renderer
+
+`QbittorrentSpeedWidget` renders via the shared `LineSeriesChart` component (extracted from `PrometheusChartWidget`). No new charting code or charting dependency.
+
+### Requirement: SS-118 — LineSeriesChart extraction is non-regressive
+
+The extraction of the recharts body into `frontend/src/components/LineSeriesChart.tsx` MUST leave `PrometheusChartWidget`'s behavior and tests green; `PrometheusChartWidget` becomes a thin wrapper.
+
+### Requirement: SS-119 — service_id column added
+
+`media_items` gains a `service_id TEXT NOT NULL DEFAULT ''` column via an idempotent harness migration. The `media_index.db` file location is UNCHANGED.
+
+### Requirement: SS-120 — Existing rows backfill
+
+All pre-existing `media_items` rows receive `service_id = ''` (via the column DEFAULT), preserving their visibility.
+
+### Requirement: SS-121 — replace_items is scoped (bug fix)
+
+`replace_items(service_id=...)` deletes only `WHERE service_id = ?` (not a global `DELETE FROM media_items`). This FIXES the latent global-clear bug where rebuilding for one Jellyfin instance wiped another's rows. A regression test MUST prove service A's rows survive service B's rebuild.
+
+### Requirement: SS-122 — query is backward-compatible
+
+`query(service_id="")` returns all rows (no filter); `query(service_id="X")` scopes to service X. Existing tests that pass no `service_id` MUST continue to pass unchanged.
+
+### Requirement: SS-123 — Worker threads service_id into rows
+
+The media index worker (which already receives `--service-id`) passes it into `replace_items` so newly-built rows are stamped with the real service_id.
+
+### Requirement: SS-124 — Registered as a harness concern
+
+MediaIndex registers `media_items` as a harness-owned table so it participates in cascade-delete.
+
+### Requirement: SS-125 — delete_service triggers harness cascade
+
+`settings_store.delete_service` calls `ServiceDataHarness.cascade_delete(service_id)` after its existing cleanup, in a best-effort try/except (a cascade failure MUST NOT crash the service deletion; it logs and continues).
+
+### Requirement: SS-126 — End-to-end cascade across both concerns
+
+Deleting a service removes both its qBittorrent samples AND its media rows. An integration test MUST prove this across both concerns, and MUST prove rows of OTHER services are preserved.
+
+### Requirement: SS-127 — Backend tests + lint green
+
+`PYTHONPATH=src python3 -m pytest -q` and `PYTHONPATH=src python3 -m ruff check src tests` from `backend/` MUST pass, including new tests for: harness lifecycle, qBit store, qBit client (login/403/maindata), qBit widget adapter (3 branches), MediaIndex scoped replace_items regression, and cascade-delete integration.
+
+### Requirement: SS-128 — Frontend build + lint green
+
+`npm run build` and `npm run lint` from `frontend/` MUST pass (0 errors). New widget tests cover loading/error/rendered states; PrometheusChartWidget tests stay green (SS-118).
diff --git a/openspec/changes/service-storage-harness/sync-report.md b/openspec/changes/service-storage-harness/sync-report.md
new file mode 100644
index 0000000..db85ddb
--- /dev/null
+++ b/openspec/changes/service-storage-harness/sync-report.md
@@ -0,0 +1,166 @@
+# Sync Report — `service-storage-harness`
+
+> Phase: **sync** · Change: `service-storage-harness` · Repo: `/home/user/manage`
+> Mode: file-backed (`artifactStore: openspec`). No source-code edits; only OpenSpec artifacts were
+> written. Not committed (parent owns the commit). The change folder was **not** moved (that is
+> `sdd-archive`'s job).
+
+**Status: SYNCED.** A new canonical domain `openspec/specs/service-storage/spec.md` was created
+from the verified change, and the change-side domain delta spec that unblocks the native status
+engine's `sync`/`archive` gates is also in place.
+
+---
+
+## 1. Executive summary
+
+The `service-storage-harness` change shipped a **complete but flat** `openspec/changes/service-storage-harness/spec.md`
+(28 requirements, SS-101 … SS-128) with **no** per-domain delta spec under
+`openspec/changes/service-storage-harness/specs//`. `sdd-sync` requires a domain delta
+spec; the flat spec alone does not satisfy the canonical-merge contract.
+
+Verify already returned **PASS** (verdict in `verify-report.md`; all four gates green — backend
+`pytest` 322 passed, `ruff` clean, frontend `npm run build` exit 0, `npm run lint` 0 errors).
+Functional coverage was **28/28 fully PASS**. The verify report's single CRITICAL was an **archive**
+blocker (30 unchecked task checkboxes + missing `apply-progress.md`); `apply-progress.md` now exists
+and reconciles the 35 tasks (the task tracker condition does **not** block `sdd-sync` of the green
+code).
+
+This sync **reconciles** the flat-spec-vs-domain-spec gap:
+
+1. Authored the missing **change-side domain delta spec** —
+ `openspec/changes/service-storage-harness/specs/service-storage/spec.md` — using a clean
+ `## ADDED Requirements` structure that preserves the exact requirement IDs (SS-101 … SS-128) and
+ text from the verified flat `spec.md`. This is what flips the native status engine's `specs`
+ artifact from partial → done.
+2. **Synced** the end-state into the **canonical store** —
+ `openspec/specs/service-storage/spec.md` — the actual sync target. Because the canonical
+ `service-storage` domain did not previously exist, the native helper rule applies: *when the
+ canonical spec does not exist, the change spec becomes the new canonical spec.* The two files
+ therefore carry identical requirement bodies (delta under `## ADDED Requirements`; canonical
+ under `## Requirements`).
+
+Domain name **`service-storage`** was chosen (per the dispatch brief) because it covers the full
+new model: the lifecycle-only `ServiceDataHarness` layer, the qBittorrent store + client + widget
+stack, and the MediaIndex migration that established the per-service storage pattern. It is distinct
+from the existing canonical domains `web-ui` (MUI→shadcn migration) and `prometheus-charting`
+(direct Prometheus metric visualization), neither of which was **touched**.
+
+## 2. Structured status & actionContext findings
+
+The native `gentle-pi.sdd-status` passed by the parent reports `changeName: null` with
+`blockedReasons: ["Change selection is ambiguous: mobile-responsive-parity, service-storage-harness,
+services-as-hub-ia."]` because the engine auto-detected three active changes. This sync task was
+**explicitly assigned** `service-storage-harness`; the ambiguity is a parent-resolution artifact
+and does not block this phase (`isNonAuthoritative: false`).
+
+- `artifactStore: openspec`; change root `openspec/changes/service-storage-harness/`.
+- Artifacts present: `proposal.md`, `spec.md`, `design.md`, `tasks.md`, `verify-report.md`,
+ `apply-progress.md`.
+- `verify: PASS` (verify-report verdict; gates green at `c9404f0`).
+- `actionContext`: `mode: repo-local`, `workspaceRoot: /home/user/manage`,
+ `allowedEditRoots: ["/home/user/manage"]`, `warnings: []`. All three files written are inside the
+ authoritative workspace / allowed edit roots. ✓
+- `relationships.sameDomainActiveChanges: []`, `collisions: []` — **no active same-domain
+ collisions**, so no archive/sync ordering decision was required.
+- The new `service-storage` domain is distinct from the existing `web-ui` and `prometheus-charting`
+ canonical domains; both were left untouched.
+
+**Post-sync structural change:** `openspec/changes/service-storage-harness/specs/service-storage/spec.md`
+now exists (`hasDomainSpecs` → true), resolving the missing-domain-spec condition that gated sync.
+The flat `spec.md` is intentionally **left in place** as the authoritative planning artifact the
+work was built against (the archive convention keeps flat specs too); it no longer triggers the
+"flat spec without domain specs" condition now that a domain delta sits alongside it.
+
+## 3. Domains synced & canonical files updated
+
+| Domain | Change-side delta (source) | Canonical (sync target) | Action |
+|---|---|---|---|
+| `service-storage` | `openspec/changes/service-storage-harness/specs/service-storage/spec.md` | `openspec/specs/service-storage/spec.md` | **NEW domain** — `## ADDED Requirements` copied into canonical as a new spec |
+
+- **Canonical file created:** `openspec/specs/service-storage/spec.md` (28 requirements).
+- **Change-side delta created:** `openspec/changes/service-storage-harness/specs/service-storage/spec.md`
+ (28 requirements, all `## ADDED Requirements`).
+
+## 4. Requirement delta (ADDED / MODIFIED / REMOVED)
+
+- **ADDED (28)** — all to the new `service-storage` domain (canonical did not exist pre-change).
+ IDs and text preserved verbatim from the verified flat `spec.md`. Grouped logically:
+ - *ServiceDataHarness (lifecycle layer)* — SS-101, SS-102, SS-103, SS-104
+ - *QbittorrentSampleStore* — SS-105, SS-106, SS-107
+ - *QbittorrentClient* — SS-108, SS-109, SS-110
+ - *qBittorrent widget source adapter* — SS-111, SS-112, SS-113, SS-114, SS-115
+ - *qBittorrent frontend widgets* — SS-116, SS-117, SS-118
+ - *MediaIndex migration onto harness* — SS-119, SS-120, SS-121, SS-122, SS-123, SS-124
+ - *Cascade-delete wiring* — SS-125, SS-126
+ - *Test and build greenness* — SS-127, SS-128
+- **MODIFIED (0)** — none (new domain; no pre-existing canonical requirements to replace).
+- **REMOVED (0)** — none.
+- **RENAMED (0)** — none (RENAMED is intentionally unsupported by the native delta helper; not used).
+
+## 5. Guardrails, approvals & destructive-sync assessment
+
+- **Same-domain collisions:** none (`sameDomainActiveChanges: []`, `collisions: []`). The new
+ `service-storage` domain does not overlap the existing `web-ui` or `prometheus-charting`
+ canonical domains. No ordering decision was needed.
+- **Destructive sync:** **not applicable.** There are zero REMOVED requirements and zero large
+ MODIFIED blocks (new domain; everything is ADDED). No destructive-sync parent approval was
+ required beyond the explicit reconciliation instruction in the task.
+- **Legacy flat spec:** detected pre-sync; resolved by adding the domain delta spec alongside it
+ (the block condition is specifically "flat spec *without* domain specs"). The flat spec was left
+ in place as a planning artifact.
+- **`web-ui` / `prometheus-charting` canonical isolation:** the existing
+ `openspec/specs/web-ui/spec.md` (MUI→shadcn rework) and `openspec/specs/prometheus-charting/spec.md`
+ (direct Prometheus charting) were **not modified** — verified untouched by `git status`. The three
+ domains are independent.
+
+## 6. Validation / checks performed (file-backed, read-only)
+
+Run from `/home/user/manage` (no source edits, no test re-runs — those are owned by verify and were
+already green at `c9404f0`):
+
+| Check | Command | Result |
+|---|---|---|
+| Canonical store populated | `ls openspec/specs/service-storage/spec.md` | present ✓ |
+| Change-side domain spec present | `ls openspec/changes/service-storage-harness/specs/service-storage/spec.md` | present ✓ |
+| Requirement-ID parity (flat ↔ delta ↔ canonical) | `grep -oE 'SS-[0-9]+'` all three files, `sort -u` | **28 == 28 == 28**, identical IDs SS-101…SS-128 ✓ |
+| Body-text parity (delta ↔ canonical) | `diff` of the `^### Requirement:` region of both files | **identical** ✓ |
+| Delta is pure ADDED | count `## ADDED/MODIFIED/REMOVED/RENAMED Requirements` | ADDED=1, MODIFIED=0, REMOVED=0, RENAMED=0 ✓ (no destructive sync) |
+| Other canonicals untouched | `git status --porcelain openspec/specs/web-ui openspec/specs/prometheus-charting` | empty (not modified) ✓ |
+| No edits outside openspec | `git status --porcelain` (filtered) | only `openspec/specs/service-storage/`, `openspec/changes/service-storage-harness/specs/`, and this report added ✓ |
+| Markdown validity | write-time lint | all three files "Markdown clean" ✓ |
+
+## 7. Carry-over items for the archive summary
+
+These verify-phase findings are non-blocking for sync and should land in the archive summary:
+
+1. **[CRITICAL-process, archive-only] Unchecked task checkboxes.** At verify time, 30
+ implementation/verification task checkboxes (§1.1–5.5) were unchecked and `apply-progress.md`
+ was missing. `apply-progress.md` now exists (created after the verify pass, reconciling all 35
+ tasks). `sdd-archive` should re-scan the native status engine to confirm `tasks: done` /
+ `applyProgress: present` before moving the change to archive, and tick any remaining unchecked
+ boxes if needed.
+2. **[WARNING] Slice 2 over the 400-line review budget** (~644 source insertions vs the 400-line
+ budget / ~350–400 forecast). Additive feature slice (3 widgets + `LineSeriesChart` extraction +
+ tests); boundary is exactly the qBit-widget feature, no scope creep. No `size:exception`
+ recorded; non-blocking — record the actual in the archive summary.
+3. **[INFO] `LineSeriesChart.test.tsx` is smoke-only** (asserts mount, not rendered `` series).
+ Non-blocking coverage note.
+4. **[INFO] Stale generated `.pi-map.md`** files predate the new modules (`service_data.py`,
+ `qbittorrent_store.py`, `clients/qbittorrent.py`, `Qbittorrent*.tsx`, `LineSeriesChart.tsx`);
+ not deliverable source. Regenerate via `project_map_patch` / `project_map_validate`.
+
+## 8. Next recommended phase
+
+→ **`sdd-archive`** (clean). Confirm the native status re-scan reports `specs: done` / `sync: ready`
+/ `archive: ready`, then move the change to
+`openspec/changes/archive/2026-07-09-service-storage-harness`, carrying over the items in §7 into
+the archive summary. Do **not** commit or push — the parent owns the commit with explicit paths.
+
+---
+
+### Appendix — Files written by this sync (OpenSpec only; no source code)
+
+- `openspec/changes/service-storage-harness/specs/service-storage/spec.md` — **change-side
+ domain delta (`## ADDED Requirements`), 28 requirements SS-101…SS-128.**
+- `openspec/specs/service-storage/spec.md` — **canonical spec (new domain), 28 requirements.**
+- `openspec/changes/service-storage-harness/sync-report.md` — this report.
diff --git a/openspec/specs/service-storage/spec.md b/openspec/specs/service-storage/spec.md
new file mode 100644
index 0000000..fbce4d3
--- /dev/null
+++ b/openspec/specs/service-storage/spec.md
@@ -0,0 +1,138 @@
+# Service Storage
+
+> Domain: `service-storage` · **Canonical specification.** Synced from change `service-storage-harness`.
+>
+> This is the merged end-state of standing up a lifecycle-only per-service data layer and migrating
+> the qBittorrent integration and the MediaIndex onto it. It captures the durable, post-change
+> contracts for the `ServiceDataHarness` lifecycle layer, the qBittorrent store/client/widget stack,
+> the MediaIndex migration, and cross-concern cascade-delete — not the per-slice delivery strategy
+> (which remains on record in the change's `spec.md` / `tasks.md` under
+> `openspec/changes/service-storage-harness/`).
+
+## Purpose
+
+Define WHAT must be true of Manage's per-service data lifecycle and storage layer after the change:
+a lifecycle-only `ServiceDataHarness` provisions per-concern databases, runs idempotent
+per-integration migrations, and cascades `service_id` deletes across all owned tables — without
+offering any generic value table or CRUD. The qBittorrent integration (cookie-auth client +
+time-windowed sample store + three-branch widget adapter + three frontend widgets) is the first
+concern built on the harness. The MediaIndex migrates onto the harness as the second concern,
+gaining a `service_id` column and a scoped `replace_items` that fixes the latent global-clear bug
+where rebuilding one Jellyfin instance wiped another's rows. The shared `LineSeriesChart` renderer
+(extracted non-regressively from `PrometheusChartWidget`) backs the qBittorrent speed widget.
+Deleting a service cascades across both concerns while preserving other services' rows. This spec
+is acceptance-focused and verifiable; it deliberately does not prescribe implementation.
+
+## Requirements
+
+### Requirement: SS-101 — Harness is lifecycle-only
+
+A `ServiceDataHarness` class exists in `backend/src/media_library_viewer_api/services/service_data.py` that owns ONLY lifecycle concerns: per-concern DB provisioning, per-integration migrations, `service_id` cascade-delete. It MUST NOT provide generic data operations (no generic value table, no generic CRUD).
+
+### Requirement: SS-102 — Concern registration
+
+An integration/concern registers via a dataclass carrying: DB filename, ordered migration SQL list, owned-tables list, and `service_id` column name. The harness stores registered concerns.
+
+### Requirement: SS-103 — Idempotent migrations
+
+`run_migrations` runs each concern's migration statements and MUST be idempotent — specifically, re-running `ALTER TABLE ... ADD COLUMN` on an already-migrated DB MUST NOT raise (the harness catches "duplicate column name" per-statement).
+
+### Requirement: SS-104 — Cascade-delete across concerns
+
+`cascade_delete(service_id)` iterates every registered concern and, for each owned table, executes `DELETE FROM WHERE = ?`. It MUST cover every registered concern (qBittorrent samples + media items after this change).
+
+### Requirement: SS-105 — Schema
+
+`QbittorrentSampleStore` owns a `qbittorrent_speed_samples` table with columns `(service_id, ts, dl_speed, up_speed)` and an index on `(service_id, ts)`, in a dedicated `qbittorrent.db` file (per-concern topology).
+
+### Requirement: SS-106 — append/window/prune operations
+
+The store exposes `append(service_id, ts, dl_speed, up_speed)`, `window(service_id, since_ts)` returning ordered rows, and prunes per-append to `MAX_SAMPLES = 120`.
+
+### Requirement: SS-107 — Registered as a harness concern
+
+The qBittorrent sample store is registered with the harness so its table participates in cascade-delete (SS-104).
+
+### Requirement: SS-108 — Cookie login
+
+`QbittorrentClient` authenticates via `POST /api/v2/auth/login` with username/password, stores the resulting cookie, and reuses it for subsequent requests. Credentials are resolved from the `ServiceRecord` secrets (Fernet-encrypted at rest).
+
+### Requirement: SS-109 — 403 re-login
+
+On HTTP 403 the client MUST transparently re-login once and retry the request.
+
+### Requirement: SS-110 — maindata fetch
+
+The client exposes `maindata()` calling `/api/v2/sync/maindata` and returning its dict. Errors (timeout, connection, non-2xx) propagate as exceptions for the adapter to catch.
+
+### Requirement: SS-111 — Three widget kinds dispatched
+
+`QbittorrentWidgetSource.fetch(service, widget_kind, config)` dispatches on `widget_kind` ∈ {`totals`, `active`, `speed`}, resolving the client inline from `ServiceRecord` (same pattern as `PrometheusWidgetSource`).
+
+### Requirement: SS-112 — totals = item count
+
+`totals` returns the count of currently-listed torrents from `maindata()` as `{total: int}`. It is NOT cumulative transfer bytes.
+
+### Requirement: SS-113 — active = downloading/uploading filter
+
+`active` returns the subset of torrents whose `state` is `downloading` or `uploading` as `{torrents: [...]}`.
+
+### Requirement: SS-114 — speed appends sample + returns series
+
+`speed` reads the current dl/up speeds from `maindata()`, appends a sample via `QbittorrentSampleStore.append`, and returns `{series: [{label: "download", points: [...]}, {label: "upload", points: [...]}]}` from `.window()` — the exact shape `LineSeriesChart` consumes (timestamps in JS milliseconds).
+
+### Requirement: SS-115 — Errors degrade gracefully
+
+Adapter errors (auth failure, timeout, connection) return `{error: str}` and MUST NOT raise.
+
+### Requirement: SS-116 — Three widget components
+
+`QbittorrentTotalsWidget`, `QbittorrentActiveTorrentsWidget`, `QbittorrentSpeedWidget` exist under `frontend/src/widgets/` and are bound to the `qbittorrent` service binding in `integrations/registry.ts` with their respective kinds.
+
+### Requirement: SS-117 — Speed widget reuses shared renderer
+
+`QbittorrentSpeedWidget` renders via the shared `LineSeriesChart` component (extracted from `PrometheusChartWidget`). No new charting code or charting dependency.
+
+### Requirement: SS-118 — LineSeriesChart extraction is non-regressive
+
+The extraction of the recharts body into `frontend/src/components/LineSeriesChart.tsx` MUST leave `PrometheusChartWidget`'s behavior and tests green; `PrometheusChartWidget` becomes a thin wrapper.
+
+### Requirement: SS-119 — service_id column added
+
+`media_items` gains a `service_id TEXT NOT NULL DEFAULT ''` column via an idempotent harness migration. The `media_index.db` file location is UNCHANGED.
+
+### Requirement: SS-120 — Existing rows backfill
+
+All pre-existing `media_items` rows receive `service_id = ''` (via the column DEFAULT), preserving their visibility.
+
+### Requirement: SS-121 — replace_items is scoped (bug fix)
+
+`replace_items(service_id=...)` deletes only `WHERE service_id = ?` (not a global `DELETE FROM media_items`). This FIXES the latent global-clear bug where rebuilding for one Jellyfin instance wiped another's rows. A regression test MUST prove service A's rows survive service B's rebuild.
+
+### Requirement: SS-122 — query is backward-compatible
+
+`query(service_id="")` returns all rows (no filter); `query(service_id="X")` scopes to service X. Existing tests that pass no `service_id` MUST continue to pass unchanged.
+
+### Requirement: SS-123 — Worker threads service_id into rows
+
+The media index worker (which already receives `--service-id`) passes it into `replace_items` so newly-built rows are stamped with the real service_id.
+
+### Requirement: SS-124 — Registered as a harness concern
+
+MediaIndex registers `media_items` as a harness-owned table so it participates in cascade-delete.
+
+### Requirement: SS-125 — delete_service triggers harness cascade
+
+`settings_store.delete_service` calls `ServiceDataHarness.cascade_delete(service_id)` after its existing cleanup, in a best-effort try/except (a cascade failure MUST NOT crash the service deletion; it logs and continues).
+
+### Requirement: SS-126 — End-to-end cascade across both concerns
+
+Deleting a service removes both its qBittorrent samples AND its media rows. An integration test MUST prove this across both concerns, and MUST prove rows of OTHER services are preserved.
+
+### Requirement: SS-127 — Backend tests + lint green
+
+`PYTHONPATH=src python3 -m pytest -q` and `PYTHONPATH=src python3 -m ruff check src tests` from `backend/` MUST pass, including new tests for: harness lifecycle, qBit store, qBit client (login/403/maindata), qBit widget adapter (3 branches), MediaIndex scoped replace_items regression, and cascade-delete integration.
+
+### Requirement: SS-128 — Frontend build + lint green
+
+`npm run build` and `npm run lint` from `frontend/` MUST pass (0 errors). New widget tests cover loading/error/rendered states; PrometheusChartWidget tests stay green (SS-118).