@camstack/ui-library 1.2.304 → 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.
- package/dist/{MotionZonesSettings-C3Tj12yc.js → MotionZonesSettings-DR1g9ja3.js} +1 -1
- package/dist/{PrivacyMaskSettings-C9wfHkiB.js → PrivacyMaskSettings-CtetT6Jl.js} +1 -1
- package/dist/{SceneMonitorEditor-30H29vN4.js → SceneMonitorEditor-BQhVO2F2.js} +1 -1
- package/dist/composites/cap-settings/EventHeatmap.d.ts +8 -2
- package/dist/composites/charts/BarSeries.d.ts +49 -0
- package/dist/composites/charts/Sparkline.d.ts +26 -0
- package/dist/composites/charts/StateTimeline.d.ts +47 -0
- package/dist/composites/charts/TimeSeriesChart.d.ts +48 -0
- package/dist/composites/charts/bar-series-model.d.ts +25 -0
- package/dist/composites/charts/chart-data.d.ts +87 -0
- package/dist/composites/charts/chart-frame.d.ts +53 -0
- package/dist/composites/charts/chart-palette.d.ts +46 -0
- package/dist/composites/charts/chart-scale.d.ts +29 -0
- package/dist/composites/charts/chart-uplot.d.ts +44 -0
- package/dist/composites/charts/chart-window.d.ts +19 -0
- package/dist/composites/charts/index.d.ts +16 -0
- package/dist/composites/charts/time-series-model.d.ts +47 -0
- package/dist/composites/charts/uplot-host.d.ts +34 -0
- package/dist/composites/device-history/DeviceHistoryView.d.ts +26 -0
- package/dist/composites/device-history/HistoryChart.d.ts +31 -0
- package/dist/composites/device-history/HistoryWindowBar.d.ts +16 -0
- package/dist/composites/device-history/history-model.d.ts +121 -0
- package/dist/composites/device-history/history-reads.d.ts +43 -0
- package/dist/composites/device-history/history-selection.d.ts +55 -0
- package/dist/composites/device-history/history-window.d.ts +31 -0
- package/dist/composites/device-history/index.d.ts +14 -0
- package/dist/composites/device-history/use-device-history.d.ts +37 -0
- package/dist/composites/device-history/use-history-panels.d.ts +10 -0
- package/dist/composites/index.d.ts +2 -0
- package/dist/composites/log-history-paging.d.ts +62 -0
- package/dist/composites/log-stream.d.ts +6 -10
- package/dist/generated/system-hooks.d.ts +2 -0
- package/dist/hooks/use-log-history.d.ts +26 -0
- package/dist/{icons-B1AFXUaZ.cjs → icons-BTux33dH.cjs} +2 -35
- package/dist/{icons-BT8c2hxp.js → icons-D178phML.js} +1 -34
- package/dist/index.cjs +3509 -122
- package/dist/index.js +3430 -129
- package/dist/uPlot.esm-BbxBUnav.cjs +3767 -0
- package/dist/uPlot.esm-C_8LiAPm.js +3767 -0
- package/package.json +2 -1
- 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-
|
|
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
|
|
@@ -10754,6 +10787,8 @@ var useDeviceHistoryListNodeSeries = trpc.deviceHistory.listNodeSeries.useQuery;
|
|
|
10754
10787
|
var useDeviceHistoryQuery = trpc.deviceHistory.query.useQuery;
|
|
10755
10788
|
/** Generated alias around `trpc.deviceHistory.queryNode.useQuery`. */
|
|
10756
10789
|
var useDeviceHistoryQueryNode = trpc.deviceHistory.queryNode.useQuery;
|
|
10790
|
+
/** Generated alias around `trpc.deviceHistory.queryMany.useQuery`. */
|
|
10791
|
+
var useDeviceHistoryQueryMany = trpc.deviceHistory.queryMany.useQuery;
|
|
10757
10792
|
/** Generated alias around `trpc.deviceHistory.declareSeries.useMutation`. */
|
|
10758
10793
|
var useDeviceHistoryDeclareSeries = trpc.deviceHistory.declareSeries.useMutation;
|
|
10759
10794
|
/** Generated alias around `trpc.deviceHistory.record.useMutation`. */
|
|
@@ -20883,7 +20918,7 @@ function bucketMsForSpan(spanMs) {
|
|
|
20883
20918
|
* - `pct` is via pctForMs (first=0, last=100).
|
|
20884
20919
|
* - `label` is a localised HH:MM clock string.
|
|
20885
20920
|
*/
|
|
20886
|
-
function axisTicks(fromMs, toMs, count) {
|
|
20921
|
+
function axisTicks$1(fromMs, toMs, count) {
|
|
20887
20922
|
if (count <= 1) return [{
|
|
20888
20923
|
pct: 0,
|
|
20889
20924
|
label: new Date(fromMs).toLocaleTimeString(void 0, {
|
|
@@ -21089,43 +21124,1193 @@ function ErrorBox({ message, title, onRetry, action, variant = "danger", classNa
|
|
|
21089
21124
|
});
|
|
21090
21125
|
}
|
|
21091
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
|
|
21092
22247
|
//#region src/composites/cap-settings/EventHeatmap.tsx
|
|
21093
|
-
|
|
21094
|
-
|
|
21095
|
-
|
|
21096
|
-
|
|
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
|
|
21097
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
|
+
*/
|
|
21098
22267
|
function EventHeatmap({ windowFrom, windowTo, buckets, selectedIndex, onSelectBucket }) {
|
|
21099
22268
|
const t = useUiText();
|
|
21100
|
-
const
|
|
21101
|
-
const
|
|
21102
|
-
|
|
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);
|
|
21103
22282
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
21104
|
-
className: "
|
|
21105
|
-
children:
|
|
21106
|
-
|
|
21107
|
-
|
|
21108
|
-
|
|
21109
|
-
|
|
21110
|
-
|
|
21111
|
-
|
|
21112
|
-
|
|
21113
|
-
|
|
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}}", {
|
|
21114
22306
|
time: new Date(b.startMs).toLocaleTimeString(void 0, {
|
|
21115
22307
|
hour: "2-digit",
|
|
21116
22308
|
minute: "2-digit"
|
|
21117
22309
|
}),
|
|
21118
22310
|
motion: b.counts.motion,
|
|
21119
22311
|
object: b.counts.object
|
|
21120
|
-
})
|
|
21121
|
-
|
|
21122
|
-
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"}`,
|
|
21123
|
-
style: {
|
|
21124
|
-
left: `${left}%`,
|
|
21125
|
-
width: `${Math.max(.6, bucketWidthPct)}%`,
|
|
21126
|
-
height: `${heightPct}%`
|
|
21127
|
-
}
|
|
21128
|
-
}, b.index);
|
|
22312
|
+
});
|
|
22313
|
+
}
|
|
21129
22314
|
})
|
|
21130
22315
|
});
|
|
21131
22316
|
}
|
|
@@ -21318,7 +22503,7 @@ function ClipBandBar({ day, onDayChange, view, dayBounds, onViewChange, buckets,
|
|
|
21318
22503
|
dayBounds,
|
|
21319
22504
|
onViewChange
|
|
21320
22505
|
]);
|
|
21321
|
-
const ticks = axisTicks(view.fromMs, view.toMs, 5);
|
|
22506
|
+
const ticks = axisTicks$1(view.fromMs, view.toMs, 5);
|
|
21322
22507
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
21323
22508
|
className: "space-y-1",
|
|
21324
22509
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TimelineControls, {
|
|
@@ -26115,7 +27300,7 @@ function RecordingTimeline({ deviceId }) {
|
|
|
26115
27300
|
const coverageLoading = availabilityQuery.isLoading;
|
|
26116
27301
|
const densityLoading = eventDensityQuery.isLoading;
|
|
26117
27302
|
const drillLoading = (objectDrill.isLoading || motionDrill.isLoading || audioDrill.isLoading) && drillEnabled;
|
|
26118
|
-
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]);
|
|
26119
27304
|
const displayPlayheadMs = framePush ? scrubState.scrubbing ? scrubState.scrubEpoch : scrubState.playheadEpoch : playheadMs;
|
|
26120
27305
|
const showRateToggle = framePush && (recordedMode === "playing" || recordedMode === "paused");
|
|
26121
27306
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -29914,6 +31099,25 @@ function AddonGlobalSettingsForm({ trpc, addonId, nodeId, title, disabled, onAft
|
|
|
29914
31099
|
}
|
|
29915
31100
|
//#endregion
|
|
29916
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
|
+
}
|
|
29917
31121
|
function names(device) {
|
|
29918
31122
|
const backend = device.backend.toLowerCase();
|
|
29919
31123
|
const dev = device.device.toLowerCase();
|
|
@@ -29929,10 +31133,7 @@ function names(device) {
|
|
|
29929
31133
|
label: "Intel iGPU",
|
|
29930
31134
|
short: "iGPU"
|
|
29931
31135
|
};
|
|
29932
|
-
if (backend === "edgetpu") return
|
|
29933
|
-
label: "Coral Edge TPU",
|
|
29934
|
-
short: "Coral"
|
|
29935
|
-
};
|
|
31136
|
+
if (backend === "edgetpu") return coralNames(dev);
|
|
29936
31137
|
if (backend === "coreml") return {
|
|
29937
31138
|
label: "Apple Neural Engine",
|
|
29938
31139
|
short: "ANE"
|
|
@@ -39832,6 +41033,202 @@ function useLiveBuffer(max) {
|
|
|
39832
41033
|
reset
|
|
39833
41034
|
]);
|
|
39834
41035
|
}
|
|
41036
|
+
//#endregion
|
|
41037
|
+
//#region src/hooks/use-debounced-string.ts
|
|
41038
|
+
/**
|
|
41039
|
+
* useDebouncedString — return `value` after `delay` ms have passed
|
|
41040
|
+
* without further changes. Resets the timer on every change.
|
|
41041
|
+
*
|
|
41042
|
+
* Useful for search inputs that should NOT push to URL state on every
|
|
41043
|
+
* keystroke. The DOM input stays controlled by an external `value`
|
|
41044
|
+
* (caller's `useState`); this hook produces a debounced derivation.
|
|
41045
|
+
*/
|
|
41046
|
+
function useDebouncedString(value, delayMs) {
|
|
41047
|
+
const [debounced, setDebounced] = (0, react$1.useState)(value);
|
|
41048
|
+
(0, react$1.useEffect)(() => {
|
|
41049
|
+
const t = setTimeout(() => setDebounced(value), delayMs);
|
|
41050
|
+
return () => clearTimeout(t);
|
|
41051
|
+
}, [value, delayMs]);
|
|
41052
|
+
return debounced;
|
|
41053
|
+
}
|
|
41054
|
+
//#endregion
|
|
41055
|
+
//#region src/composites/log-history-paging.ts
|
|
41056
|
+
/**
|
|
41057
|
+
* Accumulate slices until `limit` entries, `done`, or an abort. An abort keeps
|
|
41058
|
+
* what was already collected and the cursor reached, so "Load older" resumes
|
|
41059
|
+
* where the scan stopped instead of starting again.
|
|
41060
|
+
*/
|
|
41061
|
+
async function collectPage(options) {
|
|
41062
|
+
const { fetchSlice, limit, signal, onProgress } = options;
|
|
41063
|
+
const entries = [];
|
|
41064
|
+
let cursor = options.before;
|
|
41065
|
+
let scannedBytes = 0;
|
|
41066
|
+
let source;
|
|
41067
|
+
for (;;) {
|
|
41068
|
+
if (signal.aborted) return {
|
|
41069
|
+
entries,
|
|
41070
|
+
nextBefore: cursor ?? null,
|
|
41071
|
+
done: false,
|
|
41072
|
+
cancelled: true,
|
|
41073
|
+
scannedBytes,
|
|
41074
|
+
source
|
|
41075
|
+
};
|
|
41076
|
+
let page;
|
|
41077
|
+
try {
|
|
41078
|
+
page = await fetchSlice(cursor, Math.max(1, limit - entries.length), signal);
|
|
41079
|
+
} catch (error) {
|
|
41080
|
+
if (signal.aborted) return {
|
|
41081
|
+
entries,
|
|
41082
|
+
nextBefore: cursor ?? null,
|
|
41083
|
+
done: false,
|
|
41084
|
+
cancelled: true,
|
|
41085
|
+
scannedBytes,
|
|
41086
|
+
source
|
|
41087
|
+
};
|
|
41088
|
+
throw error;
|
|
41089
|
+
}
|
|
41090
|
+
entries.push(...page.entries);
|
|
41091
|
+
scannedBytes += page.scannedBytes;
|
|
41092
|
+
source = page.source;
|
|
41093
|
+
onProgress?.(scannedBytes);
|
|
41094
|
+
const exhausted = page.done || page.nextBefore === null;
|
|
41095
|
+
if (page.cancelled === true) return {
|
|
41096
|
+
entries,
|
|
41097
|
+
nextBefore: page.nextBefore,
|
|
41098
|
+
done: exhausted,
|
|
41099
|
+
cancelled: true,
|
|
41100
|
+
scannedBytes,
|
|
41101
|
+
source
|
|
41102
|
+
};
|
|
41103
|
+
if (exhausted || entries.length >= limit) return {
|
|
41104
|
+
entries,
|
|
41105
|
+
nextBefore: page.nextBefore,
|
|
41106
|
+
done: exhausted,
|
|
41107
|
+
cancelled: false,
|
|
41108
|
+
scannedBytes,
|
|
41109
|
+
source
|
|
41110
|
+
};
|
|
41111
|
+
cursor = page.nextBefore ?? void 0;
|
|
41112
|
+
}
|
|
41113
|
+
}
|
|
41114
|
+
/** Stable identity of an entry: the server id, else the legacy composite key. */
|
|
41115
|
+
function logEntryKey(entry) {
|
|
41116
|
+
if (entry.id !== void 0 && entry.id !== "") return `id:${entry.id}`;
|
|
41117
|
+
return `${entry.timestamp}|${entry.level}|${entry.scope ?? ""}|${entry.message}`;
|
|
41118
|
+
}
|
|
41119
|
+
/**
|
|
41120
|
+
* History (newest first, as paged) merged with live entries (arrival order),
|
|
41121
|
+
* deduped by key — a live entry wins (richer tags/meta) — and returned
|
|
41122
|
+
* chronologically, newest LAST. Ties keep insertion order.
|
|
41123
|
+
*/
|
|
41124
|
+
function mergeLogEntries(history, live) {
|
|
41125
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
41126
|
+
for (const entry of history) byKey.set(logEntryKey(entry), entry);
|
|
41127
|
+
for (const entry of live) byKey.set(logEntryKey(entry), entry);
|
|
41128
|
+
return [...byKey.values()].toSorted((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
41129
|
+
}
|
|
41130
|
+
//#endregion
|
|
41131
|
+
//#region src/hooks/use-log-history.ts
|
|
41132
|
+
/**
|
|
41133
|
+
* useLogHistory — pages the server-served log history (D898).
|
|
41134
|
+
*
|
|
41135
|
+
* The first page and every older page go through `collectPage`, which keeps
|
|
41136
|
+
* calling `logs.query` with `before: nextBefore` while the server answers a
|
|
41137
|
+
* short, not-done slice. Changing the filters aborts whatever is in flight and
|
|
41138
|
+
* restarts from the first page; `cancel()` aborts the scan but keeps what was
|
|
41139
|
+
* found and the cursor reached, so "Load older" resumes from there.
|
|
41140
|
+
*/
|
|
41141
|
+
var INITIAL = {
|
|
41142
|
+
entries: [],
|
|
41143
|
+
nextBefore: null,
|
|
41144
|
+
done: false,
|
|
41145
|
+
status: "loading",
|
|
41146
|
+
scannedBytes: 0,
|
|
41147
|
+
source: void 0,
|
|
41148
|
+
error: null
|
|
41149
|
+
};
|
|
41150
|
+
function useLogHistory(query, limit) {
|
|
41151
|
+
const utils = trpc.useUtils();
|
|
41152
|
+
const [state, setState] = (0, react$1.useState)(INITIAL);
|
|
41153
|
+
const stateRef = (0, react$1.useRef)(state);
|
|
41154
|
+
stateRef.current = state;
|
|
41155
|
+
const queryRef = (0, react$1.useRef)(query);
|
|
41156
|
+
queryRef.current = query;
|
|
41157
|
+
const abortRef = (0, react$1.useRef)(null);
|
|
41158
|
+
const queryKey = JSON.stringify(query);
|
|
41159
|
+
const run = (0, react$1.useCallback)(async (before, reset) => {
|
|
41160
|
+
abortRef.current?.abort();
|
|
41161
|
+
const controller = new AbortController();
|
|
41162
|
+
abortRef.current = controller;
|
|
41163
|
+
setState((prev) => ({
|
|
41164
|
+
...reset ? INITIAL : prev,
|
|
41165
|
+
status: "loading",
|
|
41166
|
+
scannedBytes: 0,
|
|
41167
|
+
error: null
|
|
41168
|
+
}));
|
|
41169
|
+
const fetchSlice = (cursor, remaining, signal) => utils.client.logs.query.query({
|
|
41170
|
+
...queryRef.current,
|
|
41171
|
+
limit: remaining,
|
|
41172
|
+
...cursor !== void 0 ? { before: cursor } : {}
|
|
41173
|
+
}, { signal });
|
|
41174
|
+
try {
|
|
41175
|
+
const out = await collectPage({
|
|
41176
|
+
fetchSlice,
|
|
41177
|
+
limit,
|
|
41178
|
+
...before !== void 0 ? { before } : {},
|
|
41179
|
+
signal: controller.signal,
|
|
41180
|
+
onProgress: (scannedBytes) => {
|
|
41181
|
+
if (abortRef.current === controller) setState((prev) => ({
|
|
41182
|
+
...prev,
|
|
41183
|
+
scannedBytes
|
|
41184
|
+
}));
|
|
41185
|
+
}
|
|
41186
|
+
});
|
|
41187
|
+
if (abortRef.current !== controller) return;
|
|
41188
|
+
setState((prev) => ({
|
|
41189
|
+
entries: reset ? out.entries : [...prev.entries, ...out.entries],
|
|
41190
|
+
nextBefore: out.nextBefore,
|
|
41191
|
+
done: out.done,
|
|
41192
|
+
status: "idle",
|
|
41193
|
+
scannedBytes: out.scannedBytes,
|
|
41194
|
+
source: out.source ?? prev.source,
|
|
41195
|
+
error: null
|
|
41196
|
+
}));
|
|
41197
|
+
} catch (error) {
|
|
41198
|
+
if (abortRef.current !== controller) return;
|
|
41199
|
+
setState((prev) => ({
|
|
41200
|
+
...prev,
|
|
41201
|
+
status: "idle",
|
|
41202
|
+
error: error instanceof Error ? error.message : String(error)
|
|
41203
|
+
}));
|
|
41204
|
+
}
|
|
41205
|
+
}, [utils, limit]);
|
|
41206
|
+
(0, react$1.useEffect)(() => {
|
|
41207
|
+
run(void 0, true);
|
|
41208
|
+
return () => {
|
|
41209
|
+
abortRef.current?.abort();
|
|
41210
|
+
abortRef.current = null;
|
|
41211
|
+
};
|
|
41212
|
+
}, [run, queryKey]);
|
|
41213
|
+
const loadOlder = (0, react$1.useCallback)(() => {
|
|
41214
|
+
const current = stateRef.current;
|
|
41215
|
+
if (current.status === "loading" || current.done) return;
|
|
41216
|
+
run(current.nextBefore ?? void 0, false);
|
|
41217
|
+
}, [run]);
|
|
41218
|
+
const cancel = (0, react$1.useCallback)(() => {
|
|
41219
|
+
abortRef.current?.abort();
|
|
41220
|
+
}, []);
|
|
41221
|
+
return {
|
|
41222
|
+
entries: state.entries,
|
|
41223
|
+
status: state.status,
|
|
41224
|
+
done: state.done,
|
|
41225
|
+
scannedBytes: state.scannedBytes,
|
|
41226
|
+
source: state.source,
|
|
41227
|
+
error: state.error,
|
|
41228
|
+
loadOlder,
|
|
41229
|
+
cancel
|
|
41230
|
+
};
|
|
41231
|
+
}
|
|
39835
41232
|
/**
|
|
39836
41233
|
* True when the viewport is scrolled to (or within `threshold` px of) the
|
|
39837
41234
|
* bottom of its content.
|
|
@@ -39888,12 +41285,6 @@ var LEVEL_STYLES = {
|
|
|
39888
41285
|
color: "text-gray-500"
|
|
39889
41286
|
}
|
|
39890
41287
|
};
|
|
39891
|
-
var LEVEL_SEVERITY = {
|
|
39892
|
-
debug: 0,
|
|
39893
|
-
info: 1,
|
|
39894
|
-
warn: 2,
|
|
39895
|
-
error: 3
|
|
39896
|
-
};
|
|
39897
41288
|
var LEVEL_OPTIONS = [
|
|
39898
41289
|
"debug",
|
|
39899
41290
|
"info",
|
|
@@ -39902,15 +41293,18 @@ var LEVEL_OPTIONS = [
|
|
|
39902
41293
|
];
|
|
39903
41294
|
/** Max live entries kept in the ring buffer. */
|
|
39904
41295
|
var MAX_LIVE_ENTRIES$1 = 500;
|
|
39905
|
-
|
|
39906
|
-
|
|
39907
|
-
|
|
39908
|
-
|
|
39909
|
-
|
|
41296
|
+
/** Text filter debounce before it goes to the server. */
|
|
41297
|
+
var TEXT_DEBOUNCE_MS = 300;
|
|
41298
|
+
/** Distance from the top (px) that asks for the next older page. */
|
|
41299
|
+
var TOP_LOAD_THRESHOLD_PX = 24;
|
|
41300
|
+
var BYTES_PER_MB = 1024 * 1024;
|
|
41301
|
+
function LogStream({ agentId: propsAgentId, addonId: propsAddonId, deviceId: propsDeviceId, containerDeviceId: propsContainerDeviceId, integrationId: propsIntegrationId, requestId: propsRequestId, level: initialLevel, maxHeight = "max-h-96", showScope = false, showDeviceChip = false, showFilters = true, showSourceSelect, limit = 100, liveBuffer: externalBuffer, onClose, className }) {
|
|
39910
41302
|
const t = useUiText();
|
|
41303
|
+
const sourceSelectOn = showSourceSelect ?? showFilters;
|
|
39911
41304
|
const [localAddonId, setLocalAddonId] = (0, react$1.useState)("");
|
|
39912
41305
|
const [localDeviceId, setLocalDeviceId] = (0, react$1.useState)("");
|
|
39913
41306
|
const [levelFilter, setLevelFilter] = (0, react$1.useState)(initialLevel ?? "info");
|
|
41307
|
+
const [sourceChoice, setSourceChoice] = (0, react$1.useState)(void 0);
|
|
39914
41308
|
const agentId = propsAgentId;
|
|
39915
41309
|
const addonId = propsAddonId ?? (localAddonId || void 0);
|
|
39916
41310
|
const deviceId = propsDeviceId ?? (localDeviceId ? Number(localDeviceId) : void 0);
|
|
@@ -39955,48 +41349,63 @@ function LogStream({ agentId: propsAgentId, addonId: propsAddonId, deviceId: pro
|
|
|
39955
41349
|
setExpandAll(false);
|
|
39956
41350
|
}
|
|
39957
41351
|
}, [scopeKey, buffer]);
|
|
39958
|
-
const
|
|
39959
|
-
|
|
39960
|
-
|
|
39961
|
-
|
|
39962
|
-
|
|
41352
|
+
const debouncedText = useDebouncedString(searchText, TEXT_DEBOUNCE_MS).trim();
|
|
41353
|
+
const sourcesQuery = trpc.logs.sources.useQuery(void 0, { staleTime: 6e4 });
|
|
41354
|
+
const readableSources = (0, react$1.useMemo)(() => (sourcesQuery.data ?? []).filter((source) => source.readable), [sourcesQuery.data]);
|
|
41355
|
+
const history = useLogHistory((0, react$1.useMemo)(() => ({
|
|
41356
|
+
...sourceChoice ? { source: sourceChoice } : {},
|
|
41357
|
+
level: levelFilter,
|
|
41358
|
+
...debouncedText ? { text: debouncedText } : {},
|
|
41359
|
+
...tags ? { tags } : {}
|
|
41360
|
+
}), [
|
|
41361
|
+
sourceChoice,
|
|
41362
|
+
levelFilter,
|
|
41363
|
+
debouncedText,
|
|
41364
|
+
tags
|
|
41365
|
+
]), limit);
|
|
41366
|
+
trpc.logs.subscribe.useSubscription({
|
|
41367
|
+
level: levelFilter,
|
|
41368
|
+
...debouncedText ? { text: debouncedText } : {},
|
|
41369
|
+
...tags ? { tags } : {}
|
|
41370
|
+
}, { onData: (entry) => {
|
|
39963
41371
|
buffer.append(entry);
|
|
39964
41372
|
} });
|
|
41373
|
+
const filterKey = `${sourceChoice ?? ""}|${levelFilter}|${debouncedText}`;
|
|
41374
|
+
const prevFilterRef = (0, react$1.useRef)(filterKey);
|
|
41375
|
+
(0, react$1.useEffect)(() => {
|
|
41376
|
+
if (prevFilterRef.current === filterKey) return;
|
|
41377
|
+
prevFilterRef.current = filterKey;
|
|
41378
|
+
buffer.reset();
|
|
41379
|
+
setFollow(true);
|
|
41380
|
+
}, [filterKey, buffer]);
|
|
39965
41381
|
const allLogs = (0, react$1.useMemo)(() => {
|
|
39966
|
-
const
|
|
39967
|
-
|
|
39968
|
-
level: log.level,
|
|
39969
|
-
message: log.message,
|
|
39970
|
-
scope: log.scope,
|
|
39971
|
-
tags: log.tags,
|
|
39972
|
-
meta: log.meta
|
|
39973
|
-
}));
|
|
39974
|
-
const byKey = /* @__PURE__ */ new Map();
|
|
39975
|
-
for (const log of initial) byKey.set(`${log.timestamp}|${log.level}|${log.scope ?? ""}|${log.message}`, log);
|
|
39976
|
-
for (const log of liveLogs) byKey.set(`${log.timestamp}|${log.level}|${log.scope ?? ""}|${log.message}`, log);
|
|
39977
|
-
const merged = [...byKey.values()].toSorted((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
39978
|
-
const levelFiltered = (clearedAt > 0 ? merged.filter((log) => new Date(log.timestamp).getTime() > clearedAt) : merged).filter((log) => passesLevel(log.level, levelFilter));
|
|
39979
|
-
const needle = searchText.trim().toLowerCase();
|
|
39980
|
-
if (!needle) return levelFiltered;
|
|
39981
|
-
return levelFiltered.filter((log) => {
|
|
39982
|
-
if (log.message.toLowerCase().includes(needle)) return true;
|
|
39983
|
-
if (log.scope && log.scope.toLowerCase().includes(needle)) return true;
|
|
39984
|
-
if (log.tags) {
|
|
39985
|
-
for (const v of Object.values(log.tags)) if (v !== void 0 && String(v).toLowerCase().includes(needle)) return true;
|
|
39986
|
-
}
|
|
39987
|
-
if (log.meta) for (const v of Object.values(log.meta)) {
|
|
39988
|
-
if (v === void 0 || v === null) continue;
|
|
39989
|
-
if (formatMetaValue(v).toLowerCase().includes(needle)) return true;
|
|
39990
|
-
}
|
|
39991
|
-
return false;
|
|
39992
|
-
});
|
|
41382
|
+
const merged = mergeLogEntries(history.entries, liveLogs);
|
|
41383
|
+
return clearedAt > 0 ? merged.filter((log) => new Date(log.timestamp).getTime() > clearedAt) : merged;
|
|
39993
41384
|
}, [
|
|
39994
|
-
|
|
41385
|
+
history.entries,
|
|
39995
41386
|
liveLogs,
|
|
39996
|
-
|
|
39997
|
-
clearedAt,
|
|
39998
|
-
searchText
|
|
41387
|
+
clearedAt
|
|
39999
41388
|
]);
|
|
41389
|
+
const prependAnchorRef = (0, react$1.useRef)(null);
|
|
41390
|
+
const requestOlder = (0, react$1.useCallback)(() => {
|
|
41391
|
+
if (history.status === "loading" || history.done) return;
|
|
41392
|
+
const el = scrollRef.current;
|
|
41393
|
+
if (el) prependAnchorRef.current = {
|
|
41394
|
+
height: el.scrollHeight,
|
|
41395
|
+
top: el.scrollTop
|
|
41396
|
+
};
|
|
41397
|
+
history.loadOlder();
|
|
41398
|
+
}, [history]);
|
|
41399
|
+
(0, react$1.useLayoutEffect)(() => {
|
|
41400
|
+
const anchor = prependAnchorRef.current;
|
|
41401
|
+
const el = scrollRef.current;
|
|
41402
|
+
if (!anchor || !el || history.status !== "idle") return;
|
|
41403
|
+
prependAnchorRef.current = null;
|
|
41404
|
+
const next = anchor.top + (el.scrollHeight - anchor.height);
|
|
41405
|
+
if (Math.abs(next - el.scrollTop) < 1) return;
|
|
41406
|
+
programmaticScrollRef.current = true;
|
|
41407
|
+
el.scrollTop = next;
|
|
41408
|
+
}, [history.status, history.entries.length]);
|
|
40000
41409
|
const scrollToBottom = (0, react$1.useCallback)((behavior) => {
|
|
40001
41410
|
const el = scrollRef.current;
|
|
40002
41411
|
if (!el) return;
|
|
@@ -40007,15 +41416,16 @@ function LogStream({ agentId: propsAgentId, addonId: propsAddonId, deviceId: pro
|
|
|
40007
41416
|
});
|
|
40008
41417
|
}, []);
|
|
40009
41418
|
const [hasNewWhilePaused, setHasNewWhilePaused] = (0, react$1.useState)(false);
|
|
40010
|
-
const
|
|
41419
|
+
const lastKey = allLogs.length > 0 ? logEntryKey(allLogs[allLogs.length - 1]) : "";
|
|
41420
|
+
const prevLastKeyRef = (0, react$1.useRef)(lastKey);
|
|
40011
41421
|
(0, react$1.useLayoutEffect)(() => {
|
|
40012
|
-
const
|
|
40013
|
-
|
|
40014
|
-
if (!
|
|
41422
|
+
const newestChanged = lastKey !== "" && lastKey !== prevLastKeyRef.current;
|
|
41423
|
+
prevLastKeyRef.current = lastKey;
|
|
41424
|
+
if (!newestChanged) return;
|
|
40015
41425
|
if (follow) scrollToBottom();
|
|
40016
41426
|
else setHasNewWhilePaused(true);
|
|
40017
41427
|
}, [
|
|
40018
|
-
|
|
41428
|
+
lastKey,
|
|
40019
41429
|
follow,
|
|
40020
41430
|
scrollToBottom
|
|
40021
41431
|
]);
|
|
@@ -40035,8 +41445,9 @@ function LogStream({ agentId: propsAgentId, addonId: propsAddonId, deviceId: pro
|
|
|
40035
41445
|
if (!el) return;
|
|
40036
41446
|
const pinned = isPinnedToBottom(el, 40);
|
|
40037
41447
|
setFollow((prev) => prev === pinned ? prev : pinned);
|
|
40038
|
-
|
|
40039
|
-
|
|
41448
|
+
if (el.scrollTop <= TOP_LOAD_THRESHOLD_PX) requestOlder();
|
|
41449
|
+
}, [requestOlder]);
|
|
41450
|
+
const expandableKeys = (0, react$1.useMemo)(() => allLogs.filter((log) => log.tags && Object.keys(log.tags).length > 0 || log.meta && Object.keys(log.meta).length > 0).map(logEntryKey), [allLogs]);
|
|
40040
41451
|
const toggleExpandAll = (0, react$1.useCallback)(() => {
|
|
40041
41452
|
setExpandAll((prev) => {
|
|
40042
41453
|
if (prev) setExpandedRows(/* @__PURE__ */ new Set());
|
|
@@ -40083,7 +41494,7 @@ function LogStream({ agentId: propsAgentId, addonId: propsAddonId, deviceId: pro
|
|
|
40083
41494
|
setTimeout(() => setCopiedRowKey((prev) => prev === key ? null : prev), 1200);
|
|
40084
41495
|
});
|
|
40085
41496
|
}, []);
|
|
40086
|
-
const rowKey =
|
|
41497
|
+
const rowKey = logEntryKey;
|
|
40087
41498
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
40088
41499
|
className: cn("h-full min-h-0 flex flex-col", className),
|
|
40089
41500
|
onClick: (e) => e.stopPropagation(),
|
|
@@ -40118,6 +41529,23 @@ function LogStream({ agentId: propsAgentId, addonId: propsAddonId, deviceId: pro
|
|
|
40118
41529
|
children: [l, "+"]
|
|
40119
41530
|
}, l))
|
|
40120
41531
|
}),
|
|
41532
|
+
sourceSelectOn && readableSources.length > 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
41533
|
+
className: "w-36",
|
|
41534
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Select, {
|
|
41535
|
+
"aria-label": t("uiLibrary.logStream.source", "Log source"),
|
|
41536
|
+
className: "h-5 text-[10px]",
|
|
41537
|
+
value: sourceChoice ?? readableSources.find((x) => x.isDefault)?.addonId ?? "",
|
|
41538
|
+
onChange: (e) => setSourceChoice(e.target.value),
|
|
41539
|
+
options: readableSources.map((x) => ({
|
|
41540
|
+
value: x.addonId,
|
|
41541
|
+
label: x.addonId
|
|
41542
|
+
}))
|
|
41543
|
+
})
|
|
41544
|
+
}),
|
|
41545
|
+
sourceSelectOn && readableSources.length <= 1 && history.source && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
41546
|
+
className: "text-[9px] text-foreground-subtle",
|
|
41547
|
+
children: t("uiLibrary.logStream.sourceName", "Source: {{name}}", { name: history.source })
|
|
41548
|
+
}),
|
|
40121
41549
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolbarButton, {
|
|
40122
41550
|
icon: ArrowDownToLine,
|
|
40123
41551
|
label: follow ? t("uiLibrary.logStream.following", "Following") : t("uiLibrary.logStream.paused", "Paused"),
|
|
@@ -40216,11 +41644,16 @@ function LogStream({ agentId: propsAgentId, addonId: propsAddonId, deviceId: pro
|
|
|
40216
41644
|
onScroll: handleScroll,
|
|
40217
41645
|
className: cn("flex-1 min-h-0 overflow-auto font-mono text-[10px]", maxHeight),
|
|
40218
41646
|
children: [
|
|
40219
|
-
|
|
40220
|
-
|
|
40221
|
-
|
|
41647
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(HistoryEdge, {
|
|
41648
|
+
status: history.status,
|
|
41649
|
+
done: history.done,
|
|
41650
|
+
scannedBytes: history.scannedBytes,
|
|
41651
|
+
hasRows: allLogs.length > 0,
|
|
41652
|
+
error: history.error,
|
|
41653
|
+
onLoadOlder: requestOlder,
|
|
41654
|
+
onCancel: history.cancel
|
|
40222
41655
|
}),
|
|
40223
|
-
|
|
41656
|
+
history.status === "idle" && history.done && allLogs.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
40224
41657
|
className: "py-4 text-center text-foreground-subtle",
|
|
40225
41658
|
children: t("uiLibrary.logStream.noLogs", "No logs found")
|
|
40226
41659
|
}),
|
|
@@ -40344,6 +41777,49 @@ function DeviceChip({ deviceId, deviceName }) {
|
|
|
40344
41777
|
children: label
|
|
40345
41778
|
});
|
|
40346
41779
|
}
|
|
41780
|
+
/**
|
|
41781
|
+
* The top of the list: the state of the older-history scan. A scan that is
|
|
41782
|
+
* still looking (short page, source not done) says how much it has read and
|
|
41783
|
+
* can be cancelled; the end of the source is stated quietly.
|
|
41784
|
+
*/
|
|
41785
|
+
function HistoryEdge({ status, done, scannedBytes, hasRows, error, onLoadOlder, onCancel }) {
|
|
41786
|
+
const t = useUiText();
|
|
41787
|
+
if (status === "loading") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
41788
|
+
className: "flex items-center justify-center gap-2 py-2 text-foreground-subtle",
|
|
41789
|
+
children: [
|
|
41790
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoaderCircle, { className: "h-3.5 w-3.5 animate-spin" }),
|
|
41791
|
+
scannedBytes > 0 ? t("uiLibrary.logStream.searching", "Searching older logs… ({{mb}} MB scanned)", { mb: (scannedBytes / BYTES_PER_MB).toFixed(1) }) : t("uiLibrary.logStream.loading", "Loading logs..."),
|
|
41792
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
41793
|
+
variant: "outline",
|
|
41794
|
+
size: "sm",
|
|
41795
|
+
onClick: onCancel,
|
|
41796
|
+
children: t("uiLibrary.logStream.cancel", "Cancel")
|
|
41797
|
+
})
|
|
41798
|
+
]
|
|
41799
|
+
});
|
|
41800
|
+
if (error) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
41801
|
+
className: "flex items-center justify-center gap-2 py-2 text-danger",
|
|
41802
|
+
children: [error, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
41803
|
+
variant: "outline",
|
|
41804
|
+
size: "sm",
|
|
41805
|
+
onClick: onLoadOlder,
|
|
41806
|
+
children: t("uiLibrary.logStream.retry", "Retry")
|
|
41807
|
+
})]
|
|
41808
|
+
});
|
|
41809
|
+
if (done) return hasRows ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
41810
|
+
className: "py-2 text-center text-foreground-subtle",
|
|
41811
|
+
children: t("uiLibrary.logStream.beginning", "Beginning of logs")
|
|
41812
|
+
}) : null;
|
|
41813
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
41814
|
+
className: "flex justify-center py-1.5",
|
|
41815
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
41816
|
+
variant: "outline",
|
|
41817
|
+
size: "sm",
|
|
41818
|
+
onClick: onLoadOlder,
|
|
41819
|
+
children: t("uiLibrary.logStream.loadOlder", "Load older")
|
|
41820
|
+
})
|
|
41821
|
+
});
|
|
41822
|
+
}
|
|
40347
41823
|
function ScopeBadge$2({ label, value }) {
|
|
40348
41824
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
40349
41825
|
className: "text-[9px] px-1.5 py-0.5 rounded-full bg-primary/10 text-primary font-medium",
|
|
@@ -51147,7 +52623,7 @@ function AlarmSegments({ state, availableModes, busy, onDisarm, onArm }) {
|
|
|
51147
52623
|
}, mode))]
|
|
51148
52624
|
});
|
|
51149
52625
|
}
|
|
51150
|
-
function messageOf(err) {
|
|
52626
|
+
function messageOf$1(err) {
|
|
51151
52627
|
return err instanceof Error ? err.message : String(err);
|
|
51152
52628
|
}
|
|
51153
52629
|
/**
|
|
@@ -51189,7 +52665,7 @@ function useAlarmControl(trpc, deviceId, optimistic) {
|
|
|
51189
52665
|
cap.disarm({}).catch((err) => {
|
|
51190
52666
|
if (latestCallRef.current !== call) return;
|
|
51191
52667
|
rollback();
|
|
51192
|
-
setError(messageOf(err));
|
|
52668
|
+
setError(messageOf$1(err));
|
|
51193
52669
|
});
|
|
51194
52670
|
};
|
|
51195
52671
|
const handleArm = (mode) => {
|
|
@@ -51199,7 +52675,7 @@ function useAlarmControl(trpc, deviceId, optimistic) {
|
|
|
51199
52675
|
cap.arm({ mode }).catch((err) => {
|
|
51200
52676
|
if (latestCallRef.current !== call) return;
|
|
51201
52677
|
rollback();
|
|
51202
|
-
setError(messageOf(err));
|
|
52678
|
+
setError(messageOf$1(err));
|
|
51203
52679
|
});
|
|
51204
52680
|
};
|
|
51205
52681
|
return {
|
|
@@ -52387,24 +53863,6 @@ function DeviceBatchActionsBar(props) {
|
|
|
52387
53863
|
})] });
|
|
52388
53864
|
}
|
|
52389
53865
|
//#endregion
|
|
52390
|
-
//#region src/hooks/use-debounced-string.ts
|
|
52391
|
-
/**
|
|
52392
|
-
* useDebouncedString — return `value` after `delay` ms have passed
|
|
52393
|
-
* without further changes. Resets the timer on every change.
|
|
52394
|
-
*
|
|
52395
|
-
* Useful for search inputs that should NOT push to URL state on every
|
|
52396
|
-
* keystroke. The DOM input stays controlled by an external `value`
|
|
52397
|
-
* (caller's `useState`); this hook produces a debounced derivation.
|
|
52398
|
-
*/
|
|
52399
|
-
function useDebouncedString(value, delayMs) {
|
|
52400
|
-
const [debounced, setDebounced] = (0, react$1.useState)(value);
|
|
52401
|
-
(0, react$1.useEffect)(() => {
|
|
52402
|
-
const t = setTimeout(() => setDebounced(value), delayMs);
|
|
52403
|
-
return () => clearTimeout(t);
|
|
52404
|
-
}, [value, delayMs]);
|
|
52405
|
-
return debounced;
|
|
52406
|
-
}
|
|
52407
|
-
//#endregion
|
|
52408
53866
|
//#region src/hooks/use-persisted-page-size.ts
|
|
52409
53867
|
/**
|
|
52410
53868
|
* usePersistedPageSize — a "rows per page" choice remembered per viewer.
|
|
@@ -63810,6 +65268,1849 @@ function RetrainReviewBadge({ kind, approximate = false, className, ...props })
|
|
|
63810
65268
|
});
|
|
63811
65269
|
}
|
|
63812
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
|
|
63813
67114
|
//#region src/contexts/zone-editing.tsx
|
|
63814
67115
|
/**
|
|
63815
67116
|
* Per-device zone-editing state shared between the player overlay
|
|
@@ -64669,6 +67970,7 @@ exports.AudioWaveform = AudioWaveform;
|
|
|
64669
67970
|
exports.AutomationHeroCard = AutomationHeroCard;
|
|
64670
67971
|
exports.AutomationInlineControl = AutomationInlineControl;
|
|
64671
67972
|
exports.AutotrackSection = AutotrackSection;
|
|
67973
|
+
exports.BOOLEAN_STATES = BOOLEAN_STATES;
|
|
64672
67974
|
exports.BTN_COMPACT = BTN_COMPACT;
|
|
64673
67975
|
exports.BTN_COMPACT_DANGER = BTN_COMPACT_DANGER;
|
|
64674
67976
|
exports.BTN_COMPACT_PRIMARY = BTN_COMPACT_PRIMARY;
|
|
@@ -64677,6 +67979,7 @@ exports.Baby = Baby;
|
|
|
64677
67979
|
exports.BackToStepsButton = BackToStepsButton;
|
|
64678
67980
|
exports.Badge = Badge;
|
|
64679
67981
|
exports.Ban = Ban;
|
|
67982
|
+
exports.BarSeries = BarSeries;
|
|
64680
67983
|
exports.Bath = Bath;
|
|
64681
67984
|
exports.Battery = Battery;
|
|
64682
67985
|
exports.BatteryBadge = BatteryBadge;
|
|
@@ -64701,6 +68004,7 @@ exports.Button = Button;
|
|
|
64701
68004
|
exports.ButtonControl = ButtonControl;
|
|
64702
68005
|
exports.ButtonHeroCard = ButtonHeroCard;
|
|
64703
68006
|
exports.CENTER = CENTER;
|
|
68007
|
+
exports.CHART_SERIES_TOKENS = CHART_SERIES_TOKENS;
|
|
64704
68008
|
exports.CHIP_ACTIVE = CHIP_ACTIVE;
|
|
64705
68009
|
exports.CHIP_BASE = CHIP_BASE;
|
|
64706
68010
|
exports.CHIP_INACTIVE = CHIP_INACTIVE;
|
|
@@ -64718,6 +68022,11 @@ exports.Car = Car;
|
|
|
64718
68022
|
exports.Card = Card;
|
|
64719
68023
|
exports.Cast = Cast;
|
|
64720
68024
|
exports.Cat = Cat;
|
|
68025
|
+
exports.ChartFrame = ChartFrame;
|
|
68026
|
+
exports.ChartLine = ChartLine;
|
|
68027
|
+
exports.ChartPlaceholder = ChartPlaceholder;
|
|
68028
|
+
exports.ChartSwatch = ChartSwatch;
|
|
68029
|
+
exports.ChartWindowToggle = ChartWindowToggle;
|
|
64721
68030
|
exports.Check = Check;
|
|
64722
68031
|
exports.Checkbox = Checkbox;
|
|
64723
68032
|
exports.ChevronDown = ChevronDown;
|
|
@@ -64800,6 +68109,7 @@ exports.DeviceExportPanel = DeviceExportPanel;
|
|
|
64800
68109
|
exports.DeviceExportSetup = DeviceExportSetup;
|
|
64801
68110
|
exports.DeviceFilterBar = DeviceFilterBar;
|
|
64802
68111
|
exports.DeviceGrid = DeviceGrid;
|
|
68112
|
+
exports.DeviceHistoryView = DeviceHistoryView;
|
|
64803
68113
|
exports.DeviceItem = DeviceItem;
|
|
64804
68114
|
exports.DeviceList = DeviceList;
|
|
64805
68115
|
exports.DeviceMultiSelectField = DeviceMultiSelectField;
|
|
@@ -64834,6 +68144,7 @@ exports.Droplets = Droplets;
|
|
|
64834
68144
|
exports.DummyHeroCard = DummyHeroCard;
|
|
64835
68145
|
exports.DummyInline = DummyInline;
|
|
64836
68146
|
exports.EMPTY_DATA_TABLE_FILTER = EMPTY_DATA_TABLE_FILTER;
|
|
68147
|
+
exports.EMPTY_HISTORY_PANELS = EMPTY_HISTORY_PANELS;
|
|
64837
68148
|
exports.EVENT_KIND_ICONS = EVENT_KIND_ICONS;
|
|
64838
68149
|
exports.EllipsisVertical = EllipsisVertical;
|
|
64839
68150
|
exports.EmptyState = EmptyState;
|
|
@@ -64869,6 +68180,7 @@ exports.FloatingPanel = FloatingPanel;
|
|
|
64869
68180
|
exports.Footprints = Footprints;
|
|
64870
68181
|
exports.FormField = FormField$1;
|
|
64871
68182
|
exports.Funnel = Funnel;
|
|
68183
|
+
exports.GAP_INTERVAL_FACTOR = GAP_INTERVAL_FACTOR;
|
|
64872
68184
|
exports.GRID_GAP = GRID_GAP;
|
|
64873
68185
|
exports.GRID_PAIRED = GRID_PAIRED;
|
|
64874
68186
|
exports.GRID_QUICK_STATS = GRID_QUICK_STATS;
|
|
@@ -64882,6 +68194,11 @@ exports.GripHorizontal = GripHorizontal;
|
|
|
64882
68194
|
exports.GripTrack = GripTrack;
|
|
64883
68195
|
exports.GroupedModelSelector = GroupedModelSelector;
|
|
64884
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;
|
|
64885
68202
|
exports.HOST_WIDGETS = HOST_WIDGETS;
|
|
64886
68203
|
exports.HUB_SESSION_RUN_MS = HUB_SESSION_RUN_MS;
|
|
64887
68204
|
exports.Hammer = Hammer;
|
|
@@ -64889,6 +68206,9 @@ exports.Hand = Hand;
|
|
|
64889
68206
|
exports.HardDrive = HardDrive;
|
|
64890
68207
|
exports.Hash = Hash;
|
|
64891
68208
|
exports.Heater = Heater;
|
|
68209
|
+
exports.History = History;
|
|
68210
|
+
exports.HistoryChart = HistoryChart;
|
|
68211
|
+
exports.HistoryWindowBar = HistoryWindowBar;
|
|
64892
68212
|
exports.HlsVideo = HlsVideo;
|
|
64893
68213
|
exports.House = House;
|
|
64894
68214
|
exports.HoverZoomImage = HoverZoomImage;
|
|
@@ -64974,6 +68294,7 @@ exports.NodePicker = NodePicker;
|
|
|
64974
68294
|
exports.NodeSelectField = NodeSelectField;
|
|
64975
68295
|
exports.NotificationTargetSelectField = NotificationTargetSelectField;
|
|
64976
68296
|
exports.NotificationTargetsField = NotificationTargetsField;
|
|
68297
|
+
exports.OCCUPANCY_KEY = OCCUPANCY_KEY;
|
|
64977
68298
|
exports.OPTION_SELECT_SEARCH_MIN_OPTIONS = OPTION_SELECT_SEARCH_MIN_OPTIONS;
|
|
64978
68299
|
exports.OfflineBadge = OfflineBadge;
|
|
64979
68300
|
exports.OptionSelect = OptionSelect;
|
|
@@ -65029,6 +68350,7 @@ exports.QrCode = QrCode;
|
|
|
65029
68350
|
exports.QrCode$1 = QrCode$1;
|
|
65030
68351
|
exports.RECONNECT_POLICY = RECONNECT_POLICY;
|
|
65031
68352
|
exports.RECORDED_PLAYBACK_MODES = RECORDED_PLAYBACK_MODES;
|
|
68353
|
+
exports.RESOLUTION_BUCKET_MS = RESOLUTION_BUCKET_MS;
|
|
65032
68354
|
exports.RIGHT = RIGHT;
|
|
65033
68355
|
exports.ROI_AREA_WARN_FRACTION = ROI_AREA_WARN_FRACTION;
|
|
65034
68356
|
exports.ROLE_DESCRIPTOR = ROLE_DESCRIPTOR;
|
|
@@ -65116,11 +68438,13 @@ exports.SnapshotButton = SnapshotButton;
|
|
|
65116
68438
|
exports.Snowflake = Snowflake;
|
|
65117
68439
|
exports.SortHeaderButton = SortHeaderButton;
|
|
65118
68440
|
exports.Sparkles = Sparkles;
|
|
68441
|
+
exports.Sparkline = Sparkline;
|
|
65119
68442
|
exports.Sprout = Sprout;
|
|
65120
68443
|
exports.Square = Square;
|
|
65121
68444
|
exports.StableTabPanel = StableTabPanel;
|
|
65122
68445
|
exports.Star = Star;
|
|
65123
68446
|
exports.StatCard = StatCard;
|
|
68447
|
+
exports.StateTimeline = StateTimeline;
|
|
65124
68448
|
exports.StateValuesStream = StateValuesStream;
|
|
65125
68449
|
exports.StatusBadge = StatusBadge;
|
|
65126
68450
|
exports.StepDetailDock = StepDetailDock;
|
|
@@ -65158,6 +68482,7 @@ exports.ThemeProvider = require_theme_index.ThemeProvider$1;
|
|
|
65158
68482
|
exports.Thermometer = Thermometer;
|
|
65159
68483
|
exports.ThermostatHeroCard = ThermostatHeroCard;
|
|
65160
68484
|
exports.ThermostatInlineControl = ThermostatInlineControl;
|
|
68485
|
+
exports.TimeSeriesChart = TimeSeriesChart;
|
|
65161
68486
|
exports.Timer = Timer;
|
|
65162
68487
|
exports.TimezoneSelector = TimezoneSelector;
|
|
65163
68488
|
exports.ToggleLeft = ToggleLeft;
|
|
@@ -65220,22 +68545,31 @@ exports.ZoomOut = ZoomOut;
|
|
|
65220
68545
|
exports.acceleratorLabel = acceleratorLabel;
|
|
65221
68546
|
exports.acceleratorShortLabel = acceleratorShortLabel;
|
|
65222
68547
|
exports.agentColumnKey = agentColumnKey;
|
|
68548
|
+
exports.alignSeries = alignSeries;
|
|
65223
68549
|
exports.allDeviceTypeFilterOptions = allDeviceTypeFilterOptions;
|
|
68550
|
+
exports.anchorNow = anchorNow;
|
|
65224
68551
|
exports.applyFleetQueryDefaults = applyFleetQueryDefaults;
|
|
65225
68552
|
exports.applyPickerFilters = applyPickerFilters;
|
|
65226
68553
|
exports.ariaSortForColumn = ariaSortForColumn;
|
|
65227
68554
|
exports.assignNodeColorId = _camstack_types.assignNodeColorId;
|
|
68555
|
+
exports.axisTicks = axisTicks;
|
|
65228
68556
|
exports.badgeFor = badgeFor;
|
|
65229
68557
|
exports.badgeFor$1 = badgeFor;
|
|
65230
68558
|
exports.bindsToModel = bindsToModel;
|
|
68559
|
+
exports.booleanLabels = booleanLabels;
|
|
68560
|
+
exports.bucketTotals = bucketTotals;
|
|
65231
68561
|
exports.buildColorByNodeId = buildColorByNodeId;
|
|
65232
68562
|
exports.buildDataRecordRows = buildDataRecordRows;
|
|
65233
68563
|
exports.buildPeerConnectionConfig = buildPeerConnectionConfig;
|
|
65234
68564
|
exports.buildStepTreeFromSchema = buildStepTreeFromSchema;
|
|
68565
|
+
exports.buildTimeSeriesModel = buildTimeSeriesModel;
|
|
65235
68566
|
exports.catalogHasDraft = catalogHasDraft;
|
|
68567
|
+
exports.chartKeys = chartKeys;
|
|
68568
|
+
exports.chartSeriesStyle = chartSeriesStyle;
|
|
65236
68569
|
exports.childEntityId = childEntityId;
|
|
65237
68570
|
exports.childListName = childListName;
|
|
65238
68571
|
exports.chipOverflow = chipOverflow;
|
|
68572
|
+
exports.chunk = chunk;
|
|
65239
68573
|
exports.classifyAddressScope = classifyAddressScope;
|
|
65240
68574
|
exports.classifyCapability = classifyCapability;
|
|
65241
68575
|
exports.classifySessionVerdict = classifySessionVerdict;
|
|
@@ -65263,6 +68597,9 @@ exports.dataRecordSearchText = dataRecordSearchText;
|
|
|
65263
68597
|
exports.dataRecordType = dataRecordType;
|
|
65264
68598
|
exports.dataTableFacetGroups = dataTableFacetGroups;
|
|
65265
68599
|
exports.dataTableFacets = dataTableFacets;
|
|
68600
|
+
exports.decodeCustomWindow = decodeCustomWindow;
|
|
68601
|
+
exports.decodeSeriesRef = decodeSeriesRef;
|
|
68602
|
+
exports.defaultAxisSpec = defaultAxisSpec;
|
|
65266
68603
|
exports.defaultOfField = defaultOfField;
|
|
65267
68604
|
exports.defaultSelectClusterRoots = defaultSelectClusterRoots;
|
|
65268
68605
|
exports.defaultSingleClusterRoot = defaultSingleClusterRoot;
|
|
@@ -65286,10 +68623,14 @@ exports.draftFromQuery = draftFromQuery;
|
|
|
65286
68623
|
exports.effectivePageSize = effectivePageSize;
|
|
65287
68624
|
exports.emptyDevicesWidens = emptyDevicesWidens;
|
|
65288
68625
|
exports.emptySessionRun = emptySessionRun;
|
|
68626
|
+
exports.encodeCustomWindow = encodeCustomWindow;
|
|
68627
|
+
exports.encodeSeriesRef = encodeSeriesRef;
|
|
68628
|
+
exports.energyBuckets = energyBuckets;
|
|
65289
68629
|
exports.ensureMfHostInit = ensureMfHostInit;
|
|
65290
68630
|
exports.eventKindLabel = eventKindLabel;
|
|
65291
68631
|
exports.filterDataTableRows = filterDataTableRows;
|
|
65292
68632
|
exports.filterDeviceOptions = filterDeviceOptions;
|
|
68633
|
+
exports.filterOccupancyLines = filterOccupancyLines;
|
|
65293
68634
|
exports.filterOptionSelectOptions = filterOptions;
|
|
65294
68635
|
exports.filterPickerDevices = filterPickerDevices;
|
|
65295
68636
|
exports.finalizeSessionRun = finalizeSessionRun;
|
|
@@ -65297,25 +68638,34 @@ exports.findTimezone = _camstack_types.findTimezone;
|
|
|
65297
68638
|
exports.flattenPickerGrouping = flattenPickerGrouping;
|
|
65298
68639
|
exports.flattenPickerRows = flattenPickerRows;
|
|
65299
68640
|
exports.fleetViewOf = fleetViewOf;
|
|
68641
|
+
exports.formatChartBytes = formatChartBytes;
|
|
68642
|
+
exports.formatChartNumber = formatChartNumber;
|
|
68643
|
+
exports.formatChartValue = formatChartValue;
|
|
65300
68644
|
exports.formatControlDateTime = formatControlDateTime;
|
|
65301
68645
|
exports.formatLastSeen = formatLastSeen;
|
|
65302
68646
|
exports.formatNumeric = formatNumeric;
|
|
65303
68647
|
exports.formatSettingValue = formatSettingValue;
|
|
68648
|
+
exports.formatSpan = formatSpan;
|
|
68649
|
+
exports.formatTimeLabel = formatTimeLabel;
|
|
65304
68650
|
exports.freezeVerdict = freezeVerdict;
|
|
65305
68651
|
exports.fuzzyMatch = fuzzyMatch;
|
|
68652
|
+
exports.gapThresholdMs = gapThresholdMs;
|
|
65306
68653
|
exports.getClassColor = getClassColor;
|
|
65307
68654
|
exports.getPhaseVisual = getPhaseVisual;
|
|
65308
68655
|
exports.groupAgentColumns = groupAgentColumns;
|
|
65309
68656
|
exports.groupChildrenByLayout = groupChildrenByLayout;
|
|
68657
|
+
exports.groupDeviceSeries = groupDeviceSeries;
|
|
65310
68658
|
exports.groupPickerDevices = groupPickerDevices;
|
|
65311
68659
|
exports.groupPickerDevicesBy = groupPickerDevicesBy;
|
|
65312
68660
|
exports.hardwareLabel = hardwareLabel;
|
|
68661
|
+
exports.historyPageHref = historyPageHref;
|
|
65313
68662
|
exports.hubOrigin = hubOrigin;
|
|
65314
68663
|
exports.hubPath = hubPath;
|
|
65315
68664
|
exports.hubPath$1 = hubPath;
|
|
65316
68665
|
exports.hubUrl = hubUrl;
|
|
65317
68666
|
exports.hubWsUrl = hubWsUrl;
|
|
65318
68667
|
exports.humanDuration = humanDuration;
|
|
68668
|
+
exports.humanizeName = humanizeName;
|
|
65319
68669
|
exports.humidifierTint = humidifierTint;
|
|
65320
68670
|
exports.iconGlyphsFailed = iconGlyphsFailed;
|
|
65321
68671
|
exports.iconGlyphsLoaded = iconGlyphsLoaded;
|
|
@@ -65323,15 +68673,19 @@ exports.initialScrubState = initialScrubState;
|
|
|
65323
68673
|
exports.insertAtSelection = insertAtSelection;
|
|
65324
68674
|
exports.isAbsentProvider = isAbsentProvider;
|
|
65325
68675
|
exports.isAbsentProvider$1 = isAbsentProvider;
|
|
68676
|
+
exports.isByteUnit = isByteUnit;
|
|
65326
68677
|
exports.isDataTableFilterActive = isDataTableFilterActive;
|
|
65327
68678
|
exports.isFieldVisible = isFieldVisible;
|
|
65328
68679
|
exports.isGroupExpanded = isGroupExpanded;
|
|
68680
|
+
exports.isHistoryWindowId = isHistoryWindowId;
|
|
65329
68681
|
exports.isKeychainFieldValue = isKeychainFieldValue;
|
|
65330
68682
|
exports.isRedactedSecret = isRedactedSecret;
|
|
68683
|
+
exports.isRefusal = isRefusal;
|
|
65331
68684
|
exports.isSecretField = isSecretField;
|
|
65332
68685
|
exports.isValueField = isValueField;
|
|
65333
68686
|
exports.lawnMowerActivityMeta = lawnMowerActivityMeta;
|
|
65334
68687
|
exports.lightColors = require_theme_to_root_css.lightColors;
|
|
68688
|
+
exports.linePoints = linePoints;
|
|
65335
68689
|
exports.loadIconGlyphs = loadIconGlyphs;
|
|
65336
68690
|
exports.loadRemoteBundle = loadRemoteBundle;
|
|
65337
68691
|
exports.lucideExportName = lucideExportName;
|
|
@@ -65348,11 +68702,16 @@ exports.nextReconnectAction = nextReconnectAction;
|
|
|
65348
68702
|
exports.nextSort = nextSort;
|
|
65349
68703
|
exports.nextSortDirection = nextSortDirection;
|
|
65350
68704
|
exports.nextTableSort = nextTableSort;
|
|
68705
|
+
exports.niceMax = niceMax;
|
|
68706
|
+
exports.niceRange = niceRange;
|
|
65351
68707
|
exports.nodeColorClass = nodeColorClass;
|
|
65352
68708
|
exports.nodeSoftBgClass = nodeSoftBgClass;
|
|
65353
68709
|
exports.nodeTextColorClass = nodeTextColorClass;
|
|
65354
68710
|
exports.normalizeForSearch = normalizeForSearch;
|
|
65355
68711
|
exports.normalizeLanguage = normalizeLanguage;
|
|
68712
|
+
exports.normalizeSelection = normalizeSelection;
|
|
68713
|
+
exports.occupancyFacets = occupancyFacets;
|
|
68714
|
+
exports.occupancyLines = occupancyLines;
|
|
65356
68715
|
exports.offListSelections = offListSelections;
|
|
65357
68716
|
exports.openTopLayer = openTopLayer;
|
|
65358
68717
|
exports.optionSelectSearchText = optionSearchText;
|
|
@@ -65362,8 +68721,10 @@ exports.pageRows = pageRows;
|
|
|
65362
68721
|
exports.pageSizeLabel = pageSizeLabel;
|
|
65363
68722
|
exports.paginateRows = paginateRows;
|
|
65364
68723
|
exports.parentDetailOf = parentDetailOf;
|
|
68724
|
+
exports.parseCapKey = parseCapKey;
|
|
65365
68725
|
exports.parseIceCandidate = parseIceCandidate;
|
|
65366
68726
|
exports.parsePickerGroupBy = parsePickerGroupBy;
|
|
68727
|
+
exports.parseProducerKey = parseProducerKey;
|
|
65367
68728
|
exports.parseRecordedServerMessage = parseRecordedServerMessage;
|
|
65368
68729
|
exports.partitionOptionSelectSelected = partitionSelected;
|
|
65369
68730
|
exports.partitionSuggested = partitionSuggested;
|
|
@@ -65380,7 +68741,11 @@ exports.pickerSearchText = pickerSearchText;
|
|
|
65380
68741
|
exports.pickerZoneFilterSuspended = pickerZoneFilterSuspended;
|
|
65381
68742
|
exports.pinnedStreamChoices = pinnedStreamChoices;
|
|
65382
68743
|
exports.placePopover = placePopover;
|
|
68744
|
+
exports.previousWindow = previousWindow;
|
|
65383
68745
|
exports.providerIcons = providerIcons;
|
|
68746
|
+
exports.readHistory = readHistory;
|
|
68747
|
+
exports.readHistoryPanels = readHistoryPanels;
|
|
68748
|
+
exports.readItemId = readItemId;
|
|
65384
68749
|
exports.readStoredGroupBy = readStoredGroupBy;
|
|
65385
68750
|
exports.reduceSessionRun = reduceSessionRun;
|
|
65386
68751
|
exports.registerAddonBundle = registerAddonBundle;
|
|
@@ -65396,6 +68761,7 @@ exports.resolveDisplayIcon = resolveDisplayIcon;
|
|
|
65396
68761
|
exports.resolveEffectiveDefaultModel = resolveEffectiveDefaultModel;
|
|
65397
68762
|
exports.resolveEffectiveStepModel = resolveEffectiveStepModel;
|
|
65398
68763
|
exports.resolveEventKindIcon = resolveEventKindIcon;
|
|
68764
|
+
exports.resolveHistoryWindow = resolveHistoryWindow;
|
|
65399
68765
|
exports.resolveOptionSelectSearchable = resolveSearchable;
|
|
65400
68766
|
exports.resolveParentNames = resolveParentNames;
|
|
65401
68767
|
exports.resolvePrimaryChild = resolvePrimaryChild;
|
|
@@ -65412,10 +68778,14 @@ exports.scrubReducer = scrubReducer;
|
|
|
65412
68778
|
exports.searchSteps = searchSteps;
|
|
65413
68779
|
exports.secretDisplayValue = secretDisplayValue;
|
|
65414
68780
|
exports.secretSubmitValue = secretSubmitValue;
|
|
68781
|
+
exports.segmentsFromChanges = segmentsFromChanges;
|
|
65415
68782
|
exports.selectPickerGroup = selectPickerGroup;
|
|
65416
68783
|
exports.selectedDeviceOptions = selectedDeviceOptions;
|
|
65417
68784
|
exports.serializeRecordedCommand = serializeRecordedCommand;
|
|
68785
|
+
exports.seriesRefId = seriesRefId;
|
|
65418
68786
|
exports.settingsModelIdFor = settingsModelIdFor;
|
|
68787
|
+
exports.sharedGapSpans = sharedGapSpans;
|
|
68788
|
+
exports.shiftPoints = shiftPoints;
|
|
65419
68789
|
exports.shouldCommit = shouldCommit;
|
|
65420
68790
|
exports.shouldEmit = shouldEmit;
|
|
65421
68791
|
exports.shouldShowAddNewFooter = shouldShowAddNewFooter;
|
|
@@ -65424,6 +68794,11 @@ exports.shouldUseSingleNode = shouldUseSingleNode;
|
|
|
65424
68794
|
exports.showsNoCamerasHint = showsNoCamerasHint;
|
|
65425
68795
|
exports.sortRows = sortRows;
|
|
65426
68796
|
exports.sortRowsByColumn = sortRowsByColumn;
|
|
68797
|
+
exports.stackBuckets = stackBuckets;
|
|
68798
|
+
exports.stackSeries = stackSeries;
|
|
68799
|
+
exports.stateAt = stateAt;
|
|
68800
|
+
exports.stateRow = stateRow;
|
|
68801
|
+
exports.stateStyle = stateStyle;
|
|
65427
68802
|
exports.statusIcons = statusIcons;
|
|
65428
68803
|
exports.statusLine = statusLine;
|
|
65429
68804
|
exports.statusLine$1 = statusLine;
|
|
@@ -65433,10 +68808,13 @@ exports.stepLocalFields = stepLocalFields;
|
|
|
65433
68808
|
exports.stepModelOptions = stepModelOptions;
|
|
65434
68809
|
exports.stripParentNamePrefix = stripParentNamePrefix;
|
|
65435
68810
|
exports.summarizeHubCandidates = summarizeHubCandidates;
|
|
68811
|
+
exports.swatchFillClass = swatchFillClass;
|
|
68812
|
+
exports.swatchLineClass = swatchLineClass;
|
|
65436
68813
|
exports.tabForSelector = tabForSelector;
|
|
65437
68814
|
exports.tankAlert = tankAlert;
|
|
65438
68815
|
exports.themeToCss = require_theme_to_root_css.themeToCss;
|
|
65439
68816
|
exports.themeToRootCss = require_theme_to_root_css.themeToRootCss;
|
|
68817
|
+
exports.timeInState = timeInState;
|
|
65440
68818
|
exports.toStationaryOverlayBox = toStationaryOverlayBox;
|
|
65441
68819
|
exports.toggleDataTableFacet = toggleDataTableFacet;
|
|
65442
68820
|
exports.toggleDataTableGroupFacet = toggleDataTableGroupFacet;
|
|
@@ -65446,6 +68824,7 @@ exports.trainingRefusalText = trainingRefusalText;
|
|
|
65446
68824
|
exports.translateAddonKey = translateAddonKey;
|
|
65447
68825
|
exports.trpc = trpc;
|
|
65448
68826
|
exports.typeLabel = typeLabel;
|
|
68827
|
+
exports.unitAxes = unitAxes;
|
|
65449
68828
|
exports.useAccessoriesGetStatus = useAccessoriesGetStatus;
|
|
65450
68829
|
exports.useAccessoriesSetChildHidden = useAccessoriesSetChildHidden;
|
|
65451
68830
|
exports.useAddonPagesListPages = useAddonPagesListPages;
|
|
@@ -65703,6 +69082,7 @@ exports.useDeviceHistoryGetStatus = useDeviceHistoryGetStatus;
|
|
|
65703
69082
|
exports.useDeviceHistoryListNodeSeries = useDeviceHistoryListNodeSeries;
|
|
65704
69083
|
exports.useDeviceHistoryListSeries = useDeviceHistoryListSeries;
|
|
65705
69084
|
exports.useDeviceHistoryQuery = useDeviceHistoryQuery;
|
|
69085
|
+
exports.useDeviceHistoryQueryMany = useDeviceHistoryQueryMany;
|
|
65706
69086
|
exports.useDeviceHistoryQueryNode = useDeviceHistoryQueryNode;
|
|
65707
69087
|
exports.useDeviceHistoryRecord = useDeviceHistoryRecord;
|
|
65708
69088
|
exports.useDeviceHistoryRecordBuckets = useDeviceHistoryRecordBuckets;
|
|
@@ -65826,6 +69206,7 @@ exports.useDeviceProviderTestCreationField = useDeviceProviderTestCreationField;
|
|
|
65826
69206
|
exports.useDeviceProxy = useDeviceProxy;
|
|
65827
69207
|
exports.useDeviceProxy$1 = useDeviceProxy;
|
|
65828
69208
|
exports.useDeviceQuickDetailOpen = useDeviceQuickDetailOpen;
|
|
69209
|
+
exports.useDeviceSeriesList = useDeviceSeriesList;
|
|
65829
69210
|
exports.useDeviceSnapshot = useDeviceSnapshot;
|
|
65830
69211
|
exports.useDeviceSnapshotImage = useDeviceSnapshotImage;
|
|
65831
69212
|
exports.useDeviceSnapshotImage$1 = useDeviceSnapshotImage;
|
|
@@ -65892,6 +69273,9 @@ exports.useFeatureProbeGetStatus = useFeatureProbeGetStatus;
|
|
|
65892
69273
|
exports.useFloodGetStatus = useFloodGetStatus;
|
|
65893
69274
|
exports.useGasGetStatus = useGasGetStatus;
|
|
65894
69275
|
exports.useHeatingValveGetStatus = useHeatingValveGetStatus;
|
|
69276
|
+
exports.useHistoryPanels = useHistoryPanels;
|
|
69277
|
+
exports.useHistoryReader = useHistoryReader;
|
|
69278
|
+
exports.useHistoryReads = useHistoryReads;
|
|
65895
69279
|
exports.useHumidifierGetStatus = useHumidifierGetStatus;
|
|
65896
69280
|
exports.useHumidifierSetMode = useHumidifierSetMode;
|
|
65897
69281
|
exports.useHumidifierSetOn = useHumidifierSetOn;
|
|
@@ -66742,6 +70126,9 @@ exports.waterHeaterPhase = waterHeaterPhase;
|
|
|
66742
70126
|
exports.waterHeaterTint = waterHeaterTint;
|
|
66743
70127
|
exports.weatherConditionMeta = weatherConditionMeta;
|
|
66744
70128
|
exports.weatherTint = weatherTint;
|
|
70129
|
+
exports.withHiddenChart = withHiddenChart;
|
|
70130
|
+
exports.withPanel = withPanel;
|
|
66745
70131
|
exports.withResolvedParentDetail = withResolvedParentDetail;
|
|
66746
70132
|
exports.withUseRootModel = withUseRootModel;
|
|
70133
|
+
exports.withoutPanel = withoutPanel;
|
|
66747
70134
|
exports.writeStoredGroupBy = writeStoredGroupBy;
|