From a5e54a8b93cc7d623fe66b1158f30ed71514f6ad Mon Sep 17 00:00:00 2001 From: Julian Cuni Date: Sun, 5 Jul 2026 16:37:53 +0200 Subject: [PATCH] =?UTF-8?q?fix(devices):=20bucket=20camera=20health-check?= =?UTF-8?q?=20detail=20=E2=80=94=20stop=20per-frame=20log/status=20churn?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The device monitor logs + re-emits a status only when state OR detail changes, but the camera probe's detail was the exact snapshot byte count, which differs on every JPEG frame — so healthy cameras "changed" on nearly every poll, writing a log line + websocket event each time (inflating the freshly budgeted container logs). The detail is now a stable power-of-two bucket ("snapshot ≈16 KB" / "≈256 KB") that moves only on a real shift (stream/resolution change); an empty-ish 200 body is flagged as "<1 KB" rather than bucketed away. Failure details (auth/HTTP/timeout) unchanged. 3 tests pin the no-flap behavior. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V --- packages/devices/src/drivers/camera.test.ts | 30 +++++++++++++++++++++ packages/devices/src/drivers/camera.ts | 15 ++++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/packages/devices/src/drivers/camera.test.ts b/packages/devices/src/drivers/camera.test.ts index 7899c47..5b205c6 100644 --- a/packages/devices/src/drivers/camera.test.ts +++ b/packages/devices/src/drivers/camera.test.ts @@ -113,3 +113,33 @@ describe("hikvision snapshot stream selection (main vs sub)", () => { expect(pathFor({ host: "1.2.3.4", channel: 1, stream: 9 })).toBe("/ISAPI/Streaming/channels/101/picture"); }); }); + +describe("healthCheck detail is a STABLE size bucket (log-noise fix, 2026-07-05)", () => { + // The device monitor logs + re-emits whenever the detail string changes. JPEG + // frame size differs on every frame, so an exact byte count made healthy cameras + // "change" on nearly every poll. The detail must stay identical across ordinary + // frame-size jitter and only move on a real shift (different stream/res, tiny body). + it("frames of similar size land in the same bucket", async () => { + const cam = makeCamera(); + digestGet.mockResolvedValueOnce(reply(200, "x".repeat(16_716))); + const a = await cam.healthCheck(); + digestGet.mockResolvedValueOnce(reply(200, "x".repeat(17_902))); + const b = await cam.healthCheck(); + expect(a).toEqual({ status: "ready", detail: "snapshot ≈16 KB" }); + expect(b.detail).toBe(a.detail); // jitter does NOT change the detail + }); + + it("a genuinely different size (sub vs main stream) lands in a different bucket", async () => { + const cam = makeCamera(); + digestGet.mockResolvedValueOnce(reply(200, "x".repeat(299_395))); + const big = await cam.healthCheck(); + expect(big.detail).toBe("snapshot ≈256 KB"); + }); + + it("an empty-ish 200 body is flagged, not bucketed away", async () => { + const cam = makeCamera(); + digestGet.mockResolvedValueOnce(reply(200, "xx")); + const tiny = await cam.healthCheck(); + expect(tiny.detail).toBe("snapshot <1 KB"); + }); +}); diff --git a/packages/devices/src/drivers/camera.ts b/packages/devices/src/drivers/camera.ts index 54c521a..00f36b0 100644 --- a/packages/devices/src/drivers/camera.ts +++ b/packages/devices/src/drivers/camera.ts @@ -40,6 +40,19 @@ const SNAPSHOT_RETRY_BASE_MS = 250; const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); +/** Coarse, STABLE size label for the health-check detail: nearest power-of-two KB + * (`≈16 KB`, `≈256 KB`). JPEG frame size varies with every frame, and the device + * monitor logs + re-emits a status whenever the detail string changes — an exact + * byte count made every healthy camera "change" on nearly every poll, spamming the + * rotated container logs. A pow-2 bucket keeps the diagnostic value (a suddenly + * tiny frame still shows) while flapping only on a real scene/stream shift. */ +function sizeBucket(bytes: number): string { + const kb = bytes / 1024; + if (kb < 1) return "<1 KB"; // empty-ish 200 body — suspicious, worth seeing as-is + const pow = Math.round(Math.log2(kb)); + return `≈${2 ** pow} KB`; +} + class HttpCamera implements CameraDevice { readonly #host: string; readonly #port: number; @@ -85,7 +98,7 @@ class HttpCamera implements CameraDevice { try { const res = await this.#get(); if (res.status === 200) - return { status: "ready", detail: `${res.body.length} bytes` }; + return { status: "ready", detail: `snapshot ${sizeBucket(res.body.length)}` }; if (res.status === 401) return { status: "degraded",