Files
kandev/docs/plans/storage-progressive-loading/plan.md
T
2026-08-02 20:11:23 +01:00

148 lines
7.9 KiB
Markdown

---
spec: docs/specs/system-page/storage-maintenance.md
created: 2026-08-02
status: complete
---
# Implementation Plan: Progressive Storage Loading and Totals
## Overview
Decouple the lightweight Storage policy read from the scan-backed overview, then let the frontend
publish policy, history, quarantine, and analysis results independently. Add derived totals for the
analysis snapshot and current quarantine list, with explicit partial semantics when a top-level
measurement is unavailable. Preserve the existing `GET /storage` response for compatibility.
## Backend
### Lightweight policy read
- In `apps/backend/internal/system/storage/handler.go`, register
`GET /api/v1/system/storage/settings` beside the existing `PATCH` route.
- Implement `getSettings` by calling `SettingsManager.GetSettings` and
`OverviewReader.SettingsCapabilities`; return `{ settings, capabilities }` without calling
`OverviewReader.Get`, `RunLister.ListRuns`, or any analysis provider.
- Keep `GET /api/v1/system/storage` unchanged so existing clients still receive
`{ settings, capabilities, summary, analyzed_at, last_run }`.
- Cover the route directly in `apps/backend/internal/system/storage/handler_test.go` with a
recording overview reader that fails the test if `Get` is invoked, and through the composed
System router in `apps/backend/internal/system/storage_routes_test.go`.
## Frontend
### API and state contracts
- Add `StoragePolicyResponse` to `apps/web/lib/types/system.ts` and
`fetchStoragePolicy()` to `apps/web/lib/api/domains/system-api.ts` for the new lightweight GET.
- Add `policy: StoragePolicyResponse | null` and `setSystemStoragePolicy` to the System Zustand
slice in `apps/web/lib/state/slices/system/types.ts` and `system-slice.ts`.
- Extend the existing API and slice tests for the route, response shape, and policy update.
### Independent loading and refresh
- Refactor `apps/web/hooks/domains/system/use-storage-maintenance.ts` so policy, overview, runs,
and quarantine requests start together but commit independently. Track per-section loading and
error state plus per-section request generations so a slow older response cannot overwrite newer
data from the same section.
- Remove first-load completion from the global `pendingAction`; policy-save coordination depends
on `policyLoading`, while analysis, history, and quarantine can remain independently pending.
- Make save refresh only the lightweight policy contract, and apply the existing adoption response
directly to policy state. Restore refreshes quarantine without waiting for an overview scan.
Terminal analysis/cleanup/delete jobs refresh the affected sections concurrently so history and
quarantine can settle even when the invalidated overview needs a new scan.
- Update `storage-maintenance-settings.tsx` to derive drafts and scheduling copy from `policy`, and
pass section loading/error state to the analysis, history, and quarantine cards.
- Give `storage-overview-card.tsx`, `storage-run-history.tsx`, and
`storage-quarantine-card.tsx` distinct progress, empty, and error states; a failure in one card
must not suppress sibling cards.
### Totals
- Add `apps/web/components/settings/system/storage/storage-totals.ts` with pure helpers that:
- sum workspace total, quarantine, managed and distinct user Go caches, managed-container
writable layers, Docker image layers, and Docker build cache;
- exclude active/candidate workspace and unused-image subset bytes to avoid double-counting;
- return a `partial` flag when a top-level measurement is unavailable or missing; and
- sum `size_bytes` across every listed quarantine entry.
- Render a localized **Total counted** value and partial indicator in
`storage-overview-card.tsx`, and a localized total in `storage-quarantine-card.tsx`.
- Add the new user-facing strings to `apps/web/src/locales/en/settings.json`; do not localize
identifiers, test IDs, confirmation tokens, or values used in logic.
### Mobile design contract
- Desktop outcome: the existing full-width card stack renders lightweight cards before analysis,
and totals appear in the relevant card headers without changing actions or navigation.
- Mobile entry point and exemplar: keep the Settings sheet → System → Storage path and existing
one-column card composition proven by
`apps/web/e2e/tests/system/mobile-storage-maintenance.spec.ts`.
- Mobile hierarchy: policy, history, and quarantine remain ordinary inline cards in the page's
single document scroll; Storage analysis owns only its card-level progress state. No drawer,
overlay, fixed control, or second scroll owner is introduced.
- Touch behavior and state are shared with desktop. Existing action targets stay at least 44px;
the totals are read-only and add no touch affordance. The mobile E2E scenario delays analysis,
proves the other cards and totals render, and asserts no horizontal document overflow.
## Tests
- **Lightweight policy route:** `apps/backend/internal/system/storage/handler_test.go` verifies the
response and proves no overview scan or run-history read occurs.
- **Composed route:** `apps/backend/internal/system/storage_routes_test.go` verifies
`GET /api/v1/system/storage/settings` is registered and returns `200`.
- **API and store:** `apps/web/lib/api/domains/system-api.test.ts` and
`apps/web/lib/state/slices/system/system-slice.test.ts` verify the new policy contract.
- **Progressive controller:** `apps/web/hooks/domains/system/use-storage-maintenance.test.tsx`
uses deferred promises to prove policy, runs, and quarantine publish before overview; covers
isolated failures, same-section stale-response protection, and mutation-specific refreshes.
- **Totals:** `apps/web/components/settings/system/storage/storage-totals.test.ts` verifies exact
sums, subset exclusion, partial analysis, zero/empty data, and quarantine aggregation.
- **Rendered states:** `storage-maintenance-settings.test.tsx`,
`storage-overview-card.test.tsx`, `storage-run-history.test.tsx`, and
`storage-quarantine-card.test.tsx` verify policy composition, loading/error/empty distinctions,
and localized totals.
## E2E Tests
- **Desktop progressive load:** extend
`apps/web/e2e/tests/system/storage-maintenance.spec.ts` to delay `GET /storage`, then verify the
policy controls, maintenance history, and quarantine list are visible before analysis finishes;
after release, verify the analysis total and quarantine total.
- **Mobile parity:** extend
`apps/web/e2e/tests/system/mobile-storage-maintenance.spec.ts` with the same delayed-analysis
outcome on `mobile-chrome`, including totals and absence of horizontal overflow.
## Verification Results
Completed. Each task records its exact commands and outcomes in its `## Results` section.
## Implementation Waves And Parallel Candidates
Wave 1:
- [x] [Task 01: Lightweight policy endpoint](task-01-policy-endpoint.md) — done
Wave 2:
- [x] [Task 02: Progressive section loading](task-02-progressive-loading.md) — done
Wave 3:
- [x] [Task 03: Storage totals](task-03-storage-totals.md) — done
Wave 4:
- [x] [Task 04: Desktop and mobile E2E](task-04-storage-e2e.md) — done
The tasks run sequentially in the primary conversation. The waves do not authorize subagents.
## Risks
- Docker unused-image bytes are a reclaimable subset of image-layer usage; adding both would
overstate the total. The pure total helper and tests make that exclusion explicit.
- Cleanup and restore invalidate the overview cache, so an automatic overview refresh may scan
again. Section-specific refreshes must let history and quarantine update without awaiting it.
- Empty arrays currently mean both “not loaded” and “loaded with no items”; explicit section state
is required to prevent misleading empty-state copy during requests.
- Capability reads ping Docker. They are expected to be much cheaper than `DiskUsage` and
filesystem traversal, but remain isolated from the overview scan and failure-scoped to policy.