diff --git a/packages/request-handler/src/unifi-os.ts b/packages/request-handler/src/unifi-os.ts index 1696b948b..6432ca45a 100644 --- a/packages/request-handler/src/unifi-os.ts +++ b/packages/request-handler/src/unifi-os.ts @@ -8,7 +8,13 @@ export const unifiOsRequestHandler = createIntegrationRequestHandler - {/* Sous le seuil de points, ou sans mesure courante, on montre les - chiffres seuls : un graphe tracé sur un point unique dessine une - ligne plate, qui se lit comme un réseau au repos alors qu'on n'a - simplement rien encore mesuré. */} - {!showThroughputChart && ( + {summary.wan.throughput === null && ( - {summary.wan.throughput === null - ? EMPTY_VALUE - : `↑${formatBitsPerSecond(summary.wan.throughput.txBps)} ↓${formatBitsPerSecond(summary.wan.throughput.rxBps)}`} + {EMPTY_VALUE} )} - {showThroughputChart && summary.wan.throughput !== null && ( + {/* Le graphe est rendu dès la première mesure : avec un seul point il + reste vide, ce qui évite un saut de mise en page au second — et une + aire vide n'affirme rien, contrairement à une ligne plate. */} + {summary.wan.throughput !== null && ( ( - + ({ index, up, down }); @@ -32,6 +27,31 @@ describe("appendThroughputPoint", () => { expect(appendThroughputPoint(existing, null)).toEqual(existing); }); + /** + * The gateway refreshes its statistics every 30 s (measured: `lastHeartbeatAt` + * advances in exact 30 s steps). Polling faster returns the same numbers + * repeatedly. Recording them would draw a staircase and burn the history + * window on duplicates rather than on time. + */ + test("ignores a reading identical to the last recorded one", () => { + const existing = [point(0, 10, 20)]; + + expect(appendThroughputPoint(existing, { txBps: 10, rxBps: 20 })).toEqual(existing); + }); + + test("records a reading that differs in either direction", () => { + const existing = [point(0, 10, 20)]; + + expect(appendThroughputPoint(existing, { txBps: 11, rxBps: 20 })).toEqual([...existing, point(1, 11, 20)]); + expect(appendThroughputPoint(existing, { txBps: 10, rxBps: 21 })).toEqual([...existing, point(1, 10, 21)]); + }); + + test("compares only against the most recent point, so a value may recur later", () => { + const history = appendThroughputPoint([point(0, 10, 20), point(1, 30, 40)], { txBps: 10, rxBps: 20 }); + + expect(history).toEqual([point(0, 10, 20), point(1, 30, 40), point(2, 10, 20)]); + }); + test("keeps only the most recent points once the cap is reached", () => { const full = Array.from({ length: MAX_THROUGHPUT_POINTS }, (_, index) => point(index, index, index)); @@ -50,21 +70,3 @@ describe("appendThroughputPoint", () => { expect(existing).toEqual([point(0, 10, 20)]); }); }); - -describe("canRenderThroughputChart", () => { - /** - * One point drawn as a line is a flat line, which reads as "a network at - * rest" when the truth is "we do not know yet". Below the threshold the - * widget shows the numbers alone. - */ - test("refuses to draw a chart from a single point", () => { - expect(canRenderThroughputChart([])).toBe(false); - expect(canRenderThroughputChart([point(0, 1, 2)])).toBe(false); - }); - - test("draws once there are at least two points to join", () => { - const enough = Array.from({ length: MIN_POINTS_FOR_CHART }, (_, index) => point(index, 1, 2)); - - expect(canRenderThroughputChart(enough)).toBe(true); - }); -}); diff --git a/packages/widgets/src/unifi-os/throughput-history.ts b/packages/widgets/src/unifi-os/throughput-history.ts index 98ff381a8..a732219d8 100644 --- a/packages/widgets/src/unifi-os/throughput-history.ts +++ b/packages/widgets/src/unifi-os/throughput-history.ts @@ -19,15 +19,19 @@ export interface ThroughputPoint { /** At the widget's 60 s refetch interval this holds roughly an hour of history. */ export const MAX_THROUGHPUT_POINTS = 60; -/** Two points are the fewest that can be joined into a line. */ -export const MIN_POINTS_FOR_CHART = 2; - /** * Appends one reading, oldest points falling off the front once the cap is hit. * - * A `null` reading — the statistics call failed, or no gateway was identified — - * is skipped rather than recorded as zero. Zero is a measurement, and drawing - * one would show a plunge to the baseline that never happened. + * Two readings are refused: + * + * - `null` — the statistics call failed, or no gateway was identified. Skipped + * rather than recorded as zero: zero is a measurement, and drawing one would + * show a plunge to the baseline that never happened. + * - a repeat of the previous point. The gateway refreshes its statistics every + * 30 s (measured: `lastHeartbeatAt` advances in exact 30 s steps), while the + * widget polls faster than that, so the same numbers come back several times + * in a row. Recording them would draw a staircase and spend the history + * window on duplicates instead of on elapsed time. */ export const appendThroughputPoint = ( history: ThroughputPoint[], @@ -35,14 +39,10 @@ export const appendThroughputPoint = ( ): ThroughputPoint[] => { if (throughput === null) return history; + const last = history.at(-1); + if (last !== undefined && last.up === throughput.txBps && last.down === throughput.rxBps) return history; + const nextIndex = (history.at(-1)?.index ?? -1) + 1; return [...history, { index: nextIndex, up: throughput.txBps, down: throughput.rxBps }].slice(-MAX_THROUGHPUT_POINTS); }; - -/** - * A single point renders as a flat line, which reads as a quiet network when - * the truth is that nothing has been measured yet. Below the threshold the - * widget shows the current figures on their own. - */ -export const canRenderThroughputChart = (history: ThroughputPoint[]): boolean => history.length >= MIN_POINTS_FOR_CHART;