# Apply Progress: Per-Instance Hook Scoping

**Change:** `per-instance-hook-scoping`
**Phase:** apply-progress
**Date:** 2026-07-09
**Status:** complete — all 17 tasks done, all gates green, verified (see `verify-report.md`)

## Slice delivered

Single slice (commit `3bc7ce5`, +268/−73, 12 files) — small correctness fix, well within the 400-line budget.

### Backend

- `routers/backups.py` — `get_backup_jobs`, `get_backup_runs`, `get_backup_alerts` gain `service_id: str | None = None`; threaded into store calls. `get_backup_dashboard` **NOT touched** (design decision 5 — widget path, PI-117 risk) (PI-110).
- `services/settings_store.py`:
  - `list_backup_jobs(service_id=None)` — direct `WHERE service_id = ?` when truthy.
  - `list_backup_runs(..., service_id=None)` + `list_backup_alerts(..., service_id=None)` — subquery `job_id IN (SELECT id FROM backup_jobs WHERE service_id = ?)` per the schema asymmetry (only `backup_jobs` has the column; runs/alerts attributed via FK chain).
  - Skip filter on None/empty → backward-compat (PI-111, PI-112).
- Tests (`test_backups.py`, +83 lines): `service_id=None` → all; `service_id="X"` → only X's; cross-instance preservation (seed A+B, scope A, confirm B's excluded).

### Frontend

- `hooks/useObservability.ts` — `useAlertmanagerAlerts`, `useAlertmanagerStatus`, `usePrometheusStatus` gain optional `serviceId` in `queryKey` (`["observability","alerts", serviceId ?? ""]` etc.). `usePrometheusTargets` + `useMonitoringMachines` **unchanged** (global by design — PI-107) (PI-101..PI-104).
- `hooks/useBackups.ts` — `useBackupJobs`, `useBackupRuns`, `useBackupAlerts` gain `serviceId` in `queryKey`. `useBackupDashboard` **unchanged** (PI-105, PI-106).
- `api/client.ts` + `api/backups.ts` — 6 fetch fns gain optional `serviceId` → `?service_id=` via the shared `get(path, params)` helper. `fetchBackupDashboard` **NOT touched** (PI-108, PI-109).
- `pages/service-tabs/AlertsTab.tsx`, `MetricsTab.tsx`, `JobsTab.tsx` — call hooks with `instance.id`; the prior TODO comments about global scoping removed (PI-113..PI-115). The instance switcher on ServicePage naturally re-scopes: `instance.id` changes → `queryKey` changes → React Query refetches.
- Tests: `useBackups.test.ts` (+85) covers queryKey isolation + serviceId threading; `AlertsTab.test.tsx` updated for the instance.id wiring.

## Deviations from tasks.md / design

- None functional. `fetchBackupDashboard` / `useBackupDashboard` / `get_backup_dashboard` deliberately untouched (design decision 5, PI-117 preserved).

## Final gate results

| Gate | Result |
|---|---|
| `backend && PYTHONPATH=src python3 -m pytest -q` | **368 passed** (+6 new), 2 warnings (pre-existing pythonjsonlogger) |
| `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` | **165 passed** (+7 new) |

## Verification

See `verify-report.md` — adversarial fresh-context review: **21/21 PASS**, all gates green. No blocking code findings. Archive blocker is doc-only (this file + ticked tasks clear it). Non-blocking: weak queryKey-isolation assertion in `useBackups.test.ts` (wiring correct in source; test could assert actual cache keys — LOW).
