@camstack/ui-library 1.2.305 → 1.2.306

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/{MotionZonesSettings-C3Tj12yc.js → MotionZonesSettings-DR1g9ja3.js} +1 -1
  2. package/dist/{PrivacyMaskSettings-C9wfHkiB.js → PrivacyMaskSettings-CtetT6Jl.js} +1 -1
  3. package/dist/{SceneMonitorEditor-30H29vN4.js → SceneMonitorEditor-BQhVO2F2.js} +1 -1
  4. package/dist/composites/cap-settings/EventHeatmap.d.ts +8 -2
  5. package/dist/composites/charts/BarSeries.d.ts +49 -0
  6. package/dist/composites/charts/Sparkline.d.ts +26 -0
  7. package/dist/composites/charts/StateTimeline.d.ts +47 -0
  8. package/dist/composites/charts/TimeSeriesChart.d.ts +48 -0
  9. package/dist/composites/charts/bar-series-model.d.ts +25 -0
  10. package/dist/composites/charts/chart-data.d.ts +87 -0
  11. package/dist/composites/charts/chart-frame.d.ts +53 -0
  12. package/dist/composites/charts/chart-palette.d.ts +46 -0
  13. package/dist/composites/charts/chart-scale.d.ts +29 -0
  14. package/dist/composites/charts/chart-uplot.d.ts +44 -0
  15. package/dist/composites/charts/chart-window.d.ts +19 -0
  16. package/dist/composites/charts/index.d.ts +16 -0
  17. package/dist/composites/charts/time-series-model.d.ts +47 -0
  18. package/dist/composites/charts/uplot-host.d.ts +34 -0
  19. package/dist/composites/device-history/DeviceHistoryView.d.ts +26 -0
  20. package/dist/composites/device-history/HistoryChart.d.ts +31 -0
  21. package/dist/composites/device-history/HistoryWindowBar.d.ts +16 -0
  22. package/dist/composites/device-history/history-model.d.ts +121 -0
  23. package/dist/composites/device-history/history-reads.d.ts +43 -0
  24. package/dist/composites/device-history/history-selection.d.ts +55 -0
  25. package/dist/composites/device-history/history-window.d.ts +31 -0
  26. package/dist/composites/device-history/index.d.ts +14 -0
  27. package/dist/composites/device-history/use-device-history.d.ts +37 -0
  28. package/dist/composites/device-history/use-history-panels.d.ts +10 -0
  29. package/dist/composites/index.d.ts +2 -0
  30. package/dist/{icons-B1AFXUaZ.cjs → icons-BTux33dH.cjs} +2 -35
  31. package/dist/{icons-BT8c2hxp.js → icons-D178phML.js} +1 -34
  32. package/dist/index.cjs +3172 -45
  33. package/dist/index.js +3094 -52
  34. package/dist/uPlot.esm-BbxBUnav.cjs +3767 -0
  35. package/dist/uPlot.esm-C_8LiAPm.js +3767 -0
  36. package/package.json +2 -1
  37. package/src/tailwind/camstack-theme.css +104 -2
package/dist/index.cjs CHANGED
@@ -888,6 +888,19 @@ var Cat = require_createLucideIcon.createLucideIcon("cat", [
888
888
  * This source code is licensed under the ISC license.
889
889
  * See the LICENSE file in the root directory of this source tree.
890
890
  */
891
+ var ChartLine = require_createLucideIcon.createLucideIcon("chart-line", [["path", {
892
+ d: "M3 3v16a2 2 0 0 0 2 2h16",
893
+ key: "c24i48"
894
+ }], ["path", {
895
+ d: "m19 9-5 5-4-4-3 3",
896
+ key: "2osh9i"
897
+ }]]);
898
+ /**
899
+ * @license lucide-react v0.576.0 - ISC
900
+ *
901
+ * This source code is licensed under the ISC license.
902
+ * See the LICENSE file in the root directory of this source tree.
903
+ */
891
904
  var Check = require_createLucideIcon.createLucideIcon("check", [["path", {
892
905
  d: "M20 6 9 17l-5-5",
893
906
  key: "1gmf2c"
@@ -2233,6 +2246,26 @@ var Heater = require_createLucideIcon.createLucideIcon("heater", [
2233
2246
  * This source code is licensed under the ISC license.
2234
2247
  * See the LICENSE file in the root directory of this source tree.
2235
2248
  */
2249
+ var History = require_createLucideIcon.createLucideIcon("history", [
2250
+ ["path", {
2251
+ d: "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8",
2252
+ key: "1357e3"
2253
+ }],
2254
+ ["path", {
2255
+ d: "M3 3v5h5",
2256
+ key: "1xhq8a"
2257
+ }],
2258
+ ["path", {
2259
+ d: "M12 7v5l4 2",
2260
+ key: "1fdv2h"
2261
+ }]
2262
+ ]);
2263
+ /**
2264
+ * @license lucide-react v0.576.0 - ISC
2265
+ *
2266
+ * This source code is licensed under the ISC license.
2267
+ * See the LICENSE file in the root directory of this source tree.
2268
+ */
2236
2269
  var House = require_createLucideIcon.createLucideIcon("house", [["path", {
2237
2270
  d: "M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8",
2238
2271
  key: "5wwlr5"
@@ -5819,7 +5852,7 @@ function allDeviceTypeFilterOptions(t = defaultUiText) {
5819
5852
  */
5820
5853
  var fullTable;
5821
5854
  var pending;
5822
- var failure;
5855
+ var failure$1;
5823
5856
  var version = 0;
5824
5857
  var listeners$1 = /* @__PURE__ */ new Set();
5825
5858
  function publish() {
@@ -5836,12 +5869,12 @@ function lucideExportName(key) {
5836
5869
  */
5837
5870
  function loadIconGlyphs() {
5838
5871
  if (fullTable !== void 0) return Promise.resolve();
5839
- if (failure !== void 0) return Promise.reject(failure);
5840
- pending ??= Promise.resolve().then(() => require("./icons-B1AFXUaZ.cjs")).then((module) => {
5872
+ if (failure$1 !== void 0) return Promise.reject(failure$1);
5873
+ pending ??= Promise.resolve().then(() => require("./icons-BTux33dH.cjs")).then((module) => {
5841
5874
  fullTable = module;
5842
5875
  publish();
5843
5876
  }, (error) => {
5844
- failure = error;
5877
+ failure$1 = error;
5845
5878
  publish();
5846
5879
  throw error;
5847
5880
  });
@@ -5849,7 +5882,7 @@ function loadIconGlyphs() {
5849
5882
  }
5850
5883
  /** Has the lazy glyph chunk failed to load? (Then only the fast table answers.) */
5851
5884
  function iconGlyphsFailed() {
5852
- return failure !== void 0;
5885
+ return failure$1 !== void 0;
5853
5886
  }
5854
5887
  /** Is the full table loaded? */
5855
5888
  function iconGlyphsLoaded() {
@@ -5866,7 +5899,7 @@ function resolveDisplayIcon(name) {
5866
5899
  const fast = FAST_ICON_GLYPHS[key];
5867
5900
  if (fast !== void 0) return fast;
5868
5901
  if (fullTable !== void 0) return fullTable[lucideExportName(key)];
5869
- if (failure === void 0) loadIconGlyphs().catch(() => void 0);
5902
+ if (failure$1 === void 0) loadIconGlyphs().catch(() => void 0);
5870
5903
  }
5871
5904
  function subscribe(listener) {
5872
5905
  listeners$1.add(listener);
@@ -9588,7 +9621,7 @@ var TILE_ROW_PX = 32;
9588
9621
  /** Matches the `h-72` of the scroll area. */
9589
9622
  var VIEWPORT_PX = 288;
9590
9623
  var OVERSCAN_PX = 96;
9591
- function chunk(keys) {
9624
+ function chunk$1(keys) {
9592
9625
  const rows = [];
9593
9626
  for (let i = 0; i < keys.length; i += TILES_PER_ROW) rows.push(keys.slice(i, i + TILES_PER_ROW));
9594
9627
  return rows;
@@ -9598,7 +9631,7 @@ function heightOf(row) {
9598
9631
  }
9599
9632
  /** An empty query shows every category as a section; a query shows one ranked list. */
9600
9633
  function buildRows(query, extraWords) {
9601
- if (query.trim().length > 0) return chunk((0, _camstack_types.searchIcons)(query, extraWords).map((h) => h.entry.key)).map((row, i) => ({
9634
+ if (query.trim().length > 0) return chunk$1((0, _camstack_types.searchIcons)(query, extraWords).map((h) => h.entry.key)).map((row, i) => ({
9602
9635
  kind: "tiles",
9603
9636
  id: `r${i}`,
9604
9637
  keys: row
@@ -9609,7 +9642,7 @@ function buildRows(query, extraWords) {
9609
9642
  kind: "header",
9610
9643
  id: `h-${category}`,
9611
9644
  category
9612
- }, ...chunk(keys).map((row, i) => ({
9645
+ }, ...chunk$1(keys).map((row, i) => ({
9613
9646
  kind: "tiles",
9614
9647
  id: `${category}-${i}`,
9615
9648
  keys: row
@@ -20885,7 +20918,7 @@ function bucketMsForSpan(spanMs) {
20885
20918
  * - `pct` is via pctForMs (first=0, last=100).
20886
20919
  * - `label` is a localised HH:MM clock string.
20887
20920
  */
20888
- function axisTicks(fromMs, toMs, count) {
20921
+ function axisTicks$1(fromMs, toMs, count) {
20889
20922
  if (count <= 1) return [{
20890
20923
  pct: 0,
20891
20924
  label: new Date(fromMs).toLocaleTimeString(void 0, {
@@ -21091,43 +21124,1193 @@ function ErrorBox({ message, title, onRetry, action, variant = "danger", classNa
21091
21124
  });
21092
21125
  }
21093
21126
  //#endregion
21127
+ //#region src/composites/charts/chart-palette.ts
21128
+ /**
21129
+ * The chart family's colours — theme tokens only.
21130
+ *
21131
+ * Categorical identity comes from the eight `--color-chart-series-*` tokens in
21132
+ * `tailwind/camstack-theme.css`, in FIXED order (a blue, b magenta, c yellow,
21133
+ * d aqua, e orange, f violet, g green, h red). A series keeps its slot for as
21134
+ * long as it is on screen: the caller assigns the index, never the rank, so a
21135
+ * filter that hides one series does not repaint the others.
21136
+ *
21137
+ * Past eight, a slot is reused with a dash pattern (composite encoding) — a
21138
+ * generated ninth hue would be indistinguishable from one of the eight.
21139
+ *
21140
+ * Canvas cannot read `var(--…)`, so a token is resolved through
21141
+ * `getComputedStyle` at DRAW time: a theme switch (`.dark` / `.light` on
21142
+ * `<html>`) followed by a redraw picks the new value up with no re-creation.
21143
+ */
21144
+ /** The categorical slots, in their fixed order. */
21145
+ var CHART_SERIES_TOKENS = [
21146
+ "chart-series-a",
21147
+ "chart-series-b",
21148
+ "chart-series-c",
21149
+ "chart-series-d",
21150
+ "chart-series-e",
21151
+ "chart-series-f",
21152
+ "chart-series-g",
21153
+ "chart-series-h"
21154
+ ];
21155
+ /** Dash patterns (canvas `setLineDash`, CSS px) for the 1st, 2nd and 3rd lap of the palette. */
21156
+ var DASH_LAPS = [
21157
+ [],
21158
+ [6, 4],
21159
+ [2, 3]
21160
+ ];
21161
+ /** The style of the series at `index` (0-based, its stable identity slot). */
21162
+ function chartSeriesStyle(index) {
21163
+ const i = Math.max(0, Math.floor(index));
21164
+ return {
21165
+ token: CHART_SERIES_TOKENS[i % CHART_SERIES_TOKENS.length] ?? "chart-series-a",
21166
+ dash: DASH_LAPS[Math.floor(i / CHART_SERIES_TOKENS.length) % DASH_LAPS.length] ?? []
21167
+ };
21168
+ }
21169
+ /** Spelled out so Tailwind finds every class (a built class name emits no rule). */
21170
+ var SWATCH_FILL = {
21171
+ "chart-series-a": "bg-chart-series-a",
21172
+ "chart-series-b": "bg-chart-series-b",
21173
+ "chart-series-c": "bg-chart-series-c",
21174
+ "chart-series-d": "bg-chart-series-d",
21175
+ "chart-series-e": "bg-chart-series-e",
21176
+ "chart-series-f": "bg-chart-series-f",
21177
+ "chart-series-g": "bg-chart-series-g",
21178
+ "chart-series-h": "bg-chart-series-h",
21179
+ border: "bg-border"
21180
+ };
21181
+ var SWATCH_LINE = {
21182
+ "chart-series-a": "border-chart-series-a",
21183
+ "chart-series-b": "border-chart-series-b",
21184
+ "chart-series-c": "border-chart-series-c",
21185
+ "chart-series-d": "border-chart-series-d",
21186
+ "chart-series-e": "border-chart-series-e",
21187
+ "chart-series-f": "border-chart-series-f",
21188
+ "chart-series-g": "border-chart-series-g",
21189
+ "chart-series-h": "border-chart-series-h",
21190
+ border: "border-border"
21191
+ };
21192
+ /** The utility that fills a swatch with `token`. */
21193
+ function swatchFillClass(token) {
21194
+ return SWATCH_FILL[token];
21195
+ }
21196
+ /** The utility that colours a dashed swatch's line with `token`. */
21197
+ function swatchLineClass(token) {
21198
+ return SWATCH_LINE[token];
21199
+ }
21200
+ /**
21201
+ * Resolve a theme token to the colour the canvas paints. Reads the computed
21202
+ * value of `--color-<token>` on the chart's own root (so a scoped theme block
21203
+ * wins); answers `transparent` when the document has no value (a test DOM).
21204
+ */
21205
+ function resolveChartColor(token, el) {
21206
+ if (typeof window === "undefined" || typeof getComputedStyle !== "function") return "transparent";
21207
+ const root = el ?? document.documentElement;
21208
+ const value = getComputedStyle(root).getPropertyValue(`--color-${token}`).trim();
21209
+ return value.length > 0 ? value : "transparent";
21210
+ }
21211
+ var SIX_DIGIT_HEX = /^#[0-9a-f]{6}$/i;
21212
+ /**
21213
+ * `color` with `alpha` (0..1). A resolved token is a six-digit hex, which
21214
+ * becomes the eight-digit form every canvas accepts; anything else falls back
21215
+ * to `color-mix`.
21216
+ */
21217
+ function withAlpha(color, alpha) {
21218
+ const a = Math.min(1, Math.max(0, alpha));
21219
+ if (SIX_DIGIT_HEX.test(color)) return `${color}${Math.round(a * 255).toString(16).padStart(2, "0")}`;
21220
+ return `color-mix(in srgb, ${color} ${String(Math.round(a * 100))}%, transparent)`;
21221
+ }
21222
+ //#endregion
21223
+ //#region src/composites/charts/chart-frame.tsx
21224
+ function ChartFrame({ title, actions, readoutTime, legend, summary, footer, placeholder, controls, className, children }) {
21225
+ const single = legend.length === 1 ? legend[0] : void 0;
21226
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("figure", {
21227
+ className: cn("min-w-0 m-0", className),
21228
+ "data-chart-frame": "",
21229
+ children: [
21230
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
21231
+ className: "flex items-baseline justify-between gap-2 mb-1 min-w-0",
21232
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("figcaption", {
21233
+ className: "text-[10px] uppercase tracking-wider text-foreground-subtle truncate min-w-0",
21234
+ children: title
21235
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
21236
+ className: "flex items-center gap-2 shrink-0",
21237
+ children: [
21238
+ readoutTime && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
21239
+ className: "text-[10px] tabular-nums text-foreground-subtle",
21240
+ children: readoutTime
21241
+ }),
21242
+ single?.value !== void 0 && placeholder === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LegendValue, { entry: single }),
21243
+ actions
21244
+ ]
21245
+ })]
21246
+ }),
21247
+ placeholder ?? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
21248
+ role: "img",
21249
+ "aria-label": summary,
21250
+ className: "min-w-0",
21251
+ children
21252
+ }),
21253
+ placeholder === void 0 && controls,
21254
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
21255
+ className: "sr-only",
21256
+ children: summary
21257
+ }),
21258
+ legend.length > 1 && placeholder === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
21259
+ className: "flex flex-wrap items-center gap-x-3 gap-y-0.5 mt-1 list-none p-0 m-0",
21260
+ children: legend.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
21261
+ className: "min-w-0",
21262
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LegendItem, { entry })
21263
+ }, entry.id))
21264
+ }),
21265
+ footer && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
21266
+ className: "mt-1 text-[10px] text-foreground-subtle",
21267
+ children: footer
21268
+ })
21269
+ ]
21270
+ });
21271
+ }
21272
+ /** The swatch: a filled square, or a dashed bar for a dashed series. */
21273
+ function ChartSwatch({ token, dashed }) {
21274
+ return dashed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
21275
+ "aria-hidden": "true",
21276
+ className: cn("inline-block h-0 w-3 border-t-2 border-dashed shrink-0", swatchLineClass(token))
21277
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
21278
+ "aria-hidden": "true",
21279
+ className: cn("inline-block h-2 w-2 rounded-[2px] shrink-0", swatchFillClass(token))
21280
+ });
21281
+ }
21282
+ function LegendItem({ entry }) {
21283
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
21284
+ className: cn("inline-flex items-center gap-1 text-[10px] min-w-0", entry.muted ? "text-foreground-subtle" : "text-foreground-muted"),
21285
+ children: [
21286
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChartSwatch, {
21287
+ token: entry.token,
21288
+ dashed: entry.dashed
21289
+ }),
21290
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
21291
+ className: "truncate",
21292
+ children: entry.label
21293
+ }),
21294
+ entry.value !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
21295
+ className: "tabular-nums text-foreground",
21296
+ children: entry.value
21297
+ })
21298
+ ]
21299
+ });
21300
+ }
21301
+ function LegendValue({ entry }) {
21302
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
21303
+ className: "inline-flex items-center gap-1 text-[11px] tabular-nums text-foreground",
21304
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChartSwatch, {
21305
+ token: entry.token,
21306
+ dashed: entry.dashed
21307
+ }), entry.value]
21308
+ });
21309
+ }
21310
+ /** The plot-sized box for an empty or pending chart: a dashed outline and one line of text. */
21311
+ function ChartPlaceholder({ message, height }) {
21312
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
21313
+ className: cn(height === "sm" ? "h-8" : height === "md" ? "h-24" : "h-40", "flex items-center justify-center rounded border border-dashed border-border text-[10px] text-foreground-subtle text-center px-2"),
21314
+ children: message
21315
+ });
21316
+ }
21317
+ //#endregion
21318
+ //#region src/composites/charts/chart-data.ts
21319
+ /** Join several series onto one x array, inserting a `null` inside every hole wider than its `gapMs`. */
21320
+ function alignSeries(inputs) {
21321
+ const sorted = inputs.map((s) => [...s.points].sort((a, b) => a.t - b.t));
21322
+ const breaks = sorted.map((pts, i) => gapBreaks(pts, inputs[i]?.gapMs));
21323
+ const xset = /* @__PURE__ */ new Set();
21324
+ for (const pts of sorted) for (const p of pts) xset.add(p.t);
21325
+ for (const b of breaks) for (const t of b) xset.add(t);
21326
+ const xs = [...xset].sort((a, b) => a - b);
21327
+ const index = /* @__PURE__ */ new Map();
21328
+ xs.forEach((x, i) => index.set(x, i));
21329
+ const blank = () => xs.map(() => void 0);
21330
+ const ys = [];
21331
+ const mins = [];
21332
+ const maxs = [];
21333
+ sorted.forEach((pts, si) => {
21334
+ const y = blank();
21335
+ const lo = blank();
21336
+ const hi = blank();
21337
+ for (const t of breaks[si] ?? []) {
21338
+ const i = index.get(t);
21339
+ if (i !== void 0) {
21340
+ y[i] = null;
21341
+ lo[i] = null;
21342
+ hi[i] = null;
21343
+ }
21344
+ }
21345
+ for (const p of pts) {
21346
+ const i = index.get(p.t);
21347
+ if (i === void 0) continue;
21348
+ y[i] = p.v;
21349
+ if (p.min !== void 0) lo[i] = p.min;
21350
+ if (p.max !== void 0) hi[i] = p.max;
21351
+ }
21352
+ ys.push(y);
21353
+ mins.push(lo);
21354
+ maxs.push(hi);
21355
+ });
21356
+ return {
21357
+ xs,
21358
+ ys,
21359
+ mins,
21360
+ maxs
21361
+ };
21362
+ }
21363
+ /** The x of a `null` placed inside every hole: one millisecond after the sample before it. */
21364
+ function gapBreaks(points, gapMs) {
21365
+ if (gapMs === void 0 || !(gapMs > 0)) return [];
21366
+ const out = [];
21367
+ for (let i = 1; i < points.length; i++) {
21368
+ const prev = points[i - 1];
21369
+ const cur = points[i];
21370
+ if (prev && cur && cur.t - prev.t > gapMs) out.push(prev.t + 1);
21371
+ }
21372
+ return out;
21373
+ }
21374
+ /**
21375
+ * The holes every series shares (the intersection), so the chart shades only
21376
+ * what NOBODY measured. Series without `gapMs` never contribute a hole and so
21377
+ * make the intersection empty: a held value is not unmeasured.
21378
+ */
21379
+ function sharedGapSpans(inputs) {
21380
+ if (inputs.length === 0) return [];
21381
+ let acc = null;
21382
+ for (const s of inputs) {
21383
+ const own = ownGaps(s);
21384
+ acc = acc === null ? own : intersectSpans(acc, own);
21385
+ if (acc.length === 0) return [];
21386
+ }
21387
+ return acc ?? [];
21388
+ }
21389
+ function ownGaps(s) {
21390
+ const gapMs = s.gapMs;
21391
+ if (gapMs === void 0 || !(gapMs > 0)) return [];
21392
+ const pts = [...s.points].sort((a, b) => a.t - b.t);
21393
+ const out = [];
21394
+ for (let i = 1; i < pts.length; i++) {
21395
+ const prev = pts[i - 1];
21396
+ const cur = pts[i];
21397
+ if (prev && cur && cur.t - prev.t > gapMs) out.push({
21398
+ from: prev.t,
21399
+ to: cur.t
21400
+ });
21401
+ }
21402
+ return out;
21403
+ }
21404
+ function intersectSpans(a, b) {
21405
+ const out = [];
21406
+ for (const x of a) for (const y of b) {
21407
+ const from = Math.max(x.from, y.from);
21408
+ const to = Math.min(x.to, y.to);
21409
+ if (to > from) out.push({
21410
+ from,
21411
+ to
21412
+ });
21413
+ }
21414
+ return out;
21415
+ }
21416
+ /**
21417
+ * Cumulative sums for a stacked area: `out[i] = ys[0] + … + ys[i]`. A stack
21418
+ * level is unknown (`null`) as soon as any layer under it is unknown, and an
21419
+ * alignment filler stays a filler.
21420
+ */
21421
+ function stackSeries(ys) {
21422
+ const out = [];
21423
+ ys.forEach((row, si) => {
21424
+ const below = si === 0 ? null : out[si - 1];
21425
+ out.push(row.map((v, i) => {
21426
+ if (v === void 0) return void 0;
21427
+ if (v === null) return null;
21428
+ if (below === null || below === void 0) return v;
21429
+ const b = below[i];
21430
+ if (b === null) return null;
21431
+ if (b === void 0) return v;
21432
+ return b + v;
21433
+ }));
21434
+ });
21435
+ return out;
21436
+ }
21437
+ /**
21438
+ * The value a reader sees at index `idx`: the nearest sample at or before it
21439
+ * that is not an alignment filler. A `null` there is answered as unknown.
21440
+ */
21441
+ function valueAtOrBefore(xs, ys, idx) {
21442
+ for (let i = Math.min(idx, xs.length - 1); i >= 0; i--) {
21443
+ const v = ys[i];
21444
+ if (v !== void 0) return v;
21445
+ }
21446
+ return null;
21447
+ }
21448
+ /**
21449
+ * Turn change points into segments clipped to [from, to]. `before` is the
21450
+ * state in force when the window opens (the last change before it); without
21451
+ * it the window starts unknown until the first change.
21452
+ */
21453
+ function segmentsFromChanges(changes, from, to, before = null) {
21454
+ const sorted = [...changes].filter((c) => c.t < to).sort((a, b) => a.t - b.t);
21455
+ const out = [];
21456
+ let cursor = from;
21457
+ let state = before;
21458
+ for (const c of sorted) {
21459
+ if (c.t <= from) {
21460
+ state = c.state;
21461
+ continue;
21462
+ }
21463
+ if (c.state === state) continue;
21464
+ out.push({
21465
+ from: cursor,
21466
+ to: c.t,
21467
+ state
21468
+ });
21469
+ cursor = c.t;
21470
+ state = c.state;
21471
+ }
21472
+ if (to > cursor) out.push({
21473
+ from: cursor,
21474
+ to,
21475
+ state
21476
+ });
21477
+ return out.filter((s) => s.to > s.from && s.state !== null);
21478
+ }
21479
+ /** Total milliseconds per state over the segments (unknown spans are not counted). */
21480
+ function timeInState(segments) {
21481
+ const out = /* @__PURE__ */ new Map();
21482
+ for (const s of segments) {
21483
+ if (s.state === null) continue;
21484
+ out.set(s.state, (out.get(s.state) ?? 0) + Math.max(0, s.to - s.from));
21485
+ }
21486
+ return out;
21487
+ }
21488
+ /** The state in force at `t`, or `null` when nothing was recorded there. */
21489
+ function stateAt(segments, t) {
21490
+ for (const s of segments) if (t >= s.from && t < s.to) return s.state;
21491
+ return null;
21492
+ }
21493
+ var MINUTE$1 = 6e4;
21494
+ var HOUR = 60 * MINUTE$1;
21495
+ var DAY = 24 * HOUR;
21496
+ /** `2h 14m`, `3d 4h`, `45s` — two units at most, for a legend. */
21497
+ function formatSpan(ms) {
21498
+ if (!(ms > 0)) return "0s";
21499
+ if (ms < MINUTE$1) return `${String(Math.round(ms / 1e3))}s`;
21500
+ if (ms < HOUR) return `${String(Math.floor(ms / MINUTE$1))}m`;
21501
+ if (ms < DAY) {
21502
+ const h = Math.floor(ms / HOUR);
21503
+ const m = Math.floor(ms % HOUR / MINUTE$1);
21504
+ return m > 0 ? `${String(h)}h ${String(m)}m` : `${String(h)}h`;
21505
+ }
21506
+ const d = Math.floor(ms / DAY);
21507
+ const h = Math.floor(ms % DAY / HOUR);
21508
+ return h > 0 ? `${String(d)}d ${String(h)}h` : `${String(d)}d`;
21509
+ }
21510
+ var TIME_HM = {
21511
+ hour: "2-digit",
21512
+ minute: "2-digit"
21513
+ };
21514
+ var TIME_HMS = {
21515
+ hour: "2-digit",
21516
+ minute: "2-digit",
21517
+ second: "2-digit"
21518
+ };
21519
+ var DATE_DM = {
21520
+ day: "2-digit",
21521
+ month: "short"
21522
+ };
21523
+ var DATE_MY = {
21524
+ month: "short",
21525
+ year: "2-digit"
21526
+ };
21527
+ /**
21528
+ * A wall-clock label at the precision the window's length justifies:
21529
+ * seconds under ten minutes, `HH:MM` within two days, the date past that,
21530
+ * month + year past ~4 months.
21531
+ */
21532
+ function formatTimeLabel(t, spanMs) {
21533
+ const d = new Date(t);
21534
+ if (spanMs < 10 * MINUTE$1) return d.toLocaleTimeString(void 0, TIME_HMS);
21535
+ if (spanMs <= 2 * DAY) return d.toLocaleTimeString(void 0, TIME_HM);
21536
+ if (spanMs <= 120 * DAY) return d.toLocaleDateString(void 0, DATE_DM);
21537
+ return d.toLocaleDateString(void 0, DATE_MY);
21538
+ }
21539
+ /** A cursor readout time: always the full instant at the window's precision. */
21540
+ function formatCursorTime(t, spanMs) {
21541
+ const d = new Date(t);
21542
+ if (spanMs <= 2 * DAY) return d.toLocaleTimeString(void 0, spanMs < 10 * MINUTE$1 ? TIME_HMS : TIME_HM);
21543
+ return `${d.toLocaleDateString(void 0, DATE_DM)} ${d.toLocaleTimeString(void 0, TIME_HM)}`;
21544
+ }
21545
+ //#endregion
21546
+ //#region src/composites/charts/chart-scale.ts
21547
+ var LINEAR_MAX_MANTISSAS = [
21548
+ 1,
21549
+ 2,
21550
+ 5,
21551
+ 10
21552
+ ];
21553
+ var BYTE_MAX_MANTISSAS = [
21554
+ 1,
21555
+ 2,
21556
+ 4,
21557
+ 8,
21558
+ 16,
21559
+ 32,
21560
+ 64,
21561
+ 128,
21562
+ 256,
21563
+ 512,
21564
+ 1024
21565
+ ];
21566
+ /** Steps a reader adds up without arithmetic; 1.25 / 1.66 are what an equal split of anything gives. */
21567
+ var READABLE_STEP_MANTISSAS = [
21568
+ 1,
21569
+ 1.5,
21570
+ 2,
21571
+ 2.5,
21572
+ 3,
21573
+ 4,
21574
+ 5
21575
+ ];
21576
+ /** Interval counts to try, in preference order: four is as many gridlines as a short plot keeps legible. */
21577
+ var AXIS_INTERVAL_CANDIDATES = [
21578
+ 4,
21579
+ 3,
21580
+ 5,
21581
+ 2
21582
+ ];
21583
+ var MANTISSA_EPSILON = 1e-9;
21584
+ function isReadableStep(step, scale) {
21585
+ if (!Number.isFinite(step) || step <= 0) return false;
21586
+ if (scale === "bytes") {
21587
+ if (!Number.isInteger(step)) return false;
21588
+ const mantissa = step / 1024 ** Math.floor(Math.log(step) / Math.log(1024));
21589
+ return READABLE_STEP_MANTISSAS.some((m) => Math.abs(mantissa - m) < MANTISSA_EPSILON);
21590
+ }
21591
+ const mantissa = step / 10 ** Math.floor(Math.log10(step));
21592
+ return READABLE_STEP_MANTISSAS.some((m) => Math.abs(mantissa - m) < MANTISSA_EPSILON);
21593
+ }
21594
+ /**
21595
+ * Nice upper bound of a value axis that starts at zero. `floor` is returned
21596
+ * VERBATIM when nothing exceeds it, so an idle series keeps the scale the
21597
+ * caller chose instead of a 0–0.3 axis on which noise looks like load.
21598
+ */
21599
+ function niceMax(values, floor, scale = "linear") {
21600
+ let max = 0;
21601
+ for (const v of values) if (v !== null && Number.isFinite(v) && v > max) max = v;
21602
+ if (max <= floor) return floor;
21603
+ if (scale === "bytes") {
21604
+ const magnitude = 1024 ** Math.floor(Math.log(max) / Math.log(1024));
21605
+ for (const mantissa of BYTE_MAX_MANTISSAS) {
21606
+ const candidate = mantissa * magnitude;
21607
+ if (candidate >= max) return candidate;
21608
+ }
21609
+ return 1024 * magnitude;
21610
+ }
21611
+ const magnitude = 10 ** Math.floor(Math.log10(max));
21612
+ for (const mantissa of LINEAR_MAX_MANTISSAS) {
21613
+ const candidate = mantissa * magnitude;
21614
+ if (candidate >= max) return candidate;
21615
+ }
21616
+ return 10 * magnitude;
21617
+ }
21618
+ /** Gridline values for an axis topping out at `max`, ascending, `max` last; zero is the baseline, not a tick. */
21619
+ function axisTicks(max, scale = "linear") {
21620
+ if (!Number.isFinite(max) || max <= 0) return [];
21621
+ const intervals = AXIS_INTERVAL_CANDIDATES.find((d) => isReadableStep(max / d, scale)) ?? AXIS_INTERVAL_CANDIDATES[AXIS_INTERVAL_CANDIDATES.length - 1] ?? 2;
21622
+ const step = max / intervals;
21623
+ const ticks = [];
21624
+ for (let i = 1; i <= intervals; i++) ticks.push(step * i);
21625
+ return ticks;
21626
+ }
21627
+ /**
21628
+ * Nice [min, max] for a series that need NOT start at zero (a temperature, a
21629
+ * dBFS level): the data range padded by a tenth and rounded outward to a
21630
+ * readable step. A flat series gets a ±1 window so it is not a degenerate axis.
21631
+ */
21632
+ function niceRange(values) {
21633
+ let lo = Number.POSITIVE_INFINITY;
21634
+ let hi = Number.NEGATIVE_INFINITY;
21635
+ for (const v of values) {
21636
+ if (v === null || !Number.isFinite(v)) continue;
21637
+ if (v < lo) lo = v;
21638
+ if (v > hi) hi = v;
21639
+ }
21640
+ if (!Number.isFinite(lo) || !Number.isFinite(hi)) return null;
21641
+ if (hi - lo < 1e-9) return [lo - 1, hi + 1];
21642
+ const pad = (hi - lo) * .1;
21643
+ const span = hi - lo + 2 * pad;
21644
+ const magnitude = 10 ** Math.floor(Math.log10(span / 4));
21645
+ const step = [
21646
+ 1,
21647
+ 2,
21648
+ 2.5,
21649
+ 5,
21650
+ 10
21651
+ ].map((m) => m * magnitude).find((st) => span / st <= 6) ?? magnitude * 10;
21652
+ return [Math.floor((lo - pad) / step) * step, Math.ceil((hi + pad) / step) * step];
21653
+ }
21654
+ /**
21655
+ * How many sampling intervals of silence make a HOLE rather than a late tick:
21656
+ * past any plausible delivery jitter, well inside the shortest outage worth seeing.
21657
+ */
21658
+ var GAP_INTERVAL_FACTOR = 2.5;
21659
+ /** The gap threshold for a series sampled every `intervalMs` — derived, never a literal. */
21660
+ function gapThresholdMs(intervalMs) {
21661
+ return intervalMs * GAP_INTERVAL_FACTOR;
21662
+ }
21663
+ //#endregion
21664
+ //#region src/composites/charts/chart-uplot.ts
21665
+ var AXIS_FONT_PX = 10;
21666
+ function axisFont() {
21667
+ const family = typeof document !== "undefined" && document.body ? getComputedStyle(document.body).fontFamily : "sans-serif";
21668
+ return `${String(AXIS_FONT_PX)}px ${family}`;
21669
+ }
21670
+ /** The x axis: wall-clock time under the plot, at the window's precision. */
21671
+ function timeAxis(spanMs, show = true) {
21672
+ return {
21673
+ show,
21674
+ scale: "x",
21675
+ side: 2,
21676
+ size: show ? 22 : 0,
21677
+ gap: 4,
21678
+ font: axisFont(),
21679
+ stroke: () => resolveChartColor("foreground-subtle"),
21680
+ grid: { show: false },
21681
+ ticks: {
21682
+ show,
21683
+ stroke: () => resolveChartColor("border"),
21684
+ width: 1,
21685
+ size: 3
21686
+ },
21687
+ space: 70,
21688
+ values: (_u, splits) => splits.map((t) => formatTimeLabel(t, spanMs()))
21689
+ };
21690
+ }
21691
+ /** A y scale's range from the values it carries, per the axis spec. */
21692
+ function valueRange(values, spec) {
21693
+ if (spec.fixed) return spec.fixed;
21694
+ const defined = values.filter((v) => typeof v === "number" && Number.isFinite(v));
21695
+ if ((spec.zeroBased ?? true) && defined.every((v) => v >= 0)) return [0, niceMax(defined, spec.floor ?? 0, spec.scale ?? "linear") || 1];
21696
+ return niceRange(defined) ?? [0, 1];
21697
+ }
21698
+ /** Steps a reader adds up without arithmetic, as mantissas over a power of ten. */
21699
+ var READABLE_STEPS = [
21700
+ 1,
21701
+ 2,
21702
+ 2.5,
21703
+ 5
21704
+ ];
21705
+ /** Tick positions for a value axis over `range`: readable steps, 3–6 intervals. */
21706
+ function valueSplits(range, spec) {
21707
+ const [lo, hi] = range;
21708
+ if (lo === 0 && hi > 0) return [...axisTicks(hi, spec.scale ?? "linear")];
21709
+ const span = hi - lo;
21710
+ if (!(span > 0)) return [lo];
21711
+ const magnitude = 10 ** Math.floor(Math.log10(span / 4));
21712
+ const step = [...READABLE_STEPS, 10].map((m) => m * magnitude).find((st) => span / st <= 6) ?? span / 4;
21713
+ const out = [];
21714
+ for (let v = Math.ceil(lo / step - 1e-9) * step; v <= hi + 1e-9; v += step) out.push(Math.round(v / step) * step);
21715
+ return out;
21716
+ }
21717
+ /** One value axis. `side` 3 = left, 1 = right. */
21718
+ function valueAxis(scaleKey, side, format, showGrid) {
21719
+ return {
21720
+ scale: scaleKey,
21721
+ side,
21722
+ font: axisFont(),
21723
+ size: (_u, values) => {
21724
+ const longest = (values ?? []).reduce((m, v) => Math.max(m, String(v).length), 0);
21725
+ return Math.max(28, Math.min(72, longest * 6 + 10));
21726
+ },
21727
+ gap: 4,
21728
+ stroke: () => resolveChartColor("foreground-subtle"),
21729
+ grid: {
21730
+ show: showGrid,
21731
+ stroke: () => withAlpha(resolveChartColor("foreground-subtle"), .18),
21732
+ width: 1
21733
+ },
21734
+ ticks: { show: false },
21735
+ values: (_u, splits) => splits.map((v) => format(v))
21736
+ };
21737
+ }
21738
+ /** The cursor: a vertical crosshair, synced across every chart with the same key. */
21739
+ function chartCursor(syncKey) {
21740
+ return {
21741
+ y: false,
21742
+ drag: {
21743
+ x: false,
21744
+ y: false,
21745
+ setScale: false
21746
+ },
21747
+ points: {
21748
+ size: 6,
21749
+ width: 1.5
21750
+ },
21751
+ focus: { prox: 16 },
21752
+ ...syncKey !== void 0 ? { sync: {
21753
+ key: syncKey,
21754
+ setSeries: false
21755
+ } } : {}
21756
+ };
21757
+ }
21758
+ /**
21759
+ * Shade every hole nobody measured, UNDER the series, so an outage reads as
21760
+ * missing ground and never as a quiet period.
21761
+ */
21762
+ function gapShadeHook(spans) {
21763
+ return (u) => {
21764
+ const list = spans();
21765
+ if (list.length === 0) return;
21766
+ const ctx = u.ctx;
21767
+ ctx.save();
21768
+ ctx.fillStyle = withAlpha(resolveChartColor("foreground-subtle"), .12);
21769
+ for (const s of list) {
21770
+ const x0 = u.valToPos(s.from, "x", true);
21771
+ const x1 = u.valToPos(s.to, "x", true);
21772
+ ctx.fillRect(x0, u.bbox.top, Math.max(1, x1 - x0), u.bbox.height);
21773
+ }
21774
+ ctx.restore();
21775
+ };
21776
+ }
21777
+ var BYTE_UNITS = new Set([
21778
+ "B",
21779
+ "bytes",
21780
+ "byte"
21781
+ ]);
21782
+ /** Is this unit a byte count (binary axis and formatter)? */
21783
+ function isByteUnit(unit) {
21784
+ return unit !== null && unit !== void 0 && BYTE_UNITS.has(unit);
21785
+ }
21786
+ /** Units that live far from zero: their axis is padded around the data instead. */
21787
+ var FLOATING_UNITS = new Set([
21788
+ "°C",
21789
+ "°F",
21790
+ "K",
21791
+ "dBFS",
21792
+ "dB",
21793
+ "dBm",
21794
+ "hPa",
21795
+ "mbar",
21796
+ "inHg"
21797
+ ]);
21798
+ /** The default axis spec for a unit. */
21799
+ function defaultAxisSpec(unit) {
21800
+ if (isByteUnit(unit)) return { scale: "bytes" };
21801
+ if (unit === "%") return { floor: 10 };
21802
+ if (unit !== null && unit !== void 0 && FLOATING_UNITS.has(unit)) return { zeroBased: false };
21803
+ return {};
21804
+ }
21805
+ /** Binary byte count: `512 B`, `3.4 MB`, `1.2 GB`. */
21806
+ function formatChartBytes(bytes) {
21807
+ if (!Number.isFinite(bytes)) return "—";
21808
+ if (Math.abs(bytes) < 1024) return `${String(Math.round(bytes))} B`;
21809
+ const units = [
21810
+ "KB",
21811
+ "MB",
21812
+ "GB",
21813
+ "TB",
21814
+ "PB"
21815
+ ];
21816
+ let value = bytes / 1024;
21817
+ let unit = "KB";
21818
+ for (const u of units) {
21819
+ unit = u;
21820
+ if (Math.abs(value) < 1024) break;
21821
+ value /= 1024;
21822
+ }
21823
+ return `${Math.abs(value) < 100 ? value.toFixed(1) : value.toFixed(0)} ${unit}`;
21824
+ }
21825
+ /** A number at a precision its magnitude justifies. */
21826
+ function formatChartNumber(v) {
21827
+ if (!Number.isFinite(v)) return "—";
21828
+ const abs = Math.abs(v);
21829
+ if (abs >= 1e3) return Math.round(v).toLocaleString();
21830
+ if (abs >= 100) return v.toFixed(0);
21831
+ if (abs >= 10) return trimZeros(v.toFixed(1));
21832
+ if (abs >= 1) return trimZeros(v.toFixed(2));
21833
+ if (abs === 0) return "0";
21834
+ return trimZeros(v.toFixed(3));
21835
+ }
21836
+ function trimZeros(s) {
21837
+ return s.includes(".") ? s.replace(/\.?0+$/, "") : s;
21838
+ }
21839
+ /** The default value formatter: bytes binary, `%` glued, any other unit after a space. */
21840
+ function formatChartValue(v, unit) {
21841
+ if (isByteUnit(unit)) return formatChartBytes(v);
21842
+ const n = formatChartNumber(v);
21843
+ if (unit === null || unit === void 0 || unit === "" || unit === "count") return n;
21844
+ if (unit === "%") return `${n}%`;
21845
+ return `${n} ${unit}`;
21846
+ }
21847
+ //#endregion
21848
+ //#region src/composites/charts/chart-window.tsx
21849
+ function ChartWindowToggle({ options, value, onChange, ariaLabel, className }) {
21850
+ if (options.length < 2) return null;
21851
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
21852
+ role: "radiogroup",
21853
+ "aria-label": ariaLabel,
21854
+ className: cn("inline-flex items-center gap-0.5 border border-border rounded-md p-0.5 bg-background", className),
21855
+ children: options.map(({ id, label }) => {
21856
+ const active = value === id;
21857
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
21858
+ type: "button",
21859
+ role: "radio",
21860
+ "aria-checked": active,
21861
+ onClick: () => onChange(id),
21862
+ className: cn("px-2 py-0.5 rounded text-[10px] font-medium transition-colors", active ? "bg-primary/10 text-primary" : "text-foreground-subtle hover:text-foreground"),
21863
+ children: label
21864
+ }, id);
21865
+ })
21866
+ });
21867
+ }
21868
+ //#endregion
21869
+ //#region src/composites/charts/bar-series-model.ts
21870
+ /** Cumulative tops per layer, bottom layer first: `tops[l][i] = values[0..l]` summed. */
21871
+ function stackBuckets(buckets, layers) {
21872
+ const tops = [];
21873
+ for (let l = 0; l < layers; l++) tops.push(buckets.map((b) => {
21874
+ let sum = null;
21875
+ for (let k = 0; k <= l; k++) {
21876
+ const v = b.values[k];
21877
+ if (v === null || v === void 0) continue;
21878
+ sum = (sum ?? 0) + v;
21879
+ }
21880
+ return sum;
21881
+ }));
21882
+ return tops;
21883
+ }
21884
+ /** Total per layer and overall over the window (unknown buckets add nothing). */
21885
+ function bucketTotals(buckets, layers) {
21886
+ const perLayer = Array.from({ length: layers }, () => 0);
21887
+ let known = false;
21888
+ for (const b of buckets) for (let l = 0; l < layers; l++) {
21889
+ const v = b.values[l];
21890
+ if (v === null || v === void 0) continue;
21891
+ known = true;
21892
+ perLayer[l] = (perLayer[l] ?? 0) + v;
21893
+ }
21894
+ return {
21895
+ perLayer,
21896
+ total: perLayer.reduce((a, b) => a + b, 0),
21897
+ known
21898
+ };
21899
+ }
21900
+ /** Index of the bucket that holds `t`, or `null`. */
21901
+ function bucketIndexAt(buckets, bucketMs, t) {
21902
+ for (let i = 0; i < buckets.length; i++) {
21903
+ const b = buckets[i];
21904
+ if (b && t >= b.t && t < b.t + bucketMs) return i;
21905
+ }
21906
+ return null;
21907
+ }
21908
+ //#endregion
21909
+ //#region src/composites/charts/uplot-host.tsx
21910
+ /**
21911
+ * The one place the chart family touches uPlot.
21912
+ *
21913
+ * - uPlot is imported LAZILY: its module reads `matchMedia` at load time, and
21914
+ * a document without a 2D canvas (a test DOM, a print preview) has nothing
21915
+ * for it to draw on. There the chart's DOM half — title, legend, readout and
21916
+ * the accessible summary — still renders; only the plot is absent.
21917
+ * - Width follows the container (`ResizeObserver`); the height is the caller's.
21918
+ * - A theme switch (`.dark` / `.light` on `<html>`) only REDRAWS: every colour
21919
+ * is a function that resolves its token at draw time (`chart-palette.ts`).
21920
+ * - `optionsKey` decides when the plot is rebuilt (series set, axes); new
21921
+ * `data` under the same key is a cheap `setData`.
21922
+ */
21923
+ var ctorPromise = null;
21924
+ /** A 2D canvas this document can draw on — false in a test DOM. */
21925
+ function canDrawCanvas() {
21926
+ if (typeof document === "undefined" || typeof window === "undefined") return false;
21927
+ if (typeof window.matchMedia !== "function") return false;
21928
+ try {
21929
+ return document.createElement("canvas").getContext("2d") !== null;
21930
+ } catch {
21931
+ return false;
21932
+ }
21933
+ }
21934
+ function loadUPlot() {
21935
+ if (ctorPromise === null) ctorPromise = canDrawCanvas() ? Promise.resolve().then(() => require("./uPlot.esm-BbxBUnav.cjs")).then((m) => m.default).catch(() => null) : Promise.resolve(null);
21936
+ return ctorPromise;
21937
+ }
21938
+ function UPlotHost({ optionsKey, buildOptions, data, height, onInstance, className, inline = false }) {
21939
+ const hostRef = (0, react$1.useRef)(null);
21940
+ const plotRef = (0, react$1.useRef)(null);
21941
+ const [ctor, setCtor] = (0, react$1.useState)(null);
21942
+ const latest = (0, react$1.useRef)({
21943
+ buildOptions,
21944
+ data,
21945
+ onInstance
21946
+ });
21947
+ latest.current = {
21948
+ buildOptions,
21949
+ data,
21950
+ onInstance
21951
+ };
21952
+ (0, react$1.useEffect)(() => {
21953
+ let alive = true;
21954
+ loadUPlot().then((c) => {
21955
+ if (alive && c !== null) setCtor(() => c);
21956
+ });
21957
+ return () => {
21958
+ alive = false;
21959
+ };
21960
+ }, []);
21961
+ (0, react$1.useEffect)(() => {
21962
+ const el = hostRef.current;
21963
+ if (ctor === null || el === null) return;
21964
+ const width = Math.max(1, Math.floor(el.clientWidth));
21965
+ const u = new ctor({
21966
+ ...latest.current.buildOptions(width, ctor),
21967
+ width,
21968
+ height
21969
+ }, latest.current.data, el);
21970
+ plotRef.current = u;
21971
+ latest.current.onInstance?.(u);
21972
+ return () => {
21973
+ latest.current.onInstance?.(null);
21974
+ u.destroy();
21975
+ plotRef.current = null;
21976
+ };
21977
+ }, [
21978
+ ctor,
21979
+ optionsKey,
21980
+ height
21981
+ ]);
21982
+ (0, react$1.useEffect)(() => {
21983
+ plotRef.current?.setData(data);
21984
+ }, [data]);
21985
+ (0, react$1.useEffect)(() => {
21986
+ const el = hostRef.current;
21987
+ if (el === null || typeof ResizeObserver === "undefined") return;
21988
+ const ro = new ResizeObserver((entries) => {
21989
+ const w = Math.floor(entries[0]?.contentRect.width ?? 0);
21990
+ const u = plotRef.current;
21991
+ if (u !== null && w > 0 && w !== u.width) u.setSize({
21992
+ width: w,
21993
+ height
21994
+ });
21995
+ });
21996
+ ro.observe(el);
21997
+ return () => ro.disconnect();
21998
+ }, [height]);
21999
+ (0, react$1.useEffect)(() => {
22000
+ if (typeof MutationObserver === "undefined") return;
22001
+ const mo = new MutationObserver(() => plotRef.current?.redraw(false, true));
22002
+ mo.observe(document.documentElement, {
22003
+ attributes: true,
22004
+ attributeFilter: ["class"]
22005
+ });
22006
+ return () => mo.disconnect();
22007
+ }, []);
22008
+ const setHost = (el) => {
22009
+ hostRef.current = el;
22010
+ };
22011
+ return inline ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
22012
+ ref: setHost,
22013
+ className: className ?? "block w-full min-w-0",
22014
+ "data-chart-plot": ""
22015
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
22016
+ ref: setHost,
22017
+ className: className ?? "w-full min-w-0",
22018
+ "data-chart-plot": ""
22019
+ });
22020
+ }
22021
+ //#endregion
22022
+ //#region src/composites/charts/BarSeries.tsx
22023
+ /**
22024
+ * BarSeries — bucketed counts or deltas on wall time (uPlot), optionally
22025
+ * stacked, with a window total (kWh per hour with the day's sum, detections
22026
+ * per 5 minutes by class, event density under a recording timeline).
22027
+ *
22028
+ * Bars sit on their bucket's real span, so a missing bucket is a missing bar
22029
+ * and never a zero bar. `compact` drops the frame and axes for a strip that
22030
+ * lives inside another control; there a bucket can be selected (`onSelect`)
22031
+ * and every bucket is also a real button for keyboard and screen readers.
22032
+ */
22033
+ function BarSeries({ title, layers, buckets, bucketMs, range, unit = null, formatValue, height, stacked, showTotal = false, syncKey, pending = false, emptyMessage, actions, footer, windowToggle, compact = false, minBarFraction = 0, selectedIndex = null, onSelect, bucketLabel, bucketTestId, className }) {
22034
+ const t = useUiText();
22035
+ const plotHeight = height ?? (compact ? 24 : 100);
22036
+ const isStacked = stacked ?? layers.length > 1;
22037
+ const fmt = (0, react$1.useCallback)((v) => (formatValue ?? formatChartValue)(v, unit), [formatValue, unit]);
22038
+ const tops = (0, react$1.useMemo)(() => isStacked ? stackBuckets(buckets, layers.length) : layers.map((_l, li) => buckets.map((b) => b.values[li] ?? null)), [
22039
+ buckets,
22040
+ layers,
22041
+ isStacked
22042
+ ]);
22043
+ const totals = (0, react$1.useMemo)(() => bucketTotals(buckets, layers.length), [buckets, layers.length]);
22044
+ const [cursorIdx, setCursorIdx] = (0, react$1.useState)(null);
22045
+ const live = (0, react$1.useRef)({
22046
+ buckets,
22047
+ tops,
22048
+ range,
22049
+ selectedIndex,
22050
+ onSelect,
22051
+ bucketLabel,
22052
+ fmt
22053
+ });
22054
+ live.current = {
22055
+ buckets,
22056
+ tops,
22057
+ range,
22058
+ selectedIndex,
22059
+ onSelect,
22060
+ bucketLabel,
22061
+ fmt
22062
+ };
22063
+ const optionsKey = `${layers.map((l) => `${l.id}:${l.colorIndex}`).join(",")}#${compact ? "C" : ""}#${isStacked ? "S" : ""}#${syncKey ?? ""}#${String(bucketMs)}`;
22064
+ const data = (0, react$1.useMemo)(() => {
22065
+ const top = tops.at(-1) ?? [];
22066
+ const peak = isStacked ? top : buckets.map((_b, i) => Math.max(...tops.map((row) => row[i] ?? 0)));
22067
+ return [buckets.map((b) => b.t), peak.map((v) => v ?? null)];
22068
+ }, [
22069
+ buckets,
22070
+ tops,
22071
+ isStacked,
22072
+ selectedIndex
22073
+ ]);
22074
+ const buildOptions = (0, react$1.useCallback)(() => {
22075
+ const spec = { floor: 0 };
22076
+ return {
22077
+ ms: 1,
22078
+ legend: { show: false },
22079
+ padding: compact ? [
22080
+ 0,
22081
+ 0,
22082
+ 0,
22083
+ 0
22084
+ ] : [
22085
+ 8,
22086
+ 30,
22087
+ 0,
22088
+ 0
22089
+ ],
22090
+ scales: {
22091
+ x: {
22092
+ time: true,
22093
+ range: () => [live.current.range.from, live.current.range.to]
22094
+ },
22095
+ y: { range: (self) => {
22096
+ const r = valueRange([...self.data[1] ?? []], spec);
22097
+ return [r[0], r[1]];
22098
+ } }
22099
+ },
22100
+ axes: compact ? [{
22101
+ show: false,
22102
+ scale: "x"
22103
+ }, {
22104
+ show: false,
22105
+ scale: "y"
22106
+ }] : [timeAxis(() => live.current.range.to - live.current.range.from), {
22107
+ ...valueAxis("y", 3, (v) => live.current.fmt(v), true),
22108
+ splits: (_s, _i, min, max) => valueSplits([min, max], spec)
22109
+ }],
22110
+ series: [{}, {
22111
+ scale: "y",
22112
+ stroke: () => "transparent",
22113
+ points: { show: false },
22114
+ paths: () => null
22115
+ }],
22116
+ cursor: {
22117
+ ...chartCursor(syncKey),
22118
+ points: { show: false }
22119
+ },
22120
+ hooks: {
22121
+ draw: [(u) => {
22122
+ const { buckets: bs, tops: ts, selectedIndex: sel } = live.current;
22123
+ const ctx = u.ctx;
22124
+ const baseY = u.valToPos(0, "y", true);
22125
+ const minPx = minBarFraction * u.bbox.height;
22126
+ const gap = Math.max(1, Math.round(devicePixelRatio));
22127
+ ctx.save();
22128
+ bs.forEach((b, i) => {
22129
+ const x0 = u.valToPos(b.t, "x", true);
22130
+ const x1 = u.valToPos(b.t + bucketMs, "x", true);
22131
+ const w = Math.max(1, x1 - x0 - gap);
22132
+ let prevY = baseY;
22133
+ layers.forEach((layer, li) => {
22134
+ const top = ts[li]?.[i];
22135
+ if (top === null || top === void 0) return;
22136
+ if (!(top > (isStacked && li > 0 ? ts[li - 1]?.[i] ?? 0 : 0))) return;
22137
+ let yTop = u.valToPos(top, "y", true);
22138
+ const yBottom = isStacked ? prevY : baseY;
22139
+ if (li === layers.length - 1 && baseY - yTop < minPx) yTop = baseY - minPx;
22140
+ ctx.fillStyle = resolveChartColor(chartSeriesStyle(layer.colorIndex).token);
22141
+ const h = Math.max(1, yBottom - yTop - (isStacked && li > 0 ? gap : 0));
22142
+ ctx.fillRect(x0, yTop, w, h);
22143
+ prevY = yTop;
22144
+ });
22145
+ if (sel === i) {
22146
+ ctx.strokeStyle = resolveChartColor("primary");
22147
+ ctx.lineWidth = gap * 1.5;
22148
+ ctx.strokeRect(x0, u.bbox.top + gap, w, u.bbox.height - 2 * gap);
22149
+ }
22150
+ });
22151
+ ctx.restore();
22152
+ }],
22153
+ setCursor: [(u) => {
22154
+ const left = u.cursor.left ?? -1;
22155
+ const idx = left < 0 ? null : bucketIndexAt(live.current.buckets, bucketMs, u.posToVal(left, "x"));
22156
+ setCursorIdx(idx);
22157
+ const b = idx === null ? void 0 : live.current.buckets[idx];
22158
+ const label = b === void 0 || idx === null ? "" : live.current.bucketLabel?.(b, idx) ?? "";
22159
+ u.over.title = label;
22160
+ }],
22161
+ ready: [(u) => {
22162
+ u.over.addEventListener("click", () => {
22163
+ const left = u.cursor.left ?? -1;
22164
+ if (left < 0) return;
22165
+ const idx = bucketIndexAt(live.current.buckets, bucketMs, u.posToVal(left, "x"));
22166
+ if (idx !== null) live.current.onSelect?.(idx);
22167
+ });
22168
+ }]
22169
+ }
22170
+ };
22171
+ }, [optionsKey, minBarFraction]);
22172
+ const span = range.to - range.from;
22173
+ const hovered = cursorIdx === null ? void 0 : buckets[cursorIdx];
22174
+ const legend = layers.map((l, li) => {
22175
+ const v = hovered ? hovered.values[li] : totals.known ? totals.perLayer[li] : void 0;
22176
+ return {
22177
+ id: l.id,
22178
+ label: l.label,
22179
+ token: chartSeriesStyle(l.colorIndex).token,
22180
+ value: v === null || v === void 0 ? hovered ? "—" : void 0 : fmt(v)
22181
+ };
22182
+ });
22183
+ const summary = t("uiLibrary.charts.barSummary", "{{title}}, {{from}} to {{to}}: total {{total}}", {
22184
+ title,
22185
+ from: formatTimeLabel(range.from, span),
22186
+ to: formatTimeLabel(range.to, span),
22187
+ total: totals.known ? fmt(totals.total) : t("uiLibrary.charts.unknown", "unknown")
22188
+ });
22189
+ const buttons = onSelect !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
22190
+ className: "sr-only",
22191
+ children: buckets.map((b, i) => {
22192
+ const sum = b.values.reduce((a, v) => a + (v ?? 0), 0);
22193
+ if (sum <= 0) return null;
22194
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
22195
+ type: "button",
22196
+ "data-testid": bucketTestId,
22197
+ "aria-pressed": selectedIndex === i,
22198
+ onClick: () => onSelect(i),
22199
+ children: bucketLabel?.(b, i) ?? `${formatTimeLabel(b.t, span)} ${fmt(sum)}`
22200
+ }, b.t);
22201
+ })
22202
+ }) : null;
22203
+ const plot = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UPlotHost, {
22204
+ optionsKey,
22205
+ buildOptions,
22206
+ data,
22207
+ height: plotHeight
22208
+ });
22209
+ if (compact) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
22210
+ className: cn("relative min-w-0", className),
22211
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
22212
+ role: "img",
22213
+ "aria-label": summary,
22214
+ children: plot
22215
+ }), buttons]
22216
+ });
22217
+ const totalLine = showTotal && totals.known ? t("uiLibrary.charts.total", "Total {{value}}", { value: fmt(totals.total) }) : null;
22218
+ const empty = !totals.known;
22219
+ const sizeClass = plotHeight <= 64 ? "sm" : plotHeight <= 120 ? "md" : "lg";
22220
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChartFrame, {
22221
+ title,
22222
+ actions: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
22223
+ totalLine && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
22224
+ className: "text-[11px] tabular-nums text-foreground",
22225
+ children: totalLine
22226
+ }),
22227
+ windowToggle && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChartWindowToggle, { ...windowToggle }),
22228
+ actions
22229
+ ] }),
22230
+ readoutTime: hovered ? formatCursorTime(hovered.t, span) : null,
22231
+ legend: layers.length > 1 || hovered ? legend : [],
22232
+ summary,
22233
+ footer,
22234
+ placeholder: pending ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChartPlaceholder, {
22235
+ message: t("uiLibrary.charts.loading", "Loading…"),
22236
+ height: sizeClass
22237
+ }) : empty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChartPlaceholder, {
22238
+ message: emptyMessage ?? t("uiLibrary.charts.empty", "Nothing was recorded in this window."),
22239
+ height: sizeClass
22240
+ }) : void 0,
22241
+ controls: buttons,
22242
+ className,
22243
+ children: plot
22244
+ });
22245
+ }
22246
+ //#endregion
21094
22247
  //#region src/composites/cap-settings/EventHeatmap.tsx
21095
- var KIND_BG = {
21096
- motion: "bg-emerald-500",
21097
- object: "bg-violet-500",
21098
- audio: "bg-orange-500"
22248
+ /** Stack order, bottom first; the categorical slots keep motion green, object violet, audio orange. */
22249
+ var KINDS = [
22250
+ "motion",
22251
+ "object",
22252
+ "audio"
22253
+ ];
22254
+ var KIND_SLOT = {
22255
+ motion: 6,
22256
+ object: 5,
22257
+ audio: 4
21099
22258
  };
22259
+ /** A non-empty bucket is never thinner than this fraction of the strip. */
22260
+ var MIN_BAR_FRACTION = .15;
22261
+ /**
22262
+ * Event-density strip under a recording timeline: one stacked bar per bucket
22263
+ * (motion / object / audio), height normalised to the busiest bucket, on the
22264
+ * timeline's own window. A click (or the bucket's button, for keyboard and
22265
+ * screen readers) selects the bucket — it drives the drill-down strip.
22266
+ */
21100
22267
  function EventHeatmap({ windowFrom, windowTo, buckets, selectedIndex, onSelectBucket }) {
21101
22268
  const t = useUiText();
21102
- const peak = buckets.reduce((m, b) => Math.max(m, b.total), 0);
21103
- const firstBucket = buckets[0];
21104
- const bucketWidthPct = firstBucket ? pctForMs(firstBucket.endMs, windowFrom, windowTo) - pctForMs(firstBucket.startMs, windowFrom, windowTo) : 0;
22269
+ const nonEmpty = (0, react$1.useMemo)(() => buckets.filter((b) => b.total > 0 && b.dominant !== null), [buckets]);
22270
+ const bars = (0, react$1.useMemo)(() => nonEmpty.map((b) => ({
22271
+ t: b.startMs,
22272
+ values: KINDS.map((k) => b.counts[k])
22273
+ })), [nonEmpty]);
22274
+ const layers = (0, react$1.useMemo)(() => KINDS.map((k) => ({
22275
+ id: k,
22276
+ label: t(`uiLibrary.eventHeatmap.kind.${k}`, k),
22277
+ colorIndex: KIND_SLOT[k]
22278
+ })), [t]);
22279
+ const first = buckets[0];
22280
+ const bucketMs = first ? Math.max(1, first.endMs - first.startMs) : 1;
22281
+ const selectedPos = selectedIndex === null ? null : nonEmpty.findIndex((b) => b.index === selectedIndex);
21105
22282
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
21106
- className: "relative h-6 rounded bg-background/40",
21107
- children: buckets.map((b) => {
21108
- if (b.total === 0 || b.dominant === null) return null;
21109
- const heightPct = peak > 0 ? Math.max(15, b.total / peak * 100) : 0;
21110
- const left = pctForMs(b.startMs, windowFrom, windowTo);
21111
- const selected = b.index === selectedIndex;
21112
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
21113
- type: "button",
21114
- "data-testid": "heatmap-bar",
21115
- title: t("uiLibrary.eventHeatmap.bucketTitle", "{{time}} · motion {{motion}} · object {{object}}", {
22283
+ className: "rounded bg-background/40",
22284
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BarSeries, {
22285
+ compact: true,
22286
+ title: t("uiLibrary.eventHeatmap.title", "Event density"),
22287
+ layers,
22288
+ buckets: bars,
22289
+ bucketMs,
22290
+ range: {
22291
+ from: windowFrom,
22292
+ to: windowTo
22293
+ },
22294
+ stacked: true,
22295
+ minBarFraction: MIN_BAR_FRACTION,
22296
+ selectedIndex: selectedPos === null || selectedPos < 0 ? null : selectedPos,
22297
+ onSelect: (pos) => {
22298
+ const b = nonEmpty[pos];
22299
+ if (b) onSelectBucket(b.index);
22300
+ },
22301
+ bucketTestId: "heatmap-bar",
22302
+ bucketLabel: (_bar, pos) => {
22303
+ const b = nonEmpty[pos];
22304
+ if (!b) return "";
22305
+ return t("uiLibrary.eventHeatmap.bucketTitle", "{{time}} · motion {{motion}} · object {{object}}", {
21116
22306
  time: new Date(b.startMs).toLocaleTimeString(void 0, {
21117
22307
  hour: "2-digit",
21118
22308
  minute: "2-digit"
21119
22309
  }),
21120
22310
  motion: b.counts.motion,
21121
22311
  object: b.counts.object
21122
- }),
21123
- onClick: () => onSelectBucket(b.index),
21124
- className: `absolute bottom-0 rounded-t-sm transition-opacity ${KIND_BG[b.dominant]} ${selected ? "opacity-100 ring-1 ring-amber-400" : "opacity-80 hover:opacity-100"}`,
21125
- style: {
21126
- left: `${left}%`,
21127
- width: `${Math.max(.6, bucketWidthPct)}%`,
21128
- height: `${heightPct}%`
21129
- }
21130
- }, b.index);
22312
+ });
22313
+ }
21131
22314
  })
21132
22315
  });
21133
22316
  }
@@ -21320,7 +22503,7 @@ function ClipBandBar({ day, onDayChange, view, dayBounds, onViewChange, buckets,
21320
22503
  dayBounds,
21321
22504
  onViewChange
21322
22505
  ]);
21323
- const ticks = axisTicks(view.fromMs, view.toMs, 5);
22506
+ const ticks = axisTicks$1(view.fromMs, view.toMs, 5);
21324
22507
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
21325
22508
  className: "space-y-1",
21326
22509
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TimelineControls, {
@@ -26117,7 +27300,7 @@ function RecordingTimeline({ deviceId }) {
26117
27300
  const coverageLoading = availabilityQuery.isLoading;
26118
27301
  const densityLoading = eventDensityQuery.isLoading;
26119
27302
  const drillLoading = (objectDrill.isLoading || motionDrill.isLoading || audioDrill.isLoading) && drillEnabled;
26120
- const ticks = (0, react$1.useMemo)(() => axisTicks(view.fromMs, view.toMs, 5), [view.fromMs, view.toMs]);
27303
+ const ticks = (0, react$1.useMemo)(() => axisTicks$1(view.fromMs, view.toMs, 5), [view.fromMs, view.toMs]);
26121
27304
  const displayPlayheadMs = framePush ? scrubState.scrubbing ? scrubState.scrubEpoch : scrubState.playheadEpoch : playheadMs;
26122
27305
  const showRateToggle = framePush && (recordedMode === "playing" || recordedMode === "paused");
26123
27306
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -29916,6 +31099,25 @@ function AddonGlobalSettingsForm({ trpc, addonId, nodeId, title, disabled, onAft
29916
31099
  }
29917
31100
  //#endregion
29918
31101
  //#region src/composites/accelerator-label.ts
31102
+ var CORAL_UNIT = /^(usb|pci)(?::(\d+))?$/;
31103
+ /**
31104
+ * One Coral per node keeps the plain name; a second unit, or one on PCIe, says
31105
+ * which: `edgetpu:usb` "Coral Edge TPU", `edgetpu:usb:1` "Coral Edge TPU (USB 2)",
31106
+ * `edgetpu:pci` "Coral Edge TPU (PCIe)", `edgetpu:pci:1` "Coral Edge TPU (PCIe 2)".
31107
+ */
31108
+ function coralNames(dev) {
31109
+ const match = CORAL_UNIT.exec(dev);
31110
+ if (match === null || match[1] === "usb" && match[2] === void 0) return {
31111
+ label: "Coral Edge TPU",
31112
+ short: "Coral"
31113
+ };
31114
+ const bus = match[1] === "pci" ? "PCIe" : "USB";
31115
+ const unit = match[2] === void 0 ? bus : `${bus} ${Number(match[2]) + 1}`;
31116
+ return {
31117
+ label: `Coral Edge TPU (${unit})`,
31118
+ short: `Coral ${unit}`
31119
+ };
31120
+ }
29919
31121
  function names(device) {
29920
31122
  const backend = device.backend.toLowerCase();
29921
31123
  const dev = device.device.toLowerCase();
@@ -29931,10 +31133,7 @@ function names(device) {
29931
31133
  label: "Intel iGPU",
29932
31134
  short: "iGPU"
29933
31135
  };
29934
- if (backend === "edgetpu") return {
29935
- label: "Coral Edge TPU",
29936
- short: "Coral"
29937
- };
31136
+ if (backend === "edgetpu") return coralNames(dev);
29938
31137
  if (backend === "coreml") return {
29939
31138
  label: "Apple Neural Engine",
29940
31139
  short: "ANE"
@@ -51424,7 +52623,7 @@ function AlarmSegments({ state, availableModes, busy, onDisarm, onArm }) {
51424
52623
  }, mode))]
51425
52624
  });
51426
52625
  }
51427
- function messageOf(err) {
52626
+ function messageOf$1(err) {
51428
52627
  return err instanceof Error ? err.message : String(err);
51429
52628
  }
51430
52629
  /**
@@ -51466,7 +52665,7 @@ function useAlarmControl(trpc, deviceId, optimistic) {
51466
52665
  cap.disarm({}).catch((err) => {
51467
52666
  if (latestCallRef.current !== call) return;
51468
52667
  rollback();
51469
- setError(messageOf(err));
52668
+ setError(messageOf$1(err));
51470
52669
  });
51471
52670
  };
51472
52671
  const handleArm = (mode) => {
@@ -51476,7 +52675,7 @@ function useAlarmControl(trpc, deviceId, optimistic) {
51476
52675
  cap.arm({ mode }).catch((err) => {
51477
52676
  if (latestCallRef.current !== call) return;
51478
52677
  rollback();
51479
- setError(messageOf(err));
52678
+ setError(messageOf$1(err));
51480
52679
  });
51481
52680
  };
51482
52681
  return {
@@ -64069,6 +65268,1849 @@ function RetrainReviewBadge({ kind, approximate = false, className, ...props })
64069
65268
  });
64070
65269
  }
64071
65270
  //#endregion
65271
+ //#region src/composites/charts/time-series-model.ts
65272
+ /**
65273
+ * The pure half of `TimeSeriesChart`: which series go on which value axis,
65274
+ * the aligned data uPlot draws, and what the readout says at an index.
65275
+ */
65276
+ /** The unit key a series lives on (`''` for no / unknown unit). */
65277
+ function unitKey(unit) {
65278
+ return unit ?? "";
65279
+ }
65280
+ /** Units in order of first appearance — one value axis each. */
65281
+ function unitAxes(series) {
65282
+ const out = [];
65283
+ for (const s of series) {
65284
+ const k = unitKey(s.unit);
65285
+ if (!out.includes(k)) out.push(k);
65286
+ }
65287
+ return out;
65288
+ }
65289
+ /** Align, stack (non-compare series only) and find the shared holes. */
65290
+ function buildTimeSeriesModel(series, stacked) {
65291
+ const aligned = alignSeries(series.map((s) => ({
65292
+ points: s.points,
65293
+ gapMs: s.gapMs
65294
+ })));
65295
+ let drawn = aligned.ys;
65296
+ if (stacked) {
65297
+ const primaryIdx = series.flatMap((s, i) => s.compare ? [] : [i]);
65298
+ const stackedRows = stackSeries(primaryIdx.map((i) => aligned.ys[i] ?? []));
65299
+ drawn = aligned.ys.map((row, i) => {
65300
+ const at = primaryIdx.indexOf(i);
65301
+ return at >= 0 ? stackedRows[at] ?? row : row;
65302
+ });
65303
+ }
65304
+ const empty = !aligned.ys.some((row) => row.some((v) => typeof v === "number"));
65305
+ return {
65306
+ xs: aligned.xs,
65307
+ raw: aligned.ys,
65308
+ drawn,
65309
+ mins: aligned.mins,
65310
+ maxs: aligned.maxs,
65311
+ units: unitAxes(series),
65312
+ gaps: sharedGapSpans(series.filter((s) => !s.compare).map((s) => ({
65313
+ points: s.points,
65314
+ gapMs: s.gapMs
65315
+ }))),
65316
+ empty
65317
+ };
65318
+ }
65319
+ /** The value per series a reader sees at `idx` (`null` = cursor off → the latest). */
65320
+ function readoutAt(model, idx) {
65321
+ const at = idx ?? model.xs.length - 1;
65322
+ return model.raw.map((row) => at < 0 ? null : valueAtOrBefore(model.xs, row, at));
65323
+ }
65324
+ /** The last known value of each series (for the accessible summary). */
65325
+ function latestValues(model) {
65326
+ return readoutAt(model, null);
65327
+ }
65328
+ //#endregion
65329
+ //#region src/composites/charts/TimeSeriesChart.tsx
65330
+ /**
65331
+ * TimeSeriesChart — gauges and counters over wall time (uPlot).
65332
+ *
65333
+ * Lines, an optional min/max band, stacked areas, null gaps (a hole breaks
65334
+ * the line AND is shaded), one value axis per unit (left, right, left …),
65335
+ * a synced cursor across every chart sharing `syncKey`, an optional window
65336
+ * toggle. Identity is never colour alone: one series is named by the title,
65337
+ * two or more always carry a legend with direct values.
65338
+ *
65339
+ * A device page draws ONE unit per chart; overlaying units (several axes) is
65340
+ * for the cross-device History page, where the reader chose the series.
65341
+ */
65342
+ var DEFAULT_HEIGHT = 120;
65343
+ function TimeSeriesChart({ title, series, range, height = DEFAULT_HEIGHT, stacked = false, area = false, syncKey, formatValue, axisFor, emptyMessage, pending = false, actions, footer, windowToggle, className }) {
65344
+ const t = useUiText();
65345
+ const model = (0, react$1.useMemo)(() => buildTimeSeriesModel(series, stacked), [series, stacked]);
65346
+ const [cursorIdx, setCursorIdx] = (0, react$1.useState)(null);
65347
+ const modelRef = (0, react$1.useRef)(model);
65348
+ modelRef.current = model;
65349
+ const rangeRef = (0, react$1.useRef)(range);
65350
+ rangeRef.current = range;
65351
+ const fmt = (0, react$1.useCallback)((v, unit) => (formatValue ?? formatChartValue)(v, unit), [formatValue]);
65352
+ const spanMs = () => {
65353
+ const r = rangeRef.current;
65354
+ if (r) return r.to - r.from;
65355
+ const xs = modelRef.current.xs;
65356
+ return xs.length > 1 ? (xs.at(-1) ?? 0) - (xs[0] ?? 0) : 0;
65357
+ };
65358
+ const unitOf = (s) => s.unit ?? null;
65359
+ const specFor = (0, react$1.useCallback)((unit) => (axisFor ?? defaultAxisSpec)(unit === "" ? null : unit), [axisFor]);
65360
+ const optionsKey = [
65361
+ series.map((s) => `${s.id}|${unitKey(s.unit)}|${s.colorIndex}|${s.interpolation ?? ""}|${s.band ? 1 : 0}|${s.compare ? 1 : 0}`).join(","),
65362
+ stacked ? "S" : "",
65363
+ area ? "A" : "",
65364
+ syncKey ?? "",
65365
+ range ? "R" : ""
65366
+ ].join("#");
65367
+ const data = (0, react$1.useMemo)(() => {
65368
+ const ys = [];
65369
+ series.forEach((s, i) => {
65370
+ ys.push([...model.drawn[i] ?? []]);
65371
+ if (s.band) {
65372
+ ys.push([...model.mins[i] ?? []]);
65373
+ ys.push([...model.maxs[i] ?? []]);
65374
+ }
65375
+ });
65376
+ return [[...model.xs], ...ys];
65377
+ }, [model, series]);
65378
+ const buildOptions = (0, react$1.useCallback)((_width, lib) => {
65379
+ const units = modelRef.current.units;
65380
+ const scales = { x: {
65381
+ time: true,
65382
+ ...rangeRef.current ? { range: () => [rangeRef.current?.from ?? 0, rangeRef.current?.to ?? 1] } : {}
65383
+ } };
65384
+ const axes = [timeAxis(spanMs)];
65385
+ units.forEach((u, ui) => {
65386
+ const key = `y:${u}`;
65387
+ const spec = specFor(u);
65388
+ scales[key] = { range: (self) => {
65389
+ const vals = [];
65390
+ self.series.forEach((s, si) => {
65391
+ if (si === 0 || s.scale !== key) return;
65392
+ for (const v of self.data[si] ?? []) vals.push(v);
65393
+ });
65394
+ const r = valueRange(vals, spec);
65395
+ return [r[0], r[1]];
65396
+ } };
65397
+ const unit = u === "" ? null : u;
65398
+ const ax = valueAxis(key, ui % 2 === 0 ? 3 : 1, (v) => fmt(v, unit), ui === 0);
65399
+ axes.push({
65400
+ ...ax,
65401
+ splits: (_self, _axisIdx, min, max) => valueSplits([min, max], spec)
65402
+ });
65403
+ });
65404
+ const uSeries = [{}];
65405
+ const bands = [];
65406
+ const defs = series;
65407
+ const primaryCount = defs.filter((d) => !d.compare).length;
65408
+ let seriesIdx = 1;
65409
+ let prevStackIdx = null;
65410
+ defs.forEach((d) => {
65411
+ const style = chartSeriesStyle(d.colorIndex);
65412
+ const color = () => resolveChartColor(style.token);
65413
+ const paths = d.interpolation === "step" ? lib.paths.stepped?.({ align: 1 }) : lib.paths.linear?.();
65414
+ const isStacked = stacked && !d.compare;
65415
+ const fillSingle = !stacked && area && primaryCount === 1 && !d.compare;
65416
+ const line = {
65417
+ label: d.label,
65418
+ scale: `y:${unitKey(d.unit)}`,
65419
+ stroke: d.compare ? () => withAlpha(color(), .6) : color,
65420
+ width: d.compare ? 1.25 : 2,
65421
+ dash: d.compare ? [4, 4] : style.dash.length > 0 ? [...style.dash] : void 0,
65422
+ spanGaps: false,
65423
+ points: { show: false },
65424
+ ...paths ? { paths } : {},
65425
+ ...isStacked && prevStackIdx === null ? {
65426
+ fill: () => withAlpha(color(), .85),
65427
+ fillTo: 0
65428
+ } : {},
65429
+ ...fillSingle ? { fill: () => withAlpha(color(), .15) } : {}
65430
+ };
65431
+ uSeries.push(line);
65432
+ const thisIdx = seriesIdx;
65433
+ if (isStacked && prevStackIdx !== null) bands.push({
65434
+ series: [thisIdx, prevStackIdx],
65435
+ fill: () => withAlpha(color(), .85)
65436
+ });
65437
+ if (isStacked) prevStackIdx = thisIdx;
65438
+ seriesIdx += 1;
65439
+ if (d.band) {
65440
+ const hidden = {
65441
+ scale: `y:${unitKey(d.unit)}`,
65442
+ stroke: () => "transparent",
65443
+ width: 0,
65444
+ points: { show: false },
65445
+ spanGaps: false,
65446
+ ...paths ? { paths } : {}
65447
+ };
65448
+ uSeries.push({
65449
+ ...hidden,
65450
+ label: `${d.label} min`
65451
+ }, {
65452
+ ...hidden,
65453
+ label: `${d.label} max`
65454
+ });
65455
+ bands.push({
65456
+ series: [seriesIdx + 1, seriesIdx],
65457
+ fill: () => withAlpha(color(), .15)
65458
+ });
65459
+ seriesIdx += 2;
65460
+ }
65461
+ });
65462
+ return {
65463
+ ms: 1,
65464
+ legend: { show: false },
65465
+ padding: [
65466
+ 8,
65467
+ 30,
65468
+ 0,
65469
+ 0
65470
+ ],
65471
+ scales,
65472
+ axes,
65473
+ series: uSeries,
65474
+ bands,
65475
+ cursor: chartCursor(syncKey),
65476
+ hooks: {
65477
+ drawClear: [gapShadeHook(() => modelRef.current.gaps)],
65478
+ setCursor: [(u) => setCursorIdx(u.cursor.idx ?? null)]
65479
+ }
65480
+ };
65481
+ }, [
65482
+ optionsKey,
65483
+ fmt,
65484
+ specFor
65485
+ ]);
65486
+ const shown = readoutAt(model, cursorIdx);
65487
+ const legend = series.map((s, i) => {
65488
+ const style = chartSeriesStyle(s.colorIndex);
65489
+ const v = shown[i];
65490
+ return {
65491
+ id: s.id,
65492
+ label: s.label,
65493
+ token: style.token,
65494
+ dashed: s.compare === true || style.dash.length > 0,
65495
+ muted: s.compare === true,
65496
+ value: v === null || v === void 0 ? model.empty ? void 0 : "—" : fmt(v, unitOf(s))
65497
+ };
65498
+ });
65499
+ const readoutTime = cursorIdx !== null && model.xs[cursorIdx] !== void 0 ? formatCursorTime(model.xs[cursorIdx] ?? 0, spanMs()) : null;
65500
+ const summary = (0, react$1.useMemo)(() => {
65501
+ const latest = latestValues(model);
65502
+ const from = range?.from ?? model.xs[0];
65503
+ const to = range?.to ?? model.xs.at(-1);
65504
+ const parts = series.map((s, i) => {
65505
+ const v = latest[i];
65506
+ return `${s.label} ${v === null || v === void 0 ? t("uiLibrary.charts.unknown", "unknown") : fmt(v, unitOf(s))}`;
65507
+ }).join("; ");
65508
+ if (from === void 0 || to === void 0 || !(to > from)) return t("uiLibrary.charts.seriesSummaryNoRange", "{{title}}: {{parts}}", {
65509
+ title,
65510
+ parts
65511
+ });
65512
+ return t("uiLibrary.charts.seriesSummary", "{{title}}, {{from}} to {{to}}: {{parts}}", {
65513
+ title,
65514
+ from: formatTimeLabel(from, to - from),
65515
+ to: formatTimeLabel(to, to - from),
65516
+ parts
65517
+ });
65518
+ }, [
65519
+ model,
65520
+ series,
65521
+ range,
65522
+ title,
65523
+ t,
65524
+ fmt
65525
+ ]);
65526
+ const toolbar = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [windowToggle && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChartWindowToggle, { ...windowToggle }), actions] });
65527
+ const sizeClass = height <= 64 ? "sm" : height <= 120 ? "md" : "lg";
65528
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChartFrame, {
65529
+ title,
65530
+ actions: toolbar,
65531
+ readoutTime,
65532
+ legend,
65533
+ summary,
65534
+ footer,
65535
+ placeholder: pending ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChartPlaceholder, {
65536
+ message: t("uiLibrary.charts.loading", "Loading…"),
65537
+ height: sizeClass
65538
+ }) : model.empty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChartPlaceholder, {
65539
+ message: emptyMessage ?? t("uiLibrary.charts.empty", "Nothing was recorded in this window."),
65540
+ height: sizeClass
65541
+ }) : void 0,
65542
+ className,
65543
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UPlotHost, {
65544
+ optionsKey,
65545
+ buildOptions,
65546
+ data,
65547
+ height
65548
+ })
65549
+ });
65550
+ }
65551
+ //#endregion
65552
+ //#region src/composites/charts/StateTimeline.tsx
65553
+ /**
65554
+ * StateTimeline — boolean and enum states as bands over wall time (uPlot).
65555
+ *
65556
+ * One row per state series. A state is a filled band in its categorical slot
65557
+ * (the ordinal of the value in its `states` list, so a colour follows the
65558
+ * state, never its rank); a boolean's `false` is a recessive fill, and a span
65559
+ * nothing was recorded for is left EMPTY — unknown never looks like a state.
65560
+ * The legend gives the time spent in each state over the window ("open
65561
+ * 2h 14m"), and the cursor (synced through `syncKey`) names the state under it.
65562
+ */
65563
+ /** Pixel height of one row band — matches the `h-5` label cells. */
65564
+ var ROW_PX = 20;
65565
+ var AXIS_PX = 22;
65566
+ /** The colour role of a state on a row: a categorical slot, or the recessive "off". */
65567
+ function stateStyle(row, state) {
65568
+ const base = row.colorIndex ?? 0;
65569
+ if (row.kind === "boolean") return state === row.states[1] ? { token: chartSeriesStyle(base).token } : { token: "border" };
65570
+ return { token: chartSeriesStyle(base + Math.max(0, row.states.indexOf(state))).token };
65571
+ }
65572
+ function StateTimeline({ title, rows, range, syncKey, pending = false, emptyMessage, actions, className }) {
65573
+ const t = useUiText();
65574
+ const [cursorT, setCursorT] = (0, react$1.useState)(null);
65575
+ const rowsRef = (0, react$1.useRef)(rows);
65576
+ rowsRef.current = rows;
65577
+ const rangeRef = (0, react$1.useRef)(range);
65578
+ rangeRef.current = range;
65579
+ const span = range.to - range.from;
65580
+ const empty = rows.every((r) => r.segments.length === 0);
65581
+ const labelOf = (row, s) => row.stateLabel?.(s) ?? s;
65582
+ const optionsKey = `${rows.map((r) => `${r.id}:${r.states.join("|")}`).join(",")}#${syncKey ?? ""}`;
65583
+ const data = (0, react$1.useMemo)(() => [[range.from, range.to], [null, null]], [range.from, range.to]);
65584
+ const buildOptions = (0, react$1.useCallback)(() => ({
65585
+ ms: 1,
65586
+ legend: { show: false },
65587
+ padding: [
65588
+ 0,
65589
+ 30,
65590
+ 0,
65591
+ 40
65592
+ ],
65593
+ scales: {
65594
+ x: {
65595
+ time: true,
65596
+ range: () => [rangeRef.current.from, rangeRef.current.to]
65597
+ },
65598
+ y: { range: () => [0, 1] }
65599
+ },
65600
+ axes: [timeAxis(() => rangeRef.current.to - rangeRef.current.from), {
65601
+ show: false,
65602
+ scale: "y"
65603
+ }],
65604
+ series: [{}, {
65605
+ scale: "y",
65606
+ stroke: () => "transparent",
65607
+ points: { show: false }
65608
+ }],
65609
+ cursor: {
65610
+ ...chartCursor(syncKey),
65611
+ points: { show: false }
65612
+ },
65613
+ hooks: {
65614
+ draw: [(u) => {
65615
+ const ctx = u.ctx;
65616
+ const list = rowsRef.current;
65617
+ const pr = u.bbox.height / Math.max(1, list.length);
65618
+ ctx.save();
65619
+ list.forEach((row, ri) => {
65620
+ const y = u.bbox.top + ri * pr;
65621
+ ctx.fillStyle = withAlpha(resolveChartColor("foreground-subtle"), .06);
65622
+ ctx.fillRect(u.bbox.left, y + 2, u.bbox.width, pr - 4);
65623
+ for (const seg of row.segments) {
65624
+ if (seg.state === null) continue;
65625
+ const x0 = u.valToPos(Math.max(seg.from, rangeRef.current.from), "x", true);
65626
+ const x1 = u.valToPos(Math.min(seg.to, rangeRef.current.to), "x", true);
65627
+ if (!(x1 > x0)) continue;
65628
+ ctx.fillStyle = resolveChartColor(stateStyle(row, seg.state).token);
65629
+ ctx.fillRect(x0, y + 2, Math.max(1, x1 - x0), pr - 4);
65630
+ }
65631
+ });
65632
+ ctx.restore();
65633
+ }],
65634
+ setCursor: [(u) => {
65635
+ const left = u.cursor.left ?? -1;
65636
+ setCursorT(left < 0 ? null : u.posToVal(left, "x"));
65637
+ }]
65638
+ }
65639
+ }), [optionsKey]);
65640
+ const durations = (0, react$1.useMemo)(() => rows.map((r) => r.totals ?? timeInState(r.segments)), [rows]);
65641
+ const summary = rows.map((row, i) => {
65642
+ const parts = row.states.map((s) => {
65643
+ const ms = durations[i]?.get(s) ?? 0;
65644
+ return ms > 0 ? `${labelOf(row, s)} ${formatSpan(ms)}` : null;
65645
+ }).filter((p) => p !== null).join(", ");
65646
+ return `${row.label}: ${parts.length > 0 ? parts : t("uiLibrary.charts.noState", "nothing recorded")}`;
65647
+ }).join("; ");
65648
+ const fullSummary = t("uiLibrary.charts.stateSummary", "{{title}}, {{from}} to {{to}}: {{parts}}", {
65649
+ title,
65650
+ from: formatTimeLabel(range.from, span),
65651
+ to: formatTimeLabel(range.to, span),
65652
+ parts: summary
65653
+ });
65654
+ const multi = rows.length > 1;
65655
+ const placeholder = pending ? t("uiLibrary.charts.loading", "Loading…") : empty ? emptyMessage ?? t("uiLibrary.charts.empty", "Nothing was recorded in this window.") : null;
65656
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("figure", {
65657
+ className: cn("min-w-0 m-0", className),
65658
+ "data-chart-frame": "",
65659
+ children: [
65660
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
65661
+ className: "flex items-baseline justify-between gap-2 mb-1 min-w-0",
65662
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("figcaption", {
65663
+ className: "text-[10px] uppercase tracking-wider text-foreground-subtle truncate min-w-0",
65664
+ children: title
65665
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
65666
+ className: "flex items-center gap-2 shrink-0",
65667
+ children: [cursorT !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
65668
+ className: "text-[10px] tabular-nums text-foreground-subtle",
65669
+ children: formatCursorTime(cursorT, span)
65670
+ }), actions]
65671
+ })]
65672
+ }),
65673
+ placeholder !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChartPlaceholder, {
65674
+ message: placeholder,
65675
+ height: "sm"
65676
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
65677
+ role: "img",
65678
+ "aria-label": fullSummary,
65679
+ className: "flex min-w-0",
65680
+ children: [multi && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
65681
+ className: "w-20 sm:w-28 shrink-0 pr-2",
65682
+ "aria-hidden": "true",
65683
+ children: rows.map((r) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
65684
+ className: "h-5 flex items-center text-[10px] text-foreground-muted truncate",
65685
+ children: r.label
65686
+ }, r.id))
65687
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
65688
+ className: "flex-1 min-w-0",
65689
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UPlotHost, {
65690
+ optionsKey,
65691
+ buildOptions,
65692
+ data,
65693
+ height: rows.length * ROW_PX + AXIS_PX
65694
+ })
65695
+ })]
65696
+ }),
65697
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
65698
+ className: "sr-only",
65699
+ children: fullSummary
65700
+ }),
65701
+ placeholder === null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
65702
+ className: "mt-1 space-y-0.5 list-none p-0 m-0",
65703
+ children: rows.map((row, i) => {
65704
+ const now = cursorT === null ? null : stateAt(row.segments, cursorT);
65705
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
65706
+ className: "flex flex-wrap items-center gap-x-3 gap-y-0.5 min-w-0",
65707
+ children: [multi && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
65708
+ className: "text-[10px] text-foreground-muted",
65709
+ children: row.label
65710
+ }), row.states.map((s) => {
65711
+ const ms = durations[i]?.get(s) ?? 0;
65712
+ if (ms === 0) return null;
65713
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
65714
+ className: cn("inline-flex items-center gap-1 text-[10px]", now === s ? "text-foreground" : "text-foreground-muted"),
65715
+ children: [
65716
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChartSwatch, { token: stateStyle(row, s).token }),
65717
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: labelOf(row, s) }),
65718
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
65719
+ className: "tabular-nums text-foreground",
65720
+ children: formatSpan(ms)
65721
+ })
65722
+ ]
65723
+ }, s);
65724
+ })]
65725
+ }, row.id);
65726
+ })
65727
+ })
65728
+ ]
65729
+ });
65730
+ }
65731
+ //#endregion
65732
+ //#region src/composites/charts/Sparkline.tsx
65733
+ /**
65734
+ * Sparkline — one series, no axes, no cursor: a trend beside a number in a
65735
+ * row (a training job's mAP per epoch). The x is whatever the caller orders
65736
+ * by (an epoch, a time); the value next to it names what the line is.
65737
+ */
65738
+ function Sparkline({ points, ariaLabel, token = "chart-series-a", height = 32, widthClassName = "w-40", zeroBased = true, className }) {
65739
+ const data = (0, react$1.useMemo)(() => [points.map((p) => p.x), points.map((p) => p.v)], [points]);
65740
+ const buildOptions = (0, react$1.useCallback)((_w, lib) => ({
65741
+ legend: { show: false },
65742
+ cursor: { show: false },
65743
+ padding: [
65744
+ 2,
65745
+ 1,
65746
+ 2,
65747
+ 1
65748
+ ],
65749
+ scales: {
65750
+ x: { time: false },
65751
+ y: { range: (_u, min, max) => {
65752
+ const lo = zeroBased ? Math.min(0, min) : min;
65753
+ return [lo, max > lo ? max : lo + 1];
65754
+ } }
65755
+ },
65756
+ axes: [{ show: false }, { show: false }],
65757
+ series: [{}, {
65758
+ stroke: () => resolveChartColor(token),
65759
+ width: 1.5,
65760
+ points: { show: false },
65761
+ spanGaps: false,
65762
+ ...lib.paths.linear ? { paths: lib.paths.linear() } : {}
65763
+ }]
65764
+ }), [token, zeroBased]);
65765
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
65766
+ role: "img",
65767
+ "aria-label": ariaLabel,
65768
+ className: cn("inline-block align-middle", widthClassName, className),
65769
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UPlotHost, {
65770
+ optionsKey: `${token}#${zeroBased ? "z" : ""}`,
65771
+ buildOptions,
65772
+ data,
65773
+ height,
65774
+ inline: true
65775
+ })
65776
+ });
65777
+ }
65778
+ //#endregion
65779
+ //#region src/composites/device-history/history-window.ts
65780
+ var HISTORY_WINDOW_IDS = [
65781
+ "24h",
65782
+ "7d",
65783
+ "30d",
65784
+ "1y",
65785
+ "custom"
65786
+ ];
65787
+ var H = 36e5;
65788
+ var D = 24 * H;
65789
+ var HISTORY_WINDOW_MS = {
65790
+ "24h": D,
65791
+ "7d": 7 * D,
65792
+ "30d": 30 * D,
65793
+ "1y": 365 * D
65794
+ };
65795
+ function isHistoryWindowId(v) {
65796
+ return v !== null && v !== void 0 && HISTORY_WINDOW_IDS.includes(v);
65797
+ }
65798
+ /**
65799
+ * The window a preset means at `now`. `custom` without a valid range (from
65800
+ * before to) falls back to the last 24 hours rather than an empty window.
65801
+ */
65802
+ function resolveHistoryWindow(id, now, custom) {
65803
+ if (id === "custom") {
65804
+ if (custom && custom.to > custom.from) return {
65805
+ from: custom.from,
65806
+ to: custom.to
65807
+ };
65808
+ return {
65809
+ from: now - HISTORY_WINDOW_MS["24h"],
65810
+ to: now
65811
+ };
65812
+ }
65813
+ return {
65814
+ from: now - HISTORY_WINDOW_MS[id],
65815
+ to: now
65816
+ };
65817
+ }
65818
+ /** The period of the same length just before `w`, and the shift that lays it over `w`. */
65819
+ function previousWindow(w) {
65820
+ const span = w.to - w.from;
65821
+ return {
65822
+ window: {
65823
+ from: w.from - span,
65824
+ to: w.to - span
65825
+ },
65826
+ shiftMs: span
65827
+ };
65828
+ }
65829
+ /** Encode a custom range for the URL: `<fromMs>-<toMs>`. */
65830
+ function encodeCustomWindow(w) {
65831
+ return `${String(Math.round(w.from))}-${String(Math.round(w.to))}`;
65832
+ }
65833
+ function decodeCustomWindow(raw) {
65834
+ if (!raw) return null;
65835
+ const m = /^(\d+)-(\d+)$/.exec(raw);
65836
+ if (!m) return null;
65837
+ const from = Number(m[1]);
65838
+ const to = Number(m[2]);
65839
+ return to > from ? {
65840
+ from,
65841
+ to
65842
+ } : null;
65843
+ }
65844
+ /** Milliseconds a rollup bucket of a resolution covers (`1d` is nominal across DST). */
65845
+ var RESOLUTION_BUCKET_MS = {
65846
+ "5m": 5 * 6e4,
65847
+ "1h": H,
65848
+ "1d": D
65849
+ };
65850
+ //#endregion
65851
+ //#region src/composites/device-history/history-model.ts
65852
+ /**
65853
+ * The pure half of a device's History view: which charts a device's series
65854
+ * make, in which sections, and how a `query` answer becomes chart data.
65855
+ *
65856
+ * Grouping mirrors the State panel: one section per capability in
65857
+ * `HISTORY_CATALOG` order (its fields in catalog order), then one section per
65858
+ * producer addon, sorted. Inside a section: one line chart per UNIT (never
65859
+ * two units on one axis), one state timeline per boolean or enum, the
65860
+ * derived energy as bars. A camera's occupancy is ONE chart of every
65861
+ * (zone × class) scope.
65862
+ */
65863
+ /** The producer series a camera's occupancy chart reads. */
65864
+ var OCCUPANCY_KEY = "addon:pipeline-analytics/occupancy";
65865
+ /** `cap:<capName>.<fieldPath>` → its parts; `null` for any other key. */
65866
+ function parseCapKey(key) {
65867
+ if (!key.startsWith("cap:")) return null;
65868
+ const body = key.slice(4);
65869
+ const dot = body.indexOf(".");
65870
+ if (dot <= 0) return null;
65871
+ return {
65872
+ cap: body.slice(0, dot),
65873
+ field: body.slice(dot + 1)
65874
+ };
65875
+ }
65876
+ /** `addon:<addonId>/<name>` → its parts; `null` for any other key. */
65877
+ function parseProducerKey(key) {
65878
+ if (!key.startsWith("addon:")) return null;
65879
+ const body = key.slice(6);
65880
+ const slash = body.indexOf("/");
65881
+ if (slash <= 0) return null;
65882
+ return {
65883
+ addonId: body.slice(0, slash),
65884
+ name: body.slice(slash + 1)
65885
+ };
65886
+ }
65887
+ var CATALOG_CAPS = Object.keys(_camstack_types.HISTORY_CATALOG);
65888
+ function capRank(cap) {
65889
+ const i = CATALOG_CAPS.indexOf(cap);
65890
+ return i < 0 ? CATALOG_CAPS.length : i;
65891
+ }
65892
+ function fieldRank(cap, field) {
65893
+ const fields = Object.keys(_camstack_types.HISTORY_CATALOG[cap] ?? {});
65894
+ const i = fields.indexOf(field);
65895
+ return i < 0 ? fields.length : i;
65896
+ }
65897
+ var isState = (s) => s.kind === "boolean" || s.kind === "enum";
65898
+ /** One section's charts: a line chart per unit (first-seen order), then one timeline per state. */
65899
+ function chartsOf(sectionId, title, list) {
65900
+ const out = [];
65901
+ const byUnit = /* @__PURE__ */ new Map();
65902
+ for (const s of list) {
65903
+ if (s.key === _camstack_types.DEVICE_HISTORY_ENERGY_KEY) {
65904
+ out.push({
65905
+ type: "energy",
65906
+ id: `${sectionId}:energy`,
65907
+ title: s.displayName,
65908
+ series: s
65909
+ });
65910
+ continue;
65911
+ }
65912
+ if (isState(s)) continue;
65913
+ const u = s.unit ?? "";
65914
+ byUnit.set(u, [...byUnit.get(u) ?? [], s]);
65915
+ }
65916
+ const lines = [...byUnit.entries()].map(([unit, series]) => {
65917
+ const names = [...new Set(series.map((s) => s.displayName))];
65918
+ return {
65919
+ type: "lines",
65920
+ id: `${sectionId}:unit:${unit}`,
65921
+ title: names.length === 1 ? names[0] ?? title : unit === "" ? title : `${title} (${unit})`,
65922
+ unit: unit === "" ? null : unit,
65923
+ series
65924
+ };
65925
+ });
65926
+ const states = list.filter(isState).map((s) => ({
65927
+ type: "states",
65928
+ id: `${sectionId}:state:${s.key}:${s.scope}`,
65929
+ title: s.displayName,
65930
+ series: s
65931
+ }));
65932
+ return [
65933
+ ...lines,
65934
+ ...out,
65935
+ ...states
65936
+ ];
65937
+ }
65938
+ /**
65939
+ * Group a device's series into sections. Energy (a derived key) joins the
65940
+ * `power-meter` section when the device has one, else gets its own.
65941
+ */
65942
+ function groupDeviceSeries(series, capTitle) {
65943
+ const caps = /* @__PURE__ */ new Map();
65944
+ const producers = /* @__PURE__ */ new Map();
65945
+ const occupancy = [];
65946
+ let energy = null;
65947
+ for (const s of series) {
65948
+ if (s.key === _camstack_types.DEVICE_HISTORY_ENERGY_KEY) {
65949
+ energy = s;
65950
+ continue;
65951
+ }
65952
+ if (s.key === "addon:pipeline-analytics/occupancy") {
65953
+ occupancy.push(s);
65954
+ continue;
65955
+ }
65956
+ const cap = parseCapKey(s.key);
65957
+ if (cap) {
65958
+ caps.set(cap.cap, [...caps.get(cap.cap) ?? [], s]);
65959
+ continue;
65960
+ }
65961
+ const addon = parseProducerKey(s.key)?.addonId ?? "other";
65962
+ producers.set(addon, [...producers.get(addon) ?? [], s]);
65963
+ }
65964
+ if (energy !== null) if (caps.has("power-meter")) caps.set("power-meter", [...caps.get("power-meter") ?? [], energy]);
65965
+ else caps.set("energy", [energy]);
65966
+ const capSections = [...caps.entries()].sort(([a], [b]) => capRank(a) - capRank(b) || a.localeCompare(b)).map(([cap, list]) => {
65967
+ const sorted = [...list].sort((x, y) => {
65968
+ const fx = parseCapKey(x.key)?.field ?? "";
65969
+ const fy = parseCapKey(y.key)?.field ?? "";
65970
+ return fieldRank(cap, fx) - fieldRank(cap, fy) || x.scope.localeCompare(y.scope);
65971
+ });
65972
+ const id = `cap:${cap}`;
65973
+ return {
65974
+ id,
65975
+ kind: "cap",
65976
+ name: cap,
65977
+ charts: chartsOf(id, capTitle(cap), sorted)
65978
+ };
65979
+ });
65980
+ if (occupancy.length > 0) {
65981
+ const list = producers.get("pipeline-analytics") ?? [];
65982
+ producers.set("pipeline-analytics", list);
65983
+ }
65984
+ const producerSections = [...producers.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([addon, list]) => {
65985
+ const id = `addon:${addon}`;
65986
+ const charts = chartsOf(id, capTitle(addon), list);
65987
+ return {
65988
+ id,
65989
+ kind: "producer",
65990
+ name: addon,
65991
+ charts: [...addon === "pipeline-analytics" && occupancy.length > 0 ? [{
65992
+ type: "occupancy",
65993
+ id: `${id}:occupancy`,
65994
+ title: occupancy[0]?.displayName ?? "Occupancy",
65995
+ series: [...occupancy].sort((a, b) => a.scope.localeCompare(b.scope))
65996
+ }] : [], ...charts]
65997
+ };
65998
+ });
65999
+ return [...capSections, ...producerSections].filter((s) => s.charts.length > 0);
66000
+ }
66001
+ /** The unique keys a set of charts reads (one `queryMany` item each: every scope comes back). */
66002
+ function chartKeys(charts) {
66003
+ const keys = /* @__PURE__ */ new Set();
66004
+ for (const c of charts) if (c.type === "lines" || c.type === "occupancy") for (const s of c.series) keys.add(s.key);
66005
+ else keys.add(c.series.key);
66006
+ return [...keys];
66007
+ }
66008
+ /** One series of a `query` answer, found by scope. */
66009
+ function seriesOf(result, scope) {
66010
+ return result?.series.find((s) => s.scope === scope);
66011
+ }
66012
+ /** The number a line shows for a point: time-weighted mean, else mean, else the last value. */
66013
+ function levelOf(p, kind) {
66014
+ if (kind === "counter") return p.last;
66015
+ return p.twMean ?? p.mean ?? p.last;
66016
+ }
66017
+ /**
66018
+ * Line points for a gauge or counter. A rolled-up answer carries its
66019
+ * min/max (a band). A CAP series is change-only and held, so the window
66020
+ * opens on the value in force (`before`); a producer's buckets are not held
66021
+ * and an absent bucket is a hole.
66022
+ */
66023
+ function linePoints(info, result, from) {
66024
+ const s = seriesOf(result, info.scope);
66025
+ const rollup = result !== void 0 && result.resolution !== "raw";
66026
+ const held = info.source === "cap";
66027
+ const points = (s?.points ?? []).map((p) => ({
66028
+ t: p.t,
66029
+ v: levelOf(p, info.kind),
66030
+ ...rollup && info.kind === "gauge" ? {
66031
+ min: p.min,
66032
+ max: p.max
66033
+ } : {}
66034
+ }));
66035
+ if (held && s?.before && s.before.value !== null && (points[0]?.t ?? Infinity) > from) points.unshift({
66036
+ t: from,
66037
+ v: s.before.value
66038
+ });
66039
+ return {
66040
+ points,
66041
+ gapMs: held || result === void 0 ? void 0 : gapThresholdMs(result.resolution === "raw" ? 1e4 : RESOLUTION_BUCKET_MS[result.resolution]),
66042
+ held
66043
+ };
66044
+ }
66045
+ /** Shift every point by `shiftMs` (a previous period laid over the current one). */
66046
+ function shiftPoints(points, shiftMs) {
66047
+ return points.map((p) => ({
66048
+ ...p,
66049
+ t: p.t + shiftMs
66050
+ }));
66051
+ }
66052
+ /** The state names a boolean series takes, `false` first. */
66053
+ var BOOLEAN_STATES = ["false", "true"];
66054
+ /**
66055
+ * A state series as segments plus the exact time spent in each state.
66056
+ *
66057
+ * Raw points are changes. A rollup bucket is drawn in its DOMINANT state
66058
+ * (the one held longest in it), while the totals add the bucket's real
66059
+ * per-state milliseconds — so "open 2h 14m" stays exact at any zoom.
66060
+ */
66061
+ function stateRow(info, result, from, to) {
66062
+ const states = info.kind === "boolean" ? BOOLEAN_STATES : info.values ?? [];
66063
+ const s = seriesOf(result, info.scope);
66064
+ const nameOf = (p) => info.kind === "boolean" ? p.last === null ? null : p.last >= .5 ? "true" : "false" : p.state;
66065
+ if (result === void 0 || s === void 0) return {
66066
+ segments: [],
66067
+ totals: /* @__PURE__ */ new Map(),
66068
+ states
66069
+ };
66070
+ const beforeState = s.before === null ? null : info.kind === "boolean" ? s.before.value === null ? null : s.before.value >= .5 ? "true" : "false" : s.before.state;
66071
+ if (result.resolution === "raw") {
66072
+ const segments = segmentsFromChanges(s.points.map((p) => ({
66073
+ t: p.t,
66074
+ state: nameOf(p)
66075
+ })), from, to, beforeState);
66076
+ const totals = /* @__PURE__ */ new Map();
66077
+ for (const seg of segments) if (seg.state !== null) totals.set(seg.state, (totals.get(seg.state) ?? 0) + (seg.to - seg.from));
66078
+ return {
66079
+ segments,
66080
+ totals,
66081
+ states
66082
+ };
66083
+ }
66084
+ const bucketMs = RESOLUTION_BUCKET_MS[result.resolution];
66085
+ const totals = /* @__PURE__ */ new Map();
66086
+ const segments = [];
66087
+ for (const p of s.points) {
66088
+ const end = Math.min(to, p.t + bucketMs);
66089
+ const start = Math.max(from, p.t);
66090
+ if (!(end > start)) continue;
66091
+ const perState = bucketStates(p, info, bucketMs);
66092
+ let dominant = null;
66093
+ let best = -1;
66094
+ for (const [name, ms] of perState) {
66095
+ totals.set(name, (totals.get(name) ?? 0) + ms * ((end - start) / bucketMs));
66096
+ if (ms > best) {
66097
+ best = ms;
66098
+ dominant = name;
66099
+ }
66100
+ }
66101
+ const prev = segments.at(-1);
66102
+ if (prev && prev.state === dominant && prev.to === start) segments[segments.length - 1] = {
66103
+ ...prev,
66104
+ to: end
66105
+ };
66106
+ else segments.push({
66107
+ from: start,
66108
+ to: end,
66109
+ state: dominant
66110
+ });
66111
+ }
66112
+ return {
66113
+ segments: segments.filter((x) => x.state !== null),
66114
+ totals,
66115
+ states
66116
+ };
66117
+ }
66118
+ function bucketStates(p, info, bucketMs) {
66119
+ if (info.kind === "boolean") {
66120
+ const frac = p.twMean ?? p.mean;
66121
+ if (frac === null) return p.last === null ? /* @__PURE__ */ new Map() : new Map([[p.last >= .5 ? "true" : "false", bucketMs]]);
66122
+ return new Map([["true", frac * bucketMs], ["false", (1 - frac) * bucketMs]]);
66123
+ }
66124
+ if (p.states) return new Map(Object.entries(p.states));
66125
+ return p.state === null ? /* @__PURE__ */ new Map() : new Map([[p.state, bucketMs]]);
66126
+ }
66127
+ /** Energy bars: one bucket per point, the kWh used in it (`delta`). */
66128
+ function energyBuckets(result) {
66129
+ const s = result?.series[0];
66130
+ const bucketMs = result === void 0 || result.resolution === "raw" ? RESOLUTION_BUCKET_MS["1h"] : RESOLUTION_BUCKET_MS[result.resolution];
66131
+ return {
66132
+ buckets: (s?.points ?? []).map((p) => ({
66133
+ t: p.t,
66134
+ values: [p.delta]
66135
+ })),
66136
+ bucketMs
66137
+ };
66138
+ }
66139
+ function occupancyLines(series) {
66140
+ return series.map((s) => {
66141
+ const parsed = (0, _camstack_types.parseOccupancyScope)(s.scope);
66142
+ return {
66143
+ scope: s.scope,
66144
+ zoneId: parsed.zoneId,
66145
+ className: parsed.className
66146
+ };
66147
+ });
66148
+ }
66149
+ /** The filter values an occupancy chart offers: zones and classes that exist. */
66150
+ function occupancyFacets(lines) {
66151
+ return {
66152
+ zones: [...new Set(lines.map((l) => l.zoneId))].sort(),
66153
+ classes: [...new Set(lines.flatMap((l) => l.className === void 0 ? [] : [l.className]))].sort()
66154
+ };
66155
+ }
66156
+ /**
66157
+ * The lines a filter keeps. With no class picked the per-zone AGGREGATE lines
66158
+ * show (summing classes again would double count); a class shows that class
66159
+ * in every (or the picked) zone.
66160
+ */
66161
+ function filterOccupancyLines(lines, filter) {
66162
+ return lines.filter((l) => (filter.zone === null || l.zoneId === filter.zone) && (filter.className === null ? l.className === void 0 : l.className === filter.className));
66163
+ }
66164
+ //#endregion
66165
+ //#region src/composites/device-history/HistoryChart.tsx
66166
+ /**
66167
+ * One chart of a device's History view, from its spec and the reads.
66168
+ * Lines → `TimeSeriesChart`, states → `StateTimeline`, energy → `BarSeries`
66169
+ * with the window total, occupancy → one `TimeSeriesChart` of every
66170
+ * (zone × class) scope with zone and class filters.
66171
+ *
66172
+ * A read that failed says so in place of the chart; a read that answered no
66173
+ * point is the empty state — never a flat zero.
66174
+ */
66175
+ var ALL = "__all__";
66176
+ /** Operator words for a boolean field's two states. */
66177
+ function booleanLabels(key, t) {
66178
+ const field = key.split(".").at(-1) ?? "";
66179
+ if (/open/i.test(field)) return {
66180
+ on: t("uiLibrary.history.state.open", "open"),
66181
+ off: t("uiLibrary.history.state.closed", "closed")
66182
+ };
66183
+ if (/online|connected/i.test(field)) return {
66184
+ on: t("uiLibrary.history.state.online", "online"),
66185
+ off: t("uiLibrary.history.state.offline", "offline")
66186
+ };
66187
+ if (/detected|tampered/i.test(field)) return {
66188
+ on: t("uiLibrary.history.state.detected", "detected"),
66189
+ off: t("uiLibrary.history.state.clear", "clear")
66190
+ };
66191
+ return {
66192
+ on: t("uiLibrary.history.state.on", "on"),
66193
+ off: t("uiLibrary.history.state.off", "off")
66194
+ };
66195
+ }
66196
+ function failure(entry) {
66197
+ return entry !== void 0 && !entry.ok ? entry.error : null;
66198
+ }
66199
+ function HistoryChart({ spec, entryFor, reads, window, compare, syncKey, actions }) {
66200
+ const t = useUiText();
66201
+ const pending = reads === null;
66202
+ if (spec.type === "states") {
66203
+ const entry = entryFor(spec.series.key, false);
66204
+ const err = failure(entry);
66205
+ if (err) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReadFailed, {
66206
+ title: spec.title,
66207
+ error: err,
66208
+ actions
66209
+ });
66210
+ const result = entry?.ok ? entry.result : void 0;
66211
+ const row = stateRow(spec.series, result, window.from, window.to);
66212
+ const labels = booleanLabels(spec.series.key, t);
66213
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StateTimeline, {
66214
+ title: spec.title,
66215
+ range: window,
66216
+ syncKey,
66217
+ pending,
66218
+ actions,
66219
+ rows: [{
66220
+ id: spec.id,
66221
+ label: spec.title,
66222
+ kind: spec.series.kind === "boolean" ? "boolean" : "enum",
66223
+ states: row.states,
66224
+ segments: row.segments,
66225
+ totals: row.totals,
66226
+ stateLabel: spec.series.kind === "boolean" ? (s) => s === "true" ? labels.on : labels.off : void 0
66227
+ }]
66228
+ });
66229
+ }
66230
+ if (spec.type === "energy") {
66231
+ const entry = entryFor(spec.series.key, false);
66232
+ const err = failure(entry);
66233
+ if (err) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReadFailed, {
66234
+ title: spec.title,
66235
+ error: err,
66236
+ actions
66237
+ });
66238
+ const { buckets, bucketMs } = energyBuckets(entry?.ok ? entry.result : void 0);
66239
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BarSeries, {
66240
+ title: spec.title,
66241
+ layers: [{
66242
+ id: "kwh",
66243
+ label: spec.title,
66244
+ colorIndex: 4
66245
+ }],
66246
+ buckets,
66247
+ bucketMs,
66248
+ range: window,
66249
+ unit: spec.series.unit ?? "kWh",
66250
+ showTotal: true,
66251
+ syncKey,
66252
+ pending,
66253
+ actions
66254
+ });
66255
+ }
66256
+ if (spec.type === "occupancy") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OccupancyChart, {
66257
+ spec,
66258
+ entryFor,
66259
+ reads,
66260
+ window,
66261
+ compare,
66262
+ syncKey,
66263
+ actions
66264
+ });
66265
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LinesChart, {
66266
+ title: spec.title,
66267
+ series: spec.series,
66268
+ entryFor,
66269
+ reads,
66270
+ window,
66271
+ compare,
66272
+ syncKey,
66273
+ actions,
66274
+ labelOf: (s) => spec.series.length > 1 || s.scope !== "" ? seriesLabel(s) : spec.title
66275
+ });
66276
+ }
66277
+ function seriesLabel(s) {
66278
+ return s.scope === "" ? s.displayName : `${s.displayName} · ${s.scope}`;
66279
+ }
66280
+ function LinesChart({ title, series, entryFor, reads, window, compare, syncKey, actions, labelOf, toolbar, colorOf }) {
66281
+ const t = useUiText();
66282
+ const errors = [...new Set(series.map((s) => failure(entryFor(s.key, false))).filter((e) => e !== null))];
66283
+ const defs = (0, react$1.useMemo)(() => {
66284
+ const out = [];
66285
+ series.forEach((s, i) => {
66286
+ const cur = entryFor(s.key, false);
66287
+ const result = cur?.ok ? cur.result : void 0;
66288
+ const shaped = linePoints(s, result, window.from);
66289
+ const color = colorOf?.(s, i) ?? i;
66290
+ const interpolation = shaped.held && result?.resolution === "raw" ? "step" : "linear";
66291
+ out.push({
66292
+ id: `${s.key}|${s.scope}`,
66293
+ label: labelOf(s),
66294
+ unit: s.unit,
66295
+ colorIndex: color,
66296
+ points: shaped.points,
66297
+ gapMs: shaped.gapMs,
66298
+ interpolation,
66299
+ band: series.length === 1 && result !== void 0 && result.resolution !== "raw"
66300
+ });
66301
+ if (compare && reads?.previous) {
66302
+ const prev = entryFor(s.key, true);
66303
+ const prevResult = prev?.ok ? prev.result : void 0;
66304
+ if (prevResult) {
66305
+ const prevShaped = linePoints(s, prevResult, window.from - reads.shiftMs);
66306
+ out.push({
66307
+ id: `${s.key}|${s.scope}|previous`,
66308
+ label: t("uiLibrary.history.previousOf", "{{label}} (previous period)", { label: labelOf(s) }),
66309
+ unit: s.unit,
66310
+ colorIndex: color,
66311
+ points: shiftPoints(prevShaped.points, reads.shiftMs),
66312
+ gapMs: prevShaped.gapMs,
66313
+ interpolation,
66314
+ compare: true
66315
+ });
66316
+ }
66317
+ }
66318
+ });
66319
+ return out;
66320
+ }, [
66321
+ series,
66322
+ entryFor,
66323
+ window.from,
66324
+ compare,
66325
+ reads,
66326
+ labelOf,
66327
+ colorOf,
66328
+ t
66329
+ ]);
66330
+ if (errors.length > 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReadFailed, {
66331
+ title,
66332
+ error: errors.join("; "),
66333
+ actions
66334
+ });
66335
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TimeSeriesChart, {
66336
+ title,
66337
+ series: defs,
66338
+ range: window,
66339
+ syncKey,
66340
+ pending: reads === null,
66341
+ actions: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [toolbar, actions] })
66342
+ });
66343
+ }
66344
+ function OccupancyChart({ spec, entryFor, reads, window, compare, syncKey, actions }) {
66345
+ const t = useUiText();
66346
+ const [zone, setZone] = (0, react$1.useState)(ALL);
66347
+ const [cls, setCls] = (0, react$1.useState)(ALL);
66348
+ const lines = (0, react$1.useMemo)(() => occupancyLines(spec.series), [spec.series]);
66349
+ const facets = (0, react$1.useMemo)(() => occupancyFacets(lines), [lines]);
66350
+ const kept = new Set(filterOccupancyLines(lines, {
66351
+ zone: zone === ALL ? null : zone,
66352
+ className: cls === ALL ? null : cls
66353
+ }).map((l) => l.scope));
66354
+ const shown = spec.series.filter((s) => kept.has(s.scope));
66355
+ const zoneName = (z) => z === "" ? t("uiLibrary.history.wholeFrame", "Whole frame") : z;
66356
+ const labelOf = (s) => {
66357
+ const l = lines.find((x) => x.scope === s.scope);
66358
+ if (!l) return s.scope;
66359
+ return l.className === void 0 ? zoneName(l.zoneId) : `${zoneName(l.zoneId)} · ${l.className}`;
66360
+ };
66361
+ const colorOf = (s) => {
66362
+ const own = lines.find((l) => l.scope === s.scope);
66363
+ const peers = lines.filter((l) => l.className === own?.className);
66364
+ return Math.max(0, peers.findIndex((l) => l.scope === s.scope));
66365
+ };
66366
+ const toolbar = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [facets.zones.length > 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Select, {
66367
+ "aria-label": t("uiLibrary.history.filterZone", "Zone"),
66368
+ value: zone,
66369
+ onChange: (e) => setZone(e.target.value),
66370
+ className: "h-6 text-[10px] py-0",
66371
+ options: [{
66372
+ value: ALL,
66373
+ label: t("uiLibrary.history.allZones", "All zones")
66374
+ }, ...facets.zones.map((z) => ({
66375
+ value: z,
66376
+ label: zoneName(z)
66377
+ }))]
66378
+ }), facets.classes.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Select, {
66379
+ "aria-label": t("uiLibrary.history.filterClass", "Class"),
66380
+ value: cls,
66381
+ onChange: (e) => setCls(e.target.value),
66382
+ className: "h-6 text-[10px] py-0",
66383
+ options: [{
66384
+ value: ALL,
66385
+ label: t("uiLibrary.history.allClasses", "All classes")
66386
+ }, ...facets.classes.map((c) => ({
66387
+ value: c,
66388
+ label: c
66389
+ }))]
66390
+ })] });
66391
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LinesChart, {
66392
+ title: spec.title,
66393
+ series: shown,
66394
+ entryFor,
66395
+ reads,
66396
+ window,
66397
+ compare,
66398
+ syncKey,
66399
+ actions,
66400
+ toolbar,
66401
+ labelOf,
66402
+ colorOf
66403
+ });
66404
+ }
66405
+ function ReadFailed({ title, error, actions }) {
66406
+ const t = useUiText();
66407
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("figure", {
66408
+ className: "min-w-0 m-0",
66409
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
66410
+ className: "flex items-baseline justify-between gap-2 mb-1",
66411
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("figcaption", {
66412
+ className: "text-[10px] uppercase tracking-wider text-foreground-subtle truncate",
66413
+ children: title
66414
+ }), actions]
66415
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChartPlaceholder, {
66416
+ height: "md",
66417
+ message: t("uiLibrary.history.readFailed", "This history could not be read: {{error}}", { error })
66418
+ })]
66419
+ });
66420
+ }
66421
+ //#endregion
66422
+ //#region src/composites/device-history/history-selection.ts
66423
+ /**
66424
+ * A history SELECTION — which series a chart overlays — and its two
66425
+ * spellings: the History page URL (`?s=` repeated) and the account
66426
+ * preference row of saved panels (`admin.history-panels`, D894).
66427
+ *
66428
+ * A series is (subject, key, scope). The subject is a device (by numeric id)
66429
+ * or a node (by id); the scope `''` is the series' aggregate.
66430
+ */
66431
+ var HistorySubjectSchema = zod.z.discriminatedUnion("kind", [zod.z.object({
66432
+ kind: zod.z.literal("device"),
66433
+ deviceId: zod.z.number().int()
66434
+ }), zod.z.object({
66435
+ kind: zod.z.literal("node"),
66436
+ nodeId: zod.z.string().min(1)
66437
+ })]);
66438
+ var HistorySeriesRefSchema = zod.z.object({
66439
+ subject: HistorySubjectSchema,
66440
+ key: zod.z.string().min(1),
66441
+ scope: zod.z.string()
66442
+ });
66443
+ /** The most series one overlay may carry — one `queryMany` batch. */
66444
+ var HISTORY_SELECTION_MAX = 24;
66445
+ /** Stable identity of a series ref (React key, dedup, colour slot). */
66446
+ function seriesRefId(ref) {
66447
+ return `${ref.subject.kind === "device" ? `d:${String(ref.subject.deviceId)}` : `n:${ref.subject.nodeId}`}|${ref.key}|${ref.scope}`;
66448
+ }
66449
+ /** One `?s=` value: `d,<deviceId>,<key>,<scope>` or `n,<nodeId>,<key>,<scope>`, each part URI-encoded. */
66450
+ function encodeSeriesRef(ref) {
66451
+ return [
66452
+ ...ref.subject.kind === "device" ? ["d", String(ref.subject.deviceId)] : ["n", ref.subject.nodeId],
66453
+ ref.key,
66454
+ ref.scope
66455
+ ].map((p) => encodeURIComponent(p)).join(",");
66456
+ }
66457
+ function decodeSeriesRef(raw) {
66458
+ const parts = raw.split(",");
66459
+ if (parts.length !== 4) return null;
66460
+ let decoded;
66461
+ try {
66462
+ decoded = parts.map((p) => decodeURIComponent(p));
66463
+ } catch {
66464
+ return null;
66465
+ }
66466
+ const [kind, id, key, scope] = decoded;
66467
+ if (key === void 0 || key === "" || scope === void 0 || id === void 0) return null;
66468
+ if (kind === "d") {
66469
+ const deviceId = Number(id);
66470
+ if (!Number.isInteger(deviceId)) return null;
66471
+ return {
66472
+ subject: {
66473
+ kind: "device",
66474
+ deviceId
66475
+ },
66476
+ key,
66477
+ scope
66478
+ };
66479
+ }
66480
+ if (kind === "n" && id !== "") return {
66481
+ subject: {
66482
+ kind: "node",
66483
+ nodeId: id
66484
+ },
66485
+ key,
66486
+ scope
66487
+ };
66488
+ return null;
66489
+ }
66490
+ /** Unique refs, in order, capped at {@link HISTORY_SELECTION_MAX}. */
66491
+ function normalizeSelection(refs) {
66492
+ const seen = /* @__PURE__ */ new Set();
66493
+ const out = [];
66494
+ for (const r of refs) {
66495
+ const id = seriesRefId(r);
66496
+ if (seen.has(id)) continue;
66497
+ seen.add(id);
66498
+ out.push(r);
66499
+ if (out.length >= 24) break;
66500
+ }
66501
+ return out;
66502
+ }
66503
+ /** The History page path showing `refs` over `window`. */
66504
+ function historyPageHref(refs, window) {
66505
+ const params = new URLSearchParams();
66506
+ for (const r of normalizeSelection(refs)) params.append("s", encodeSeriesRef(r));
66507
+ if (window !== void 0 && window !== "custom") params.set("window", window);
66508
+ return `${HISTORY_PAGE_PATH}?${params.toString()}`;
66509
+ }
66510
+ /** The cross-device History page's route. */
66511
+ var HISTORY_PAGE_PATH = "/system/history";
66512
+ /** The account preference namespace that holds saved panels and hidden charts. */
66513
+ var HISTORY_PANELS_NAMESPACE = "admin.history-panels";
66514
+ var WindowIdSchema = zod.z.enum(HISTORY_WINDOW_IDS);
66515
+ var HistoryPanelSchema = zod.z.object({
66516
+ id: zod.z.string().min(1),
66517
+ name: zod.z.string().min(1).max(80),
66518
+ window: WindowIdSchema,
66519
+ selection: zod.z.array(HistorySeriesRefSchema).max(24)
66520
+ });
66521
+ var HistoryPanelsValueSchema = zod.z.object({
66522
+ version: zod.z.literal(1),
66523
+ panels: zod.z.array(HistoryPanelSchema).max(50),
66524
+ hidden: zod.z.record(zod.z.string(), zod.z.array(zod.z.string()).max(200))
66525
+ });
66526
+ var EMPTY_HISTORY_PANELS = {
66527
+ version: 1,
66528
+ panels: [],
66529
+ hidden: {}
66530
+ };
66531
+ /**
66532
+ * Read a stored row's value. A row this client cannot read (another
66533
+ * version, a hand edit) is answered as empty rather than throwing — and the
66534
+ * caller must not write over it blindly: `parsed: false` says so.
66535
+ */
66536
+ function readHistoryPanels(value) {
66537
+ if (value === void 0 || value === null) return {
66538
+ value: EMPTY_HISTORY_PANELS,
66539
+ parsed: true
66540
+ };
66541
+ const r = HistoryPanelsValueSchema.safeParse(value);
66542
+ return r.success ? {
66543
+ value: r.data,
66544
+ parsed: true
66545
+ } : {
66546
+ value: EMPTY_HISTORY_PANELS,
66547
+ parsed: false
66548
+ };
66549
+ }
66550
+ /** A copy of `value` with chart `chartId` hidden (or shown again) on device `deviceId`. */
66551
+ function withHiddenChart(value, deviceId, chartId, hidden) {
66552
+ const key = String(deviceId);
66553
+ const current = value.hidden[key] ?? [];
66554
+ const next = hidden ? current.includes(chartId) ? current : [...current, chartId] : current.filter((c) => c !== chartId);
66555
+ const rest = Object.fromEntries(Object.entries(value.hidden).filter(([k]) => k !== key));
66556
+ return {
66557
+ ...value,
66558
+ hidden: next.length > 0 ? {
66559
+ ...rest,
66560
+ [key]: next
66561
+ } : rest
66562
+ };
66563
+ }
66564
+ /** A copy of `value` with `panel` saved (replacing one with the same id). */
66565
+ function withPanel(value, panel) {
66566
+ const others = value.panels.filter((p) => p.id !== panel.id);
66567
+ return {
66568
+ ...value,
66569
+ panels: [...others, panel]
66570
+ };
66571
+ }
66572
+ function withoutPanel(value, panelId) {
66573
+ return {
66574
+ ...value,
66575
+ panels: value.panels.filter((p) => p.id !== panelId)
66576
+ };
66577
+ }
66578
+ //#endregion
66579
+ //#region src/composites/device-history/HistoryWindowBar.tsx
66580
+ /** `datetime-local` value for an instant, in the browser's zone. */
66581
+ function toLocalInput(ms) {
66582
+ const d = new Date(ms);
66583
+ const pad = (n) => String(n).padStart(2, "0");
66584
+ return `${String(d.getFullYear())}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
66585
+ }
66586
+ function fromLocalInput(v) {
66587
+ const ms = new Date(v).getTime();
66588
+ return Number.isFinite(ms) ? ms : null;
66589
+ }
66590
+ function HistoryWindowBar({ value, onChange, custom, onCustomChange, compare, onCompareChange }) {
66591
+ const t = useUiText();
66592
+ const now = Date.now();
66593
+ const range = custom ?? {
66594
+ from: now - 864e5,
66595
+ to: now
66596
+ };
66597
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
66598
+ className: "flex flex-wrap items-center gap-x-3 gap-y-2 min-w-0",
66599
+ children: [
66600
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChartWindowToggle, {
66601
+ ariaLabel: t("uiLibrary.history.window", "Time window"),
66602
+ value,
66603
+ onChange,
66604
+ options: [
66605
+ {
66606
+ id: "24h",
66607
+ label: t("uiLibrary.history.window24h", "24 h")
66608
+ },
66609
+ {
66610
+ id: "7d",
66611
+ label: t("uiLibrary.history.window7d", "7 d")
66612
+ },
66613
+ {
66614
+ id: "30d",
66615
+ label: t("uiLibrary.history.window30d", "30 d")
66616
+ },
66617
+ {
66618
+ id: "1y",
66619
+ label: t("uiLibrary.history.window1y", "1 y")
66620
+ },
66621
+ {
66622
+ id: "custom",
66623
+ label: t("uiLibrary.history.windowCustom", "Custom")
66624
+ }
66625
+ ]
66626
+ }),
66627
+ value === "custom" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
66628
+ className: "flex flex-wrap items-center gap-2",
66629
+ children: [
66630
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
66631
+ type: "datetime-local",
66632
+ "aria-label": t("uiLibrary.history.from", "From"),
66633
+ className: "h-7 w-auto text-xs",
66634
+ value: toLocalInput(range.from),
66635
+ onChange: (e) => {
66636
+ const from = fromLocalInput(e.target.value);
66637
+ if (from !== null && from < range.to) onCustomChange({
66638
+ from,
66639
+ to: range.to
66640
+ });
66641
+ }
66642
+ }),
66643
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
66644
+ className: "text-[10px] text-foreground-subtle",
66645
+ children: "–"
66646
+ }),
66647
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
66648
+ type: "datetime-local",
66649
+ "aria-label": t("uiLibrary.history.to", "To"),
66650
+ className: "h-7 w-auto text-xs",
66651
+ value: toLocalInput(range.to),
66652
+ onChange: (e) => {
66653
+ const to = fromLocalInput(e.target.value);
66654
+ if (to !== null && to > range.from) onCustomChange({
66655
+ from: range.from,
66656
+ to
66657
+ });
66658
+ }
66659
+ })
66660
+ ]
66661
+ }),
66662
+ compare !== void 0 && onCompareChange && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
66663
+ className: "inline-flex items-center gap-1.5 text-[11px] text-foreground-muted cursor-pointer select-none",
66664
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Checkbox, {
66665
+ checked: compare,
66666
+ onChange: (e) => onCompareChange(e.target.checked)
66667
+ }), t("uiLibrary.history.compare", "Compare with previous period")]
66668
+ })
66669
+ ]
66670
+ });
66671
+ }
66672
+ //#endregion
66673
+ //#region src/composites/device-history/history-reads.ts
66674
+ /**
66675
+ * Batched history reads. `queryMany` answers up to 24 reads over one window
66676
+ * in one call — but it is admin-only (its subjects are nested where the
66677
+ * device scope gate cannot see them). A principal it refuses reads each
66678
+ * DEVICE series through `query` instead; a node series has no other door and
66679
+ * is reported as refused.
66680
+ *
66681
+ * Every item gets an entry, in order: an answer, or the error that item met.
66682
+ * A failed item never hides the others, and is never an empty series.
66683
+ */
66684
+ var DEFAULT_MAX_POINTS = 800;
66685
+ /** Split into batches of at most `size`. */
66686
+ function chunk(items, size) {
66687
+ const out = [];
66688
+ for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
66689
+ return out;
66690
+ }
66691
+ /** Did the hub refuse this principal (rather than fail)? */
66692
+ function isRefusal(err) {
66693
+ if (typeof err !== "object" || err === null) return false;
66694
+ const data = "data" in err ? err.data : void 0;
66695
+ const code = typeof data === "object" && data !== null && "code" in data ? data.code : void 0;
66696
+ return code === "FORBIDDEN" || code === "UNAUTHORIZED";
66697
+ }
66698
+ function messageOf(err) {
66699
+ return err instanceof Error ? err.message : String(err);
66700
+ }
66701
+ /** Read every item over one window; one entry per item, in order. */
66702
+ async function readHistory(reader, items, window) {
66703
+ const maxPoints = window.maxPoints ?? DEFAULT_MAX_POINTS;
66704
+ const out = [];
66705
+ for (const batch of chunk(items, _camstack_types.DEVICE_HISTORY_QUERY_MANY_MAX_ITEMS)) try {
66706
+ const r = await reader.queryMany({
66707
+ items: [...batch],
66708
+ from: window.from,
66709
+ to: window.to,
66710
+ resolution: "auto",
66711
+ maxPoints
66712
+ });
66713
+ batch.forEach((_item, i) => {
66714
+ out.push(r.results[i] ?? {
66715
+ ok: false,
66716
+ error: "no answer for this item"
66717
+ });
66718
+ });
66719
+ } catch (err) {
66720
+ if (!isRefusal(err)) {
66721
+ const error = messageOf(err);
66722
+ for (const _item of batch) out.push({
66723
+ ok: false,
66724
+ error
66725
+ });
66726
+ continue;
66727
+ }
66728
+ out.push(...await readOneByOne(reader, batch, window.from, window.to, maxPoints, messageOf(err)));
66729
+ }
66730
+ return out;
66731
+ }
66732
+ async function readOneByOne(reader, batch, from, to, maxPoints, refusal) {
66733
+ return Promise.all(batch.map(async (item) => {
66734
+ if (!("deviceId" in item)) return {
66735
+ ok: false,
66736
+ error: refusal
66737
+ };
66738
+ try {
66739
+ return {
66740
+ ok: true,
66741
+ result: await reader.query({
66742
+ deviceId: item.deviceId,
66743
+ key: item.key,
66744
+ ...item.scope !== void 0 ? { scope: item.scope } : {},
66745
+ from,
66746
+ to,
66747
+ resolution: "auto",
66748
+ maxPoints
66749
+ })
66750
+ };
66751
+ } catch (err) {
66752
+ return {
66753
+ ok: false,
66754
+ error: messageOf(err)
66755
+ };
66756
+ }
66757
+ }));
66758
+ }
66759
+ //#endregion
66760
+ //#region src/composites/device-history/use-device-history.ts
66761
+ /**
66762
+ * The data half of the History views: is device-history installed, which
66763
+ * series exist, and their reads over a window (plus the previous period when
66764
+ * comparing). Each stage keeps "not answered" apart from "answered nothing"
66765
+ * (D315): a pending list is never an empty one, a failed read is never a
66766
+ * flat line.
66767
+ */
66768
+ var MINUTE = 6e4;
66769
+ /**
66770
+ * The instant a window ends at, rounded so the query key is stable between
66771
+ * renders and the view refreshes at a pace its width justifies.
66772
+ */
66773
+ function anchorNow(now, spanMs) {
66774
+ const step = spanMs <= HISTORY_WINDOW_MS["24h"] ? MINUTE : spanMs <= HISTORY_WINDOW_MS["7d"] ? 5 * MINUTE : spanMs <= HISTORY_WINDOW_MS["30d"] ? 15 * MINUTE : 60 * MINUTE;
66775
+ return Math.ceil(now / step) * step;
66776
+ }
66777
+ /** The tRPC client as the two doors `readHistory` needs. */
66778
+ function useHistoryReader() {
66779
+ const client = useSystem().trpcClient;
66780
+ return (0, react$1.useMemo)(() => ({
66781
+ queryMany: (input) => client.deviceHistory.queryMany.query(input),
66782
+ query: (input) => client.deviceHistory.query.query(input)
66783
+ }), [client]);
66784
+ }
66785
+ /** A device's series list, gated on device-history being present. */
66786
+ function useDeviceSeriesList(deviceId) {
66787
+ const presence = useCapabilityPresence("device-history");
66788
+ const client = useSystem().trpcClient;
66789
+ const q = (0, _tanstack_react_query.useQuery)({
66790
+ queryKey: [
66791
+ "device-history",
66792
+ "listSeries",
66793
+ deviceId
66794
+ ],
66795
+ queryFn: () => client.deviceHistory.listSeries.query({ deviceId }),
66796
+ enabled: presence === "present",
66797
+ staleTime: 6e4
66798
+ });
66799
+ return {
66800
+ presence,
66801
+ list: q.status === "success" ? {
66802
+ status: "ok",
66803
+ data: q.data
66804
+ } : q.status === "error" ? {
66805
+ status: "error",
66806
+ error: q.error.message
66807
+ } : { status: "pending" }
66808
+ };
66809
+ }
66810
+ /** Identity of a read item: subject + key (+ scope when narrowed). */
66811
+ function readItemId(item) {
66812
+ return `${"deviceId" in item ? `d:${String(item.deviceId)}` : `n:${item.forNodeId}`}|${item.key}|${item.scope ?? "*"}`;
66813
+ }
66814
+ /** Read `items` over `window` (and the previous period when `compare`). */
66815
+ function useHistoryReads(items, window, compare, enabled = true) {
66816
+ const reader = useHistoryReader();
66817
+ const ids = items.map(readItemId);
66818
+ const q = (0, _tanstack_react_query.useQuery)({
66819
+ queryKey: [
66820
+ "device-history",
66821
+ "reads",
66822
+ ids,
66823
+ window.from,
66824
+ window.to,
66825
+ compare
66826
+ ],
66827
+ queryFn: async () => {
66828
+ const prev = previousWindow(window);
66829
+ const [cur, before] = await Promise.all([readHistory(reader, items, window), compare ? readHistory(reader, items, prev.window) : Promise.resolve(null)]);
66830
+ const toMap = (entries) => new Map(entries.map((e, i) => [ids[i] ?? String(i), e]));
66831
+ return {
66832
+ current: toMap(cur),
66833
+ previous: before === null ? null : toMap(before),
66834
+ shiftMs: prev.shiftMs
66835
+ };
66836
+ },
66837
+ enabled: enabled && items.length > 0,
66838
+ staleTime: 3e4,
66839
+ placeholderData: (previous) => previous
66840
+ });
66841
+ if (q.status === "success") return {
66842
+ status: "ok",
66843
+ data: q.data
66844
+ };
66845
+ if (q.status === "error") return {
66846
+ status: "error",
66847
+ error: q.error.message
66848
+ };
66849
+ return { status: "pending" };
66850
+ }
66851
+ //#endregion
66852
+ //#region src/composites/device-history/use-history-panels.ts
66853
+ /**
66854
+ * Saved history panels and hidden charts, in the signed-in user's ACCOUNT
66855
+ * preferences (D894): one row, namespace `admin.history-panels`, synced to
66856
+ * every device signed in as that user.
66857
+ *
66858
+ * A write is a compare-and-set on the revision it was based on; a conflict
66859
+ * re-applies the same change to the row as it now stands, once. A row this
66860
+ * client cannot read is never overwritten.
66861
+ */
66862
+ var QUERY_KEY = ["preferences", HISTORY_PANELS_NAMESPACE];
66863
+ function useHistoryPanels() {
66864
+ const client = useSystem().trpcClient;
66865
+ const qc = (0, _tanstack_react_query.useQueryClient)();
66866
+ const q = (0, _tanstack_react_query.useQuery)({
66867
+ queryKey: QUERY_KEY,
66868
+ queryFn: async () => {
66869
+ const row = (await client.preferences.get.query({ namespaces: [HISTORY_PANELS_NAMESPACE] })).rows.find((x) => x.namespace === HISTORY_PANELS_NAMESPACE);
66870
+ return row === void 0 ? null : {
66871
+ value: row.value,
66872
+ revision: row.revision
66873
+ };
66874
+ },
66875
+ staleTime: 3e4
66876
+ });
66877
+ const row = q.data ?? null;
66878
+ const parsed = readHistoryPanels(row?.value);
66879
+ const update = (0, react$1.useCallback)(async (change) => {
66880
+ const write = async (base, retry) => {
66881
+ const current = readHistoryPanels(base?.value);
66882
+ if (!current.parsed) throw new Error("history-panels: stored row is not readable");
66883
+ const next = change(current.value);
66884
+ const res = await client.preferences.put.mutate({
66885
+ namespace: HISTORY_PANELS_NAMESPACE,
66886
+ value: { ...next },
66887
+ baseRevision: base?.revision ?? 0
66888
+ });
66889
+ if (res.status === "saved") {
66890
+ qc.setQueryData(QUERY_KEY, {
66891
+ value: res.row.value,
66892
+ revision: res.row.revision
66893
+ });
66894
+ return;
66895
+ }
66896
+ const fresh = res.current === null ? null : {
66897
+ value: res.current.value,
66898
+ revision: res.current.revision
66899
+ };
66900
+ qc.setQueryData(QUERY_KEY, fresh);
66901
+ if (!retry) throw new Error("history-panels: the row changed while saving");
66902
+ await write(fresh, false);
66903
+ };
66904
+ await write(row, true);
66905
+ }, [
66906
+ client,
66907
+ qc,
66908
+ row
66909
+ ]);
66910
+ return {
66911
+ status: q.status === "success" ? "ok" : q.status === "error" ? "error" : "pending",
66912
+ value: q.status === "success" ? parsed.value : EMPTY_HISTORY_PANELS,
66913
+ writable: q.status === "success" && parsed.parsed,
66914
+ update
66915
+ };
66916
+ }
66917
+ //#endregion
66918
+ //#region src/composites/device-history/DeviceHistoryView.tsx
66919
+ /**
66920
+ * DeviceHistoryView — a device's recorded history (device-history, D850):
66921
+ * the device page's History tab, and — with `fixedWindow="24h"` — the last
66922
+ * day on a camera's Live Stats tab. One code path for both.
66923
+ *
66924
+ * Grouping mirrors the State panel: one collapsible section per capability
66925
+ * (catalog order), then per producer addon; inside, one chart per unit and
66926
+ * one timeline per state. Every chart shares one cursor. A chart's ⋯ menu
66927
+ * opens its series on the History page or hides it (remembered in the
66928
+ * user's account preferences).
66929
+ *
66930
+ * Absent and pending are different screens (D315): device-history not
66931
+ * installed, the list not answered yet, and "nothing recorded" each say so.
66932
+ */
66933
+ /** `climate-control` → `Climate control`. */
66934
+ function humanizeName(name) {
66935
+ const words = name.replace(/[-_]/g, " ").trim();
66936
+ return words.length === 0 ? name : words.charAt(0).toUpperCase() + words.slice(1);
66937
+ }
66938
+ function refsOf(deviceId, spec) {
66939
+ return (spec.type === "lines" || spec.type === "occupancy" ? spec.series : [spec.series]).map((s) => ({
66940
+ subject: {
66941
+ kind: "device",
66942
+ deviceId
66943
+ },
66944
+ key: s.key,
66945
+ scope: s.scope
66946
+ }));
66947
+ }
66948
+ function DeviceHistoryView({ deviceId, fixedWindow, onNavigate }) {
66949
+ const t = useUiText();
66950
+ const [windowParam, setWindowParam] = useUrlQueryParam("hw");
66951
+ const [customParam, setCustomParam] = useUrlQueryParam("hr");
66952
+ const [compareParam, setCompareParam] = useUrlQueryParam("hc");
66953
+ const windowId = fixedWindow ?? (isHistoryWindowId(windowParam) ? windowParam : "24h");
66954
+ const custom = fixedWindow ? null : decodeCustomWindow(customParam);
66955
+ const compare = fixedWindow === void 0 && compareParam === "1";
66956
+ const spanGuess = resolveHistoryWindow(windowId, Date.now(), custom);
66957
+ const range = resolveHistoryWindow(windowId, anchorNow(Date.now(), spanGuess.to - spanGuess.from), custom);
66958
+ const { presence, list } = useDeviceSeriesList(deviceId);
66959
+ const panels = useHistoryPanels();
66960
+ const hidden = (0, react$1.useMemo)(() => new Set(panels.value.hidden[String(deviceId)] ?? []), [panels.value.hidden, deviceId]);
66961
+ const [collapsed, setCollapsed] = (0, react$1.useState)(/* @__PURE__ */ new Set());
66962
+ const sections = (0, react$1.useMemo)(() => list.status === "ok" ? groupDeviceSeries(list.data, humanizeName) : [], [list]);
66963
+ const visible = (0, react$1.useMemo)(() => sections.map((s) => ({
66964
+ ...s,
66965
+ charts: s.charts.filter((c) => !hidden.has(c.id))
66966
+ })).filter((s) => s.charts.length > 0), [sections, hidden]);
66967
+ const hiddenCount = sections.reduce((n, s) => n + s.charts.filter((c) => hidden.has(c.id)).length, 0);
66968
+ const reads = useHistoryReads((0, react$1.useMemo)(() => chartKeys(visible.flatMap((s) => s.charts)).map((key) => ({
66969
+ deviceId,
66970
+ key
66971
+ })), [visible, deviceId]), range, compare, list.status === "ok");
66972
+ const readsData = reads.status === "ok" ? reads.data : null;
66973
+ const entryFor = (0, react$1.useCallback)((key, previous) => {
66974
+ const id = readItemId({
66975
+ deviceId,
66976
+ key
66977
+ });
66978
+ if (reads.status === "error") return {
66979
+ ok: false,
66980
+ error: reads.error
66981
+ };
66982
+ return (previous ? readsData?.previous : readsData?.current)?.get(id);
66983
+ }, [
66984
+ deviceId,
66985
+ reads,
66986
+ readsData
66987
+ ]);
66988
+ const navigate = (0, react$1.useCallback)((path) => {
66989
+ if (onNavigate) onNavigate(path);
66990
+ else globalThis.location.assign(`${routerBasename()}${path}`);
66991
+ }, [onNavigate]);
66992
+ const setHidden = (chartId, hide) => {
66993
+ if (!panels.writable) return;
66994
+ panels.update((v) => withHiddenChart(v, deviceId, chartId, hide));
66995
+ };
66996
+ const showAllHidden = () => {
66997
+ if (!panels.writable) return;
66998
+ panels.update((v) => {
66999
+ let next = v;
67000
+ for (const id of hidden) next = withHiddenChart(next, deviceId, id, false);
67001
+ return next;
67002
+ });
67003
+ };
67004
+ if (presence === "pending") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HistorySkeleton, {});
67005
+ if (presence === "unknown") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBox, {
67006
+ title: t("uiLibrary.history.presenceUnknownTitle", "Could not reach the hub"),
67007
+ message: t("uiLibrary.history.presenceUnknown", "The hub did not say whether device history is installed. Try again in a moment.")
67008
+ });
67009
+ if (presence === "absent") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyState, {
67010
+ icon: History,
67011
+ title: t("uiLibrary.history.absentTitle", "Device history is not running on this hub"),
67012
+ description: t("uiLibrary.history.absent", "Nothing is recorded until the device-history system addon is enabled.")
67013
+ });
67014
+ if (list.status === "pending") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HistorySkeleton, {});
67015
+ if (list.status === "error") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBox, {
67016
+ title: t("uiLibrary.history.listFailedTitle", "History could not be read"),
67017
+ message: list.error
67018
+ });
67019
+ if (sections.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyState, {
67020
+ icon: ChartLine,
67021
+ title: t("uiLibrary.history.emptyTitle", "Nothing recorded for this device yet"),
67022
+ description: t("uiLibrary.history.empty", "A value appears here once it changes, or at the next five-minute check.")
67023
+ });
67024
+ const syncKey = `device-history:${String(deviceId)}`;
67025
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
67026
+ className: STACK_GAP,
67027
+ children: [fixedWindow === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
67028
+ className: "flex flex-wrap items-center justify-between gap-2",
67029
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(HistoryWindowBar, {
67030
+ value: windowId,
67031
+ onChange: (id) => setWindowParam(id === "24h" ? null : id),
67032
+ custom,
67033
+ onCustomChange: (w) => setCustomParam(encodeCustomWindow(w)),
67034
+ compare,
67035
+ onCompareChange: (on) => setCompareParam(on ? "1" : null)
67036
+ }), hiddenCount > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
67037
+ variant: "ghost",
67038
+ size: "sm",
67039
+ onClick: showAllHidden,
67040
+ disabled: !panels.writable,
67041
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(EyeOff, {
67042
+ className: "h-3.5 w-3.5",
67043
+ "aria-hidden": "true"
67044
+ }), t("uiLibrary.history.showHidden", {
67045
+ one: "Show {{count}} hidden chart",
67046
+ other: "Show {{count}} hidden charts"
67047
+ }, { count: hiddenCount })]
67048
+ })]
67049
+ }), visible.map((section) => {
67050
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CollapsibleCard, {
67051
+ expanded: !collapsed.has(section.id),
67052
+ onExpandedChange: (next) => setCollapsed((prev) => {
67053
+ const s = new Set(prev);
67054
+ if (next) s.delete(section.id);
67055
+ else s.add(section.id);
67056
+ return s;
67057
+ }),
67058
+ header: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
67059
+ className: "flex items-center gap-2 min-w-0",
67060
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
67061
+ className: "text-xs font-semibold text-foreground truncate",
67062
+ children: humanizeName(section.name)
67063
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
67064
+ className: "text-[10px] text-foreground-subtle shrink-0",
67065
+ children: t("uiLibrary.history.chartCount", {
67066
+ one: "{{count}} chart",
67067
+ other: "{{count}} charts"
67068
+ }, { count: section.charts.length })
67069
+ })]
67070
+ }),
67071
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
67072
+ className: "grid grid-cols-1 gap-4 2xl:grid-cols-2",
67073
+ children: section.charts.map((spec) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HistoryChart, {
67074
+ spec,
67075
+ entryFor,
67076
+ reads: readsData,
67077
+ window: range,
67078
+ compare,
67079
+ syncKey,
67080
+ actions: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KebabMenu, {
67081
+ title: t("uiLibrary.history.chartMenu", "Chart actions"),
67082
+ items: [{
67083
+ id: "open",
67084
+ label: t("uiLibrary.history.openInHistory", "Open in History"),
67085
+ icon: ChartLine,
67086
+ onClick: () => navigate(historyPageHref(refsOf(deviceId, spec), windowId))
67087
+ }, {
67088
+ id: "hide",
67089
+ label: t("uiLibrary.history.hide", "Hide"),
67090
+ icon: EyeOff,
67091
+ hidden: fixedWindow !== void 0,
67092
+ disabled: !panels.writable,
67093
+ onClick: () => setHidden(spec.id, true)
67094
+ }]
67095
+ })
67096
+ }, spec.id))
67097
+ })
67098
+ }, section.id);
67099
+ })]
67100
+ });
67101
+ }
67102
+ function HistorySkeleton() {
67103
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
67104
+ className: STACK_GAP,
67105
+ "aria-busy": "true",
67106
+ children: [
67107
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Skeleton, { className: "h-7 w-72" }),
67108
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Skeleton, { className: "h-40 w-full" }),
67109
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Skeleton, { className: "h-40 w-full" })
67110
+ ]
67111
+ });
67112
+ }
67113
+ //#endregion
64072
67114
  //#region src/contexts/zone-editing.tsx
64073
67115
  /**
64074
67116
  * Per-device zone-editing state shared between the player overlay
@@ -64928,6 +67970,7 @@ exports.AudioWaveform = AudioWaveform;
64928
67970
  exports.AutomationHeroCard = AutomationHeroCard;
64929
67971
  exports.AutomationInlineControl = AutomationInlineControl;
64930
67972
  exports.AutotrackSection = AutotrackSection;
67973
+ exports.BOOLEAN_STATES = BOOLEAN_STATES;
64931
67974
  exports.BTN_COMPACT = BTN_COMPACT;
64932
67975
  exports.BTN_COMPACT_DANGER = BTN_COMPACT_DANGER;
64933
67976
  exports.BTN_COMPACT_PRIMARY = BTN_COMPACT_PRIMARY;
@@ -64936,6 +67979,7 @@ exports.Baby = Baby;
64936
67979
  exports.BackToStepsButton = BackToStepsButton;
64937
67980
  exports.Badge = Badge;
64938
67981
  exports.Ban = Ban;
67982
+ exports.BarSeries = BarSeries;
64939
67983
  exports.Bath = Bath;
64940
67984
  exports.Battery = Battery;
64941
67985
  exports.BatteryBadge = BatteryBadge;
@@ -64960,6 +68004,7 @@ exports.Button = Button;
64960
68004
  exports.ButtonControl = ButtonControl;
64961
68005
  exports.ButtonHeroCard = ButtonHeroCard;
64962
68006
  exports.CENTER = CENTER;
68007
+ exports.CHART_SERIES_TOKENS = CHART_SERIES_TOKENS;
64963
68008
  exports.CHIP_ACTIVE = CHIP_ACTIVE;
64964
68009
  exports.CHIP_BASE = CHIP_BASE;
64965
68010
  exports.CHIP_INACTIVE = CHIP_INACTIVE;
@@ -64977,6 +68022,11 @@ exports.Car = Car;
64977
68022
  exports.Card = Card;
64978
68023
  exports.Cast = Cast;
64979
68024
  exports.Cat = Cat;
68025
+ exports.ChartFrame = ChartFrame;
68026
+ exports.ChartLine = ChartLine;
68027
+ exports.ChartPlaceholder = ChartPlaceholder;
68028
+ exports.ChartSwatch = ChartSwatch;
68029
+ exports.ChartWindowToggle = ChartWindowToggle;
64980
68030
  exports.Check = Check;
64981
68031
  exports.Checkbox = Checkbox;
64982
68032
  exports.ChevronDown = ChevronDown;
@@ -65059,6 +68109,7 @@ exports.DeviceExportPanel = DeviceExportPanel;
65059
68109
  exports.DeviceExportSetup = DeviceExportSetup;
65060
68110
  exports.DeviceFilterBar = DeviceFilterBar;
65061
68111
  exports.DeviceGrid = DeviceGrid;
68112
+ exports.DeviceHistoryView = DeviceHistoryView;
65062
68113
  exports.DeviceItem = DeviceItem;
65063
68114
  exports.DeviceList = DeviceList;
65064
68115
  exports.DeviceMultiSelectField = DeviceMultiSelectField;
@@ -65093,6 +68144,7 @@ exports.Droplets = Droplets;
65093
68144
  exports.DummyHeroCard = DummyHeroCard;
65094
68145
  exports.DummyInline = DummyInline;
65095
68146
  exports.EMPTY_DATA_TABLE_FILTER = EMPTY_DATA_TABLE_FILTER;
68147
+ exports.EMPTY_HISTORY_PANELS = EMPTY_HISTORY_PANELS;
65096
68148
  exports.EVENT_KIND_ICONS = EVENT_KIND_ICONS;
65097
68149
  exports.EllipsisVertical = EllipsisVertical;
65098
68150
  exports.EmptyState = EmptyState;
@@ -65128,6 +68180,7 @@ exports.FloatingPanel = FloatingPanel;
65128
68180
  exports.Footprints = Footprints;
65129
68181
  exports.FormField = FormField$1;
65130
68182
  exports.Funnel = Funnel;
68183
+ exports.GAP_INTERVAL_FACTOR = GAP_INTERVAL_FACTOR;
65131
68184
  exports.GRID_GAP = GRID_GAP;
65132
68185
  exports.GRID_PAIRED = GRID_PAIRED;
65133
68186
  exports.GRID_QUICK_STATS = GRID_QUICK_STATS;
@@ -65141,6 +68194,11 @@ exports.GripHorizontal = GripHorizontal;
65141
68194
  exports.GripTrack = GripTrack;
65142
68195
  exports.GroupedModelSelector = GroupedModelSelector;
65143
68196
  exports.GroupedTilePicker = GroupedTilePicker;
68197
+ exports.HISTORY_PAGE_PATH = HISTORY_PAGE_PATH;
68198
+ exports.HISTORY_PANELS_NAMESPACE = HISTORY_PANELS_NAMESPACE;
68199
+ exports.HISTORY_SELECTION_MAX = HISTORY_SELECTION_MAX;
68200
+ exports.HISTORY_WINDOW_IDS = HISTORY_WINDOW_IDS;
68201
+ exports.HISTORY_WINDOW_MS = HISTORY_WINDOW_MS;
65144
68202
  exports.HOST_WIDGETS = HOST_WIDGETS;
65145
68203
  exports.HUB_SESSION_RUN_MS = HUB_SESSION_RUN_MS;
65146
68204
  exports.Hammer = Hammer;
@@ -65148,6 +68206,9 @@ exports.Hand = Hand;
65148
68206
  exports.HardDrive = HardDrive;
65149
68207
  exports.Hash = Hash;
65150
68208
  exports.Heater = Heater;
68209
+ exports.History = History;
68210
+ exports.HistoryChart = HistoryChart;
68211
+ exports.HistoryWindowBar = HistoryWindowBar;
65151
68212
  exports.HlsVideo = HlsVideo;
65152
68213
  exports.House = House;
65153
68214
  exports.HoverZoomImage = HoverZoomImage;
@@ -65233,6 +68294,7 @@ exports.NodePicker = NodePicker;
65233
68294
  exports.NodeSelectField = NodeSelectField;
65234
68295
  exports.NotificationTargetSelectField = NotificationTargetSelectField;
65235
68296
  exports.NotificationTargetsField = NotificationTargetsField;
68297
+ exports.OCCUPANCY_KEY = OCCUPANCY_KEY;
65236
68298
  exports.OPTION_SELECT_SEARCH_MIN_OPTIONS = OPTION_SELECT_SEARCH_MIN_OPTIONS;
65237
68299
  exports.OfflineBadge = OfflineBadge;
65238
68300
  exports.OptionSelect = OptionSelect;
@@ -65288,6 +68350,7 @@ exports.QrCode = QrCode;
65288
68350
  exports.QrCode$1 = QrCode$1;
65289
68351
  exports.RECONNECT_POLICY = RECONNECT_POLICY;
65290
68352
  exports.RECORDED_PLAYBACK_MODES = RECORDED_PLAYBACK_MODES;
68353
+ exports.RESOLUTION_BUCKET_MS = RESOLUTION_BUCKET_MS;
65291
68354
  exports.RIGHT = RIGHT;
65292
68355
  exports.ROI_AREA_WARN_FRACTION = ROI_AREA_WARN_FRACTION;
65293
68356
  exports.ROLE_DESCRIPTOR = ROLE_DESCRIPTOR;
@@ -65375,11 +68438,13 @@ exports.SnapshotButton = SnapshotButton;
65375
68438
  exports.Snowflake = Snowflake;
65376
68439
  exports.SortHeaderButton = SortHeaderButton;
65377
68440
  exports.Sparkles = Sparkles;
68441
+ exports.Sparkline = Sparkline;
65378
68442
  exports.Sprout = Sprout;
65379
68443
  exports.Square = Square;
65380
68444
  exports.StableTabPanel = StableTabPanel;
65381
68445
  exports.Star = Star;
65382
68446
  exports.StatCard = StatCard;
68447
+ exports.StateTimeline = StateTimeline;
65383
68448
  exports.StateValuesStream = StateValuesStream;
65384
68449
  exports.StatusBadge = StatusBadge;
65385
68450
  exports.StepDetailDock = StepDetailDock;
@@ -65417,6 +68482,7 @@ exports.ThemeProvider = require_theme_index.ThemeProvider$1;
65417
68482
  exports.Thermometer = Thermometer;
65418
68483
  exports.ThermostatHeroCard = ThermostatHeroCard;
65419
68484
  exports.ThermostatInlineControl = ThermostatInlineControl;
68485
+ exports.TimeSeriesChart = TimeSeriesChart;
65420
68486
  exports.Timer = Timer;
65421
68487
  exports.TimezoneSelector = TimezoneSelector;
65422
68488
  exports.ToggleLeft = ToggleLeft;
@@ -65479,22 +68545,31 @@ exports.ZoomOut = ZoomOut;
65479
68545
  exports.acceleratorLabel = acceleratorLabel;
65480
68546
  exports.acceleratorShortLabel = acceleratorShortLabel;
65481
68547
  exports.agentColumnKey = agentColumnKey;
68548
+ exports.alignSeries = alignSeries;
65482
68549
  exports.allDeviceTypeFilterOptions = allDeviceTypeFilterOptions;
68550
+ exports.anchorNow = anchorNow;
65483
68551
  exports.applyFleetQueryDefaults = applyFleetQueryDefaults;
65484
68552
  exports.applyPickerFilters = applyPickerFilters;
65485
68553
  exports.ariaSortForColumn = ariaSortForColumn;
65486
68554
  exports.assignNodeColorId = _camstack_types.assignNodeColorId;
68555
+ exports.axisTicks = axisTicks;
65487
68556
  exports.badgeFor = badgeFor;
65488
68557
  exports.badgeFor$1 = badgeFor;
65489
68558
  exports.bindsToModel = bindsToModel;
68559
+ exports.booleanLabels = booleanLabels;
68560
+ exports.bucketTotals = bucketTotals;
65490
68561
  exports.buildColorByNodeId = buildColorByNodeId;
65491
68562
  exports.buildDataRecordRows = buildDataRecordRows;
65492
68563
  exports.buildPeerConnectionConfig = buildPeerConnectionConfig;
65493
68564
  exports.buildStepTreeFromSchema = buildStepTreeFromSchema;
68565
+ exports.buildTimeSeriesModel = buildTimeSeriesModel;
65494
68566
  exports.catalogHasDraft = catalogHasDraft;
68567
+ exports.chartKeys = chartKeys;
68568
+ exports.chartSeriesStyle = chartSeriesStyle;
65495
68569
  exports.childEntityId = childEntityId;
65496
68570
  exports.childListName = childListName;
65497
68571
  exports.chipOverflow = chipOverflow;
68572
+ exports.chunk = chunk;
65498
68573
  exports.classifyAddressScope = classifyAddressScope;
65499
68574
  exports.classifyCapability = classifyCapability;
65500
68575
  exports.classifySessionVerdict = classifySessionVerdict;
@@ -65522,6 +68597,9 @@ exports.dataRecordSearchText = dataRecordSearchText;
65522
68597
  exports.dataRecordType = dataRecordType;
65523
68598
  exports.dataTableFacetGroups = dataTableFacetGroups;
65524
68599
  exports.dataTableFacets = dataTableFacets;
68600
+ exports.decodeCustomWindow = decodeCustomWindow;
68601
+ exports.decodeSeriesRef = decodeSeriesRef;
68602
+ exports.defaultAxisSpec = defaultAxisSpec;
65525
68603
  exports.defaultOfField = defaultOfField;
65526
68604
  exports.defaultSelectClusterRoots = defaultSelectClusterRoots;
65527
68605
  exports.defaultSingleClusterRoot = defaultSingleClusterRoot;
@@ -65545,10 +68623,14 @@ exports.draftFromQuery = draftFromQuery;
65545
68623
  exports.effectivePageSize = effectivePageSize;
65546
68624
  exports.emptyDevicesWidens = emptyDevicesWidens;
65547
68625
  exports.emptySessionRun = emptySessionRun;
68626
+ exports.encodeCustomWindow = encodeCustomWindow;
68627
+ exports.encodeSeriesRef = encodeSeriesRef;
68628
+ exports.energyBuckets = energyBuckets;
65548
68629
  exports.ensureMfHostInit = ensureMfHostInit;
65549
68630
  exports.eventKindLabel = eventKindLabel;
65550
68631
  exports.filterDataTableRows = filterDataTableRows;
65551
68632
  exports.filterDeviceOptions = filterDeviceOptions;
68633
+ exports.filterOccupancyLines = filterOccupancyLines;
65552
68634
  exports.filterOptionSelectOptions = filterOptions;
65553
68635
  exports.filterPickerDevices = filterPickerDevices;
65554
68636
  exports.finalizeSessionRun = finalizeSessionRun;
@@ -65556,25 +68638,34 @@ exports.findTimezone = _camstack_types.findTimezone;
65556
68638
  exports.flattenPickerGrouping = flattenPickerGrouping;
65557
68639
  exports.flattenPickerRows = flattenPickerRows;
65558
68640
  exports.fleetViewOf = fleetViewOf;
68641
+ exports.formatChartBytes = formatChartBytes;
68642
+ exports.formatChartNumber = formatChartNumber;
68643
+ exports.formatChartValue = formatChartValue;
65559
68644
  exports.formatControlDateTime = formatControlDateTime;
65560
68645
  exports.formatLastSeen = formatLastSeen;
65561
68646
  exports.formatNumeric = formatNumeric;
65562
68647
  exports.formatSettingValue = formatSettingValue;
68648
+ exports.formatSpan = formatSpan;
68649
+ exports.formatTimeLabel = formatTimeLabel;
65563
68650
  exports.freezeVerdict = freezeVerdict;
65564
68651
  exports.fuzzyMatch = fuzzyMatch;
68652
+ exports.gapThresholdMs = gapThresholdMs;
65565
68653
  exports.getClassColor = getClassColor;
65566
68654
  exports.getPhaseVisual = getPhaseVisual;
65567
68655
  exports.groupAgentColumns = groupAgentColumns;
65568
68656
  exports.groupChildrenByLayout = groupChildrenByLayout;
68657
+ exports.groupDeviceSeries = groupDeviceSeries;
65569
68658
  exports.groupPickerDevices = groupPickerDevices;
65570
68659
  exports.groupPickerDevicesBy = groupPickerDevicesBy;
65571
68660
  exports.hardwareLabel = hardwareLabel;
68661
+ exports.historyPageHref = historyPageHref;
65572
68662
  exports.hubOrigin = hubOrigin;
65573
68663
  exports.hubPath = hubPath;
65574
68664
  exports.hubPath$1 = hubPath;
65575
68665
  exports.hubUrl = hubUrl;
65576
68666
  exports.hubWsUrl = hubWsUrl;
65577
68667
  exports.humanDuration = humanDuration;
68668
+ exports.humanizeName = humanizeName;
65578
68669
  exports.humidifierTint = humidifierTint;
65579
68670
  exports.iconGlyphsFailed = iconGlyphsFailed;
65580
68671
  exports.iconGlyphsLoaded = iconGlyphsLoaded;
@@ -65582,15 +68673,19 @@ exports.initialScrubState = initialScrubState;
65582
68673
  exports.insertAtSelection = insertAtSelection;
65583
68674
  exports.isAbsentProvider = isAbsentProvider;
65584
68675
  exports.isAbsentProvider$1 = isAbsentProvider;
68676
+ exports.isByteUnit = isByteUnit;
65585
68677
  exports.isDataTableFilterActive = isDataTableFilterActive;
65586
68678
  exports.isFieldVisible = isFieldVisible;
65587
68679
  exports.isGroupExpanded = isGroupExpanded;
68680
+ exports.isHistoryWindowId = isHistoryWindowId;
65588
68681
  exports.isKeychainFieldValue = isKeychainFieldValue;
65589
68682
  exports.isRedactedSecret = isRedactedSecret;
68683
+ exports.isRefusal = isRefusal;
65590
68684
  exports.isSecretField = isSecretField;
65591
68685
  exports.isValueField = isValueField;
65592
68686
  exports.lawnMowerActivityMeta = lawnMowerActivityMeta;
65593
68687
  exports.lightColors = require_theme_to_root_css.lightColors;
68688
+ exports.linePoints = linePoints;
65594
68689
  exports.loadIconGlyphs = loadIconGlyphs;
65595
68690
  exports.loadRemoteBundle = loadRemoteBundle;
65596
68691
  exports.lucideExportName = lucideExportName;
@@ -65607,11 +68702,16 @@ exports.nextReconnectAction = nextReconnectAction;
65607
68702
  exports.nextSort = nextSort;
65608
68703
  exports.nextSortDirection = nextSortDirection;
65609
68704
  exports.nextTableSort = nextTableSort;
68705
+ exports.niceMax = niceMax;
68706
+ exports.niceRange = niceRange;
65610
68707
  exports.nodeColorClass = nodeColorClass;
65611
68708
  exports.nodeSoftBgClass = nodeSoftBgClass;
65612
68709
  exports.nodeTextColorClass = nodeTextColorClass;
65613
68710
  exports.normalizeForSearch = normalizeForSearch;
65614
68711
  exports.normalizeLanguage = normalizeLanguage;
68712
+ exports.normalizeSelection = normalizeSelection;
68713
+ exports.occupancyFacets = occupancyFacets;
68714
+ exports.occupancyLines = occupancyLines;
65615
68715
  exports.offListSelections = offListSelections;
65616
68716
  exports.openTopLayer = openTopLayer;
65617
68717
  exports.optionSelectSearchText = optionSearchText;
@@ -65621,8 +68721,10 @@ exports.pageRows = pageRows;
65621
68721
  exports.pageSizeLabel = pageSizeLabel;
65622
68722
  exports.paginateRows = paginateRows;
65623
68723
  exports.parentDetailOf = parentDetailOf;
68724
+ exports.parseCapKey = parseCapKey;
65624
68725
  exports.parseIceCandidate = parseIceCandidate;
65625
68726
  exports.parsePickerGroupBy = parsePickerGroupBy;
68727
+ exports.parseProducerKey = parseProducerKey;
65626
68728
  exports.parseRecordedServerMessage = parseRecordedServerMessage;
65627
68729
  exports.partitionOptionSelectSelected = partitionSelected;
65628
68730
  exports.partitionSuggested = partitionSuggested;
@@ -65639,7 +68741,11 @@ exports.pickerSearchText = pickerSearchText;
65639
68741
  exports.pickerZoneFilterSuspended = pickerZoneFilterSuspended;
65640
68742
  exports.pinnedStreamChoices = pinnedStreamChoices;
65641
68743
  exports.placePopover = placePopover;
68744
+ exports.previousWindow = previousWindow;
65642
68745
  exports.providerIcons = providerIcons;
68746
+ exports.readHistory = readHistory;
68747
+ exports.readHistoryPanels = readHistoryPanels;
68748
+ exports.readItemId = readItemId;
65643
68749
  exports.readStoredGroupBy = readStoredGroupBy;
65644
68750
  exports.reduceSessionRun = reduceSessionRun;
65645
68751
  exports.registerAddonBundle = registerAddonBundle;
@@ -65655,6 +68761,7 @@ exports.resolveDisplayIcon = resolveDisplayIcon;
65655
68761
  exports.resolveEffectiveDefaultModel = resolveEffectiveDefaultModel;
65656
68762
  exports.resolveEffectiveStepModel = resolveEffectiveStepModel;
65657
68763
  exports.resolveEventKindIcon = resolveEventKindIcon;
68764
+ exports.resolveHistoryWindow = resolveHistoryWindow;
65658
68765
  exports.resolveOptionSelectSearchable = resolveSearchable;
65659
68766
  exports.resolveParentNames = resolveParentNames;
65660
68767
  exports.resolvePrimaryChild = resolvePrimaryChild;
@@ -65671,10 +68778,14 @@ exports.scrubReducer = scrubReducer;
65671
68778
  exports.searchSteps = searchSteps;
65672
68779
  exports.secretDisplayValue = secretDisplayValue;
65673
68780
  exports.secretSubmitValue = secretSubmitValue;
68781
+ exports.segmentsFromChanges = segmentsFromChanges;
65674
68782
  exports.selectPickerGroup = selectPickerGroup;
65675
68783
  exports.selectedDeviceOptions = selectedDeviceOptions;
65676
68784
  exports.serializeRecordedCommand = serializeRecordedCommand;
68785
+ exports.seriesRefId = seriesRefId;
65677
68786
  exports.settingsModelIdFor = settingsModelIdFor;
68787
+ exports.sharedGapSpans = sharedGapSpans;
68788
+ exports.shiftPoints = shiftPoints;
65678
68789
  exports.shouldCommit = shouldCommit;
65679
68790
  exports.shouldEmit = shouldEmit;
65680
68791
  exports.shouldShowAddNewFooter = shouldShowAddNewFooter;
@@ -65683,6 +68794,11 @@ exports.shouldUseSingleNode = shouldUseSingleNode;
65683
68794
  exports.showsNoCamerasHint = showsNoCamerasHint;
65684
68795
  exports.sortRows = sortRows;
65685
68796
  exports.sortRowsByColumn = sortRowsByColumn;
68797
+ exports.stackBuckets = stackBuckets;
68798
+ exports.stackSeries = stackSeries;
68799
+ exports.stateAt = stateAt;
68800
+ exports.stateRow = stateRow;
68801
+ exports.stateStyle = stateStyle;
65686
68802
  exports.statusIcons = statusIcons;
65687
68803
  exports.statusLine = statusLine;
65688
68804
  exports.statusLine$1 = statusLine;
@@ -65692,10 +68808,13 @@ exports.stepLocalFields = stepLocalFields;
65692
68808
  exports.stepModelOptions = stepModelOptions;
65693
68809
  exports.stripParentNamePrefix = stripParentNamePrefix;
65694
68810
  exports.summarizeHubCandidates = summarizeHubCandidates;
68811
+ exports.swatchFillClass = swatchFillClass;
68812
+ exports.swatchLineClass = swatchLineClass;
65695
68813
  exports.tabForSelector = tabForSelector;
65696
68814
  exports.tankAlert = tankAlert;
65697
68815
  exports.themeToCss = require_theme_to_root_css.themeToCss;
65698
68816
  exports.themeToRootCss = require_theme_to_root_css.themeToRootCss;
68817
+ exports.timeInState = timeInState;
65699
68818
  exports.toStationaryOverlayBox = toStationaryOverlayBox;
65700
68819
  exports.toggleDataTableFacet = toggleDataTableFacet;
65701
68820
  exports.toggleDataTableGroupFacet = toggleDataTableGroupFacet;
@@ -65705,6 +68824,7 @@ exports.trainingRefusalText = trainingRefusalText;
65705
68824
  exports.translateAddonKey = translateAddonKey;
65706
68825
  exports.trpc = trpc;
65707
68826
  exports.typeLabel = typeLabel;
68827
+ exports.unitAxes = unitAxes;
65708
68828
  exports.useAccessoriesGetStatus = useAccessoriesGetStatus;
65709
68829
  exports.useAccessoriesSetChildHidden = useAccessoriesSetChildHidden;
65710
68830
  exports.useAddonPagesListPages = useAddonPagesListPages;
@@ -66086,6 +69206,7 @@ exports.useDeviceProviderTestCreationField = useDeviceProviderTestCreationField;
66086
69206
  exports.useDeviceProxy = useDeviceProxy;
66087
69207
  exports.useDeviceProxy$1 = useDeviceProxy;
66088
69208
  exports.useDeviceQuickDetailOpen = useDeviceQuickDetailOpen;
69209
+ exports.useDeviceSeriesList = useDeviceSeriesList;
66089
69210
  exports.useDeviceSnapshot = useDeviceSnapshot;
66090
69211
  exports.useDeviceSnapshotImage = useDeviceSnapshotImage;
66091
69212
  exports.useDeviceSnapshotImage$1 = useDeviceSnapshotImage;
@@ -66152,6 +69273,9 @@ exports.useFeatureProbeGetStatus = useFeatureProbeGetStatus;
66152
69273
  exports.useFloodGetStatus = useFloodGetStatus;
66153
69274
  exports.useGasGetStatus = useGasGetStatus;
66154
69275
  exports.useHeatingValveGetStatus = useHeatingValveGetStatus;
69276
+ exports.useHistoryPanels = useHistoryPanels;
69277
+ exports.useHistoryReader = useHistoryReader;
69278
+ exports.useHistoryReads = useHistoryReads;
66155
69279
  exports.useHumidifierGetStatus = useHumidifierGetStatus;
66156
69280
  exports.useHumidifierSetMode = useHumidifierSetMode;
66157
69281
  exports.useHumidifierSetOn = useHumidifierSetOn;
@@ -67002,6 +70126,9 @@ exports.waterHeaterPhase = waterHeaterPhase;
67002
70126
  exports.waterHeaterTint = waterHeaterTint;
67003
70127
  exports.weatherConditionMeta = weatherConditionMeta;
67004
70128
  exports.weatherTint = weatherTint;
70129
+ exports.withHiddenChart = withHiddenChart;
70130
+ exports.withPanel = withPanel;
67005
70131
  exports.withResolvedParentDetail = withResolvedParentDetail;
67006
70132
  exports.withUseRootModel = withUseRootModel;
70133
+ exports.withoutPanel = withoutPanel;
67007
70134
  exports.writeStoredGroupBy = writeStoredGroupBy;