fix(devices): bucket camera health-check detail — stop per-frame log/status churn
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
This commit is contained in:
@@ -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");
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -40,6 +40,19 @@ const SNAPSHOT_RETRY_BASE_MS = 250;
|
|||||||
|
|
||||||
const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
|
const sleep = (ms: number) => new Promise<void>((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 {
|
class HttpCamera implements CameraDevice {
|
||||||
readonly #host: string;
|
readonly #host: string;
|
||||||
readonly #port: number;
|
readonly #port: number;
|
||||||
@@ -85,7 +98,7 @@ class HttpCamera implements CameraDevice {
|
|||||||
try {
|
try {
|
||||||
const res = await this.#get();
|
const res = await this.#get();
|
||||||
if (res.status === 200)
|
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)
|
if (res.status === 401)
|
||||||
return {
|
return {
|
||||||
status: "degraded",
|
status: "degraded",
|
||||||
|
|||||||
Reference in New Issue
Block a user