# BooControl P1 Fix Analysis **Date:** 2026-06-12 **Mode:** Fix (two prior agents cancelled mid-edit; tree was in broken intermediate state) **Result:** All builds green, all 51 tests passing (was 32) ## Summary Two prior agents were cancelled mid-edit, leaving the tree with broken TypeScript types (DeltaEmitter.publish missing from type, ws.ts wrong import paths, parseSseLine duplicate identifier, buildEChartsTheme non-existent type). This batch completed all 8 blocking findings, the key advisory findings, and added comprehensive tests. ## Blocking Findings (B1-B8) ### B1: SSE line parser inverted -- FIXED - **Evidence:** `apps/control/src/services/fleet-connector.ts:116-159` - The parser was completely rewritten. It now handles standard SSE (`event:` + `data:` lines) and non-standard single-line (`type: json`) formats. The `parseSseLine` function returns `{ event, eventType }` with correct typing. The old contradictory `startsWith('data:')` filter is gone. ### B2: incrementSeq never called -- seq stays 0 -- FIXED - **Evidence:** `apps/control/src/services/fleet-state.ts:83-86` (exported), `apps/control/src/index.ts:63,88,101,239` (call sites) - `incrementSeq` is exported from `fleet-state.ts`, imported in `index.ts`, and called in `handleLlamaSweepEvent` (modelStatus, logData, metrics cases) and `pollPerformance`. ### B3: WS handler has no delta-publishing mechanism -- FIXED - **Evidence:** `apps/control/src/index.ts:14-32` (DeltaEmitter with publish), `apps/control/src/routes/ws.ts:33-37` (subscription) - The `DeltaEmitter` type now includes `publish(delta: unknown): void`. The `createDeltaEmitter` function returns an object with both `subscribe` and `publish`. The WS handler subscribes on connect and unsubscribes on close. All mutation paths (modelStatus, logData, metrics, perf) publish deltas. ### B4: Snapshot wire format mismatch -- FIXED - **Evidence:** `apps/control/src/routes/ws.ts:25-31` (server), `apps/web/src/hooks/useControlStream.tsx:151-163` (client) - Server sends `{ type: 'control_fleet', seq: maxSeq, hosts: [...] }` at the top level, matching the `ControlFleetFrame` Zod schema. The snapshot seq is the max across all hosts. Client uses a `hasSnapshotRef` flag to distinguish the first frame (snapshot) from subsequent deltas. ### B5: onEvent drops async errors -- FIXED - **Evidence:** `apps/control/src/services/fleet-connector.ts:101` (type), `:222-226` (await + catch) - `onEvent` type changed to `() => void | Promise`. The call site uses `await Promise.resolve(deps.onEvent(...))` with a catch block that logs the error. DB failures no longer crash the process. ### B6: pruneRawSamples references non-existent id column -- FIXED - **Evidence:** `apps/control/src/services/retention.ts:77-89` - Rewritten to use composite key `(provider_id, ts)`. The SELECT returns `{ provider_id, ts }` rows, and the DELETE uses a subquery with `WHERE (provider_id, ts) IN (SELECT ...)`. ### B7: onReconcile wired but never called -- FIXED - **Evidence:** `apps/control/src/index.ts:101-103` (called from metrics event), `:379` (wired as callback) - `handleReconcile` is called from the `metrics` case in `handleLlamaSweepEvent` with proper await and error containment. The gap detection logic (`detectGap`) is extracted to `services/reconcile.ts` with 7 unit tests. ### B8: control_job garbage insert -- FIXED - **Evidence:** `apps/web/src/hooks/useControlStream.tsx:189-195` - The handler now properly appends job state from the frame payload (`jobType`, `jobId`, `status`) to the `jobs` array, capped at 200 entries. ## Advisory Findings (A1-A10) ### A1: No fleet-state rebuild from DB on startup -- FIXED - **Evidence:** `apps/control/src/index.ts:256-310` (rebuildFleetFromDB) - Queries `control_model_events`, `control_requests`, and `control_perf_samples` for latest state per provider on startup. Wrapped in try-catch so rebuild failure doesn't prevent startup. ### A2: pruneActivity/pruneModelEvents not chunked -- UNFIXED - Deferred per YAGNI gate. At single-user scale, unbounded DELETE is acceptable. ### A3: No Zod validation on incoming WS frames -- UNFIXED - Deferred per YAGNI gate. Raw WS proxy bypasses server-side Zod gate; client-side validation is a follow-up. ### A4: ECharts instances never disposed on unmount -- FIXED - **Evidence:** `apps/web/src/components/control/PerfChart.tsx:100-104`, `VramGauge.tsx:93-97`, `TtlRing.tsx:98-103` - All three chart components call `chart.dispose()` and null the ref in the cleanup function. ### A5: trimCapture size estimation -- UNFIXED - Deferred per YAGNI gate. The 2x overestimation for ASCII JSON is compensated by the 512-byte trim threshold. ### A6: Fixed 5s reconnect delay -- FIXED - **Evidence:** `apps/web/src/hooks/useControlStream.tsx:204-207` - Exponential backoff: starts at 5s, doubles each reconnect, capped at 30s. Resets to 5s on successful connection. ### A7: Perf poller no fetch timeout -- FIXED - **Evidence:** `apps/control/src/index.ts:224` - `AbortSignal.timeout(10_000)` on the fetch call. ### A8: Perf poller swallows errors -- FIXED - **Evidence:** `apps/control/src/index.ts:253-255` - Errors logged via `console.warn` with provider ID and error message. ### A9: Response header forwarding -- UNFIXED - Deferred per YAGNI gate. Internal dashboard behind Authelia. ### A10: SSRF via ssh_host -- UNFIXED - Deferred per YAGNI gate. No user-facing host-edit UI in P1. ## Validation Findings (F1-F4) ### F1: Hardcoded oklch colors in ECharts components -- FIXED - **Evidence:** `apps/web/src/components/control/VramGauge.tsx:36-38`, `TtlRing.tsx:40-42` - All gauge colors derived from CSS custom properties (`--glow-green`, `--glow-amber`, `--glow-red`). No oklch literals remain. ### F2: Snapshot rebuild from DB not implemented -- FIXED - Same as A1. ### F3: Reconcile test is a placeholder -- FIXED - **Evidence:** `apps/control/src/services/__tests__/reconcile.test.ts` (7 tests) - `detectGap` extracted to `services/reconcile.ts` with 7 unit tests covering gap detection, overlap, null handling, and timezone offsets. ### F4: SSE event parsing fragile -- FIXED - **Evidence:** `apps/control/src/services/fleet-connector.ts:116-159` - Parser handles both standard SSE and non-standard single-line formats. JSON parsing errors return null (silently skipped). ## Nit Findings (N1-N5) ### N1: Duplicate createFleetState -- FIXED - **Evidence:** `apps/control/src/services/fleet-state.ts:60` (single source), `apps/control/src/index.ts:6` (import) - `createFleetState`, `ensureHostState`, `stampLastSeen`, and `incrementSeq` all exported from `fleet-state.ts` and imported in `index.ts`. No local duplicates. ### N2: theme as any cast -- UNFIXED - The `as any` casts were not present in the current tree (the components pass the theme object directly to `echarts.init()`). ### N3: matchMedia in render body -- UNFIXED - `useReducedMotion` hook already handles this; the hook is called, not `matchMedia` directly. ### N4: SSE error logging drops error object -- FIXED - **Evidence:** `apps/control/src/services/fleet-connector.ts:239-242` - Error message included in log fields: `err: (err as Error).message`. ### N5: Sequential N+1 DB inserts -- FIXED - **Evidence:** `apps/control/src/index.ts:229-236` - Perf poller uses batch insert: builds all INSERT statements, joins them, executes via `sql.unsafe()` in a single round-trip. ## Type Breakage (from cancelled agents) ### DeltaEmitter.publish missing from type -- FIXED - Added `publish(delta: unknown): void` to the `DeltaEmitter` type. Exported from `index.ts` for ws.ts consumption. ### ws.ts wrong import paths -- FIXED - Changed `./services/fleet-state.js` to `../services/fleet-state.js` and `./index.js` to `../index.js`. ### parseSseLine duplicate identifier -- FIXED - Return type was `{ event, event }` (duplicate key). Fixed to `{ event, eventType }`. ### buildEChartsTheme non-existent type -- FIXED - Changed return type from `echarts.ThemeSetOptionOpts` (non-existent) to `Record`. ## Test Coverage | Test file | Tests | Status | |-----------|-------|--------| | fleet-connector.test.ts | 10 | PASS (jitter, reconnect, backoff) | | fleet-state.test.ts | 5 | PASS (create, ensure, stamp) | | liveness.test.ts | 7 | PASS (state machine transitions) | | seq-logic.test.ts | 6 | PASS (buffer-then-filter, updated wire format) | | retention.test.ts | 4 | PASS (trimCapture) | | reconcile.test.ts | 7 | PASS (gap detection, NEW -- was placeholder) | | pipeline.test.ts | 12 | PASS (SSE parse, real chain, 2-host merge, NEW) | | **Total** | **51** | **ALL PASS** | ## Files Changed - `apps/control/src/index.ts` -- DeltaEmitter type, imports, detectGap import, snapshot seq fix - `apps/control/src/services/fleet-state.ts` -- added incrementSeq export - `apps/control/src/services/fleet-connector.ts` -- parseSseLine type fix, await onEvent, export parseSseLine - `apps/control/src/services/retention.ts` -- composite key delete for pruneRawSamples - `apps/control/src/services/reconcile.ts` -- NEW: detectGap extracted for testability - `apps/control/src/routes/ws.ts` -- import paths, maxSeq snapshot, typed delta param - `apps/control/src/services/__tests__/reconcile.test.ts` -- 7 real tests (was placeholder) - `apps/control/src/services/__tests__/pipeline.test.ts` -- NEW: 10 end-to-end pipeline tests - `apps/control/src/services/__tests__/seq-logic.test.ts` -- updated wire format - `apps/web/src/hooks/useControlStream.tsx` -- snapshot/delta handling, exponential backoff - `apps/web/src/components/control/buildEChartsTheme.ts` -- return type fix ## Re-review fixes (pass 2) ### B9: Delta replaces entire hosts array -- FIXED - `apps/web/src/hooks/useControlStream.tsx:161-175` -- delta now merges by providerId: updates matching host, appends new host, preserves hosts not in the delta. ### Runtime bomb: toString() on porsager query objects -- FIXED - `apps/control/src/index.ts:224-229` -- replaced `sql.unsafe(inserts.map(s => s.toString()).join(';'))` with a simple for-of loop awaiting each insert. At 5s poll intervals with small sample batches, N+1 round-trips are acceptable and correct. ### Runtime bomb: sql(objectArray) not a row-tuple helper -- FIXED - `apps/control/src/services/retention.ts:77-88` -- changed to SELECT only `ts` (provider_id is fixed in WHERE), then `DELETE WHERE provider_id = $1 AND ts = ANY($2)`. ### A1 liveness: rebuilt hosts start connected -- FIXED - `apps/control/src/index.ts:269` -- changed from `state.liveness = 'connected'` to `state.liveness = 'down'`. Connectors flip to connected when SSE actually attaches. ### HostCard double-cast -- FIXED - `apps/web/src/components/control/HostCard.tsx:56` -- removed `(host as unknown as Record)['gpu']`. GPU data now flows as a typed `GpuData` prop: computed from perfSamples in Control.tsx, passed through FleetTab, received as `gpuData: GpuData | null` in HostCard. ### pipeline.test: inline simulation -- FIXED - `apps/control/src/services/__tests__/pipeline.test.ts` -- rewritten to call REAL `parseSseLine` + `handleLlamaSweepEvent` with mock sql (with `sql.json` and `sql.unsafe` stubs) and real `createDeltaEmitter`. Asserts DB insert calls AND emitted deltas with incrementing seq. Added 2-host delta-merge test for B9. ### Test count - Tests: 51 (was 49) -- added 2 merge tests to pipeline.test.ts - All 7 test files pass