@camstack/ui-library 1.2.189 → 1.2.191
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/composites/clips/ClipPanel.d.ts +8 -0
- package/dist/composites/clips/ClipTile.d.ts +7 -0
- package/dist/composites/clips/ClipsSection.d.ts +4 -0
- package/dist/composites/clips/clip-day-window.d.ts +37 -0
- package/dist/composites/clips/clip-source-state.d.ts +34 -0
- package/dist/composites/clips/clip-thumbnail-fetch.d.ts +16 -0
- package/dist/composites/clips/clip-tile-media.d.ts +27 -0
- package/dist/composites/clips/clip-view-state.d.ts +32 -0
- package/dist/composites/hls-video.d.ts +17 -2
- package/dist/composites/stream-panel.d.ts +8 -1
- package/dist/contexts/vod-playback.d.ts +13 -1
- package/dist/generated/system-hooks.d.ts +2 -0
- package/dist/hooks/use-capability-presence.d.ts +32 -0
- package/dist/index.cjs +2330 -1437
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2328 -1438
- package/dist/primitives/badge.d.ts +1 -1
- package/dist/primitives/button.d.ts +1 -1
- package/dist/primitives/icon-button.d.ts +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1167,6 +1167,30 @@ var CircleX = createLucideIcon("circle-x", [
|
|
|
1167
1167
|
* This source code is licensed under the ISC license.
|
|
1168
1168
|
* See the LICENSE file in the root directory of this source tree.
|
|
1169
1169
|
*/
|
|
1170
|
+
var Clapperboard = createLucideIcon("clapperboard", [
|
|
1171
|
+
["path", {
|
|
1172
|
+
d: "m12.296 3.464 3.02 3.956",
|
|
1173
|
+
key: "qash78"
|
|
1174
|
+
}],
|
|
1175
|
+
["path", {
|
|
1176
|
+
d: "M20.2 6 3 11l-.9-2.4c-.3-1.1.3-2.2 1.3-2.5l13.5-4c1.1-.3 2.2.3 2.5 1.3z",
|
|
1177
|
+
key: "1h7j8b"
|
|
1178
|
+
}],
|
|
1179
|
+
["path", {
|
|
1180
|
+
d: "M3 11h18v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z",
|
|
1181
|
+
key: "4lm6w1"
|
|
1182
|
+
}],
|
|
1183
|
+
["path", {
|
|
1184
|
+
d: "m6.18 5.276 3.1 3.899",
|
|
1185
|
+
key: "zjj9t3"
|
|
1186
|
+
}]
|
|
1187
|
+
]);
|
|
1188
|
+
/**
|
|
1189
|
+
* @license lucide-react v0.576.0 - ISC
|
|
1190
|
+
*
|
|
1191
|
+
* This source code is licensed under the ISC license.
|
|
1192
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
1193
|
+
*/
|
|
1170
1194
|
var Clock = createLucideIcon("clock", [["circle", {
|
|
1171
1195
|
cx: "12",
|
|
1172
1196
|
cy: "12",
|
|
@@ -7159,6 +7183,8 @@ var useValveGetStatus = trpc.valve.getStatus.useQuery;
|
|
|
7159
7183
|
var useVibrationGetStatus = trpc.vibration.getStatus.useQuery;
|
|
7160
7184
|
/** Generated alias around `trpc.videoclips.listClips.useQuery`. */
|
|
7161
7185
|
var useVideoclipsListClips = trpc.videoclips.listClips.useQuery;
|
|
7186
|
+
/** Generated alias around `trpc.videoclips.listSources.useQuery`. */
|
|
7187
|
+
var useVideoclipsListSources = trpc.videoclips.listSources.useQuery;
|
|
7162
7188
|
/** Generated alias around `trpc.videoclips.getClipPlayback.useQuery`. */
|
|
7163
7189
|
var useVideoclipsGetClipPlayback = trpc.videoclips.getClipPlayback.useQuery;
|
|
7164
7190
|
/** Generated alias around `trpc.waterHeater.setTargetTemp.useMutation`. */
|
|
@@ -17549,396 +17575,2098 @@ function ConsumablesPanel({ deviceId }) {
|
|
|
17549
17575
|
});
|
|
17550
17576
|
}
|
|
17551
17577
|
//#endregion
|
|
17552
|
-
//#region src/hooks/use-
|
|
17578
|
+
//#region src/hooks/use-capability-presence.ts
|
|
17553
17579
|
/**
|
|
17554
|
-
*
|
|
17580
|
+
* Classify a settled registry answer.
|
|
17555
17581
|
*
|
|
17556
|
-
*
|
|
17557
|
-
*
|
|
17558
|
-
*
|
|
17559
|
-
*
|
|
17582
|
+
* `null` is "no such capability". A row with an empty `providers` list is a
|
|
17583
|
+
* capability that is DECLARED but that no addon supplies — indistinguishable
|
|
17584
|
+
* to an operator from not having it, and the panel behind it would have
|
|
17585
|
+
* nothing to call. Both are `absent`.
|
|
17560
17586
|
*
|
|
17561
|
-
*
|
|
17562
|
-
*
|
|
17563
|
-
* and the live `status` slice off the device proxy.
|
|
17587
|
+
* Exported for the unit test: the mapping is the whole decision, and it is
|
|
17588
|
+
* worth pinning without a React tree around it.
|
|
17564
17589
|
*/
|
|
17565
|
-
|
|
17566
|
-
|
|
17567
|
-
|
|
17568
|
-
forward: {
|
|
17569
|
-
pan: 0,
|
|
17570
|
-
tilt: 1
|
|
17571
|
-
},
|
|
17572
|
-
left: {
|
|
17573
|
-
pan: -1,
|
|
17574
|
-
tilt: 0
|
|
17575
|
-
},
|
|
17576
|
-
right: {
|
|
17577
|
-
pan: 1,
|
|
17578
|
-
tilt: 0
|
|
17579
|
-
},
|
|
17580
|
-
spin: {
|
|
17581
|
-
pan: 0,
|
|
17582
|
-
tilt: -1
|
|
17583
|
-
}
|
|
17584
|
-
};
|
|
17585
|
-
function isAbsentProvider$1(err) {
|
|
17586
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
17587
|
-
return /not available|no provider|NOT_FOUND|PRECONDITION/i.test(message);
|
|
17590
|
+
function classifyCapability(row) {
|
|
17591
|
+
if (row === null) return "absent";
|
|
17592
|
+
return row.providers.length > 0 ? "present" : "absent";
|
|
17588
17593
|
}
|
|
17589
|
-
|
|
17590
|
-
|
|
17591
|
-
|
|
17592
|
-
|
|
17593
|
-
|
|
17594
|
-
|
|
17595
|
-
|
|
17596
|
-
|
|
17597
|
-
const
|
|
17598
|
-
|
|
17599
|
-
|
|
17600
|
-
|
|
17601
|
-
let cancelled = false;
|
|
17602
|
-
(async () => {
|
|
17603
|
-
try {
|
|
17604
|
-
const [nextFeatures, nextActions] = await Promise.all([nav.getFeatures({}), nav.listActions({})]);
|
|
17605
|
-
if (cancelled) return;
|
|
17606
|
-
setFeatures(nextFeatures);
|
|
17607
|
-
setActions(nextActions);
|
|
17608
|
-
} catch (err) {
|
|
17609
|
-
if (cancelled || isAbsentProvider$1(err)) return;
|
|
17610
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
17611
|
-
}
|
|
17612
|
-
})();
|
|
17613
|
-
return () => {
|
|
17614
|
-
cancelled = true;
|
|
17615
|
-
};
|
|
17616
|
-
}, [nav, enabled]);
|
|
17617
|
-
const wrap = useCallback(async (fn) => {
|
|
17618
|
-
if (!enabled || !nav) return;
|
|
17619
|
-
setError(null);
|
|
17620
|
-
setBusy(true);
|
|
17621
|
-
try {
|
|
17622
|
-
await fn();
|
|
17623
|
-
} catch (err) {
|
|
17624
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
17625
|
-
} finally {
|
|
17626
|
-
setBusy(false);
|
|
17627
|
-
}
|
|
17628
|
-
}, [enabled, nav]);
|
|
17629
|
-
const clearTimer = useCallback(() => {
|
|
17630
|
-
if (driveTimer.current !== null) {
|
|
17631
|
-
clearInterval(driveTimer.current);
|
|
17632
|
-
driveTimer.current = null;
|
|
17633
|
-
}
|
|
17634
|
-
}, []);
|
|
17635
|
-
const stopDrive = useCallback(async () => {
|
|
17636
|
-
clearTimer();
|
|
17637
|
-
setDriving(null);
|
|
17638
|
-
if (!nav) return;
|
|
17639
|
-
await wrap(() => nav.stop({}));
|
|
17640
|
-
}, [
|
|
17641
|
-
clearTimer,
|
|
17642
|
-
nav,
|
|
17643
|
-
wrap
|
|
17644
|
-
]);
|
|
17645
|
-
const startDrive = useCallback((direction) => {
|
|
17646
|
-
if (!nav) return;
|
|
17647
|
-
clearTimer();
|
|
17648
|
-
setDriving(direction);
|
|
17649
|
-
const vector = DIRECTION_VECTORS$1[direction];
|
|
17650
|
-
const nudge = () => {
|
|
17651
|
-
nav.move({
|
|
17652
|
-
pan: vector.pan,
|
|
17653
|
-
tilt: vector.tilt
|
|
17654
|
-
}).catch((err) => {
|
|
17655
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
17656
|
-
});
|
|
17657
|
-
};
|
|
17658
|
-
nudge();
|
|
17659
|
-
driveTimer.current = setInterval(nudge, NAVIGATION_DRIVE_INTERVAL_MS);
|
|
17660
|
-
}, [clearTimer, nav]);
|
|
17661
|
-
useEffect(() => {
|
|
17662
|
-
return () => {
|
|
17663
|
-
if (driveTimer.current !== null) {
|
|
17664
|
-
clearInterval(driveTimer.current);
|
|
17665
|
-
driveTimer.current = null;
|
|
17666
|
-
nav?.stop({}).catch(() => void 0);
|
|
17667
|
-
}
|
|
17668
|
-
};
|
|
17669
|
-
}, [nav]);
|
|
17670
|
-
const runAction = useCallback((actionId) => wrap(async () => nav?.runAction({ actionId })), [nav, wrap]);
|
|
17671
|
-
const playSound = useCallback((soundId) => wrap(async () => nav?.playSound({ soundId })), [nav, wrap]);
|
|
17672
|
-
const setLightOn = useCallback((on) => wrap(async () => nav?.setLightOn({ on })), [nav, wrap]);
|
|
17673
|
-
const setLightMode = useCallback((mode, level) => wrap(async () => nav?.setLightMode(level === void 0 ? { mode } : {
|
|
17674
|
-
mode,
|
|
17675
|
-
level
|
|
17676
|
-
})), [nav, wrap]);
|
|
17677
|
-
const setLightLevel = useCallback((level) => wrap(async () => nav?.setLightLevel({ level })), [nav, wrap]);
|
|
17678
|
-
return {
|
|
17679
|
-
features,
|
|
17680
|
-
actions,
|
|
17681
|
-
status: status ?? null,
|
|
17682
|
-
startDrive,
|
|
17683
|
-
stopDrive,
|
|
17684
|
-
runAction,
|
|
17685
|
-
playSound,
|
|
17686
|
-
setLightOn,
|
|
17687
|
-
setLightMode,
|
|
17688
|
-
setLightLevel,
|
|
17689
|
-
busy,
|
|
17690
|
-
error,
|
|
17691
|
-
driving
|
|
17692
|
-
};
|
|
17594
|
+
/**
|
|
17595
|
+
* Ask the hub's capability registry whether `capabilityName` has a provider.
|
|
17596
|
+
*
|
|
17597
|
+
* The four states map straight off React Query's `status`, so there is no
|
|
17598
|
+
* place for a nullish fallback to turn "no answer" into "no providers" — the
|
|
17599
|
+
* exact collapse this hook exists to make unrepresentable.
|
|
17600
|
+
*/
|
|
17601
|
+
function useCapabilityPresence(capabilityName) {
|
|
17602
|
+
const query = trpc.capabilities.getCapability.useQuery({ name: capabilityName });
|
|
17603
|
+
if (query.status === "pending") return "pending";
|
|
17604
|
+
if (query.status === "error") return "unknown";
|
|
17605
|
+
return classifyCapability(query.data);
|
|
17693
17606
|
}
|
|
17694
17607
|
//#endregion
|
|
17695
|
-
//#region
|
|
17696
|
-
/**
|
|
17697
|
-
|
|
17698
|
-
|
|
17699
|
-
|
|
17700
|
-
|
|
17701
|
-
|
|
17702
|
-
|
|
17703
|
-
|
|
17704
|
-
|
|
17705
|
-
|
|
17706
|
-
|
|
17707
|
-
|
|
17708
|
-
|
|
17709
|
-
|
|
17710
|
-
|
|
17711
|
-
|
|
17712
|
-
|
|
17608
|
+
//#region ../../node_modules/class-variance-authority/dist/index.mjs
|
|
17609
|
+
/**
|
|
17610
|
+
* Copyright 2022 Joe Bell. All rights reserved.
|
|
17611
|
+
*
|
|
17612
|
+
* This file is licensed to you under the Apache License, Version 2.0
|
|
17613
|
+
* (the "License"); you may not use this file except in compliance with the
|
|
17614
|
+
* License. You may obtain a copy of the License at
|
|
17615
|
+
*
|
|
17616
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
17617
|
+
*
|
|
17618
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
17619
|
+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
17620
|
+
* WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or implied. See the
|
|
17621
|
+
* License for the specific language governing permissions and limitations under
|
|
17622
|
+
* the License.
|
|
17623
|
+
*/ var falsyToString = (value) => typeof value === "boolean" ? `${value}` : value === 0 ? "0" : value;
|
|
17624
|
+
var cx = clsx;
|
|
17625
|
+
var cva = (base, config) => (props) => {
|
|
17626
|
+
var _config_compoundVariants;
|
|
17627
|
+
if ((config === null || config === void 0 ? void 0 : config.variants) == null) return cx(base, props === null || props === void 0 ? void 0 : props.class, props === null || props === void 0 ? void 0 : props.className);
|
|
17628
|
+
const { variants, defaultVariants } = config;
|
|
17629
|
+
const getVariantClassNames = Object.keys(variants).map((variant) => {
|
|
17630
|
+
const variantProp = props === null || props === void 0 ? void 0 : props[variant];
|
|
17631
|
+
const defaultVariantProp = defaultVariants === null || defaultVariants === void 0 ? void 0 : defaultVariants[variant];
|
|
17632
|
+
if (variantProp === null) return null;
|
|
17633
|
+
const variantKey = falsyToString(variantProp) || falsyToString(defaultVariantProp);
|
|
17634
|
+
return variants[variant][variantKey];
|
|
17635
|
+
});
|
|
17636
|
+
const propsWithoutUndefined = props && Object.entries(props).reduce((acc, param) => {
|
|
17637
|
+
let [key, value] = param;
|
|
17638
|
+
if (value === void 0) return acc;
|
|
17639
|
+
acc[key] = value;
|
|
17640
|
+
return acc;
|
|
17641
|
+
}, {});
|
|
17642
|
+
return cx(base, getVariantClassNames, config === null || config === void 0 ? void 0 : (_config_compoundVariants = config.compoundVariants) === null || _config_compoundVariants === void 0 ? void 0 : _config_compoundVariants.reduce((acc, param) => {
|
|
17643
|
+
let { class: cvClass, className: cvClassName, ...compoundVariantOptions } = param;
|
|
17644
|
+
return Object.entries(compoundVariantOptions).every((param) => {
|
|
17645
|
+
let [key, value] = param;
|
|
17646
|
+
return Array.isArray(value) ? value.includes({
|
|
17647
|
+
...defaultVariants,
|
|
17648
|
+
...propsWithoutUndefined
|
|
17649
|
+
}[key]) : {
|
|
17650
|
+
...defaultVariants,
|
|
17651
|
+
...propsWithoutUndefined
|
|
17652
|
+
}[key] === value;
|
|
17653
|
+
}) ? [
|
|
17654
|
+
...acc,
|
|
17655
|
+
cvClass,
|
|
17656
|
+
cvClassName
|
|
17657
|
+
] : acc;
|
|
17658
|
+
}, []), props === null || props === void 0 ? void 0 : props.class, props === null || props === void 0 ? void 0 : props.className);
|
|
17713
17659
|
};
|
|
17714
|
-
|
|
17715
|
-
|
|
17716
|
-
|
|
17717
|
-
|
|
17718
|
-
|
|
17719
|
-
|
|
17720
|
-
|
|
17721
|
-
|
|
17722
|
-
|
|
17723
|
-
|
|
17724
|
-
|
|
17725
|
-
|
|
17726
|
-
|
|
17727
|
-
|
|
17728
|
-
|
|
17729
|
-
direction: "right",
|
|
17730
|
-
label: "Turn right",
|
|
17731
|
-
Icon: RotateCw
|
|
17660
|
+
//#endregion
|
|
17661
|
+
//#region src/primitives/badge.tsx
|
|
17662
|
+
var badgeVariants = cva("inline-flex items-center rounded-full text-xs font-medium px-2 py-0.5", {
|
|
17663
|
+
variants: {
|
|
17664
|
+
variant: {
|
|
17665
|
+
default: "bg-surface-hover text-foreground",
|
|
17666
|
+
success: "bg-success/15 text-success",
|
|
17667
|
+
warning: "bg-warning/15 text-warning",
|
|
17668
|
+
danger: "bg-danger/15 text-danger",
|
|
17669
|
+
info: "bg-info/15 text-info"
|
|
17670
|
+
},
|
|
17671
|
+
styleVariant: {
|
|
17672
|
+
solid: "",
|
|
17673
|
+
outline: "border bg-transparent"
|
|
17674
|
+
}
|
|
17732
17675
|
},
|
|
17733
|
-
{
|
|
17734
|
-
|
|
17735
|
-
|
|
17736
|
-
Icon: Undo2
|
|
17676
|
+
defaultVariants: {
|
|
17677
|
+
variant: "default",
|
|
17678
|
+
styleVariant: "solid"
|
|
17737
17679
|
}
|
|
17738
|
-
|
|
17739
|
-
var
|
|
17740
|
-
|
|
17741
|
-
|
|
17742
|
-
|
|
17743
|
-
|
|
17744
|
-
|
|
17745
|
-
className
|
|
17746
|
-
|
|
17747
|
-
onClick: () => onRun(entry),
|
|
17748
|
-
title: entry.label,
|
|
17749
|
-
"aria-label": entry.label,
|
|
17750
|
-
children: [/* @__PURE__ */ jsx(Glyph, { className: "h-3.5 w-3.5" }), /* @__PURE__ */ jsx("span", { children: entry.label })]
|
|
17751
|
-
});
|
|
17752
|
-
}
|
|
17753
|
-
function NavigationPanel({ deviceId }) {
|
|
17754
|
-
const system = useSystem();
|
|
17755
|
-
const ready = Number.isFinite(deviceId);
|
|
17756
|
-
const nav = useNavigation(system.trpcClient, ready ? deviceId : 0, { enabled: ready });
|
|
17757
|
-
if (!ready) return /* @__PURE__ */ jsxs("div", {
|
|
17758
|
-
className: "flex items-center justify-center h-full text-[11px] text-foreground-subtle italic",
|
|
17759
|
-
children: [
|
|
17760
|
-
"Device #",
|
|
17761
|
-
deviceId,
|
|
17762
|
-
" not loaded."
|
|
17763
|
-
]
|
|
17680
|
+
});
|
|
17681
|
+
var Badge = forwardRef(({ className, variant, style, ...props }, ref) => {
|
|
17682
|
+
return /* @__PURE__ */ jsx("span", {
|
|
17683
|
+
ref,
|
|
17684
|
+
className: cn(badgeVariants({
|
|
17685
|
+
variant,
|
|
17686
|
+
styleVariant: style
|
|
17687
|
+
}), className),
|
|
17688
|
+
...props
|
|
17764
17689
|
});
|
|
17765
|
-
|
|
17766
|
-
|
|
17767
|
-
|
|
17768
|
-
|
|
17769
|
-
|
|
17770
|
-
|
|
17771
|
-
|
|
17772
|
-
|
|
17773
|
-
|
|
17690
|
+
});
|
|
17691
|
+
Badge.displayName = "Badge";
|
|
17692
|
+
//#endregion
|
|
17693
|
+
//#region src/primitives/button.tsx
|
|
17694
|
+
var buttonVariants = cva("inline-flex items-center justify-center rounded-md font-medium transition-colors cursor-pointer disabled:opacity-50 disabled:pointer-events-none", {
|
|
17695
|
+
variants: {
|
|
17696
|
+
variant: {
|
|
17697
|
+
primary: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
17698
|
+
secondary: "bg-surface text-foreground hover:bg-surface-hover",
|
|
17699
|
+
ghost: "hover:bg-surface-hover text-foreground-muted",
|
|
17700
|
+
danger: "bg-danger text-white hover:bg-danger/90",
|
|
17701
|
+
outline: "border border-border bg-transparent text-foreground hover:bg-surface-hover"
|
|
17702
|
+
},
|
|
17703
|
+
size: {
|
|
17704
|
+
sm: "h-7 px-2.5 text-xs gap-1.5",
|
|
17705
|
+
md: "h-8 px-3 text-sm gap-2",
|
|
17706
|
+
lg: "h-9 px-4 text-sm gap-2"
|
|
17774
17707
|
}
|
|
17775
|
-
|
|
17776
|
-
|
|
17777
|
-
|
|
17778
|
-
|
|
17779
|
-
|
|
17780
|
-
|
|
17781
|
-
|
|
17782
|
-
|
|
17783
|
-
|
|
17784
|
-
|
|
17785
|
-
|
|
17786
|
-
|
|
17787
|
-
|
|
17788
|
-
|
|
17789
|
-
status.following && /* @__PURE__ */ jsx("span", {
|
|
17790
|
-
className: "rounded-full bg-info/15 px-2 py-0.5 text-info",
|
|
17791
|
-
children: "following"
|
|
17792
|
-
}),
|
|
17793
|
-
status.flash && /* @__PURE__ */ jsx("span", {
|
|
17794
|
-
className: "rounded-full bg-warning/15 px-2 py-0.5 text-warning",
|
|
17795
|
-
children: "light on"
|
|
17796
|
-
})
|
|
17797
|
-
]
|
|
17798
|
-
}),
|
|
17799
|
-
features?.move && /* @__PURE__ */ jsxs("section", {
|
|
17800
|
-
className: "flex flex-col gap-1.5",
|
|
17801
|
-
children: [
|
|
17802
|
-
/* @__PURE__ */ jsx("span", {
|
|
17803
|
-
className: "text-[10.5px] font-semibold text-foreground",
|
|
17804
|
-
children: "Drive (hold)"
|
|
17805
|
-
}),
|
|
17806
|
-
/* @__PURE__ */ jsx("div", {
|
|
17807
|
-
className: "grid grid-cols-4 gap-1.5",
|
|
17808
|
-
children: DRIVE_BUTTONS.map(({ direction, label, Icon }) => /* @__PURE__ */ jsx("button", {
|
|
17809
|
-
type: "button",
|
|
17810
|
-
className: cn(BUTTON, "justify-center select-none", nav.driving === direction && BUTTON_ON),
|
|
17811
|
-
"aria-label": label,
|
|
17812
|
-
title: label,
|
|
17813
|
-
onPointerDown: () => nav.startDrive(direction),
|
|
17814
|
-
onPointerUp: () => void nav.stopDrive(),
|
|
17815
|
-
onPointerLeave: () => {
|
|
17816
|
-
if (nav.driving === direction) nav.stopDrive();
|
|
17817
|
-
},
|
|
17818
|
-
onPointerCancel: () => void nav.stopDrive(),
|
|
17819
|
-
children: /* @__PURE__ */ jsx(Icon, { className: "h-4 w-4" })
|
|
17820
|
-
}, direction))
|
|
17821
|
-
}),
|
|
17822
|
-
features.stop && /* @__PURE__ */ jsxs("button", {
|
|
17823
|
-
type: "button",
|
|
17824
|
-
className: cn(BUTTON, "justify-center"),
|
|
17825
|
-
onClick: () => void nav.stopDrive(),
|
|
17826
|
-
children: [/* @__PURE__ */ jsx(Hand, { className: "h-3.5 w-3.5" }), /* @__PURE__ */ jsx("span", { children: "Stop" })]
|
|
17827
|
-
})
|
|
17828
|
-
]
|
|
17829
|
-
}),
|
|
17830
|
-
features?.runAction && actions.length > 0 && /* @__PURE__ */ jsxs("section", {
|
|
17831
|
-
className: "flex flex-col gap-1.5",
|
|
17832
|
-
children: [/* @__PURE__ */ jsx("span", {
|
|
17833
|
-
className: "text-[10.5px] font-semibold text-foreground",
|
|
17834
|
-
children: "Actions"
|
|
17835
|
-
}), /* @__PURE__ */ jsx("div", {
|
|
17836
|
-
className: "flex flex-wrap gap-1.5",
|
|
17837
|
-
children: actions.map((entry) => /* @__PURE__ */ jsx(EntryButton, {
|
|
17838
|
-
entry,
|
|
17839
|
-
disabled: nav.busy,
|
|
17840
|
-
onRun: run
|
|
17841
|
-
}, entry.id))
|
|
17842
|
-
})]
|
|
17843
|
-
}),
|
|
17844
|
-
features?.playSound && sounds.length > 0 && /* @__PURE__ */ jsxs("section", {
|
|
17845
|
-
className: "flex flex-col gap-1.5",
|
|
17846
|
-
children: [/* @__PURE__ */ jsx("span", {
|
|
17847
|
-
className: "text-[10.5px] font-semibold text-foreground",
|
|
17848
|
-
children: "Sounds"
|
|
17849
|
-
}), /* @__PURE__ */ jsx("div", {
|
|
17850
|
-
className: "flex flex-wrap gap-1.5",
|
|
17851
|
-
children: sounds.map((entry) => /* @__PURE__ */ jsx(EntryButton, {
|
|
17852
|
-
entry,
|
|
17853
|
-
disabled: nav.busy,
|
|
17854
|
-
onRun: run
|
|
17855
|
-
}, entry.id))
|
|
17856
|
-
})]
|
|
17857
|
-
}),
|
|
17858
|
-
features?.light && /* @__PURE__ */ jsxs("section", {
|
|
17859
|
-
className: "flex flex-col gap-1.5 border-t border-border/40 pt-2",
|
|
17860
|
-
children: [/* @__PURE__ */ jsxs("div", {
|
|
17861
|
-
className: "flex items-center justify-between",
|
|
17862
|
-
children: [/* @__PURE__ */ jsx("span", {
|
|
17863
|
-
className: "text-[10.5px] font-semibold text-foreground",
|
|
17864
|
-
children: "Fill light"
|
|
17865
|
-
}), /* @__PURE__ */ jsxs("button", {
|
|
17866
|
-
type: "button",
|
|
17867
|
-
className: cn(BUTTON, status?.flash && BUTTON_ON),
|
|
17868
|
-
"aria-pressed": status?.flash === true,
|
|
17869
|
-
disabled: nav.busy,
|
|
17870
|
-
onClick: () => void nav.setLightOn(!(status?.flash === true)),
|
|
17871
|
-
children: [status?.flash ? /* @__PURE__ */ jsx(Flashlight, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx(FlashlightOff, { className: "h-3.5 w-3.5" }), status?.flash ? "On" : "Off"]
|
|
17872
|
-
})]
|
|
17873
|
-
}), features.lightMode && /* @__PURE__ */ jsxs("div", {
|
|
17874
|
-
className: "flex items-center gap-2",
|
|
17875
|
-
children: [
|
|
17876
|
-
/* @__PURE__ */ jsxs("select", {
|
|
17877
|
-
"aria-label": "Light mode",
|
|
17878
|
-
className: "rounded-md bg-surface-elevated/40 px-2 py-1 text-[10px] text-foreground",
|
|
17879
|
-
value: status?.lightMode ?? "auto",
|
|
17880
|
-
disabled: nav.busy,
|
|
17881
|
-
onChange: (event) => {
|
|
17882
|
-
const mode = event.target.value === "manual" ? "manual" : "auto";
|
|
17883
|
-
nav.setLightMode(mode, mode === "manual" ? status?.lightLevel : void 0);
|
|
17884
|
-
},
|
|
17885
|
-
children: [/* @__PURE__ */ jsx("option", {
|
|
17886
|
-
value: "auto",
|
|
17887
|
-
children: "Auto"
|
|
17888
|
-
}), /* @__PURE__ */ jsx("option", {
|
|
17889
|
-
value: "manual",
|
|
17890
|
-
children: "Manual"
|
|
17891
|
-
})]
|
|
17892
|
-
}),
|
|
17893
|
-
manual && /* @__PURE__ */ jsx("input", {
|
|
17894
|
-
type: "range",
|
|
17895
|
-
"aria-label": "Light level",
|
|
17896
|
-
className: "flex-1",
|
|
17897
|
-
min: NAVIGATION_LIGHT_LEVEL_MIN,
|
|
17898
|
-
max: NAVIGATION_LIGHT_LEVEL_MAX,
|
|
17899
|
-
step: 1,
|
|
17900
|
-
value: status?.lightLevel ?? NAVIGATION_LIGHT_LEVEL_MAX,
|
|
17901
|
-
disabled: nav.busy,
|
|
17902
|
-
onChange: (event) => void nav.setLightLevel(Number(event.target.value))
|
|
17903
|
-
}),
|
|
17904
|
-
manual && /* @__PURE__ */ jsx("span", {
|
|
17905
|
-
className: "text-[10px] text-foreground-subtle w-8 text-right",
|
|
17906
|
-
children: status?.lightLevel ?? NAVIGATION_LIGHT_LEVEL_MAX
|
|
17907
|
-
})
|
|
17908
|
-
]
|
|
17909
|
-
})]
|
|
17910
|
-
}),
|
|
17911
|
-
nav.error && /* @__PURE__ */ jsx("div", {
|
|
17912
|
-
className: "text-[10px] text-danger",
|
|
17913
|
-
children: nav.error
|
|
17914
|
-
})
|
|
17915
|
-
]
|
|
17708
|
+
},
|
|
17709
|
+
defaultVariants: {
|
|
17710
|
+
variant: "primary",
|
|
17711
|
+
size: "sm"
|
|
17712
|
+
}
|
|
17713
|
+
});
|
|
17714
|
+
var Button = forwardRef(({ className, variant, size, ...props }, ref) => {
|
|
17715
|
+
return /* @__PURE__ */ jsx("button", {
|
|
17716
|
+
ref,
|
|
17717
|
+
className: cn(buttonVariants({
|
|
17718
|
+
variant,
|
|
17719
|
+
size
|
|
17720
|
+
}), className),
|
|
17721
|
+
...props
|
|
17916
17722
|
});
|
|
17917
|
-
}
|
|
17723
|
+
});
|
|
17724
|
+
Button.displayName = "Button";
|
|
17918
17725
|
//#endregion
|
|
17919
|
-
//#region src/
|
|
17726
|
+
//#region src/primitives/select.tsx
|
|
17920
17727
|
/**
|
|
17921
|
-
*
|
|
17922
|
-
*
|
|
17923
|
-
* The primary export `isAbsentProvider` lets UI components distinguish
|
|
17924
|
-
* "this device doesn't have this cap" (expected — hide the widget) from
|
|
17925
|
-
* "something went wrong while calling a present cap" (unexpected — surface
|
|
17926
|
-
* the error to the operator).
|
|
17927
|
-
*
|
|
17928
|
-
* Detection strategy (two-tier):
|
|
17728
|
+
* A native `<select>` styled to match the rest of the field set.
|
|
17929
17729
|
*
|
|
17930
|
-
*
|
|
17931
|
-
*
|
|
17932
|
-
*
|
|
17933
|
-
*
|
|
17730
|
+
* `appearance-none` plus an explicit `bg-surface` / `text-foreground` is what
|
|
17731
|
+
* makes it match — and it is also what USED to make a disabled one a lie. A
|
|
17732
|
+
* browser greys a disabled control by rendering its own background and text
|
|
17733
|
+
* colour; all three of those are overridden here, so a `disabled` dropdown
|
|
17734
|
+
* looked exactly like a live one and simply ignored the click. That is the
|
|
17735
|
+
* whole of the "the language selector does not open" report of 2026-08-15:
|
|
17736
|
+
* the AI section's switch was off, every control under it was correctly inert,
|
|
17737
|
+
* and nothing on screen said so.
|
|
17934
17738
|
*
|
|
17935
|
-
*
|
|
17936
|
-
*
|
|
17937
|
-
*
|
|
17938
|
-
|
|
17939
|
-
|
|
17940
|
-
*/
|
|
17941
|
-
|
|
17739
|
+
* `disabled:` classes belong HERE and not on each call site: there are dozens
|
|
17740
|
+
* of `<Select disabled>` in this app and the one that gets forgotten is the one
|
|
17741
|
+
* that generates the ticket. Guarded by `disabled-affordance.test.tsx`.
|
|
17742
|
+
*/
|
|
17743
|
+
var Select = forwardRef(({ className, options, disabled, ...props }, ref) => {
|
|
17744
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
17745
|
+
className: "relative w-full",
|
|
17746
|
+
children: [/* @__PURE__ */ jsx("select", {
|
|
17747
|
+
ref,
|
|
17748
|
+
disabled,
|
|
17749
|
+
className: cn("h-7 w-full px-2.5 text-xs bg-surface border border-border rounded-md text-foreground appearance-none pr-7 focus:outline-none focus:ring-1 focus:ring-primary", "disabled:opacity-50 disabled:cursor-not-allowed", className),
|
|
17750
|
+
...props,
|
|
17751
|
+
children: options.map((option) => /* @__PURE__ */ jsx("option", {
|
|
17752
|
+
value: option.value,
|
|
17753
|
+
children: option.label
|
|
17754
|
+
}, option.value))
|
|
17755
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
17756
|
+
className: cn("absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none text-foreground-muted", disabled === true ? "opacity-50" : ""),
|
|
17757
|
+
children: /* @__PURE__ */ jsx(ChevronDown, { className: "h-3.5 w-3.5" })
|
|
17758
|
+
})]
|
|
17759
|
+
});
|
|
17760
|
+
});
|
|
17761
|
+
Select.displayName = "Select";
|
|
17762
|
+
//#endregion
|
|
17763
|
+
//#region src/primitives/skeleton.tsx
|
|
17764
|
+
var Skeleton = forwardRef(({ className, ...props }, ref) => {
|
|
17765
|
+
return /* @__PURE__ */ jsx("div", {
|
|
17766
|
+
ref,
|
|
17767
|
+
className: cn("animate-pulse bg-surface-hover rounded-md h-4 w-full", className),
|
|
17768
|
+
...props
|
|
17769
|
+
});
|
|
17770
|
+
});
|
|
17771
|
+
Skeleton.displayName = "Skeleton";
|
|
17772
|
+
//#endregion
|
|
17773
|
+
//#region src/hooks/use-is-mobile.ts
|
|
17774
|
+
var MOBILE_QUERY = "(max-width: 767px)";
|
|
17775
|
+
/**
|
|
17776
|
+
* Mid breakpoint — below this width the sidebar collapses to an
|
|
17777
|
+
* icons-only rail to reclaim canvas space. Above it (and outside
|
|
17778
|
+
* mobile) the full sidebar renders. Threshold matches Tailwind `lg`
|
|
17779
|
+
* so sidebar fans out at the same point grids switch to multi-column.
|
|
17780
|
+
*/
|
|
17781
|
+
var MID_QUERY = "(min-width: 768px) and (max-width: 1023px)";
|
|
17782
|
+
/**
|
|
17783
|
+
* `matchMedia` is universal in browsers but absent in bare DOM environments.
|
|
17784
|
+
* These hooks are called from shared primitives (`DataTable` picks its narrow
|
|
17785
|
+
* layout with one), so throwing here takes down the whole page that merely
|
|
17786
|
+
* rendered a table. Where the capability is missing, report the desktop
|
|
17787
|
+
* layout — a widescreen rendering is wrong-looking; a crashed page is gone.
|
|
17788
|
+
*/
|
|
17789
|
+
function matchQuery(query) {
|
|
17790
|
+
if (typeof window.matchMedia !== "function") return null;
|
|
17791
|
+
return window.matchMedia(query);
|
|
17792
|
+
}
|
|
17793
|
+
function subscribeQuery(query) {
|
|
17794
|
+
return (callback) => {
|
|
17795
|
+
const mql = matchQuery(query);
|
|
17796
|
+
if (mql === null) return () => {};
|
|
17797
|
+
mql.addEventListener("change", callback);
|
|
17798
|
+
return () => mql.removeEventListener("change", callback);
|
|
17799
|
+
};
|
|
17800
|
+
}
|
|
17801
|
+
function getSnapshot(query) {
|
|
17802
|
+
return () => matchQuery(query)?.matches ?? false;
|
|
17803
|
+
}
|
|
17804
|
+
function getServerSnapshot() {
|
|
17805
|
+
return false;
|
|
17806
|
+
}
|
|
17807
|
+
function useIsMobile() {
|
|
17808
|
+
return useSyncExternalStore(subscribeQuery(MOBILE_QUERY), getSnapshot(MOBILE_QUERY), getServerSnapshot);
|
|
17809
|
+
}
|
|
17810
|
+
/**
|
|
17811
|
+
* True when the viewport is in the medium (`md`) Tailwind range —
|
|
17812
|
+
* 768px ≤ width < 1024px. Use to drive layouts that should pack denser
|
|
17813
|
+
* than full-desktop but more legibly than mobile (e.g. icons-only
|
|
17814
|
+
* sidebar rail, narrower stat tiles).
|
|
17815
|
+
*/
|
|
17816
|
+
function useIsMidWidth() {
|
|
17817
|
+
return useSyncExternalStore(subscribeQuery(MID_QUERY), getSnapshot(MID_QUERY), getServerSnapshot);
|
|
17818
|
+
}
|
|
17819
|
+
//#endregion
|
|
17820
|
+
//#region src/composites/data-table-layout.ts
|
|
17821
|
+
/**
|
|
17822
|
+
* At this many columns and above, `auto` switches a narrow viewport to cards.
|
|
17823
|
+
* Three columns still fit a phone at a readable size; four do not.
|
|
17824
|
+
*/
|
|
17825
|
+
var CARD_MODE_MIN_COLUMNS = 4;
|
|
17826
|
+
function resolveTableLayout({ mode, columnCount, isNarrow }) {
|
|
17827
|
+
if (!isNarrow) return "table";
|
|
17828
|
+
if (mode === "scroll") return "table";
|
|
17829
|
+
if (mode === "cards") return "cards";
|
|
17830
|
+
if (columnCount === 0) return "table";
|
|
17831
|
+
return columnCount >= 4 ? "cards" : "table";
|
|
17832
|
+
}
|
|
17833
|
+
//#endregion
|
|
17834
|
+
//#region src/lib/responsive.ts
|
|
17835
|
+
/**
|
|
17836
|
+
* Responsive design tokens — three-tier system.
|
|
17837
|
+
*
|
|
17838
|
+
* The admin-ui targets three width tiers; every component should pick
|
|
17839
|
+
* the right token from this file instead of hand-coding breakpoints.
|
|
17840
|
+
* Adopting the tokens uniformly is what makes the app feel consistent
|
|
17841
|
+
* across operator devices (mobile / tablet / desktop) and what unlocks
|
|
17842
|
+
* targeted tuning (e.g. shrink every label by 1px in one place).
|
|
17843
|
+
*
|
|
17844
|
+
* Tier definitions (Tailwind breakpoints):
|
|
17845
|
+
*
|
|
17846
|
+
* • **L1 — Compact** (`< 640px`, default = mobile portrait)
|
|
17847
|
+
* Single column, tight padding, smallest text, icon-only buttons
|
|
17848
|
+
* where labels can be inferred. The operator can still see every
|
|
17849
|
+
* stat / action — no horizontal scroll, no clipped text. The user
|
|
17850
|
+
* directive: "molta informazione in poco spazio quando la width è
|
|
17851
|
+
* bassa."
|
|
17852
|
+
*
|
|
17853
|
+
* • **L2 — Standard** (`sm` — `640–1023px`, tablet + small laptop)
|
|
17854
|
+
* 2-column grids, side panels become visible, button labels return.
|
|
17855
|
+
* Targets ~768px–1024px viewports — the most common operator sizes.
|
|
17856
|
+
*
|
|
17857
|
+
* • **L3 — Spacious** (`lg+` — `≥ 1024px`, desktop)
|
|
17858
|
+
* 4-column grids, generous padding, full-width header text,
|
|
17859
|
+
* side-by-side composite layouts. The operator has elbow room.
|
|
17860
|
+
*
|
|
17861
|
+
* Conventions:
|
|
17862
|
+
* - **Always start from L1** — the bare class is always the compact
|
|
17863
|
+
* variant, then `sm:` adds room at 640px, `lg:` at 1024px. No
|
|
17864
|
+
* desktop-first breakpoints; no max-width queries.
|
|
17865
|
+
* - **Token strings are static** — kept as `as const` so callers can
|
|
17866
|
+
* compose them via `cn()` / template literals without running a
|
|
17867
|
+
* class generator at runtime.
|
|
17868
|
+
* - **Pair the token with a comment** — the comment in the file
|
|
17869
|
+
* names every breakpoint it crosses, so a reader can predict the
|
|
17870
|
+
* layout without going to Tailwind docs.
|
|
17871
|
+
*/
|
|
17872
|
+
/** Container padding for a card-style section (header + body). */
|
|
17873
|
+
var SECTION_CARD = "rounded-lg border border-border bg-surface overflow-hidden";
|
|
17874
|
+
/** Header strip inside a card (between border-b lines). */
|
|
17875
|
+
var SECTION_HEADER = "border-b border-border px-3 sm:px-4 py-1.5 sm:py-2";
|
|
17876
|
+
/** Body padding for a card-style section. */
|
|
17877
|
+
var SECTION_BODY = "p-2 sm:p-3 lg:p-4";
|
|
17878
|
+
/** Gap between cards in a vertical stack. */
|
|
17879
|
+
var STACK_GAP = "space-y-3 sm:space-y-4";
|
|
17880
|
+
/** Gap between cards in a grid (4 cells / row at lg). */
|
|
17881
|
+
var GRID_GAP = "gap-2 sm:gap-3";
|
|
17882
|
+
/** Row padding inside a divided list (e.g. InfoRow). */
|
|
17883
|
+
var LIST_ROW = "px-3 sm:px-4 py-1.5 sm:py-2";
|
|
17884
|
+
/** 1-up at L1, 2-up at L2, 4-up at L3 — quick-stat row. */
|
|
17885
|
+
var GRID_QUICK_STATS = "grid grid-cols-2 sm:grid-cols-2 lg:grid-cols-4";
|
|
17886
|
+
/** 1-up at L1, 2-up at L2+ — paired panels (motion + detection rules). */
|
|
17887
|
+
var GRID_PAIRED = "grid grid-cols-1 lg:grid-cols-2";
|
|
17888
|
+
/** Side-by-side layout that stacks below md (canvas + side panel). */
|
|
17889
|
+
var SPLIT_PANEL_OUTER = "flex flex-col-reverse md:flex-row";
|
|
17890
|
+
/** Sidebar narrow lane in a SPLIT_PANEL_OUTER layout. */
|
|
17891
|
+
var SPLIT_PANEL_SIDE = "w-full md:w-44 lg:w-52 md:flex-shrink-0 border-b md:border-b-0 md:border-l border-border";
|
|
17892
|
+
/**
|
|
17893
|
+
* The tier boundary at which a settings label/value row stops stacking.
|
|
17894
|
+
*
|
|
17895
|
+
* Below it (L1 — compact) the row is ONE column: label on its own line, value
|
|
17896
|
+
* on the next at full width. Above it the row is label-left / value-right as
|
|
17897
|
+
* before. The two-column form needs `SETTING_ROW_LABEL`'s lane plus a readable
|
|
17898
|
+
* value beside it; a phone has neither, and the failure mode is not "cramped"
|
|
17899
|
+
* but "the value is the part that gets cut" — the one thing the operator
|
|
17900
|
+
* opened the page to read.
|
|
17901
|
+
*/
|
|
17902
|
+
var SETTING_ROW_STACK_BREAKPOINT = "sm";
|
|
17903
|
+
/** Outer row: single column at L1, label/value columns from `sm` up. */
|
|
17904
|
+
var SETTING_ROW = "flex min-w-0 flex-col gap-0.5 py-1.5 sm:flex-row sm:items-center";
|
|
17905
|
+
/** Label lane. Full width (and free to wrap) at L1; a fixed lane from `sm` up. */
|
|
17906
|
+
var SETTING_ROW_LABEL = "min-w-0 text-[11px] leading-tight text-foreground-subtle sm:w-32 sm:shrink-0 sm:pr-2";
|
|
17907
|
+
/**
|
|
17908
|
+
* Value lane — the value text plus its trailing affordances (copy / reveal).
|
|
17909
|
+
* `w-full` at L1 so the value owns the whole row once the label is above it;
|
|
17910
|
+
* `sm:flex-1` makes it share the row again from `sm` up.
|
|
17911
|
+
*/
|
|
17912
|
+
var SETTING_ROW_VALUE = "flex w-full min-w-0 items-center gap-1 sm:flex-1";
|
|
17913
|
+
/**
|
|
17914
|
+
* The value text itself. `break-all` lets an opaque token (a key, a URL) wrap
|
|
17915
|
+
* at L1 instead of being cut mid-word; `sm:truncate` restores the single-line
|
|
17916
|
+
* desktop form, where the row is wide enough for truncation to be a choice
|
|
17917
|
+
* rather than data loss.
|
|
17918
|
+
*/
|
|
17919
|
+
var SETTING_ROW_VALUE_TEXT = "min-w-0 break-all text-xs text-foreground sm:truncate";
|
|
17920
|
+
/** Section header label (uppercase tracking-wider). */
|
|
17921
|
+
var TEXT_SECTION_LABEL = "text-[10px] sm:text-[11px] font-semibold text-foreground uppercase tracking-wider";
|
|
17922
|
+
/** Field label inside a row. */
|
|
17923
|
+
var TEXT_FIELD_LABEL = "text-[11px] sm:text-xs text-foreground";
|
|
17924
|
+
/** Sub-label / hint text. */
|
|
17925
|
+
var TEXT_HINT = "text-[10px] text-foreground-subtle";
|
|
17926
|
+
/** Primary value (a metric, badge, …). */
|
|
17927
|
+
var TEXT_VALUE = "text-[11px] sm:text-xs font-medium text-foreground";
|
|
17928
|
+
/** Big metric value (StatCard). */
|
|
17929
|
+
var TEXT_METRIC = "text-base sm:text-lg font-semibold leading-tight text-foreground";
|
|
17930
|
+
/** Default compact button. */
|
|
17931
|
+
var BTN_COMPACT = "inline-flex items-center justify-center gap-1 rounded border border-border bg-surface px-1.5 py-0.5 sm:px-2 sm:py-1 text-[10px] sm:text-[11px] font-medium text-foreground hover:bg-surface-hover transition-colors disabled:opacity-40";
|
|
17932
|
+
/** Primary compact button. */
|
|
17933
|
+
var BTN_COMPACT_PRIMARY = "inline-flex items-center justify-center gap-1 rounded border border-primary/40 bg-primary/15 px-1.5 py-0.5 sm:px-2 sm:py-1 text-[10px] sm:text-[11px] font-medium text-primary hover:bg-primary/25 transition-colors disabled:opacity-40";
|
|
17934
|
+
/** Danger compact button. */
|
|
17935
|
+
var BTN_COMPACT_DANGER = "inline-flex items-center justify-center gap-1 rounded border border-danger/40 bg-danger/10 px-1.5 py-0.5 sm:px-2 sm:py-1 text-[10px] sm:text-[11px] font-medium text-danger hover:bg-danger/20 transition-colors disabled:opacity-40";
|
|
17936
|
+
/** Warning compact button (e.g. cancel-drawing). */
|
|
17937
|
+
var BTN_COMPACT_WARNING = "inline-flex items-center justify-center gap-1 rounded border border-warning/40 bg-warning/10 px-1.5 py-0.5 sm:px-2 sm:py-1 text-[10px] sm:text-[11px] font-medium text-warning hover:bg-warning/20 transition-colors disabled:opacity-40";
|
|
17938
|
+
/** Compact text/select/number input. */
|
|
17939
|
+
var INPUT_COMPACT = "w-full rounded border border-border bg-background px-1.5 py-0.5 sm:px-2 sm:py-1 text-[11px] sm:text-xs text-foreground focus:border-primary focus:ring-1 focus:ring-primary/30 outline-none";
|
|
17940
|
+
/** Chip-style toggle (zone / class selectors in ZoneRulesEditor). */
|
|
17941
|
+
var CHIP_BASE = "inline-flex items-center gap-1 rounded-full border px-1.5 py-0.5 text-[10px] font-medium transition-colors";
|
|
17942
|
+
var CHIP_ACTIVE = "border-primary/50 bg-primary/15 text-primary";
|
|
17943
|
+
var CHIP_INACTIVE = "border-border bg-surface text-foreground-subtle hover:bg-surface-hover";
|
|
17944
|
+
//#endregion
|
|
17945
|
+
//#region src/composites/setting-row.tsx
|
|
17946
|
+
function SettingRow({ label, children, actions, elements = "plain", className, valueClassName }) {
|
|
17947
|
+
const LabelTag = elements === "description" ? "dt" : "span";
|
|
17948
|
+
const ValueTag = elements === "description" ? "dd" : "div";
|
|
17949
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
17950
|
+
"data-setting-row": "",
|
|
17951
|
+
className: cn(SETTING_ROW, className),
|
|
17952
|
+
children: [/* @__PURE__ */ jsx(LabelTag, {
|
|
17953
|
+
"data-setting-row-label": "",
|
|
17954
|
+
className: SETTING_ROW_LABEL,
|
|
17955
|
+
children: label
|
|
17956
|
+
}), /* @__PURE__ */ jsxs(ValueTag, {
|
|
17957
|
+
"data-setting-row-value": "",
|
|
17958
|
+
className: SETTING_ROW_VALUE,
|
|
17959
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
17960
|
+
"data-setting-row-value-text": "",
|
|
17961
|
+
className: cn(SETTING_ROW_VALUE_TEXT, valueClassName),
|
|
17962
|
+
children
|
|
17963
|
+
}), actions !== void 0 && /* @__PURE__ */ jsx("span", {
|
|
17964
|
+
className: "flex shrink-0 items-center gap-0.5",
|
|
17965
|
+
children: actions
|
|
17966
|
+
})]
|
|
17967
|
+
})]
|
|
17968
|
+
});
|
|
17969
|
+
}
|
|
17970
|
+
//#endregion
|
|
17971
|
+
//#region src/composites/sort-header-button.tsx
|
|
17972
|
+
function SortHeaderButton({ label, active, dir, onSort }) {
|
|
17973
|
+
return /* @__PURE__ */ jsxs("button", {
|
|
17974
|
+
type: "button",
|
|
17975
|
+
onClick: onSort,
|
|
17976
|
+
className: "group/sort inline-flex items-center gap-0.5 hover:text-foreground transition-colors",
|
|
17977
|
+
children: [/* @__PURE__ */ jsx("span", { children: label }), active ? dir === "asc" ? /* @__PURE__ */ jsx(ChevronUp, { className: "h-3 w-3 flex-shrink-0" }) : /* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3 flex-shrink-0" }) : /* @__PURE__ */ jsx(ChevronUp, { className: cn("h-3 w-3 flex-shrink-0 opacity-0 transition-opacity", "group-hover/sort:opacity-30") })]
|
|
17978
|
+
});
|
|
17979
|
+
}
|
|
17980
|
+
//#endregion
|
|
17981
|
+
//#region src/composites/table-sort.ts
|
|
17982
|
+
/**
|
|
17983
|
+
* The toggle rule, and the only copy of it.
|
|
17984
|
+
*
|
|
17985
|
+
* none → asc → desc → none, and a click on a DIFFERENT column starts that
|
|
17986
|
+
* column fresh at `asc`. `sameColumn` says whether the clicked column is the
|
|
17987
|
+
* one currently sorted; `currentDir` is its direction (`null` when nothing is
|
|
17988
|
+
* sorted). Returns the next direction, or `null` for "unsorted".
|
|
17989
|
+
*
|
|
17990
|
+
* Split out from {@link nextTableSort} so the device list — whose sort state
|
|
17991
|
+
* keys columns by `columnId`, not `columnKey` — can reuse the rule without a
|
|
17992
|
+
* cast and without a second copy of it.
|
|
17993
|
+
*/
|
|
17994
|
+
function nextSortDirection(sameColumn, currentDir) {
|
|
17995
|
+
if (!sameColumn || currentDir === null) return "asc";
|
|
17996
|
+
return currentDir === "asc" ? "desc" : null;
|
|
17997
|
+
}
|
|
17998
|
+
/**
|
|
17999
|
+
* Toggle a column's sort on header click: none → asc → desc → none.
|
|
18000
|
+
* Clicking a DIFFERENT column starts it fresh at `asc`.
|
|
18001
|
+
*/
|
|
18002
|
+
function nextTableSort(current, columnKey) {
|
|
18003
|
+
const dir = nextSortDirection(current !== null && current.columnKey === columnKey, current === null ? null : current.dir);
|
|
18004
|
+
return dir === null ? null : {
|
|
18005
|
+
columnKey,
|
|
18006
|
+
dir
|
|
18007
|
+
};
|
|
18008
|
+
}
|
|
18009
|
+
/**
|
|
18010
|
+
* Ascending comparison of two cell values.
|
|
18011
|
+
*
|
|
18012
|
+
* Numbers compare numerically and strings compare with `localeCompare` — a
|
|
18013
|
+
* numeric column must never be ordered as text ("100" before "9"), and a text
|
|
18014
|
+
* column must respect the locale's collation. A mixed column (one row a
|
|
18015
|
+
* number, another a string) falls back to string comparison of both, which is
|
|
18016
|
+
* the only total order available; it is not a shape any caller should build on
|
|
18017
|
+
* purpose.
|
|
18018
|
+
*
|
|
18019
|
+
* `null` is NOT handled here — {@link sortRowsByColumn} pulls it out first,
|
|
18020
|
+
* because its placement is direction-independent and a comparator that is
|
|
18021
|
+
* multiplied by the direction factor cannot express that.
|
|
18022
|
+
*/
|
|
18023
|
+
function compareValues(a, b) {
|
|
18024
|
+
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
18025
|
+
return String(a).localeCompare(String(b));
|
|
18026
|
+
}
|
|
18027
|
+
/**
|
|
18028
|
+
* Return a NEW array of `rows` ordered by `sort`.
|
|
18029
|
+
*
|
|
18030
|
+
* `valueOf` is the column's `sortValue` — it is called once per row per
|
|
18031
|
+
* comparison, so it must be cheap (read a field, do not format a string).
|
|
18032
|
+
* `sort === null` ⇒ a fresh shallow copy in the incoming order.
|
|
18033
|
+
*/
|
|
18034
|
+
function sortRowsByColumn(rows, sort, valueOf) {
|
|
18035
|
+
if (sort === null) return [...rows];
|
|
18036
|
+
const factor = sort.dir === "asc" ? 1 : -1;
|
|
18037
|
+
return rows.toSorted((a, b) => {
|
|
18038
|
+
const av = valueOf(a);
|
|
18039
|
+
const bv = valueOf(b);
|
|
18040
|
+
if (av === null || bv === null) {
|
|
18041
|
+
if (av === bv) return 0;
|
|
18042
|
+
return av === null ? 1 : -1;
|
|
18043
|
+
}
|
|
18044
|
+
return factor * compareValues(av, bv);
|
|
18045
|
+
});
|
|
18046
|
+
}
|
|
18047
|
+
/** Derive the `<th>` `aria-sort` for `columnKey` from the table's sort state. */
|
|
18048
|
+
function ariaSortForColumn(sort, columnKey) {
|
|
18049
|
+
if (sort === null || sort.columnKey !== columnKey) return "none";
|
|
18050
|
+
return sort.dir === "asc" ? "ascending" : "descending";
|
|
18051
|
+
}
|
|
18052
|
+
//#endregion
|
|
18053
|
+
//#region src/composites/data-table.tsx
|
|
18054
|
+
/**
|
|
18055
|
+
* DataTable — shared responsive table for admin pages.
|
|
18056
|
+
*
|
|
18057
|
+
* Recipes baked in:
|
|
18058
|
+
* - `overflow-x-auto` wrapper so narrow viewports don't clip the
|
|
18059
|
+
* right edge (the rounded border survives the scroll).
|
|
18060
|
+
* - `min-w-[Npx]` on the inner table so columns don't squash below
|
|
18061
|
+
* readable width before the scroll kicks in.
|
|
18062
|
+
* - `whitespace-nowrap` on headers — column titles never wrap to
|
|
18063
|
+
* two lines (`AUTO-KILL` was the regression that triggered this).
|
|
18064
|
+
* - Themed `bg-surface` + `border-border` so contrast works in both
|
|
18065
|
+
* light and dark themes.
|
|
18066
|
+
*
|
|
18067
|
+
* Column model: caller supplies `columns: DataColumn<T>[]` and
|
|
18068
|
+
* `rows: T[]`. Each column declares `header`, optional `align`,
|
|
18069
|
+
* optional `nowrap`, and a `render(row) => ReactNode` that produces
|
|
18070
|
+
* the cell content.
|
|
18071
|
+
*
|
|
18072
|
+
* <DataTable
|
|
18073
|
+
* columns={[
|
|
18074
|
+
* { key: 'username', header: 'Username', render: (u) => u.username },
|
|
18075
|
+
* { key: 'role', header: 'Role', render: (u) => <RoleBadge .../> },
|
|
18076
|
+
* { key: 'actions', header: '', align: 'right', render: (u) => <Actions .../> },
|
|
18077
|
+
* ]}
|
|
18078
|
+
* rows={users}
|
|
18079
|
+
* minWidthPx={560}
|
|
18080
|
+
* />
|
|
18081
|
+
*
|
|
18082
|
+
* If finer header markup is needed, pass `headerRender` instead of
|
|
18083
|
+
* `header` (e.g. for a sortable click affordance).
|
|
18084
|
+
*
|
|
18085
|
+
* ── Narrow viewports ──────────────────────────────────────────────────────
|
|
18086
|
+
* The table stays a table. The wrapper is `overflow-x-auto`, so a phone
|
|
18087
|
+
* scrolls the columns sideways INSIDE the table — the page body never moves
|
|
18088
|
+
* sideways, and the cells stay one line (`whitespace-nowrap`). That is the
|
|
18089
|
+
* default (`mobileMode="scroll"`). Cards are opt-in via `mobileMode="cards"`;
|
|
18090
|
+
* `auto` still applies the old column-count rule for a surface that wants it.
|
|
18091
|
+
* See `resolveTableLayout`.
|
|
18092
|
+
*
|
|
18093
|
+
* ── Scrolling the BODY, not the page ──────────────────────────────────────
|
|
18094
|
+
* `maxBodyHeight` caps the table and scrolls its rows INSIDE the container
|
|
18095
|
+
* with the column header pinned. It is ONE prop, not two, because "this list
|
|
18096
|
+
* scrolls internally" and "its header stays visible" are the same decision: a
|
|
18097
|
+
* capped table whose header scrolls away is a list of unlabelled cells.
|
|
18098
|
+
*
|
|
18099
|
+
* Use it wherever the row count is open-ended (rules, targets, delivery
|
|
18100
|
+
* history) so the page keeps its own header, filters and actions in view while
|
|
18101
|
+
* the operator moves through the rows.
|
|
18102
|
+
*/
|
|
18103
|
+
/**
|
|
18104
|
+
* The default cap for an internally-scrolling table.
|
|
18105
|
+
*
|
|
18106
|
+
* `60vh` leaves the page header, its filters and its actions on screen at
|
|
18107
|
+
* every viewport while still showing ~10 rows — and it is a fraction, so it
|
|
18108
|
+
* does not need a breakpoint of its own.
|
|
18109
|
+
*/
|
|
18110
|
+
var TABLE_SCROLL_MAX_H = "max-h-[60vh]";
|
|
18111
|
+
var ALIGN_CLASS = {
|
|
18112
|
+
left: "text-left",
|
|
18113
|
+
right: "text-right",
|
|
18114
|
+
center: "text-center"
|
|
18115
|
+
};
|
|
18116
|
+
function DataTable({ columns, rows, rowKey, onRowClick, minWidthPx = 480, emptyMessage, className, bordered = true, rowClassName, mobileMode = "scroll", maxBodyHeight, defaultSort }) {
|
|
18117
|
+
const isNarrow = useIsMobile();
|
|
18118
|
+
const [sort, setSort] = useState(defaultSort ?? null);
|
|
18119
|
+
const layout = resolveTableLayout({
|
|
18120
|
+
mode: mobileMode,
|
|
18121
|
+
columnCount: columns.length,
|
|
18122
|
+
isNarrow
|
|
18123
|
+
});
|
|
18124
|
+
const scrolls = maxBodyHeight !== void 0;
|
|
18125
|
+
const sortedColumn = useMemo(() => sort === null ? void 0 : columns.find((col) => col.key === sort.columnKey && col.sortValue !== void 0), [columns, sort]);
|
|
18126
|
+
const sortValue = sortedColumn?.sortValue;
|
|
18127
|
+
const displayRows = useMemo(() => sortValue === void 0 ? rows : sortRowsByColumn(rows, sort, sortValue), [
|
|
18128
|
+
rows,
|
|
18129
|
+
sort,
|
|
18130
|
+
sortValue
|
|
18131
|
+
]);
|
|
18132
|
+
const activeSort = sortedColumn === void 0 ? null : sort;
|
|
18133
|
+
if (rows.length === 0 && emptyMessage) return /* @__PURE__ */ jsx("div", {
|
|
18134
|
+
className: `rounded-lg ${bordered ? "border border-border" : ""} bg-surface px-3 py-4 text-xs text-foreground-subtle text-center ${className ?? ""}`,
|
|
18135
|
+
children: emptyMessage
|
|
18136
|
+
});
|
|
18137
|
+
if (rows.length === 0) return null;
|
|
18138
|
+
if (layout === "cards") return /* @__PURE__ */ jsx("div", {
|
|
18139
|
+
className: `space-y-2 ${scrolls ? `${maxBodyHeight} overflow-y-auto` : ""} ${className ?? ""}`,
|
|
18140
|
+
children: displayRows.map((row, rowIndex) => /* @__PURE__ */ jsx(DataTableCard, {
|
|
18141
|
+
row,
|
|
18142
|
+
rowIndex,
|
|
18143
|
+
columns,
|
|
18144
|
+
bordered,
|
|
18145
|
+
onRowClick,
|
|
18146
|
+
rowClassName
|
|
18147
|
+
}, rowKey ? rowKey(row, rowIndex) : rowIndex))
|
|
18148
|
+
});
|
|
18149
|
+
return /* @__PURE__ */ jsx("div", {
|
|
18150
|
+
className: [
|
|
18151
|
+
"rounded-lg",
|
|
18152
|
+
bordered ? "border border-border" : "",
|
|
18153
|
+
"bg-surface overflow-x-auto",
|
|
18154
|
+
scrolls ? `${maxBodyHeight} overflow-y-auto` : "",
|
|
18155
|
+
className ?? ""
|
|
18156
|
+
].filter(Boolean).join(" "),
|
|
18157
|
+
children: /* @__PURE__ */ jsxs("table", {
|
|
18158
|
+
className: `w-full text-xs ${scrolls ? "border-separate border-spacing-0" : ""}`,
|
|
18159
|
+
style: { minWidth: `${minWidthPx}px` },
|
|
18160
|
+
children: [/* @__PURE__ */ jsx("thead", {
|
|
18161
|
+
className: "bg-surface",
|
|
18162
|
+
children: /* @__PURE__ */ jsx("tr", { children: columns.map((col) => {
|
|
18163
|
+
const align = col.align ?? "left";
|
|
18164
|
+
const nowrap = col.nowrap ?? true;
|
|
18165
|
+
const sortable = col.sortValue !== void 0;
|
|
18166
|
+
const isSorted = activeSort !== null && activeSort.columnKey === col.key;
|
|
18167
|
+
return /* @__PURE__ */ jsx("th", {
|
|
18168
|
+
"aria-sort": sortable ? ariaSortForColumn(activeSort, col.key) : void 0,
|
|
18169
|
+
title: sortable ? void 0 : col.notSortableReason,
|
|
18170
|
+
className: [
|
|
18171
|
+
"px-3 py-2 text-foreground-subtle font-medium bg-surface border-b border-border",
|
|
18172
|
+
scrolls ? "sticky top-0 z-10" : "",
|
|
18173
|
+
ALIGN_CLASS[align],
|
|
18174
|
+
nowrap ? "whitespace-nowrap" : "",
|
|
18175
|
+
col.width ?? ""
|
|
18176
|
+
].filter(Boolean).join(" "),
|
|
18177
|
+
children: sortable ? /* @__PURE__ */ jsx(SortHeaderButton, {
|
|
18178
|
+
label: col.headerRender ? col.headerRender() : col.header,
|
|
18179
|
+
active: isSorted,
|
|
18180
|
+
dir: isSorted && activeSort !== null ? activeSort.dir : "asc",
|
|
18181
|
+
onSort: () => setSort((prev) => nextTableSort(prev, col.key))
|
|
18182
|
+
}) : col.headerRender ? col.headerRender() : col.header
|
|
18183
|
+
}, col.key);
|
|
18184
|
+
}) })
|
|
18185
|
+
}), /* @__PURE__ */ jsx("tbody", { children: displayRows.map((row, rowIndex) => {
|
|
18186
|
+
const key = rowKey ? rowKey(row, rowIndex) : rowIndex;
|
|
18187
|
+
const extra = rowClassName?.(row, rowIndex) ?? "";
|
|
18188
|
+
const interactive = onRowClick !== void 0;
|
|
18189
|
+
return /* @__PURE__ */ jsx("tr", {
|
|
18190
|
+
onClick: interactive ? () => onRowClick(row, rowIndex) : void 0,
|
|
18191
|
+
className: [interactive ? "cursor-pointer hover:bg-primary/5" : "hover:bg-primary/5", extra].filter(Boolean).join(" "),
|
|
18192
|
+
children: columns.map((col) => {
|
|
18193
|
+
const align = col.align ?? "left";
|
|
18194
|
+
const nowrap = col.nowrap ?? true;
|
|
18195
|
+
return /* @__PURE__ */ jsx("td", {
|
|
18196
|
+
className: [
|
|
18197
|
+
"px-3 py-2 text-foreground border-b border-border",
|
|
18198
|
+
ALIGN_CLASS[align],
|
|
18199
|
+
nowrap ? "whitespace-nowrap" : ""
|
|
18200
|
+
].filter(Boolean).join(" "),
|
|
18201
|
+
children: col.render(row, rowIndex)
|
|
18202
|
+
}, col.key);
|
|
18203
|
+
})
|
|
18204
|
+
}, key);
|
|
18205
|
+
}) })]
|
|
18206
|
+
})
|
|
18207
|
+
});
|
|
18208
|
+
}
|
|
18209
|
+
/**
|
|
18210
|
+
* One row as a card. Columns that carry a plain-text `header` become labelled
|
|
18211
|
+
* `SettingRow`s — the label is the column title and the value gets the full
|
|
18212
|
+
* card width. Columns with no text header (an actions column, a
|
|
18213
|
+
* `headerRender`-only column) have no label to show, so they render as a
|
|
18214
|
+
* full-width strip at the foot of the card.
|
|
18215
|
+
*/
|
|
18216
|
+
function DataTableCard({ row, rowIndex, columns, bordered, onRowClick, rowClassName }) {
|
|
18217
|
+
const labelled = columns.filter((col) => col.header !== void 0 && col.header !== "");
|
|
18218
|
+
const unlabelled = columns.filter((col) => col.header === void 0 || col.header === "");
|
|
18219
|
+
const interactive = onRowClick !== void 0;
|
|
18220
|
+
const extra = rowClassName?.(row, rowIndex) ?? "";
|
|
18221
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
18222
|
+
"data-data-table-card": "",
|
|
18223
|
+
onClick: interactive ? () => onRowClick(row, rowIndex) : void 0,
|
|
18224
|
+
className: [
|
|
18225
|
+
"rounded-lg bg-surface px-3 py-1.5 divide-y divide-border-subtle",
|
|
18226
|
+
bordered ? "border border-border" : "",
|
|
18227
|
+
interactive ? "cursor-pointer hover:bg-primary/5" : "",
|
|
18228
|
+
extra
|
|
18229
|
+
].filter(Boolean).join(" "),
|
|
18230
|
+
children: [labelled.map((col) => /* @__PURE__ */ jsx(SettingRow, {
|
|
18231
|
+
label: col.header,
|
|
18232
|
+
children: col.render(row, rowIndex)
|
|
18233
|
+
}, col.key)), unlabelled.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
18234
|
+
className: "flex flex-wrap items-center justify-end gap-2 py-1.5",
|
|
18235
|
+
children: unlabelled.map((col) => /* @__PURE__ */ jsx("div", { children: col.render(row, rowIndex) }, col.key))
|
|
18236
|
+
})]
|
|
18237
|
+
});
|
|
18238
|
+
}
|
|
18239
|
+
//#endregion
|
|
18240
|
+
//#region src/composites/device-list/pagination.tsx
|
|
18241
|
+
var NEIGHBORS = 2;
|
|
18242
|
+
function computePageButtons(currentPage, totalPages) {
|
|
18243
|
+
if (totalPages <= 1) return [{
|
|
18244
|
+
kind: "page",
|
|
18245
|
+
n: 1
|
|
18246
|
+
}];
|
|
18247
|
+
if (totalPages <= 7) {
|
|
18248
|
+
const all = [];
|
|
18249
|
+
for (let i = 1; i <= totalPages; i++) all.push({
|
|
18250
|
+
kind: "page",
|
|
18251
|
+
n: i
|
|
18252
|
+
});
|
|
18253
|
+
return all;
|
|
18254
|
+
}
|
|
18255
|
+
const result = [];
|
|
18256
|
+
const leftBound = Math.max(2, currentPage - NEIGHBORS);
|
|
18257
|
+
const rightBound = Math.min(totalPages - 1, currentPage + NEIGHBORS);
|
|
18258
|
+
result.push({
|
|
18259
|
+
kind: "page",
|
|
18260
|
+
n: 1
|
|
18261
|
+
});
|
|
18262
|
+
if (leftBound > 2) result.push({
|
|
18263
|
+
kind: "ellipsis",
|
|
18264
|
+
side: "left"
|
|
18265
|
+
});
|
|
18266
|
+
for (let i = leftBound; i <= rightBound; i++) result.push({
|
|
18267
|
+
kind: "page",
|
|
18268
|
+
n: i
|
|
18269
|
+
});
|
|
18270
|
+
if (rightBound < totalPages - 1) result.push({
|
|
18271
|
+
kind: "ellipsis",
|
|
18272
|
+
side: "right"
|
|
18273
|
+
});
|
|
18274
|
+
result.push({
|
|
18275
|
+
kind: "page",
|
|
18276
|
+
n: totalPages
|
|
18277
|
+
});
|
|
18278
|
+
return result;
|
|
18279
|
+
}
|
|
18280
|
+
function Pagination({ currentPage, totalItems, pageSize, onPageChange, itemNoun = "devices" }) {
|
|
18281
|
+
if (totalItems <= pageSize) return null;
|
|
18282
|
+
const totalPages = Math.ceil(totalItems / pageSize);
|
|
18283
|
+
const startIdx = (currentPage - 1) * pageSize + 1;
|
|
18284
|
+
const endIdx = Math.min(currentPage * pageSize, totalItems);
|
|
18285
|
+
const buttons = computePageButtons(currentPage, totalPages);
|
|
18286
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
18287
|
+
className: "flex items-center justify-between gap-4 flex-wrap pt-2 text-[11px] text-foreground-subtle",
|
|
18288
|
+
children: [/* @__PURE__ */ jsxs("span", { children: [
|
|
18289
|
+
"Showing ",
|
|
18290
|
+
startIdx,
|
|
18291
|
+
"–",
|
|
18292
|
+
endIdx,
|
|
18293
|
+
" of ",
|
|
18294
|
+
totalItems,
|
|
18295
|
+
" ",
|
|
18296
|
+
itemNoun
|
|
18297
|
+
] }), /* @__PURE__ */ jsxs("span", {
|
|
18298
|
+
className: "inline-flex items-center gap-1",
|
|
18299
|
+
children: [
|
|
18300
|
+
/* @__PURE__ */ jsx("button", {
|
|
18301
|
+
type: "button",
|
|
18302
|
+
"aria-label": "Previous page",
|
|
18303
|
+
disabled: currentPage === 1,
|
|
18304
|
+
onClick: () => onPageChange(currentPage - 1),
|
|
18305
|
+
className: cn("inline-flex items-center justify-center h-7 w-7 rounded border border-border bg-surface text-foreground-subtle", "hover:text-foreground hover:bg-surface-hover transition-colors", "disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-surface"),
|
|
18306
|
+
children: /* @__PURE__ */ jsx(ChevronLeft, { className: "h-3 w-3" })
|
|
18307
|
+
}),
|
|
18308
|
+
buttons.map((b, i) => {
|
|
18309
|
+
if (b.kind === "ellipsis") return /* @__PURE__ */ jsx("span", {
|
|
18310
|
+
className: "px-1 text-foreground-subtle/60 select-none",
|
|
18311
|
+
children: "…"
|
|
18312
|
+
}, `e-${i}`);
|
|
18313
|
+
return /* @__PURE__ */ jsx("button", {
|
|
18314
|
+
type: "button",
|
|
18315
|
+
onClick: () => onPageChange(b.n),
|
|
18316
|
+
className: cn("inline-flex items-center justify-center h-7 min-w-7 px-1.5 rounded border text-[10.5px] font-medium transition-colors", b.n === currentPage ? "border-primary/40 bg-primary/10 text-primary" : "border-border bg-surface text-foreground-subtle hover:text-foreground hover:bg-surface-hover"),
|
|
18317
|
+
children: b.n
|
|
18318
|
+
}, `p-${b.n}`);
|
|
18319
|
+
}),
|
|
18320
|
+
/* @__PURE__ */ jsx("button", {
|
|
18321
|
+
type: "button",
|
|
18322
|
+
"aria-label": "Next page",
|
|
18323
|
+
disabled: currentPage === totalPages,
|
|
18324
|
+
onClick: () => onPageChange(currentPage + 1),
|
|
18325
|
+
className: cn("inline-flex items-center justify-center h-7 w-7 rounded border border-border bg-surface text-foreground-subtle", "hover:text-foreground hover:bg-surface-hover transition-colors", "disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-surface"),
|
|
18326
|
+
children: /* @__PURE__ */ jsx(ChevronRight, { className: "h-3 w-3" })
|
|
18327
|
+
})
|
|
18328
|
+
]
|
|
18329
|
+
})]
|
|
18330
|
+
});
|
|
18331
|
+
}
|
|
18332
|
+
//#endregion
|
|
18333
|
+
//#region src/composites/empty-state.tsx
|
|
18334
|
+
function EmptyState({ icon: Icon, title, description, action, className }) {
|
|
18335
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
18336
|
+
className: cn("flex flex-col items-center justify-center gap-3 py-12", className),
|
|
18337
|
+
children: [
|
|
18338
|
+
Icon && /* @__PURE__ */ jsx(Icon, {
|
|
18339
|
+
className: "h-12 w-12 text-foreground-subtle",
|
|
18340
|
+
"aria-hidden": "true"
|
|
18341
|
+
}),
|
|
18342
|
+
/* @__PURE__ */ jsxs("div", {
|
|
18343
|
+
className: "flex flex-col items-center gap-1 text-center",
|
|
18344
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
18345
|
+
className: "text-foreground-muted text-sm font-medium",
|
|
18346
|
+
children: title
|
|
18347
|
+
}), description && /* @__PURE__ */ jsx("p", {
|
|
18348
|
+
className: "text-foreground-subtle text-xs max-w-xs",
|
|
18349
|
+
children: description
|
|
18350
|
+
})]
|
|
18351
|
+
}),
|
|
18352
|
+
action && /* @__PURE__ */ jsx("div", {
|
|
18353
|
+
className: "mt-1",
|
|
18354
|
+
children: action
|
|
18355
|
+
})
|
|
18356
|
+
]
|
|
18357
|
+
});
|
|
18358
|
+
}
|
|
18359
|
+
//#endregion
|
|
18360
|
+
//#region src/composites/error-box.tsx
|
|
18361
|
+
var VARIANT_STYLES$1 = {
|
|
18362
|
+
danger: {
|
|
18363
|
+
box: "border-danger/40 bg-danger/10",
|
|
18364
|
+
icon: "text-danger",
|
|
18365
|
+
title: "text-danger",
|
|
18366
|
+
message: "text-danger"
|
|
18367
|
+
},
|
|
18368
|
+
warning: {
|
|
18369
|
+
box: "border-amber-500/40 bg-amber-500/10",
|
|
18370
|
+
icon: "text-amber-600 dark:text-amber-400",
|
|
18371
|
+
title: "text-amber-700 dark:text-amber-300",
|
|
18372
|
+
message: "text-amber-700 dark:text-amber-300"
|
|
18373
|
+
}
|
|
18374
|
+
};
|
|
18375
|
+
function ErrorBox({ message, title, onRetry, action, variant = "danger", className }) {
|
|
18376
|
+
const styles = VARIANT_STYLES$1[variant];
|
|
18377
|
+
const Icon = variant === "warning" ? TriangleAlert : CircleAlert;
|
|
18378
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
18379
|
+
role: "alert",
|
|
18380
|
+
className: cn("flex items-start gap-2.5 rounded border px-3 py-2.5", styles.box, className),
|
|
18381
|
+
children: [
|
|
18382
|
+
/* @__PURE__ */ jsx(Icon, {
|
|
18383
|
+
className: cn("h-4 w-4 mt-0.5 shrink-0", styles.icon),
|
|
18384
|
+
"aria-hidden": "true"
|
|
18385
|
+
}),
|
|
18386
|
+
/* @__PURE__ */ jsxs("div", {
|
|
18387
|
+
className: "flex-1 min-w-0",
|
|
18388
|
+
children: [title && /* @__PURE__ */ jsx("p", {
|
|
18389
|
+
className: cn("text-xs font-semibold", styles.title),
|
|
18390
|
+
children: title
|
|
18391
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
18392
|
+
className: cn("text-xs break-words", styles.message, title ? "mt-0.5" : ""),
|
|
18393
|
+
children: message
|
|
18394
|
+
})]
|
|
18395
|
+
}),
|
|
18396
|
+
action ?? (onRetry && /* @__PURE__ */ jsxs("button", {
|
|
18397
|
+
type: "button",
|
|
18398
|
+
onClick: onRetry,
|
|
18399
|
+
className: cn("shrink-0 inline-flex items-center gap-1 rounded px-2 py-1 text-[11px] font-medium", "border border-current/30 hover:bg-current/10 transition-colors", styles.message),
|
|
18400
|
+
children: [/* @__PURE__ */ jsx(RefreshCw, {
|
|
18401
|
+
className: "h-3 w-3",
|
|
18402
|
+
"aria-hidden": "true"
|
|
18403
|
+
}), "Retry"]
|
|
18404
|
+
}))
|
|
18405
|
+
]
|
|
18406
|
+
});
|
|
18407
|
+
}
|
|
18408
|
+
//#endregion
|
|
18409
|
+
//#region src/contexts/vod-playback.tsx
|
|
18410
|
+
/**
|
|
18411
|
+
* VodPlayback — shared context coordinating recorded (VOD) playback between a
|
|
18412
|
+
* widget/section (the Recording panel) and the hero StreamPanel.
|
|
18413
|
+
*
|
|
18414
|
+
* A CONTROLLER (the Recording widget) calls `play({ url, label })`; the hero
|
|
18415
|
+
* StreamPanel CONSUMES `request` and switches into VOD mode (HLS playback
|
|
18416
|
+
* through the hub data-plane) instead of the live WebRTC stream. `clear()`
|
|
18417
|
+
* returns the hero to live.
|
|
18418
|
+
*
|
|
18419
|
+
* Built on `createSharedContext` (NOT a plain `createContext`) so it crosses
|
|
18420
|
+
* the Module-Federation boundary: an addon-shipped widget consuming
|
|
18421
|
+
* `useVodPlayback()` resolves the SAME provider instance the host mounted —
|
|
18422
|
+
* the same mechanism `player-overlays` / `zone-editing` use to let federated
|
|
18423
|
+
* widgets (zone-editor, privacy-mask) drive the live player. Consumers used
|
|
18424
|
+
* outside a provider get the inert default (no-op), so a StreamPanel mounted
|
|
18425
|
+
* on a page without the Recording widget stays live-only without crashing.
|
|
18426
|
+
*/
|
|
18427
|
+
var VodPlaybackContext = createSharedContext("camstack:vod-playback", {
|
|
18428
|
+
request: null,
|
|
18429
|
+
play: () => {},
|
|
18430
|
+
clear: () => {}
|
|
18431
|
+
});
|
|
18432
|
+
function VodPlaybackProvider({ children }) {
|
|
18433
|
+
const [request, setRequest] = useState(null);
|
|
18434
|
+
const value = useMemo(() => ({
|
|
18435
|
+
request,
|
|
18436
|
+
play: (req) => setRequest(req),
|
|
18437
|
+
clear: () => setRequest(null)
|
|
18438
|
+
}), [request]);
|
|
18439
|
+
return /* @__PURE__ */ jsx(VodPlaybackContext.Provider, {
|
|
18440
|
+
value,
|
|
18441
|
+
children
|
|
18442
|
+
});
|
|
18443
|
+
}
|
|
18444
|
+
/** @returns the VOD playback controller (inert no-op outside a provider). */
|
|
18445
|
+
function useVodPlayback() {
|
|
18446
|
+
return useContext(VodPlaybackContext);
|
|
18447
|
+
}
|
|
18448
|
+
//#endregion
|
|
18449
|
+
//#region src/primitives/card.tsx
|
|
18450
|
+
var cardVariants = cva("rounded-lg p-3", {
|
|
18451
|
+
variants: { variant: {
|
|
18452
|
+
flat: "bg-surface",
|
|
18453
|
+
bordered: "bg-surface border border-border"
|
|
18454
|
+
} },
|
|
18455
|
+
defaultVariants: { variant: "bordered" }
|
|
18456
|
+
});
|
|
18457
|
+
var Card = forwardRef(({ className, variant, ...props }, ref) => {
|
|
18458
|
+
return /* @__PURE__ */ jsx("div", {
|
|
18459
|
+
ref,
|
|
18460
|
+
className: cn(cardVariants({ variant }), className),
|
|
18461
|
+
...props
|
|
18462
|
+
});
|
|
18463
|
+
});
|
|
18464
|
+
Card.displayName = "Card";
|
|
18465
|
+
//#endregion
|
|
18466
|
+
//#region src/composites/clips/ClipPanel.tsx
|
|
18467
|
+
/**
|
|
18468
|
+
* The selected clip — what it is, how it plays, and how it is saved.
|
|
18469
|
+
*
|
|
18470
|
+
* **No second player.** `getClipPlayback` answers `{ playbackUrl, format,
|
|
18471
|
+
* served }` and the panel hands it to the SAME hero the recordings timeline
|
|
18472
|
+
* drives: `vod.play({ url, format })` → `StreamPanel playbackUrl/playbackFormat`
|
|
18473
|
+
* → `HlsVideo`'s `mp4` branch (`video.src`, no hls.js). Building a clip player
|
|
18474
|
+
* here is the thing D549 decision 22 exists to refuse.
|
|
18475
|
+
*
|
|
18476
|
+
* **`format` travels.** An MP4 handed to hls.js is parsed as a manifest and
|
|
18477
|
+
* fails, so the format the provider named is forwarded verbatim and never
|
|
18478
|
+
* defaulted here.
|
|
18479
|
+
*
|
|
18480
|
+
* **`served` is shown.** A clip that holds only one twin answers with the one
|
|
18481
|
+
* it has, and the surface SAYS so — a missing main twin must never be served
|
|
18482
|
+
* silently as if it were the asked-for quality (D549 decision 15). Both ids
|
|
18483
|
+
* are already on the row, so changing quality re-asks the hub and never
|
|
18484
|
+
* re-searches the camera.
|
|
18485
|
+
*
|
|
18486
|
+
* **Nothing is appended to the URL.** The route is `access:'authenticated'`
|
|
18487
|
+
* and the session cookie authenticates it (D549 decision 22); the same URL is
|
|
18488
|
+
* the download.
|
|
18489
|
+
*/
|
|
18490
|
+
var PROFILES = [
|
|
18491
|
+
"low",
|
|
18492
|
+
"mid",
|
|
18493
|
+
"high"
|
|
18494
|
+
];
|
|
18495
|
+
function ClipPanel({ deviceId, clip, profile, onProfileChange }) {
|
|
18496
|
+
const vod = useVodPlayback();
|
|
18497
|
+
const playback = useVideoclipsGetClipPlayback({
|
|
18498
|
+
deviceId,
|
|
18499
|
+
clipId: clip.id,
|
|
18500
|
+
profile
|
|
18501
|
+
});
|
|
18502
|
+
const data = playback.data;
|
|
18503
|
+
const label = `${new Date(clip.timeRange.startMs).toLocaleString()} · ${clip.source}`;
|
|
18504
|
+
useEffect(() => {
|
|
18505
|
+
if (data === void 0) return;
|
|
18506
|
+
vod.play({
|
|
18507
|
+
url: hubPath(data.playbackUrl),
|
|
18508
|
+
...data.format === void 0 ? {} : { format: data.format },
|
|
18509
|
+
label
|
|
18510
|
+
});
|
|
18511
|
+
}, [data, label]);
|
|
18512
|
+
return /* @__PURE__ */ jsxs(Card, {
|
|
18513
|
+
className: "space-y-2 text-sm",
|
|
18514
|
+
children: [
|
|
18515
|
+
/* @__PURE__ */ jsx("div", {
|
|
18516
|
+
className: "font-medium text-foreground",
|
|
18517
|
+
children: label
|
|
18518
|
+
}),
|
|
18519
|
+
/* @__PURE__ */ jsxs("div", {
|
|
18520
|
+
className: "flex items-center gap-1",
|
|
18521
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
18522
|
+
className: "mr-1 text-[10px] uppercase tracking-wide text-foreground-subtle",
|
|
18523
|
+
children: "Quality"
|
|
18524
|
+
}), PROFILES.map((option) => /* @__PURE__ */ jsx(Button, {
|
|
18525
|
+
variant: option === profile ? "primary" : "secondary",
|
|
18526
|
+
"aria-pressed": option === profile,
|
|
18527
|
+
onClick: () => onProfileChange(option),
|
|
18528
|
+
children: option
|
|
18529
|
+
}, option))]
|
|
18530
|
+
}),
|
|
18531
|
+
playback.status === "pending" ? /* @__PURE__ */ jsxs("div", {
|
|
18532
|
+
className: "space-y-1",
|
|
18533
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
18534
|
+
role: "status",
|
|
18535
|
+
className: "text-xs text-foreground-subtle",
|
|
18536
|
+
children: "Preparing the clip — the file is fetched and muxed once…"
|
|
18537
|
+
}), /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-48" })]
|
|
18538
|
+
}) : playback.status === "error" ? /* @__PURE__ */ jsx(ErrorBox, {
|
|
18539
|
+
title: "This clip could not be prepared",
|
|
18540
|
+
message: playback.error?.message ?? "no reason given"
|
|
18541
|
+
}) : data === void 0 ? null : /* @__PURE__ */ jsxs("div", {
|
|
18542
|
+
className: "space-y-2",
|
|
18543
|
+
children: [data.served === void 0 || data.served === profile ? null : /* @__PURE__ */ jsx(ErrorBox, {
|
|
18544
|
+
variant: "warning",
|
|
18545
|
+
message: /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
18546
|
+
"Served the ",
|
|
18547
|
+
/* @__PURE__ */ jsx("strong", { children: data.served }),
|
|
18548
|
+
" stream — this clip has no ",
|
|
18549
|
+
profile,
|
|
18550
|
+
" ",
|
|
18551
|
+
"twin."
|
|
18552
|
+
] })
|
|
18553
|
+
}), /* @__PURE__ */ jsxs("a", {
|
|
18554
|
+
href: hubPath(data.playbackUrl),
|
|
18555
|
+
download: true,
|
|
18556
|
+
className: "inline-flex items-center gap-1.5 text-xs font-medium text-primary hover:underline",
|
|
18557
|
+
children: [/* @__PURE__ */ jsx(Download, {
|
|
18558
|
+
className: "h-3.5 w-3.5",
|
|
18559
|
+
"aria-hidden": "true"
|
|
18560
|
+
}), "Download this clip"]
|
|
18561
|
+
})]
|
|
18562
|
+
})
|
|
18563
|
+
]
|
|
18564
|
+
});
|
|
18565
|
+
}
|
|
18566
|
+
//#endregion
|
|
18567
|
+
//#region src/composites/clips/clip-tile-media.ts
|
|
18568
|
+
/**
|
|
18569
|
+
* Why no still exists, in words. Keyed by the provider's own vocabulary so a
|
|
18570
|
+
* reason this table does not know still reaches the operator verbatim.
|
|
18571
|
+
*/
|
|
18572
|
+
var REFUSAL_TEXT = {
|
|
18573
|
+
sleeping: "The camera is asleep — a still would cost a wake",
|
|
18574
|
+
"camera-refused": "The camera refused the still request",
|
|
18575
|
+
"no-keyframe": "No decodable frame inside this window",
|
|
18576
|
+
unsupported: "This source cannot produce stills",
|
|
18577
|
+
"queue-full": "The camera is busy — its still queue is full",
|
|
18578
|
+
"no-catalog-row": "This clip is no longer in the catalog"
|
|
18579
|
+
};
|
|
18580
|
+
function refusal(reason) {
|
|
18581
|
+
return {
|
|
18582
|
+
kind: "unavailable",
|
|
18583
|
+
reason,
|
|
18584
|
+
text: REFUSAL_TEXT[reason] ?? `Refused: ${reason}`
|
|
18585
|
+
};
|
|
18586
|
+
}
|
|
18587
|
+
/** What to draw for one clip's tile. */
|
|
18588
|
+
function resolveTileMedia(clip) {
|
|
18589
|
+
if (clip.thumbnail !== void 0) return {
|
|
18590
|
+
kind: "vouched",
|
|
18591
|
+
url: clip.thumbnail
|
|
18592
|
+
};
|
|
18593
|
+
if (clip.thumbnailUnavailable !== void 0) return refusal(clip.thumbnailUnavailable.reason);
|
|
18594
|
+
if (clip.thumbnailMint !== void 0) return {
|
|
18595
|
+
kind: "mint",
|
|
18596
|
+
url: clip.thumbnailMint
|
|
18597
|
+
};
|
|
18598
|
+
return {
|
|
18599
|
+
kind: "none",
|
|
18600
|
+
text: "This clip carries no still and offered none"
|
|
18601
|
+
};
|
|
18602
|
+
}
|
|
18603
|
+
/**
|
|
18604
|
+
* A mint that answered 204 — latch its `x-camstack-reason` to this instant.
|
|
18605
|
+
*
|
|
18606
|
+
* The refusal is about NOW (a full queue, a sleeping camera), so it replaces
|
|
18607
|
+
* the offer for as long as the tile is mounted and is not retried behind the
|
|
18608
|
+
* operator's back. An unknown reason is shown raw rather than dropped: a
|
|
18609
|
+
* firmware inventing a refusal must not become a blank tile.
|
|
18610
|
+
*/
|
|
18611
|
+
function latchMintRefusal(reason) {
|
|
18612
|
+
return refusal(reason);
|
|
18613
|
+
}
|
|
18614
|
+
//#endregion
|
|
18615
|
+
//#region src/composites/clips/clip-thumbnail-fetch.ts
|
|
18616
|
+
/**
|
|
18617
|
+
* Asking a `thumbnailMint` route for a still — and hearing its refusal.
|
|
18618
|
+
*
|
|
18619
|
+
* The route answers **200** with a JPEG or **204** with the reason in
|
|
18620
|
+
* `x-camstack-reason` (`queue-full`, `sleeping`, `camera-refused`,
|
|
18621
|
+
* `no-keyframe`, `no-catalog-row`). An `<img src>` cannot read that header:
|
|
18622
|
+
* a 204 fires `onerror` with nothing to say, turning a named refusal into an
|
|
18623
|
+
* anonymous broken tile. So a mint goes through `fetch`.
|
|
18624
|
+
*
|
|
18625
|
+
* **Nothing is appended to the URL.** Both data-plane routes are registered
|
|
18626
|
+
* `access: 'authenticated'` and the hub's `/addon/<id>/<prefix>` proxy takes
|
|
18627
|
+
* the session cookie the admin UI sets at login, so the URL is never a
|
|
18628
|
+
* credential (D549 decision 22) — it may sit in history or a `Referer` and
|
|
18629
|
+
* open nothing.
|
|
18630
|
+
*
|
|
18631
|
+
* The refusal is about NOW and is LATCHED by the caller: a camera that just
|
|
18632
|
+
* said "busy" is not asked again behind the operator's back.
|
|
18633
|
+
*/
|
|
18634
|
+
/**
|
|
18635
|
+
* Ask a mint route for one still.
|
|
18636
|
+
*
|
|
18637
|
+
* Never rejects: a thrown fetch is a refusal like any other, because a tile
|
|
18638
|
+
* that throws is a tile that renders nothing and says nothing — the exact
|
|
18639
|
+
* failure this module exists to prevent.
|
|
18640
|
+
*/
|
|
18641
|
+
async function fetchMintedThumbnail(url, fetcher) {
|
|
18642
|
+
let response;
|
|
18643
|
+
try {
|
|
18644
|
+
response = await fetcher(url, { credentials: "same-origin" });
|
|
18645
|
+
} catch (err) {
|
|
18646
|
+
return latchMintRefusal(err instanceof Error ? err.message : String(err));
|
|
18647
|
+
}
|
|
18648
|
+
if (response.status === 204) return latchMintRefusal(response.headers.get("x-camstack-reason") ?? "no reason given");
|
|
18649
|
+
if (!response.ok) return latchMintRefusal(`HTTP ${String(response.status)}`);
|
|
18650
|
+
try {
|
|
18651
|
+
return {
|
|
18652
|
+
kind: "image",
|
|
18653
|
+
blob: await response.blob()
|
|
18654
|
+
};
|
|
18655
|
+
} catch (err) {
|
|
18656
|
+
return latchMintRefusal(err instanceof Error ? err.message : String(err));
|
|
18657
|
+
}
|
|
18658
|
+
}
|
|
18659
|
+
//#endregion
|
|
18660
|
+
//#region src/composites/clips/ClipTile.tsx
|
|
18661
|
+
/**
|
|
18662
|
+
* One clip, as a tile.
|
|
18663
|
+
*
|
|
18664
|
+
* The picture is resolved by `resolveTileMedia` — vouched JPEG, an offer, or
|
|
18665
|
+
* the reason there is none (D549 decision 5). An offer is fetched rather than
|
|
18666
|
+
* dropped into an `<img src>`, because the route's **204 + x-camstack-reason**
|
|
18667
|
+
* is invisible to an `<img>`: it fires `onerror` with nothing to say. The
|
|
18668
|
+
* refusal is LATCHED for the life of the tile and never retried behind the
|
|
18669
|
+
* operator's back.
|
|
18670
|
+
*
|
|
18671
|
+
* The row also carries what the camera itself said: its native types, the
|
|
18672
|
+
* behaviours beside the class, which storages hold it, and — for a row that
|
|
18673
|
+
* is an EVENT sitting inside no file — `playable: false` with its reason
|
|
18674
|
+
* (measured: 1 of 29 hub events on 3628). Such a row is SHOWN, never dropped
|
|
18675
|
+
* and never offered as playable-then-failing (D549 decision 19).
|
|
18676
|
+
*
|
|
18677
|
+
* The CHROME is the library's: a `Card` for the tile, `Badge` for every label
|
|
18678
|
+
* the camera gave it, `Skeleton` while a mint is in flight. Only the ladder
|
|
18679
|
+
* above is this file's own, because only it is a decision.
|
|
18680
|
+
*/
|
|
18681
|
+
function clockOf(ms) {
|
|
18682
|
+
return new Date(ms).toLocaleTimeString(void 0, {
|
|
18683
|
+
hour: "2-digit",
|
|
18684
|
+
minute: "2-digit",
|
|
18685
|
+
second: "2-digit"
|
|
18686
|
+
});
|
|
18687
|
+
}
|
|
18688
|
+
/**
|
|
18689
|
+
* Ask a mint route once, and LATCH whatever it says.
|
|
18690
|
+
*
|
|
18691
|
+
* The offer is memoised on the three fields it is derived from, not rebuilt
|
|
18692
|
+
* per render: `resolveTileMedia` returns a fresh object every call, and an
|
|
18693
|
+
* effect keyed on that object re-runs on every commit — which, because the
|
|
18694
|
+
* effect sets state, is an infinite render loop. It was one here, and it hung
|
|
18695
|
+
* the suite rather than failing it.
|
|
18696
|
+
*
|
|
18697
|
+
* The refusal is latched for the life of the tile: a camera that just said
|
|
18698
|
+
* "busy" is not asked again behind the operator's back (D549 decision 5).
|
|
18699
|
+
*/
|
|
18700
|
+
function useMintedStill(clip) {
|
|
18701
|
+
const offered = useMemo(() => resolveTileMedia(clip), [
|
|
18702
|
+
clip.thumbnail,
|
|
18703
|
+
clip.thumbnailMint,
|
|
18704
|
+
clip.thumbnailUnavailable?.reason
|
|
18705
|
+
]);
|
|
18706
|
+
const [latched, setLatched] = useState(null);
|
|
18707
|
+
const mintUrl = offered.kind === "mint" ? offered.url : null;
|
|
18708
|
+
useEffect(() => {
|
|
18709
|
+
setLatched(null);
|
|
18710
|
+
if (mintUrl === null || typeof fetch !== "function") return void 0;
|
|
18711
|
+
let objectUrl = null;
|
|
18712
|
+
let cancelled = false;
|
|
18713
|
+
(async () => {
|
|
18714
|
+
const answer = await fetchMintedThumbnail(hubPath(mintUrl), fetch);
|
|
18715
|
+
if (cancelled) return;
|
|
18716
|
+
if (answer.kind === "image") {
|
|
18717
|
+
objectUrl = URL.createObjectURL(answer.blob);
|
|
18718
|
+
setLatched({
|
|
18719
|
+
kind: "vouched",
|
|
18720
|
+
url: objectUrl
|
|
18721
|
+
});
|
|
18722
|
+
return;
|
|
18723
|
+
}
|
|
18724
|
+
setLatched(answer);
|
|
18725
|
+
})();
|
|
18726
|
+
return () => {
|
|
18727
|
+
cancelled = true;
|
|
18728
|
+
if (objectUrl !== null) URL.revokeObjectURL(objectUrl);
|
|
18729
|
+
};
|
|
18730
|
+
}, [mintUrl]);
|
|
18731
|
+
return latched ?? offered;
|
|
18732
|
+
}
|
|
18733
|
+
function ClipTile({ clip, selected, onSelect }) {
|
|
18734
|
+
const media = useMintedStill(clip);
|
|
18735
|
+
const playable = clip.playable !== false;
|
|
18736
|
+
const labels = [...clip.labels ?? [], ...clip.behaviours ?? []];
|
|
18737
|
+
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("button", {
|
|
18738
|
+
type: "button",
|
|
18739
|
+
onClick: () => onSelect(clip),
|
|
18740
|
+
disabled: !playable,
|
|
18741
|
+
"aria-current": selected ? "true" : void 0,
|
|
18742
|
+
className: "block w-full text-left disabled:cursor-not-allowed",
|
|
18743
|
+
children: /* @__PURE__ */ jsxs(Card, {
|
|
18744
|
+
className: `flex gap-3 transition-colors ${selected ? "border-primary" : ""} ${playable ? "hover:bg-surface-hover" : "opacity-70"}`,
|
|
18745
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
18746
|
+
className: "flex h-16 w-28 shrink-0 items-center justify-center overflow-hidden rounded bg-surface-hover text-[10px] text-foreground-subtle",
|
|
18747
|
+
children: media.kind === "vouched" ? /* @__PURE__ */ jsx("img", {
|
|
18748
|
+
src: media.url,
|
|
18749
|
+
alt: "",
|
|
18750
|
+
className: "h-full w-full object-cover"
|
|
18751
|
+
}) : media.kind === "mint" ? /* @__PURE__ */ jsx(Skeleton, { className: "h-full w-full rounded-none" }) : /* @__PURE__ */ jsx("span", {
|
|
18752
|
+
className: "px-1 text-center leading-tight",
|
|
18753
|
+
children: media.text
|
|
18754
|
+
})
|
|
18755
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
18756
|
+
className: "min-w-0 flex-1 space-y-1",
|
|
18757
|
+
children: [
|
|
18758
|
+
/* @__PURE__ */ jsx("div", {
|
|
18759
|
+
className: "text-sm font-medium text-foreground",
|
|
18760
|
+
children: clockOf(clip.timeRange.startMs)
|
|
18761
|
+
}),
|
|
18762
|
+
/* @__PURE__ */ jsx("div", {
|
|
18763
|
+
className: "text-[10px] text-foreground-subtle",
|
|
18764
|
+
children: clip.source
|
|
18765
|
+
}),
|
|
18766
|
+
labels.length === 0 && clip.nativeTypes === void 0 ? null : /* @__PURE__ */ jsxs("div", {
|
|
18767
|
+
className: "flex flex-wrap gap-1",
|
|
18768
|
+
children: [labels.map((label) => /* @__PURE__ */ jsx(Badge, {
|
|
18769
|
+
variant: "info",
|
|
18770
|
+
children: label
|
|
18771
|
+
}, `label:${label}`)), (clip.nativeTypes ?? []).map((native) => /* @__PURE__ */ jsx(Badge, {
|
|
18772
|
+
style: "outline",
|
|
18773
|
+
children: native
|
|
18774
|
+
}, `native:${native}`))]
|
|
18775
|
+
}),
|
|
18776
|
+
playable ? null : /* @__PURE__ */ jsxs("div", {
|
|
18777
|
+
className: "text-[11px] text-warning",
|
|
18778
|
+
children: ["Not playable — ", clip.unplayableReason ?? "the source gave no reason"]
|
|
18779
|
+
})
|
|
18780
|
+
]
|
|
18781
|
+
})]
|
|
18782
|
+
})
|
|
18783
|
+
}) });
|
|
18784
|
+
}
|
|
18785
|
+
//#endregion
|
|
18786
|
+
//#region src/composites/clips/clip-day-window.ts
|
|
18787
|
+
function pad2(value) {
|
|
18788
|
+
return value < 10 ? `0${String(value)}` : String(value);
|
|
18789
|
+
}
|
|
18790
|
+
/** Parse `YYYY-MM-DD` into its local-midnight `Date`. */
|
|
18791
|
+
function startOfDay(dayId) {
|
|
18792
|
+
const [year, month, day] = dayId.split("-").map((part) => Number(part));
|
|
18793
|
+
return new Date(year ?? 1970, (month ?? 1) - 1, day ?? 1, 0, 0, 0, 0);
|
|
18794
|
+
}
|
|
18795
|
+
/** The LOCAL calendar day an instant falls in, as `YYYY-MM-DD`. */
|
|
18796
|
+
function toDayId(instant) {
|
|
18797
|
+
return `${String(instant.getFullYear())}-${pad2(instant.getMonth() + 1)}-${pad2(instant.getDate())}`;
|
|
18798
|
+
}
|
|
18799
|
+
/** The half-open local-wall-clock window of one day. */
|
|
18800
|
+
function dayWindow$1(dayId) {
|
|
18801
|
+
const start = startOfDay(dayId);
|
|
18802
|
+
const end = startOfDay(dayId);
|
|
18803
|
+
end.setDate(end.getDate() + 1);
|
|
18804
|
+
return {
|
|
18805
|
+
since: start.getTime(),
|
|
18806
|
+
until: end.getTime()
|
|
18807
|
+
};
|
|
18808
|
+
}
|
|
18809
|
+
/** Walk `days` calendar days from `dayId` — DST-safe, because `Date` does it. */
|
|
18810
|
+
function shiftDay(dayId, days) {
|
|
18811
|
+
const moved = startOfDay(dayId);
|
|
18812
|
+
moved.setDate(moved.getDate() + days);
|
|
18813
|
+
return toDayId(moved);
|
|
18814
|
+
}
|
|
18815
|
+
/**
|
|
18816
|
+
* The day, spelled out for the header.
|
|
18817
|
+
*
|
|
18818
|
+
* An empty list has to say WHICH day it is empty for — "no clips" with no date
|
|
18819
|
+
* beside it is the ambiguity this surface exists to remove — so this label is
|
|
18820
|
+
* rendered next to every state, not only the populated one.
|
|
18821
|
+
*/
|
|
18822
|
+
function formatDayLabel(dayId) {
|
|
18823
|
+
return startOfDay(dayId).toLocaleDateString(void 0, {
|
|
18824
|
+
weekday: "short",
|
|
18825
|
+
year: "numeric",
|
|
18826
|
+
month: "short",
|
|
18827
|
+
day: "numeric"
|
|
18828
|
+
});
|
|
18829
|
+
}
|
|
18830
|
+
//#endregion
|
|
18831
|
+
//#region src/composites/clips/clip-source-state.ts
|
|
18832
|
+
var STATE_COPY = {
|
|
18833
|
+
ok: {
|
|
18834
|
+
headline: "Answering",
|
|
18835
|
+
tone: "ok",
|
|
18836
|
+
answered: true
|
|
18837
|
+
},
|
|
18838
|
+
sleeping: {
|
|
18839
|
+
headline: "Asleep — showing the catalog read while it was awake",
|
|
18840
|
+
tone: "warn",
|
|
18841
|
+
answered: true
|
|
18842
|
+
},
|
|
18843
|
+
unreachable: {
|
|
18844
|
+
headline: "Could not be reached — its clips are missing from this list",
|
|
18845
|
+
tone: "error",
|
|
18846
|
+
answered: false
|
|
18847
|
+
},
|
|
18848
|
+
"no-storage": {
|
|
18849
|
+
headline: "No storage card — this camera keeps nothing of its own",
|
|
18850
|
+
tone: "warn",
|
|
18851
|
+
answered: false
|
|
18852
|
+
},
|
|
18853
|
+
"index-empty": {
|
|
18854
|
+
headline: "The camera's calendar marks these days, and its own index lists nothing",
|
|
18855
|
+
tone: "warn",
|
|
18856
|
+
answered: false
|
|
18857
|
+
}
|
|
18858
|
+
};
|
|
18859
|
+
/** Turn one source row into the line the picker and the list show. */
|
|
18860
|
+
function describeSource(source) {
|
|
18861
|
+
const copy = STATE_COPY[source.availability.state];
|
|
18862
|
+
return {
|
|
18863
|
+
source: source.source,
|
|
18864
|
+
label: source.label,
|
|
18865
|
+
headline: copy.headline,
|
|
18866
|
+
detail: source.availability.reason ?? null,
|
|
18867
|
+
tone: copy.tone,
|
|
18868
|
+
answered: copy.answered
|
|
18869
|
+
};
|
|
18870
|
+
}
|
|
18871
|
+
var MINUTE_MS$1 = 6e4;
|
|
18872
|
+
var HOUR_MS$1 = 36e5;
|
|
18873
|
+
var DAY_MS$2 = 864e5;
|
|
18874
|
+
/**
|
|
18875
|
+
* How old a source's catalog is, or null when it did not say.
|
|
18876
|
+
*
|
|
18877
|
+
* Null is NOT "now". A source that omits `catalogAsOf` was read live and has
|
|
18878
|
+
* no age to show; inventing "just now" for a source that never told us would
|
|
18879
|
+
* be the same lie as drawing a cached list as current (D549 decision 13).
|
|
18880
|
+
*/
|
|
18881
|
+
function formatCatalogAge(catalogAsOf, now) {
|
|
18882
|
+
if (catalogAsOf === void 0) return null;
|
|
18883
|
+
const age = Math.max(0, now - catalogAsOf);
|
|
18884
|
+
if (age < MINUTE_MS$1) return "under a minute ago";
|
|
18885
|
+
if (age < HOUR_MS$1) return `${String(Math.floor(age / MINUTE_MS$1))} min ago`;
|
|
18886
|
+
if (age < DAY_MS$2) return `${String(Math.floor(age / HOUR_MS$1))} h ago`;
|
|
18887
|
+
return `${String(Math.floor(age / DAY_MS$2))} d ago`;
|
|
18888
|
+
}
|
|
18889
|
+
//#endregion
|
|
18890
|
+
//#region src/composites/clips/clip-view-state.ts
|
|
18891
|
+
function resolveClipsView(input) {
|
|
18892
|
+
if (input.clipsStatus === "error") return {
|
|
18893
|
+
kind: "failed",
|
|
18894
|
+
message: input.clipsError ?? "The clip list could not be read"
|
|
18895
|
+
};
|
|
18896
|
+
if (input.sourcesStatus === "error" && input.clips.length === 0) return {
|
|
18897
|
+
kind: "failed",
|
|
18898
|
+
message: input.sourcesError ?? "The source list could not be read"
|
|
18899
|
+
};
|
|
18900
|
+
if (input.clipsStatus === "pending" || input.sourcesStatus === "pending") return { kind: "loading" };
|
|
18901
|
+
const notices = input.sources.map(describeSource);
|
|
18902
|
+
const answered = notices.filter((notice) => notice.answered);
|
|
18903
|
+
const silent = notices.filter((notice) => !notice.answered);
|
|
18904
|
+
const clips = input.clips;
|
|
18905
|
+
if (clips.length === 0) return {
|
|
18906
|
+
kind: "empty",
|
|
18907
|
+
dayId: input.dayId,
|
|
18908
|
+
answered,
|
|
18909
|
+
silent,
|
|
18910
|
+
nobodyAnswered: answered.length === 0
|
|
18911
|
+
};
|
|
18912
|
+
return {
|
|
18913
|
+
kind: "listed",
|
|
18914
|
+
clips,
|
|
18915
|
+
answered,
|
|
18916
|
+
silent
|
|
18917
|
+
};
|
|
18918
|
+
}
|
|
18919
|
+
//#endregion
|
|
18920
|
+
//#region src/composites/clips/ClipsSection.tsx
|
|
18921
|
+
/**
|
|
18922
|
+
* `host/clips-browser` — a camera's clips, from EVERY source it has, as the
|
|
18923
|
+
* LAST SECTION of the Recording tab.
|
|
18924
|
+
*
|
|
18925
|
+
* Mounted by the framework, not by hand: `videoclips.cap.ts` declares
|
|
18926
|
+
* `deviceConfig.ui = { kind:'widget', widgetId:'host/clips-browser',
|
|
18927
|
+
* tab:'recording', topTab:true, order:100 }` and
|
|
18928
|
+
* `deriveDeviceConfigContribution` emits the section (D14). `DeviceDetail.tsx`
|
|
18929
|
+
* is not edited — placement is data. Because the admin Recordings page renders
|
|
18930
|
+
* every `location:'top-tab'` + `tab:'recording'` section behind its camera
|
|
18931
|
+
* picker (`CameraRecordingSettingsSection`), the same declaration puts this on
|
|
18932
|
+
* both surfaces with no second wiring.
|
|
18933
|
+
*
|
|
18934
|
+
* ## Built from the library, not from scratch
|
|
18935
|
+
*
|
|
18936
|
+
* It shipped as its own top tab made of five bespoke components, and the
|
|
18937
|
+
* operator rejected both: *"La UI è orribile, non segue la nostra UI affatto …
|
|
18938
|
+
* Io in realtà utilizzerei la stessa tab recordings, lì abbiamo già tutto il
|
|
18939
|
+
* necessario, una nuova sezione alla fine per le clips dove possiamo
|
|
18940
|
+
* selezionare le source con un multiselect common, non componenti nuovi
|
|
18941
|
+
* anonimi."* So every piece of chrome here is a component this repo already
|
|
18942
|
+
* has: `Select`, `DataTable`, `EmptyState`, `ErrorBox`, `Pagination`, `Card`,
|
|
18943
|
+
* `Badge`, `Button`, `Skeleton`. What the chrome CARRIES is unchanged, and it
|
|
18944
|
+
* is the feature:
|
|
18945
|
+
*
|
|
18946
|
+
* - **The source list is complete, always.** Every source the camera has,
|
|
18947
|
+
* with its availability and the age of its catalog, whichever provider is
|
|
18948
|
+
* being asked. A source never leaves the table.
|
|
18949
|
+
* - **Nothing is deduped.** A clip held in two storages appears twice, once
|
|
18950
|
+
* per source, and every tile says which (D549).
|
|
18951
|
+
* - **Four states are tellable apart**: loading, empty-with-a-cause,
|
|
18952
|
+
* degraded-per-source, and each silent source in its own words. Every one
|
|
18953
|
+
* of them used to render as "no clips", which is the defect this whole line
|
|
18954
|
+
* of work is about — a camera with no SD card (640) is not quiet, and a
|
|
18955
|
+
* camera whose own index disagrees with its own calendar (618, 6–20
|
|
18956
|
+
* September) is not quiet either.
|
|
18957
|
+
*
|
|
18958
|
+
* ## The picker WRITES nothing, and there is nothing for it to write
|
|
18959
|
+
*
|
|
18960
|
+
* A clip source is not enabled or disabled — it EXISTS, because of a
|
|
18961
|
+
* configuration that lives somewhere else. The operator put it exactly:
|
|
18962
|
+
* *"non è affatto necessario lo stato di abilitato o meno delle sources. Loro
|
|
18963
|
+
* vivono lì, ad esempio hksv c'è se la camera è esportata su homekit, reolink
|
|
18964
|
+
* la dà di suo così come il camstack."* HKSV is there if the camera is
|
|
18965
|
+
* exported to HomeKit; the Reolink source is there because Reolink is that
|
|
18966
|
+
* camera's provider; CamStack's is always there. None of that is a decision to
|
|
18967
|
+
* make in a clips list, so this surface offers no switch and writes no
|
|
18968
|
+
* authority (D557, amended). A directory-wide guard in
|
|
18969
|
+
* `clips-section-sources.test.tsx` keeps it that way.
|
|
18970
|
+
*
|
|
18971
|
+
* ## Why the picker is a SELECT and not a multiselect
|
|
18972
|
+
*
|
|
18973
|
+
* The operator asked for *"un multiselect common"* while `listClips` still
|
|
18974
|
+
* took `sources?: string[]`. It now takes ONE `provider` — *"il get videoclips
|
|
18975
|
+
* prendere un provider opzionale, di default va su camstack … l'utilizzatore è
|
|
18976
|
+
* uno solo"* — and a multi-pick control over a one-at-a-time argument offers a
|
|
18977
|
+
* sum the hub cannot answer. So the control is the shared single-choice one,
|
|
18978
|
+
* `Select`: the same component the Recordings page's own camera picker uses,
|
|
18979
|
+
* which is what *"segue la nostra UI"* means here. Absent means the camera's
|
|
18980
|
+
* BOUND provider, and the option that carries it is labelled with that
|
|
18981
|
+
* provider's own sources rather than the word "default".
|
|
18982
|
+
*
|
|
18983
|
+
* ## One provider is a fleet STATE
|
|
18984
|
+
*
|
|
18985
|
+
* `listSources` derives from `deviceManager.getBindings`, and today nothing
|
|
18986
|
+
* binds `provider-reolink` for `videoclips`, so a real camera answers with one
|
|
18987
|
+
* provider. The selector is then inert and SAYS why. An empty or silently
|
|
18988
|
+
* single-entry control would read as a defect; the binding store learning to
|
|
18989
|
+
* hold several providers for a collection cap is what changes this, and
|
|
18990
|
+
* nothing here needs to change with it.
|
|
18991
|
+
*/
|
|
18992
|
+
/** Newest-first, and bounded: a busy day on 592 lists 546 clips. */
|
|
18993
|
+
var CLIP_PAGE_LIMIT = 500;
|
|
18994
|
+
/** Tiles per page. The rest are a `Pagination` click away, never dropped. */
|
|
18995
|
+
var TILES_PER_PAGE = 60;
|
|
18996
|
+
var TONE_BADGE = {
|
|
18997
|
+
ok: "success",
|
|
18998
|
+
warn: "warning",
|
|
18999
|
+
error: "danger"
|
|
19000
|
+
};
|
|
19001
|
+
var SOURCE_COLUMNS = [
|
|
19002
|
+
{
|
|
19003
|
+
key: "source",
|
|
19004
|
+
header: "Source",
|
|
19005
|
+
render: (row) => /* @__PURE__ */ jsxs("div", {
|
|
19006
|
+
className: "min-w-0",
|
|
19007
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
19008
|
+
className: "font-medium text-foreground",
|
|
19009
|
+
children: row.notice.label
|
|
19010
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
19011
|
+
className: "text-[10px] text-foreground-subtle",
|
|
19012
|
+
children: row.notice.source
|
|
19013
|
+
})]
|
|
19014
|
+
})
|
|
19015
|
+
},
|
|
19016
|
+
{
|
|
19017
|
+
key: "state",
|
|
19018
|
+
header: "State",
|
|
19019
|
+
nowrap: false,
|
|
19020
|
+
render: (row) => /* @__PURE__ */ jsx(Badge, {
|
|
19021
|
+
variant: TONE_BADGE[row.notice.tone],
|
|
19022
|
+
children: row.notice.headline
|
|
19023
|
+
})
|
|
19024
|
+
},
|
|
19025
|
+
{
|
|
19026
|
+
key: "reason",
|
|
19027
|
+
header: "Reason",
|
|
19028
|
+
nowrap: false,
|
|
19029
|
+
render: (row) => row.notice.detail === null ? /* @__PURE__ */ jsx("span", {
|
|
19030
|
+
className: "text-foreground-subtle",
|
|
19031
|
+
children: "—"
|
|
19032
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
19033
|
+
className: "break-words",
|
|
19034
|
+
children: row.notice.detail
|
|
19035
|
+
})
|
|
19036
|
+
},
|
|
19037
|
+
{
|
|
19038
|
+
key: "read",
|
|
19039
|
+
header: "Catalog read",
|
|
19040
|
+
render: (row) => /* @__PURE__ */ jsx("span", {
|
|
19041
|
+
className: "text-foreground-subtle",
|
|
19042
|
+
children: row.catalogAge ?? "live"
|
|
19043
|
+
})
|
|
19044
|
+
}
|
|
19045
|
+
];
|
|
19046
|
+
/** The sources that contributed NOTHING to what is on screen, and why. */
|
|
19047
|
+
function SilentSources({ silent }) {
|
|
19048
|
+
if (silent.length === 0) return null;
|
|
19049
|
+
return /* @__PURE__ */ jsx(ErrorBox, {
|
|
19050
|
+
variant: "warning",
|
|
19051
|
+
title: silent.length === 1 ? "One source contributed nothing to this list" : `${String(silent.length)} sources contributed nothing to this list`,
|
|
19052
|
+
message: /* @__PURE__ */ jsx("div", {
|
|
19053
|
+
className: "space-y-1",
|
|
19054
|
+
children: silent.map((notice) => /* @__PURE__ */ jsxs("div", { children: [
|
|
19055
|
+
/* @__PURE__ */ jsx("span", {
|
|
19056
|
+
className: "font-medium",
|
|
19057
|
+
children: notice.label
|
|
19058
|
+
}),
|
|
19059
|
+
/* @__PURE__ */ jsxs("span", {
|
|
19060
|
+
className: "opacity-70",
|
|
19061
|
+
children: [
|
|
19062
|
+
" (",
|
|
19063
|
+
notice.source,
|
|
19064
|
+
")"
|
|
19065
|
+
]
|
|
19066
|
+
}),
|
|
19067
|
+
/* @__PURE__ */ jsx("div", { children: notice.headline }),
|
|
19068
|
+
notice.detail === null ? null : /* @__PURE__ */ jsx("div", {
|
|
19069
|
+
className: "opacity-80 break-words",
|
|
19070
|
+
children: notice.detail
|
|
19071
|
+
})
|
|
19072
|
+
] }, notice.source))
|
|
19073
|
+
})
|
|
19074
|
+
});
|
|
19075
|
+
}
|
|
19076
|
+
function ClipsSection({ deviceId }) {
|
|
19077
|
+
const [dayId, setDayId] = useState(() => toDayId(/* @__PURE__ */ new Date()));
|
|
19078
|
+
const [provider, setProvider] = useState("");
|
|
19079
|
+
const [page, setPage] = useState(1);
|
|
19080
|
+
const [selectedClip, setSelectedClip] = useState(null);
|
|
19081
|
+
const [profile, setProfile] = useState("low");
|
|
19082
|
+
const presence = useCapabilityPresence("videoclips");
|
|
19083
|
+
const window = useMemo(() => dayWindow$1(dayId), [dayId]);
|
|
19084
|
+
const sources = useVideoclipsListSources({ deviceId });
|
|
19085
|
+
const clips = useVideoclipsListClips({
|
|
19086
|
+
deviceId,
|
|
19087
|
+
since: window.since,
|
|
19088
|
+
until: window.until,
|
|
19089
|
+
limit: CLIP_PAGE_LIMIT,
|
|
19090
|
+
...provider === "" ? {} : { provider }
|
|
19091
|
+
});
|
|
19092
|
+
const sourceRows = sources.data ?? [];
|
|
19093
|
+
const view = resolveClipsView({
|
|
19094
|
+
dayId,
|
|
19095
|
+
clipsStatus: clips.status,
|
|
19096
|
+
clips: clips.data ?? [],
|
|
19097
|
+
sourcesStatus: sources.status,
|
|
19098
|
+
sources: sourceRows,
|
|
19099
|
+
...clips.error ? { clipsError: clips.error.message } : {},
|
|
19100
|
+
...sources.error ? { sourcesError: sources.error.message } : {}
|
|
19101
|
+
});
|
|
19102
|
+
const now = Date.now();
|
|
19103
|
+
const options = useMemo(() => {
|
|
19104
|
+
const byAddon = /* @__PURE__ */ new Map();
|
|
19105
|
+
for (const row of sourceRows) {
|
|
19106
|
+
const addonId = row.addonId;
|
|
19107
|
+
if (addonId === void 0) continue;
|
|
19108
|
+
const labels = byAddon.get(addonId);
|
|
19109
|
+
if (labels === void 0) byAddon.set(addonId, [row.label]);
|
|
19110
|
+
else labels.push(row.label);
|
|
19111
|
+
}
|
|
19112
|
+
const rest = [...byAddon].map(([value, labels]) => ({
|
|
19113
|
+
value,
|
|
19114
|
+
label: labels.join(" · ")
|
|
19115
|
+
}));
|
|
19116
|
+
const first = rest[0];
|
|
19117
|
+
return [{
|
|
19118
|
+
value: "",
|
|
19119
|
+
label: first === void 0 ? "The camera’s bound provider" : first.label
|
|
19120
|
+
}, ...rest.slice(1)];
|
|
19121
|
+
}, [sourceRows]);
|
|
19122
|
+
const tableRows = useMemo(() => sourceRows.map((row) => ({
|
|
19123
|
+
notice: describeSource(row),
|
|
19124
|
+
catalogAge: formatCatalogAge(row.availability.catalogAsOf, now)
|
|
19125
|
+
})), [sourceRows]);
|
|
19126
|
+
const goToDay = (next) => {
|
|
19127
|
+
setDayId(next);
|
|
19128
|
+
setPage(1);
|
|
19129
|
+
};
|
|
19130
|
+
const pick = (next) => {
|
|
19131
|
+
setProvider(next);
|
|
19132
|
+
setPage(1);
|
|
19133
|
+
};
|
|
19134
|
+
if (presence === "pending") return /* @__PURE__ */ jsxs("div", {
|
|
19135
|
+
className: "space-y-2",
|
|
19136
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
19137
|
+
role: "status",
|
|
19138
|
+
className: "text-xs text-foreground-subtle",
|
|
19139
|
+
children: "Asking the registry whether this cluster serves clips…"
|
|
19140
|
+
}), /* @__PURE__ */ jsx(Skeleton, { className: "h-24 w-full" })]
|
|
19141
|
+
});
|
|
19142
|
+
if (presence === "unknown") return /* @__PURE__ */ jsx(ErrorBox, {
|
|
19143
|
+
variant: "warning",
|
|
19144
|
+
title: "Clip sources unknown",
|
|
19145
|
+
message: "The capability registry could not be reached, so whether this camera has clip sources is unknown — not absent."
|
|
19146
|
+
});
|
|
19147
|
+
if (presence === "absent") return /* @__PURE__ */ jsx("div", {
|
|
19148
|
+
role: "status",
|
|
19149
|
+
children: /* @__PURE__ */ jsx(EmptyState, {
|
|
19150
|
+
icon: Clapperboard,
|
|
19151
|
+
title: "No provider of videoclips in this cluster",
|
|
19152
|
+
description: "Nothing is installed that could list a camera's stored clips."
|
|
19153
|
+
})
|
|
19154
|
+
});
|
|
19155
|
+
const visible = view.kind === "listed" ? view.clips : [];
|
|
19156
|
+
const pageClips = visible.slice((page - 1) * TILES_PER_PAGE, page * TILES_PER_PAGE);
|
|
19157
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
19158
|
+
className: "space-y-3",
|
|
19159
|
+
children: [
|
|
19160
|
+
/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h3", {
|
|
19161
|
+
className: "text-sm font-semibold text-foreground",
|
|
19162
|
+
children: "Clips"
|
|
19163
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
19164
|
+
className: "mt-1 text-[11px] text-foreground-subtle",
|
|
19165
|
+
children: "Every source this camera has, side by side — CamStack's own recordings and whatever the camera or an export keeps of its own. A source is listed because it exists, not because it was switched on here; pick sources to narrow the view."
|
|
19166
|
+
})] }),
|
|
19167
|
+
/* @__PURE__ */ jsxs("div", {
|
|
19168
|
+
className: "flex flex-wrap items-center gap-2",
|
|
19169
|
+
children: [
|
|
19170
|
+
/* @__PURE__ */ jsx("label", {
|
|
19171
|
+
htmlFor: "clips-provider",
|
|
19172
|
+
className: "text-[10px] uppercase tracking-wide text-foreground-subtle",
|
|
19173
|
+
children: "Source"
|
|
19174
|
+
}),
|
|
19175
|
+
/* @__PURE__ */ jsx("div", {
|
|
19176
|
+
className: "w-60",
|
|
19177
|
+
children: /* @__PURE__ */ jsx(Select, {
|
|
19178
|
+
id: "clips-provider",
|
|
19179
|
+
options,
|
|
19180
|
+
value: provider,
|
|
19181
|
+
disabled: options.length < 2,
|
|
19182
|
+
onChange: (e) => pick(e.target.value)
|
|
19183
|
+
})
|
|
19184
|
+
}),
|
|
19185
|
+
options.length < 2 ? /* @__PURE__ */ jsx("span", {
|
|
19186
|
+
className: "text-[11px] text-foreground-subtle",
|
|
19187
|
+
children: "This camera is bound to one clip provider today."
|
|
19188
|
+
}) : null,
|
|
19189
|
+
/* @__PURE__ */ jsxs("div", {
|
|
19190
|
+
className: "ml-auto flex items-center gap-1",
|
|
19191
|
+
children: [
|
|
19192
|
+
/* @__PURE__ */ jsx(Button, {
|
|
19193
|
+
variant: "ghost",
|
|
19194
|
+
"aria-label": "Previous day",
|
|
19195
|
+
onClick: () => goToDay(shiftDay(dayId, -1)),
|
|
19196
|
+
children: "‹"
|
|
19197
|
+
}),
|
|
19198
|
+
/* @__PURE__ */ jsx("span", {
|
|
19199
|
+
className: "px-1 text-xs font-medium text-foreground",
|
|
19200
|
+
children: formatDayLabel(dayId)
|
|
19201
|
+
}),
|
|
19202
|
+
/* @__PURE__ */ jsx(Button, {
|
|
19203
|
+
variant: "ghost",
|
|
19204
|
+
"aria-label": "Next day",
|
|
19205
|
+
onClick: () => goToDay(shiftDay(dayId, 1)),
|
|
19206
|
+
children: "›"
|
|
19207
|
+
}),
|
|
19208
|
+
/* @__PURE__ */ jsx(Button, {
|
|
19209
|
+
variant: "outline",
|
|
19210
|
+
onClick: () => goToDay(toDayId(/* @__PURE__ */ new Date())),
|
|
19211
|
+
children: "Today"
|
|
19212
|
+
})
|
|
19213
|
+
]
|
|
19214
|
+
})
|
|
19215
|
+
]
|
|
19216
|
+
}),
|
|
19217
|
+
/* @__PURE__ */ jsx("section", {
|
|
19218
|
+
"aria-label": "Clip sources",
|
|
19219
|
+
className: "space-y-1",
|
|
19220
|
+
children: sources.status === "pending" ? /* @__PURE__ */ jsx(Skeleton, { className: "h-16 w-full" }) : tableRows.length === 0 ? /* @__PURE__ */ jsx(EmptyState, {
|
|
19221
|
+
icon: Film,
|
|
19222
|
+
title: "No provider answered for this camera",
|
|
19223
|
+
description: "Nothing claims to hold clips for it."
|
|
19224
|
+
}) : /* @__PURE__ */ jsx(DataTable, {
|
|
19225
|
+
columns: SOURCE_COLUMNS,
|
|
19226
|
+
rows: [...tableRows],
|
|
19227
|
+
minWidthPx: 520
|
|
19228
|
+
})
|
|
19229
|
+
}),
|
|
19230
|
+
/* @__PURE__ */ jsx("section", {
|
|
19231
|
+
"aria-label": "Clips",
|
|
19232
|
+
className: "space-y-2",
|
|
19233
|
+
children: view.kind === "loading" ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
|
|
19234
|
+
role: "status",
|
|
19235
|
+
className: "text-xs text-foreground-subtle",
|
|
19236
|
+
children: [
|
|
19237
|
+
"Loading clips for ",
|
|
19238
|
+
formatDayLabel(dayId),
|
|
19239
|
+
"…"
|
|
19240
|
+
]
|
|
19241
|
+
}), /* @__PURE__ */ jsx(Skeleton, { className: "h-24 w-full" })] }) : view.kind === "failed" ? /* @__PURE__ */ jsx(ErrorBox, {
|
|
19242
|
+
title: "The clip list could not be read",
|
|
19243
|
+
message: view.message
|
|
19244
|
+
}) : view.kind === "empty" ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
|
|
19245
|
+
role: "status",
|
|
19246
|
+
children: /* @__PURE__ */ jsx(EmptyState, {
|
|
19247
|
+
icon: Film,
|
|
19248
|
+
title: view.nobodyAnswered ? `No source could answer for ${formatDayLabel(dayId)}` : `No clips on ${formatDayLabel(dayId)}`,
|
|
19249
|
+
...view.nobodyAnswered ? {} : { description: `Asked ${view.answered.map((notice) => notice.label).join(", ")}.` }
|
|
19250
|
+
})
|
|
19251
|
+
}), /* @__PURE__ */ jsx(SilentSources, { silent: view.silent })] }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
19252
|
+
/* @__PURE__ */ jsx(SilentSources, { silent: view.silent }),
|
|
19253
|
+
/* @__PURE__ */ jsx("ul", {
|
|
19254
|
+
className: "grid grid-cols-1 gap-2 sm:grid-cols-2 xl:grid-cols-3",
|
|
19255
|
+
children: pageClips.map((clip) => /* @__PURE__ */ jsx(ClipTile, {
|
|
19256
|
+
clip,
|
|
19257
|
+
selected: selectedClip?.id === clip.id,
|
|
19258
|
+
onSelect: setSelectedClip
|
|
19259
|
+
}, `${clip.source}:${clip.id}`))
|
|
19260
|
+
}),
|
|
19261
|
+
/* @__PURE__ */ jsx(Pagination, {
|
|
19262
|
+
currentPage: page,
|
|
19263
|
+
totalItems: visible.length,
|
|
19264
|
+
pageSize: TILES_PER_PAGE,
|
|
19265
|
+
onPageChange: setPage,
|
|
19266
|
+
itemNoun: "clips"
|
|
19267
|
+
})
|
|
19268
|
+
] })
|
|
19269
|
+
}),
|
|
19270
|
+
selectedClip === null ? null : /* @__PURE__ */ jsx(ClipPanel, {
|
|
19271
|
+
deviceId,
|
|
19272
|
+
clip: selectedClip,
|
|
19273
|
+
profile,
|
|
19274
|
+
onProfileChange: setProfile
|
|
19275
|
+
})
|
|
19276
|
+
]
|
|
19277
|
+
});
|
|
19278
|
+
}
|
|
19279
|
+
//#endregion
|
|
19280
|
+
//#region src/hooks/use-navigation.ts
|
|
19281
|
+
/**
|
|
19282
|
+
* useNavigation — drive a `navigation` robot camera from the UI.
|
|
19283
|
+
*
|
|
19284
|
+
* The cap's `move` is MOMENTARY: fluid driving is the UI sending a nudge at
|
|
19285
|
+
* 1 Hz while a direction is held and ONE `stop` on release, exactly like the
|
|
19286
|
+
* robot app's joystick. This hook owns that cadence (`startDrive` /
|
|
19287
|
+
* `stopDrive`); the provider forwards each nudge to a single drive write.
|
|
19288
|
+
*
|
|
19289
|
+
* Everything else is a plain call plus the data the panel renders from:
|
|
19290
|
+
* `features` (which primitives are on), `actions` (the enabled dictionary)
|
|
19291
|
+
* and the live `status` slice off the device proxy.
|
|
19292
|
+
*/
|
|
19293
|
+
/** The cadence the robot app drives at while a direction is held. */
|
|
19294
|
+
var NAVIGATION_DRIVE_INTERVAL_MS = 1e3;
|
|
19295
|
+
var DIRECTION_VECTORS$1 = {
|
|
19296
|
+
forward: {
|
|
19297
|
+
pan: 0,
|
|
19298
|
+
tilt: 1
|
|
19299
|
+
},
|
|
19300
|
+
left: {
|
|
19301
|
+
pan: -1,
|
|
19302
|
+
tilt: 0
|
|
19303
|
+
},
|
|
19304
|
+
right: {
|
|
19305
|
+
pan: 1,
|
|
19306
|
+
tilt: 0
|
|
19307
|
+
},
|
|
19308
|
+
spin: {
|
|
19309
|
+
pan: 0,
|
|
19310
|
+
tilt: -1
|
|
19311
|
+
}
|
|
19312
|
+
};
|
|
19313
|
+
function isAbsentProvider$1(err) {
|
|
19314
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
19315
|
+
return /not available|no provider|NOT_FOUND|PRECONDITION/i.test(message);
|
|
19316
|
+
}
|
|
19317
|
+
function useNavigation(trpc, deviceId, hookOptions) {
|
|
19318
|
+
const enabled = hookOptions?.enabled ?? true;
|
|
19319
|
+
const nav = useDeviceProxy(trpc, enabled ? deviceId : null)?.navigation;
|
|
19320
|
+
const status = useDeviceState(trpc, enabled ? deviceId : null, (s) => s.navigation);
|
|
19321
|
+
const [features, setFeatures] = useState(null);
|
|
19322
|
+
const [actions, setActions] = useState([]);
|
|
19323
|
+
const [busy, setBusy] = useState(false);
|
|
19324
|
+
const [error, setError] = useState(null);
|
|
19325
|
+
const [driving, setDriving] = useState(null);
|
|
19326
|
+
const driveTimer = useRef(null);
|
|
19327
|
+
useEffect(() => {
|
|
19328
|
+
if (!enabled || !nav) return;
|
|
19329
|
+
let cancelled = false;
|
|
19330
|
+
(async () => {
|
|
19331
|
+
try {
|
|
19332
|
+
const [nextFeatures, nextActions] = await Promise.all([nav.getFeatures({}), nav.listActions({})]);
|
|
19333
|
+
if (cancelled) return;
|
|
19334
|
+
setFeatures(nextFeatures);
|
|
19335
|
+
setActions(nextActions);
|
|
19336
|
+
} catch (err) {
|
|
19337
|
+
if (cancelled || isAbsentProvider$1(err)) return;
|
|
19338
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
19339
|
+
}
|
|
19340
|
+
})();
|
|
19341
|
+
return () => {
|
|
19342
|
+
cancelled = true;
|
|
19343
|
+
};
|
|
19344
|
+
}, [nav, enabled]);
|
|
19345
|
+
const wrap = useCallback(async (fn) => {
|
|
19346
|
+
if (!enabled || !nav) return;
|
|
19347
|
+
setError(null);
|
|
19348
|
+
setBusy(true);
|
|
19349
|
+
try {
|
|
19350
|
+
await fn();
|
|
19351
|
+
} catch (err) {
|
|
19352
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
19353
|
+
} finally {
|
|
19354
|
+
setBusy(false);
|
|
19355
|
+
}
|
|
19356
|
+
}, [enabled, nav]);
|
|
19357
|
+
const clearTimer = useCallback(() => {
|
|
19358
|
+
if (driveTimer.current !== null) {
|
|
19359
|
+
clearInterval(driveTimer.current);
|
|
19360
|
+
driveTimer.current = null;
|
|
19361
|
+
}
|
|
19362
|
+
}, []);
|
|
19363
|
+
const stopDrive = useCallback(async () => {
|
|
19364
|
+
clearTimer();
|
|
19365
|
+
setDriving(null);
|
|
19366
|
+
if (!nav) return;
|
|
19367
|
+
await wrap(() => nav.stop({}));
|
|
19368
|
+
}, [
|
|
19369
|
+
clearTimer,
|
|
19370
|
+
nav,
|
|
19371
|
+
wrap
|
|
19372
|
+
]);
|
|
19373
|
+
const startDrive = useCallback((direction) => {
|
|
19374
|
+
if (!nav) return;
|
|
19375
|
+
clearTimer();
|
|
19376
|
+
setDriving(direction);
|
|
19377
|
+
const vector = DIRECTION_VECTORS$1[direction];
|
|
19378
|
+
const nudge = () => {
|
|
19379
|
+
nav.move({
|
|
19380
|
+
pan: vector.pan,
|
|
19381
|
+
tilt: vector.tilt
|
|
19382
|
+
}).catch((err) => {
|
|
19383
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
19384
|
+
});
|
|
19385
|
+
};
|
|
19386
|
+
nudge();
|
|
19387
|
+
driveTimer.current = setInterval(nudge, NAVIGATION_DRIVE_INTERVAL_MS);
|
|
19388
|
+
}, [clearTimer, nav]);
|
|
19389
|
+
useEffect(() => {
|
|
19390
|
+
return () => {
|
|
19391
|
+
if (driveTimer.current !== null) {
|
|
19392
|
+
clearInterval(driveTimer.current);
|
|
19393
|
+
driveTimer.current = null;
|
|
19394
|
+
nav?.stop({}).catch(() => void 0);
|
|
19395
|
+
}
|
|
19396
|
+
};
|
|
19397
|
+
}, [nav]);
|
|
19398
|
+
const runAction = useCallback((actionId) => wrap(async () => nav?.runAction({ actionId })), [nav, wrap]);
|
|
19399
|
+
const playSound = useCallback((soundId) => wrap(async () => nav?.playSound({ soundId })), [nav, wrap]);
|
|
19400
|
+
const setLightOn = useCallback((on) => wrap(async () => nav?.setLightOn({ on })), [nav, wrap]);
|
|
19401
|
+
const setLightMode = useCallback((mode, level) => wrap(async () => nav?.setLightMode(level === void 0 ? { mode } : {
|
|
19402
|
+
mode,
|
|
19403
|
+
level
|
|
19404
|
+
})), [nav, wrap]);
|
|
19405
|
+
const setLightLevel = useCallback((level) => wrap(async () => nav?.setLightLevel({ level })), [nav, wrap]);
|
|
19406
|
+
return {
|
|
19407
|
+
features,
|
|
19408
|
+
actions,
|
|
19409
|
+
status: status ?? null,
|
|
19410
|
+
startDrive,
|
|
19411
|
+
stopDrive,
|
|
19412
|
+
runAction,
|
|
19413
|
+
playSound,
|
|
19414
|
+
setLightOn,
|
|
19415
|
+
setLightMode,
|
|
19416
|
+
setLightLevel,
|
|
19417
|
+
busy,
|
|
19418
|
+
error,
|
|
19419
|
+
driving
|
|
19420
|
+
};
|
|
19421
|
+
}
|
|
19422
|
+
//#endregion
|
|
19423
|
+
//#region src/composites/cap-settings/NavigationPanel.tsx
|
|
19424
|
+
/** The cap's icon HINTS are lucide names; unknown hints fall back to a spark. */
|
|
19425
|
+
var ICONS = {
|
|
19426
|
+
play: Play,
|
|
19427
|
+
pause: Pause,
|
|
19428
|
+
hand: Hand,
|
|
19429
|
+
home: House,
|
|
19430
|
+
bell: Bell,
|
|
19431
|
+
crosshair: Crosshair,
|
|
19432
|
+
"paw-print": PawPrint,
|
|
19433
|
+
footprints: Footprints,
|
|
19434
|
+
droplets: Droplets,
|
|
19435
|
+
"trash-2": Trash2,
|
|
19436
|
+
flashlight: Flashlight,
|
|
19437
|
+
"flashlight-off": FlashlightOff,
|
|
19438
|
+
cat: Cat,
|
|
19439
|
+
dog: Dog,
|
|
19440
|
+
clock: Clock
|
|
19441
|
+
};
|
|
19442
|
+
function iconFor(hint) {
|
|
19443
|
+
return ICONS[hint] ?? Sparkles;
|
|
19444
|
+
}
|
|
19445
|
+
var DRIVE_BUTTONS = [
|
|
19446
|
+
{
|
|
19447
|
+
direction: "left",
|
|
19448
|
+
label: "Turn left",
|
|
19449
|
+
Icon: RotateCcw
|
|
19450
|
+
},
|
|
19451
|
+
{
|
|
19452
|
+
direction: "forward",
|
|
19453
|
+
label: "Forward",
|
|
19454
|
+
Icon: ArrowUp
|
|
19455
|
+
},
|
|
19456
|
+
{
|
|
19457
|
+
direction: "right",
|
|
19458
|
+
label: "Turn right",
|
|
19459
|
+
Icon: RotateCw
|
|
19460
|
+
},
|
|
19461
|
+
{
|
|
19462
|
+
direction: "spin",
|
|
19463
|
+
label: "Turn around",
|
|
19464
|
+
Icon: Undo2
|
|
19465
|
+
}
|
|
19466
|
+
];
|
|
19467
|
+
var BUTTON = "inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-[10px] font-medium transition-colors bg-surface-elevated/40 text-foreground-subtle hover:bg-surface-hover hover:text-foreground disabled:opacity-50";
|
|
19468
|
+
var BUTTON_ON = "bg-success/15 text-success hover:bg-success/25";
|
|
19469
|
+
function EntryButton({ entry, disabled, onRun }) {
|
|
19470
|
+
const Glyph = iconFor(entry.icon);
|
|
19471
|
+
return /* @__PURE__ */ jsxs("button", {
|
|
19472
|
+
type: "button",
|
|
19473
|
+
className: BUTTON,
|
|
19474
|
+
disabled,
|
|
19475
|
+
onClick: () => onRun(entry),
|
|
19476
|
+
title: entry.label,
|
|
19477
|
+
"aria-label": entry.label,
|
|
19478
|
+
children: [/* @__PURE__ */ jsx(Glyph, { className: "h-3.5 w-3.5" }), /* @__PURE__ */ jsx("span", { children: entry.label })]
|
|
19479
|
+
});
|
|
19480
|
+
}
|
|
19481
|
+
function NavigationPanel({ deviceId }) {
|
|
19482
|
+
const system = useSystem();
|
|
19483
|
+
const ready = Number.isFinite(deviceId);
|
|
19484
|
+
const nav = useNavigation(system.trpcClient, ready ? deviceId : 0, { enabled: ready });
|
|
19485
|
+
if (!ready) return /* @__PURE__ */ jsxs("div", {
|
|
19486
|
+
className: "flex items-center justify-center h-full text-[11px] text-foreground-subtle italic",
|
|
19487
|
+
children: [
|
|
19488
|
+
"Device #",
|
|
19489
|
+
deviceId,
|
|
19490
|
+
" not loaded."
|
|
19491
|
+
]
|
|
19492
|
+
});
|
|
19493
|
+
const features = nav.features;
|
|
19494
|
+
const actions = nav.actions.filter((entry) => entry.enabled && entry.kind === "action");
|
|
19495
|
+
const sounds = nav.actions.filter((entry) => entry.enabled && entry.kind === "sound");
|
|
19496
|
+
const status = nav.status;
|
|
19497
|
+
const manual = status?.lightMode === "manual";
|
|
19498
|
+
const run = (entry) => {
|
|
19499
|
+
if (entry.kind === "sound") {
|
|
19500
|
+
if (entry.soundId !== void 0) nav.playSound(entry.soundId);
|
|
19501
|
+
return;
|
|
19502
|
+
}
|
|
19503
|
+
nav.runAction(entry.id);
|
|
19504
|
+
};
|
|
19505
|
+
return /* @__PURE__ */ jsxs(WidgetPanel, {
|
|
19506
|
+
title: "Navigation",
|
|
19507
|
+
icon: /* @__PURE__ */ jsx(Navigation, { className: "h-3.5 w-3.5 text-foreground-subtle" }),
|
|
19508
|
+
bodyClassName: "p-3 flex flex-col gap-3 overflow-y-auto",
|
|
19509
|
+
children: [
|
|
19510
|
+
status && /* @__PURE__ */ jsxs("div", {
|
|
19511
|
+
className: "flex flex-wrap items-center gap-1.5 text-[10px]",
|
|
19512
|
+
children: [
|
|
19513
|
+
/* @__PURE__ */ jsx("span", {
|
|
19514
|
+
className: "rounded-full bg-surface-elevated/60 px-2 py-0.5 text-foreground",
|
|
19515
|
+
children: status.mode
|
|
19516
|
+
}),
|
|
19517
|
+
status.following && /* @__PURE__ */ jsx("span", {
|
|
19518
|
+
className: "rounded-full bg-info/15 px-2 py-0.5 text-info",
|
|
19519
|
+
children: "following"
|
|
19520
|
+
}),
|
|
19521
|
+
status.flash && /* @__PURE__ */ jsx("span", {
|
|
19522
|
+
className: "rounded-full bg-warning/15 px-2 py-0.5 text-warning",
|
|
19523
|
+
children: "light on"
|
|
19524
|
+
})
|
|
19525
|
+
]
|
|
19526
|
+
}),
|
|
19527
|
+
features?.move && /* @__PURE__ */ jsxs("section", {
|
|
19528
|
+
className: "flex flex-col gap-1.5",
|
|
19529
|
+
children: [
|
|
19530
|
+
/* @__PURE__ */ jsx("span", {
|
|
19531
|
+
className: "text-[10.5px] font-semibold text-foreground",
|
|
19532
|
+
children: "Drive (hold)"
|
|
19533
|
+
}),
|
|
19534
|
+
/* @__PURE__ */ jsx("div", {
|
|
19535
|
+
className: "grid grid-cols-4 gap-1.5",
|
|
19536
|
+
children: DRIVE_BUTTONS.map(({ direction, label, Icon }) => /* @__PURE__ */ jsx("button", {
|
|
19537
|
+
type: "button",
|
|
19538
|
+
className: cn(BUTTON, "justify-center select-none", nav.driving === direction && BUTTON_ON),
|
|
19539
|
+
"aria-label": label,
|
|
19540
|
+
title: label,
|
|
19541
|
+
onPointerDown: () => nav.startDrive(direction),
|
|
19542
|
+
onPointerUp: () => void nav.stopDrive(),
|
|
19543
|
+
onPointerLeave: () => {
|
|
19544
|
+
if (nav.driving === direction) nav.stopDrive();
|
|
19545
|
+
},
|
|
19546
|
+
onPointerCancel: () => void nav.stopDrive(),
|
|
19547
|
+
children: /* @__PURE__ */ jsx(Icon, { className: "h-4 w-4" })
|
|
19548
|
+
}, direction))
|
|
19549
|
+
}),
|
|
19550
|
+
features.stop && /* @__PURE__ */ jsxs("button", {
|
|
19551
|
+
type: "button",
|
|
19552
|
+
className: cn(BUTTON, "justify-center"),
|
|
19553
|
+
onClick: () => void nav.stopDrive(),
|
|
19554
|
+
children: [/* @__PURE__ */ jsx(Hand, { className: "h-3.5 w-3.5" }), /* @__PURE__ */ jsx("span", { children: "Stop" })]
|
|
19555
|
+
})
|
|
19556
|
+
]
|
|
19557
|
+
}),
|
|
19558
|
+
features?.runAction && actions.length > 0 && /* @__PURE__ */ jsxs("section", {
|
|
19559
|
+
className: "flex flex-col gap-1.5",
|
|
19560
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
19561
|
+
className: "text-[10.5px] font-semibold text-foreground",
|
|
19562
|
+
children: "Actions"
|
|
19563
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
19564
|
+
className: "flex flex-wrap gap-1.5",
|
|
19565
|
+
children: actions.map((entry) => /* @__PURE__ */ jsx(EntryButton, {
|
|
19566
|
+
entry,
|
|
19567
|
+
disabled: nav.busy,
|
|
19568
|
+
onRun: run
|
|
19569
|
+
}, entry.id))
|
|
19570
|
+
})]
|
|
19571
|
+
}),
|
|
19572
|
+
features?.playSound && sounds.length > 0 && /* @__PURE__ */ jsxs("section", {
|
|
19573
|
+
className: "flex flex-col gap-1.5",
|
|
19574
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
19575
|
+
className: "text-[10.5px] font-semibold text-foreground",
|
|
19576
|
+
children: "Sounds"
|
|
19577
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
19578
|
+
className: "flex flex-wrap gap-1.5",
|
|
19579
|
+
children: sounds.map((entry) => /* @__PURE__ */ jsx(EntryButton, {
|
|
19580
|
+
entry,
|
|
19581
|
+
disabled: nav.busy,
|
|
19582
|
+
onRun: run
|
|
19583
|
+
}, entry.id))
|
|
19584
|
+
})]
|
|
19585
|
+
}),
|
|
19586
|
+
features?.light && /* @__PURE__ */ jsxs("section", {
|
|
19587
|
+
className: "flex flex-col gap-1.5 border-t border-border/40 pt-2",
|
|
19588
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
19589
|
+
className: "flex items-center justify-between",
|
|
19590
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
19591
|
+
className: "text-[10.5px] font-semibold text-foreground",
|
|
19592
|
+
children: "Fill light"
|
|
19593
|
+
}), /* @__PURE__ */ jsxs("button", {
|
|
19594
|
+
type: "button",
|
|
19595
|
+
className: cn(BUTTON, status?.flash && BUTTON_ON),
|
|
19596
|
+
"aria-pressed": status?.flash === true,
|
|
19597
|
+
disabled: nav.busy,
|
|
19598
|
+
onClick: () => void nav.setLightOn(!(status?.flash === true)),
|
|
19599
|
+
children: [status?.flash ? /* @__PURE__ */ jsx(Flashlight, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx(FlashlightOff, { className: "h-3.5 w-3.5" }), status?.flash ? "On" : "Off"]
|
|
19600
|
+
})]
|
|
19601
|
+
}), features.lightMode && /* @__PURE__ */ jsxs("div", {
|
|
19602
|
+
className: "flex items-center gap-2",
|
|
19603
|
+
children: [
|
|
19604
|
+
/* @__PURE__ */ jsxs("select", {
|
|
19605
|
+
"aria-label": "Light mode",
|
|
19606
|
+
className: "rounded-md bg-surface-elevated/40 px-2 py-1 text-[10px] text-foreground",
|
|
19607
|
+
value: status?.lightMode ?? "auto",
|
|
19608
|
+
disabled: nav.busy,
|
|
19609
|
+
onChange: (event) => {
|
|
19610
|
+
const mode = event.target.value === "manual" ? "manual" : "auto";
|
|
19611
|
+
nav.setLightMode(mode, mode === "manual" ? status?.lightLevel : void 0);
|
|
19612
|
+
},
|
|
19613
|
+
children: [/* @__PURE__ */ jsx("option", {
|
|
19614
|
+
value: "auto",
|
|
19615
|
+
children: "Auto"
|
|
19616
|
+
}), /* @__PURE__ */ jsx("option", {
|
|
19617
|
+
value: "manual",
|
|
19618
|
+
children: "Manual"
|
|
19619
|
+
})]
|
|
19620
|
+
}),
|
|
19621
|
+
manual && /* @__PURE__ */ jsx("input", {
|
|
19622
|
+
type: "range",
|
|
19623
|
+
"aria-label": "Light level",
|
|
19624
|
+
className: "flex-1",
|
|
19625
|
+
min: NAVIGATION_LIGHT_LEVEL_MIN,
|
|
19626
|
+
max: NAVIGATION_LIGHT_LEVEL_MAX,
|
|
19627
|
+
step: 1,
|
|
19628
|
+
value: status?.lightLevel ?? NAVIGATION_LIGHT_LEVEL_MAX,
|
|
19629
|
+
disabled: nav.busy,
|
|
19630
|
+
onChange: (event) => void nav.setLightLevel(Number(event.target.value))
|
|
19631
|
+
}),
|
|
19632
|
+
manual && /* @__PURE__ */ jsx("span", {
|
|
19633
|
+
className: "text-[10px] text-foreground-subtle w-8 text-right",
|
|
19634
|
+
children: status?.lightLevel ?? NAVIGATION_LIGHT_LEVEL_MAX
|
|
19635
|
+
})
|
|
19636
|
+
]
|
|
19637
|
+
})]
|
|
19638
|
+
}),
|
|
19639
|
+
nav.error && /* @__PURE__ */ jsx("div", {
|
|
19640
|
+
className: "text-[10px] text-danger",
|
|
19641
|
+
children: nav.error
|
|
19642
|
+
})
|
|
19643
|
+
]
|
|
19644
|
+
});
|
|
19645
|
+
}
|
|
19646
|
+
//#endregion
|
|
19647
|
+
//#region src/lib/cap-error.ts
|
|
19648
|
+
/**
|
|
19649
|
+
* Shared capability-error utilities for the admin UI.
|
|
19650
|
+
*
|
|
19651
|
+
* The primary export `isAbsentProvider` lets UI components distinguish
|
|
19652
|
+
* "this device doesn't have this cap" (expected — hide the widget) from
|
|
19653
|
+
* "something went wrong while calling a present cap" (unexpected — surface
|
|
19654
|
+
* the error to the operator).
|
|
19655
|
+
*
|
|
19656
|
+
* Detection strategy (two-tier):
|
|
19657
|
+
*
|
|
19658
|
+
* 1. **Typed reason** (preferred): tRPC's errorFormatter augments the
|
|
19659
|
+
* error shape with `data.capRouteReason` when the server throws a
|
|
19660
|
+
* CapRouteError. Read it from `err.data?.capRouteReason` to avoid
|
|
19661
|
+
* brittle string matching.
|
|
19662
|
+
*
|
|
19663
|
+
* 2. **Substring fallback** (transitional safety): older error paths
|
|
19664
|
+
* that don't carry a typed reason still emit the human-readable
|
|
19665
|
+
* message "provider not available". The fallback is ONLY engaged
|
|
19666
|
+
* when `capRouteReason` is absent — future clean-up can drop it
|
|
19667
|
+
* once every throw site has been migrated.
|
|
19668
|
+
*/
|
|
19669
|
+
/**
|
|
17942
19670
|
* Extract the typed `capRouteReason` field from a serialized tRPC error.
|
|
17943
19671
|
*
|
|
17944
19672
|
* tRPC client errors typically look like:
|
|
@@ -19332,46 +21060,6 @@ function RecordingSettings({ initial, availableProfiles, saving, onSave, sensorC
|
|
|
19332
21060
|
});
|
|
19333
21061
|
}
|
|
19334
21062
|
//#endregion
|
|
19335
|
-
//#region src/contexts/vod-playback.tsx
|
|
19336
|
-
/**
|
|
19337
|
-
* VodPlayback — shared context coordinating recorded (VOD) playback between a
|
|
19338
|
-
* widget/section (the Recording panel) and the hero StreamPanel.
|
|
19339
|
-
*
|
|
19340
|
-
* A CONTROLLER (the Recording widget) calls `play({ url, label })`; the hero
|
|
19341
|
-
* StreamPanel CONSUMES `request` and switches into VOD mode (HLS playback
|
|
19342
|
-
* through the hub data-plane) instead of the live WebRTC stream. `clear()`
|
|
19343
|
-
* returns the hero to live.
|
|
19344
|
-
*
|
|
19345
|
-
* Built on `createSharedContext` (NOT a plain `createContext`) so it crosses
|
|
19346
|
-
* the Module-Federation boundary: an addon-shipped widget consuming
|
|
19347
|
-
* `useVodPlayback()` resolves the SAME provider instance the host mounted —
|
|
19348
|
-
* the same mechanism `player-overlays` / `zone-editing` use to let federated
|
|
19349
|
-
* widgets (zone-editor, privacy-mask) drive the live player. Consumers used
|
|
19350
|
-
* outside a provider get the inert default (no-op), so a StreamPanel mounted
|
|
19351
|
-
* on a page without the Recording widget stays live-only without crashing.
|
|
19352
|
-
*/
|
|
19353
|
-
var VodPlaybackContext = createSharedContext("camstack:vod-playback", {
|
|
19354
|
-
request: null,
|
|
19355
|
-
play: () => {},
|
|
19356
|
-
clear: () => {}
|
|
19357
|
-
});
|
|
19358
|
-
function VodPlaybackProvider({ children }) {
|
|
19359
|
-
const [request, setRequest] = useState(null);
|
|
19360
|
-
const value = useMemo(() => ({
|
|
19361
|
-
request,
|
|
19362
|
-
play: (req) => setRequest(req),
|
|
19363
|
-
clear: () => setRequest(null)
|
|
19364
|
-
}), [request]);
|
|
19365
|
-
return /* @__PURE__ */ jsx(VodPlaybackContext.Provider, {
|
|
19366
|
-
value,
|
|
19367
|
-
children
|
|
19368
|
-
});
|
|
19369
|
-
}
|
|
19370
|
-
/** @returns the VOD playback controller (inert no-op outside a provider). */
|
|
19371
|
-
function useVodPlayback() {
|
|
19372
|
-
return useContext(VodPlaybackContext);
|
|
19373
|
-
}
|
|
19374
|
-
//#endregion
|
|
19375
21063
|
//#region src/lib/recorded-control-protocol.ts
|
|
19376
21064
|
/** Serialize a command for `RTCDataChannel.send` (matches the server schema). */
|
|
19377
21065
|
function serializeRecordedCommand(cmd) {
|
|
@@ -19708,82 +21396,6 @@ function useScrubController(sink, isPlayerLive = () => false) {
|
|
|
19708
21396
|
};
|
|
19709
21397
|
}
|
|
19710
21398
|
//#endregion
|
|
19711
|
-
//#region src/composites/empty-state.tsx
|
|
19712
|
-
function EmptyState({ icon: Icon, title, description, action, className }) {
|
|
19713
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
19714
|
-
className: cn("flex flex-col items-center justify-center gap-3 py-12", className),
|
|
19715
|
-
children: [
|
|
19716
|
-
Icon && /* @__PURE__ */ jsx(Icon, {
|
|
19717
|
-
className: "h-12 w-12 text-foreground-subtle",
|
|
19718
|
-
"aria-hidden": "true"
|
|
19719
|
-
}),
|
|
19720
|
-
/* @__PURE__ */ jsxs("div", {
|
|
19721
|
-
className: "flex flex-col items-center gap-1 text-center",
|
|
19722
|
-
children: [/* @__PURE__ */ jsx("p", {
|
|
19723
|
-
className: "text-foreground-muted text-sm font-medium",
|
|
19724
|
-
children: title
|
|
19725
|
-
}), description && /* @__PURE__ */ jsx("p", {
|
|
19726
|
-
className: "text-foreground-subtle text-xs max-w-xs",
|
|
19727
|
-
children: description
|
|
19728
|
-
})]
|
|
19729
|
-
}),
|
|
19730
|
-
action && /* @__PURE__ */ jsx("div", {
|
|
19731
|
-
className: "mt-1",
|
|
19732
|
-
children: action
|
|
19733
|
-
})
|
|
19734
|
-
]
|
|
19735
|
-
});
|
|
19736
|
-
}
|
|
19737
|
-
//#endregion
|
|
19738
|
-
//#region src/composites/error-box.tsx
|
|
19739
|
-
var VARIANT_STYLES$1 = {
|
|
19740
|
-
danger: {
|
|
19741
|
-
box: "border-danger/40 bg-danger/10",
|
|
19742
|
-
icon: "text-danger",
|
|
19743
|
-
title: "text-danger",
|
|
19744
|
-
message: "text-danger"
|
|
19745
|
-
},
|
|
19746
|
-
warning: {
|
|
19747
|
-
box: "border-amber-500/40 bg-amber-500/10",
|
|
19748
|
-
icon: "text-amber-600 dark:text-amber-400",
|
|
19749
|
-
title: "text-amber-700 dark:text-amber-300",
|
|
19750
|
-
message: "text-amber-700 dark:text-amber-300"
|
|
19751
|
-
}
|
|
19752
|
-
};
|
|
19753
|
-
function ErrorBox({ message, title, onRetry, action, variant = "danger", className }) {
|
|
19754
|
-
const styles = VARIANT_STYLES$1[variant];
|
|
19755
|
-
const Icon = variant === "warning" ? TriangleAlert : CircleAlert;
|
|
19756
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
19757
|
-
role: "alert",
|
|
19758
|
-
className: cn("flex items-start gap-2.5 rounded border px-3 py-2.5", styles.box, className),
|
|
19759
|
-
children: [
|
|
19760
|
-
/* @__PURE__ */ jsx(Icon, {
|
|
19761
|
-
className: cn("h-4 w-4 mt-0.5 shrink-0", styles.icon),
|
|
19762
|
-
"aria-hidden": "true"
|
|
19763
|
-
}),
|
|
19764
|
-
/* @__PURE__ */ jsxs("div", {
|
|
19765
|
-
className: "flex-1 min-w-0",
|
|
19766
|
-
children: [title && /* @__PURE__ */ jsx("p", {
|
|
19767
|
-
className: cn("text-xs font-semibold", styles.title),
|
|
19768
|
-
children: title
|
|
19769
|
-
}), /* @__PURE__ */ jsx("div", {
|
|
19770
|
-
className: cn("text-xs break-words", styles.message, title ? "mt-0.5" : ""),
|
|
19771
|
-
children: message
|
|
19772
|
-
})]
|
|
19773
|
-
}),
|
|
19774
|
-
action ?? (onRetry && /* @__PURE__ */ jsxs("button", {
|
|
19775
|
-
type: "button",
|
|
19776
|
-
onClick: onRetry,
|
|
19777
|
-
className: cn("shrink-0 inline-flex items-center gap-1 rounded px-2 py-1 text-[11px] font-medium", "border border-current/30 hover:bg-current/10 transition-colors", styles.message),
|
|
19778
|
-
children: [/* @__PURE__ */ jsx(RefreshCw, {
|
|
19779
|
-
className: "h-3 w-3",
|
|
19780
|
-
"aria-hidden": "true"
|
|
19781
|
-
}), "Retry"]
|
|
19782
|
-
}))
|
|
19783
|
-
]
|
|
19784
|
-
});
|
|
19785
|
-
}
|
|
19786
|
-
//#endregion
|
|
19787
21399
|
//#region src/composites/cap-settings/recording-spans.ts
|
|
19788
21400
|
/**
|
|
19789
21401
|
* Merge availability ranges (across every profile) into the union of distinct
|
|
@@ -20966,7 +22578,8 @@ var HOST_WIDGETS = {
|
|
|
20966
22578
|
"host/consumables-panel": ConsumablesPanel,
|
|
20967
22579
|
"host/ptz-autotrack": AutotrackSection,
|
|
20968
22580
|
"host/recording-panel": RecordingPanel,
|
|
20969
|
-
"host/camera-grid-layout": CameraGridLayoutEditor
|
|
22581
|
+
"host/camera-grid-layout": CameraGridLayoutEditor,
|
|
22582
|
+
"host/clips-browser": ClipsSection
|
|
20970
22583
|
};
|
|
20971
22584
|
//#endregion
|
|
20972
22585
|
//#region src/composites/widget-slot.tsx
|
|
@@ -25273,92 +26886,6 @@ function AgentStepEditor(props) {
|
|
|
25273
26886
|
});
|
|
25274
26887
|
}
|
|
25275
26888
|
//#endregion
|
|
25276
|
-
//#region ../../node_modules/class-variance-authority/dist/index.mjs
|
|
25277
|
-
/**
|
|
25278
|
-
* Copyright 2022 Joe Bell. All rights reserved.
|
|
25279
|
-
*
|
|
25280
|
-
* This file is licensed to you under the Apache License, Version 2.0
|
|
25281
|
-
* (the "License"); you may not use this file except in compliance with the
|
|
25282
|
-
* License. You may obtain a copy of the License at
|
|
25283
|
-
*
|
|
25284
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
25285
|
-
*
|
|
25286
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
25287
|
-
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
25288
|
-
* WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or implied. See the
|
|
25289
|
-
* License for the specific language governing permissions and limitations under
|
|
25290
|
-
* the License.
|
|
25291
|
-
*/ var falsyToString = (value) => typeof value === "boolean" ? `${value}` : value === 0 ? "0" : value;
|
|
25292
|
-
var cx = clsx;
|
|
25293
|
-
var cva = (base, config) => (props) => {
|
|
25294
|
-
var _config_compoundVariants;
|
|
25295
|
-
if ((config === null || config === void 0 ? void 0 : config.variants) == null) return cx(base, props === null || props === void 0 ? void 0 : props.class, props === null || props === void 0 ? void 0 : props.className);
|
|
25296
|
-
const { variants, defaultVariants } = config;
|
|
25297
|
-
const getVariantClassNames = Object.keys(variants).map((variant) => {
|
|
25298
|
-
const variantProp = props === null || props === void 0 ? void 0 : props[variant];
|
|
25299
|
-
const defaultVariantProp = defaultVariants === null || defaultVariants === void 0 ? void 0 : defaultVariants[variant];
|
|
25300
|
-
if (variantProp === null) return null;
|
|
25301
|
-
const variantKey = falsyToString(variantProp) || falsyToString(defaultVariantProp);
|
|
25302
|
-
return variants[variant][variantKey];
|
|
25303
|
-
});
|
|
25304
|
-
const propsWithoutUndefined = props && Object.entries(props).reduce((acc, param) => {
|
|
25305
|
-
let [key, value] = param;
|
|
25306
|
-
if (value === void 0) return acc;
|
|
25307
|
-
acc[key] = value;
|
|
25308
|
-
return acc;
|
|
25309
|
-
}, {});
|
|
25310
|
-
return cx(base, getVariantClassNames, config === null || config === void 0 ? void 0 : (_config_compoundVariants = config.compoundVariants) === null || _config_compoundVariants === void 0 ? void 0 : _config_compoundVariants.reduce((acc, param) => {
|
|
25311
|
-
let { class: cvClass, className: cvClassName, ...compoundVariantOptions } = param;
|
|
25312
|
-
return Object.entries(compoundVariantOptions).every((param) => {
|
|
25313
|
-
let [key, value] = param;
|
|
25314
|
-
return Array.isArray(value) ? value.includes({
|
|
25315
|
-
...defaultVariants,
|
|
25316
|
-
...propsWithoutUndefined
|
|
25317
|
-
}[key]) : {
|
|
25318
|
-
...defaultVariants,
|
|
25319
|
-
...propsWithoutUndefined
|
|
25320
|
-
}[key] === value;
|
|
25321
|
-
}) ? [
|
|
25322
|
-
...acc,
|
|
25323
|
-
cvClass,
|
|
25324
|
-
cvClassName
|
|
25325
|
-
] : acc;
|
|
25326
|
-
}, []), props === null || props === void 0 ? void 0 : props.class, props === null || props === void 0 ? void 0 : props.className);
|
|
25327
|
-
};
|
|
25328
|
-
//#endregion
|
|
25329
|
-
//#region src/primitives/button.tsx
|
|
25330
|
-
var buttonVariants = cva("inline-flex items-center justify-center rounded-md font-medium transition-colors cursor-pointer disabled:opacity-50 disabled:pointer-events-none", {
|
|
25331
|
-
variants: {
|
|
25332
|
-
variant: {
|
|
25333
|
-
primary: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
25334
|
-
secondary: "bg-surface text-foreground hover:bg-surface-hover",
|
|
25335
|
-
ghost: "hover:bg-surface-hover text-foreground-muted",
|
|
25336
|
-
danger: "bg-danger text-white hover:bg-danger/90",
|
|
25337
|
-
outline: "border border-border bg-transparent text-foreground hover:bg-surface-hover"
|
|
25338
|
-
},
|
|
25339
|
-
size: {
|
|
25340
|
-
sm: "h-7 px-2.5 text-xs gap-1.5",
|
|
25341
|
-
md: "h-8 px-3 text-sm gap-2",
|
|
25342
|
-
lg: "h-9 px-4 text-sm gap-2"
|
|
25343
|
-
}
|
|
25344
|
-
},
|
|
25345
|
-
defaultVariants: {
|
|
25346
|
-
variant: "primary",
|
|
25347
|
-
size: "sm"
|
|
25348
|
-
}
|
|
25349
|
-
});
|
|
25350
|
-
var Button = forwardRef(({ className, variant, size, ...props }, ref) => {
|
|
25351
|
-
return /* @__PURE__ */ jsx("button", {
|
|
25352
|
-
ref,
|
|
25353
|
-
className: cn(buttonVariants({
|
|
25354
|
-
variant,
|
|
25355
|
-
size
|
|
25356
|
-
}), className),
|
|
25357
|
-
...props
|
|
25358
|
-
});
|
|
25359
|
-
});
|
|
25360
|
-
Button.displayName = "Button";
|
|
25361
|
-
//#endregion
|
|
25362
26889
|
//#region src/primitives/icon-button.tsx
|
|
25363
26890
|
var iconButtonVariants = cva("inline-flex items-center justify-center rounded-md font-medium transition-colors cursor-pointer disabled:opacity-50 disabled:pointer-events-none", {
|
|
25364
26891
|
variants: {
|
|
@@ -25393,55 +26920,6 @@ var IconButton = forwardRef(({ className, variant, size, icon: Icon, ...props },
|
|
|
25393
26920
|
});
|
|
25394
26921
|
IconButton.displayName = "IconButton";
|
|
25395
26922
|
//#endregion
|
|
25396
|
-
//#region src/primitives/badge.tsx
|
|
25397
|
-
var badgeVariants = cva("inline-flex items-center rounded-full text-xs font-medium px-2 py-0.5", {
|
|
25398
|
-
variants: {
|
|
25399
|
-
variant: {
|
|
25400
|
-
default: "bg-surface-hover text-foreground",
|
|
25401
|
-
success: "bg-success/15 text-success",
|
|
25402
|
-
warning: "bg-warning/15 text-warning",
|
|
25403
|
-
danger: "bg-danger/15 text-danger",
|
|
25404
|
-
info: "bg-info/15 text-info"
|
|
25405
|
-
},
|
|
25406
|
-
styleVariant: {
|
|
25407
|
-
solid: "",
|
|
25408
|
-
outline: "border bg-transparent"
|
|
25409
|
-
}
|
|
25410
|
-
},
|
|
25411
|
-
defaultVariants: {
|
|
25412
|
-
variant: "default",
|
|
25413
|
-
styleVariant: "solid"
|
|
25414
|
-
}
|
|
25415
|
-
});
|
|
25416
|
-
var Badge = forwardRef(({ className, variant, style, ...props }, ref) => {
|
|
25417
|
-
return /* @__PURE__ */ jsx("span", {
|
|
25418
|
-
ref,
|
|
25419
|
-
className: cn(badgeVariants({
|
|
25420
|
-
variant,
|
|
25421
|
-
styleVariant: style
|
|
25422
|
-
}), className),
|
|
25423
|
-
...props
|
|
25424
|
-
});
|
|
25425
|
-
});
|
|
25426
|
-
Badge.displayName = "Badge";
|
|
25427
|
-
//#endregion
|
|
25428
|
-
//#region src/primitives/card.tsx
|
|
25429
|
-
var cardVariants = cva("rounded-lg p-3", {
|
|
25430
|
-
variants: { variant: {
|
|
25431
|
-
flat: "bg-surface",
|
|
25432
|
-
bordered: "bg-surface border border-border"
|
|
25433
|
-
} },
|
|
25434
|
-
defaultVariants: { variant: "bordered" }
|
|
25435
|
-
});
|
|
25436
|
-
var Card = forwardRef(({ className, variant, ...props }, ref) => {
|
|
25437
|
-
return /* @__PURE__ */ jsx("div", {
|
|
25438
|
-
ref,
|
|
25439
|
-
className: cn(cardVariants({ variant }), className),
|
|
25440
|
-
...props
|
|
25441
|
-
});
|
|
25442
|
-
});
|
|
25443
|
-
Card.displayName = "Card";
|
|
25444
|
-
//#endregion
|
|
25445
26923
|
//#region src/primitives/label.tsx
|
|
25446
26924
|
var Label = forwardRef(({ className, ...props }, ref) => {
|
|
25447
26925
|
return /* @__PURE__ */ jsx("label", {
|
|
@@ -25470,16 +26948,6 @@ var Separator = forwardRef(({ className, orientation, ...props }, ref) => {
|
|
|
25470
26948
|
});
|
|
25471
26949
|
Separator.displayName = "Separator";
|
|
25472
26950
|
//#endregion
|
|
25473
|
-
//#region src/primitives/skeleton.tsx
|
|
25474
|
-
var Skeleton = forwardRef(({ className, ...props }, ref) => {
|
|
25475
|
-
return /* @__PURE__ */ jsx("div", {
|
|
25476
|
-
ref,
|
|
25477
|
-
className: cn("animate-pulse bg-surface-hover rounded-md h-4 w-full", className),
|
|
25478
|
-
...props
|
|
25479
|
-
});
|
|
25480
|
-
});
|
|
25481
|
-
Skeleton.displayName = "Skeleton";
|
|
25482
|
-
//#endregion
|
|
25483
26951
|
//#region src/primitives/input.tsx
|
|
25484
26952
|
/**
|
|
25485
26953
|
* `disabled:opacity-50 disabled:cursor-not-allowed` for the same reason as
|
|
@@ -25521,43 +26989,6 @@ var Input = forwardRef(({ className, state, leftSlot, rightSlot, ...props }, ref
|
|
|
25521
26989
|
});
|
|
25522
26990
|
Input.displayName = "Input";
|
|
25523
26991
|
//#endregion
|
|
25524
|
-
//#region src/primitives/select.tsx
|
|
25525
|
-
/**
|
|
25526
|
-
* A native `<select>` styled to match the rest of the field set.
|
|
25527
|
-
*
|
|
25528
|
-
* `appearance-none` plus an explicit `bg-surface` / `text-foreground` is what
|
|
25529
|
-
* makes it match — and it is also what USED to make a disabled one a lie. A
|
|
25530
|
-
* browser greys a disabled control by rendering its own background and text
|
|
25531
|
-
* colour; all three of those are overridden here, so a `disabled` dropdown
|
|
25532
|
-
* looked exactly like a live one and simply ignored the click. That is the
|
|
25533
|
-
* whole of the "the language selector does not open" report of 2026-08-15:
|
|
25534
|
-
* the AI section's switch was off, every control under it was correctly inert,
|
|
25535
|
-
* and nothing on screen said so.
|
|
25536
|
-
*
|
|
25537
|
-
* `disabled:` classes belong HERE and not on each call site: there are dozens
|
|
25538
|
-
* of `<Select disabled>` in this app and the one that gets forgotten is the one
|
|
25539
|
-
* that generates the ticket. Guarded by `disabled-affordance.test.tsx`.
|
|
25540
|
-
*/
|
|
25541
|
-
var Select = forwardRef(({ className, options, disabled, ...props }, ref) => {
|
|
25542
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
25543
|
-
className: "relative w-full",
|
|
25544
|
-
children: [/* @__PURE__ */ jsx("select", {
|
|
25545
|
-
ref,
|
|
25546
|
-
disabled,
|
|
25547
|
-
className: cn("h-7 w-full px-2.5 text-xs bg-surface border border-border rounded-md text-foreground appearance-none pr-7 focus:outline-none focus:ring-1 focus:ring-primary", "disabled:opacity-50 disabled:cursor-not-allowed", className),
|
|
25548
|
-
...props,
|
|
25549
|
-
children: options.map((option) => /* @__PURE__ */ jsx("option", {
|
|
25550
|
-
value: option.value,
|
|
25551
|
-
children: option.label
|
|
25552
|
-
}, option.value))
|
|
25553
|
-
}), /* @__PURE__ */ jsx("div", {
|
|
25554
|
-
className: cn("absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none text-foreground-muted", disabled === true ? "opacity-50" : ""),
|
|
25555
|
-
children: /* @__PURE__ */ jsx(ChevronDown, { className: "h-3.5 w-3.5" })
|
|
25556
|
-
})]
|
|
25557
|
-
});
|
|
25558
|
-
});
|
|
25559
|
-
Select.displayName = "Select";
|
|
25560
|
-
//#endregion
|
|
25561
26992
|
//#region src/primitives/checkbox.tsx
|
|
25562
26993
|
var Checkbox = forwardRef(({ className, ...props }, ref) => {
|
|
25563
26994
|
return /* @__PURE__ */ jsx("input", {
|
|
@@ -25846,53 +27277,6 @@ function TooltipContent({ className, children, ...props }) {
|
|
|
25846
27277
|
});
|
|
25847
27278
|
}
|
|
25848
27279
|
//#endregion
|
|
25849
|
-
//#region src/hooks/use-is-mobile.ts
|
|
25850
|
-
var MOBILE_QUERY = "(max-width: 767px)";
|
|
25851
|
-
/**
|
|
25852
|
-
* Mid breakpoint — below this width the sidebar collapses to an
|
|
25853
|
-
* icons-only rail to reclaim canvas space. Above it (and outside
|
|
25854
|
-
* mobile) the full sidebar renders. Threshold matches Tailwind `lg`
|
|
25855
|
-
* so sidebar fans out at the same point grids switch to multi-column.
|
|
25856
|
-
*/
|
|
25857
|
-
var MID_QUERY = "(min-width: 768px) and (max-width: 1023px)";
|
|
25858
|
-
/**
|
|
25859
|
-
* `matchMedia` is universal in browsers but absent in bare DOM environments.
|
|
25860
|
-
* These hooks are called from shared primitives (`DataTable` picks its narrow
|
|
25861
|
-
* layout with one), so throwing here takes down the whole page that merely
|
|
25862
|
-
* rendered a table. Where the capability is missing, report the desktop
|
|
25863
|
-
* layout — a widescreen rendering is wrong-looking; a crashed page is gone.
|
|
25864
|
-
*/
|
|
25865
|
-
function matchQuery(query) {
|
|
25866
|
-
if (typeof window.matchMedia !== "function") return null;
|
|
25867
|
-
return window.matchMedia(query);
|
|
25868
|
-
}
|
|
25869
|
-
function subscribeQuery(query) {
|
|
25870
|
-
return (callback) => {
|
|
25871
|
-
const mql = matchQuery(query);
|
|
25872
|
-
if (mql === null) return () => {};
|
|
25873
|
-
mql.addEventListener("change", callback);
|
|
25874
|
-
return () => mql.removeEventListener("change", callback);
|
|
25875
|
-
};
|
|
25876
|
-
}
|
|
25877
|
-
function getSnapshot(query) {
|
|
25878
|
-
return () => matchQuery(query)?.matches ?? false;
|
|
25879
|
-
}
|
|
25880
|
-
function getServerSnapshot() {
|
|
25881
|
-
return false;
|
|
25882
|
-
}
|
|
25883
|
-
function useIsMobile() {
|
|
25884
|
-
return useSyncExternalStore(subscribeQuery(MOBILE_QUERY), getSnapshot(MOBILE_QUERY), getServerSnapshot);
|
|
25885
|
-
}
|
|
25886
|
-
/**
|
|
25887
|
-
* True when the viewport is in the medium (`md`) Tailwind range —
|
|
25888
|
-
* 768px ≤ width < 1024px. Use to drive layouts that should pack denser
|
|
25889
|
-
* than full-desktop but more legibly than mobile (e.g. icons-only
|
|
25890
|
-
* sidebar rail, narrower stat tiles).
|
|
25891
|
-
*/
|
|
25892
|
-
function useIsMidWidth() {
|
|
25893
|
-
return useSyncExternalStore(subscribeQuery(MID_QUERY), getSnapshot(MID_QUERY), getServerSnapshot);
|
|
25894
|
-
}
|
|
25895
|
-
//#endregion
|
|
25896
27280
|
//#region src/primitives/bottom-sheet.tsx
|
|
25897
27281
|
function BottomSheet({ open, onClose, title, children, className }) {
|
|
25898
27282
|
useEffect(() => {
|
|
@@ -29106,644 +30490,224 @@ function coverageLine(monitor) {
|
|
|
29106
30490
|
return monitor.onUncoveredCondition === "judge-anyway" ? `${parts.join(" · ")} — ${names} judged against the references you do have.` : `${parts.join(" · ")} — optional; checks in ${names} are skipped.`;
|
|
29107
30491
|
}
|
|
29108
30492
|
/**
|
|
29109
|
-
* The calm sentence for a scene that is sitting the current light out — `null`
|
|
29110
|
-
* whenever it is checking normally.
|
|
29111
|
-
*
|
|
29112
|
-
* Separate from {@link statusLine} because it is not a verdict and must not
|
|
29113
|
-
* read like one. A paused scene has not failed, has not lost its flag, and
|
|
29114
|
-
* needs nothing from the operator; the only thing worth saying is which light
|
|
29115
|
-
* it is waiting out and that it will pick itself back up.
|
|
29116
|
-
*/
|
|
29117
|
-
function suspensionLine(monitor) {
|
|
29118
|
-
if (monitor.suspendedCondition === null) return null;
|
|
29119
|
-
const light = variantLabel(monitor.suspendedCondition).toLowerCase();
|
|
29120
|
-
return `Paused — nothing was captured in ${light}, so checks are on hold until the light changes back. Capture ${light} if you want it watched around the clock.`;
|
|
29121
|
-
}
|
|
29122
|
-
var UNAVAILABLE_SENTENCE = {
|
|
29123
|
-
"no-reference-for-condition": "can’t tell — nothing has been captured for this scene yet. Press “Capture daylight” to photograph what normal looks like.",
|
|
29124
|
-
"view-shifted": "can’t tell — the camera view has moved since the reference was captured. Re-capture it.",
|
|
29125
|
-
"no-vision-profile": "can’t tell — the vision model is unreachable, so the change is on hold.",
|
|
29126
|
-
"encoder-model-changed": "can’t tell — the embedding model changed, so the stored references are unreadable. Re-capture them.",
|
|
29127
|
-
"no-snapshot": "can’t tell — this camera did not return a frame."
|
|
29128
|
-
};
|
|
29129
|
-
/**
|
|
29130
|
-
* The one line under a scene's name. It answers, in order: can it judge, what
|
|
29131
|
-
* does it say, and how long has it said it.
|
|
29132
|
-
*/
|
|
29133
|
-
function statusLine(monitor, now) {
|
|
29134
|
-
if (monitor.states.length === 0) return "Not armed yet — capture the initial state to start watching.";
|
|
29135
|
-
if (monitor.verdict === "unknown") return monitor.unavailable !== null ? UNAVAILABLE_SENTENCE[monitor.unavailable] : "can’t tell right now.";
|
|
29136
|
-
const since = monitor.verdict === "diverged" ? monitor.divergedAt : monitor.armedAt;
|
|
29137
|
-
const held = since !== null ? ` for ${humanDuration(Math.max(0, now - since))}` : "";
|
|
29138
|
-
if (monitor.latched) return monitor.verdict === "diverged" ? `CHANGED${held} — press Reset when you have dealt with it.` : `Back to normal, but still flagged as changed${held}. Press Reset to clear it.`;
|
|
29139
|
-
const paused = suspensionLine(monitor);
|
|
29140
|
-
if (paused !== null) return paused;
|
|
29141
|
-
return monitor.verdict === "diverged" ? `CHANGED${held}` : `Normal${held}`;
|
|
29142
|
-
}
|
|
29143
|
-
/** The badge word. `latched` wins over `verdict`, because the latch is the
|
|
29144
|
-
* thing the operator asked to be told and it outlives the live state.
|
|
29145
|
-
*
|
|
29146
|
-
* A scene sitting out an uncovered light reads `Paused`, in the NEUTRAL tone —
|
|
29147
|
-
* never `alarm`, and never the `danger` variant the page maps that to. A
|
|
29148
|
-
* missing IR reference must not be able to paint a red badge on a scene that
|
|
29149
|
-
* is doing exactly what it was configured to do. */
|
|
29150
|
-
function badgeFor(monitor) {
|
|
29151
|
-
if (monitor.states.length === 0) return {
|
|
29152
|
-
text: "Not armed",
|
|
29153
|
-
tone: "unknown"
|
|
29154
|
-
};
|
|
29155
|
-
if (monitor.latched) return {
|
|
29156
|
-
text: "Changed",
|
|
29157
|
-
tone: "alarm"
|
|
29158
|
-
};
|
|
29159
|
-
if (monitor.suspendedCondition !== null) return {
|
|
29160
|
-
text: "Paused",
|
|
29161
|
-
tone: "unknown"
|
|
29162
|
-
};
|
|
29163
|
-
if (monitor.verdict === "unknown") return {
|
|
29164
|
-
text: "Can’t tell",
|
|
29165
|
-
tone: "unknown"
|
|
29166
|
-
};
|
|
29167
|
-
return monitor.verdict === "diverged" ? {
|
|
29168
|
-
text: "Changed",
|
|
29169
|
-
tone: "alarm"
|
|
29170
|
-
} : {
|
|
29171
|
-
text: "Normal",
|
|
29172
|
-
tone: "ok"
|
|
29173
|
-
};
|
|
29174
|
-
}
|
|
29175
|
-
/** A rect covering most of the frame is dominated by weather, sky and parked
|
|
29176
|
-
* cars: the bin is 2 % of the pixels and CLIP will not notice it leaving. The
|
|
29177
|
-
* failure is silent INSENSITIVITY, which is the worst kind — hence a warning
|
|
29178
|
-
* rather than a refusal. */
|
|
29179
|
-
var ROI_AREA_WARN_FRACTION = .4;
|
|
29180
|
-
function roiTooLarge(roi) {
|
|
29181
|
-
return roi.width * roi.height > ROI_AREA_WARN_FRACTION;
|
|
29182
|
-
}
|
|
29183
|
-
function humanDuration(ms) {
|
|
29184
|
-
const s = Math.floor(ms / 1e3);
|
|
29185
|
-
if (s < 60) return `${s}s`;
|
|
29186
|
-
const m = Math.floor(s / 60);
|
|
29187
|
-
if (m < 60) return `${m}m`;
|
|
29188
|
-
const h = Math.floor(m / 60);
|
|
29189
|
-
if (h < 48) return `${h}h`;
|
|
29190
|
-
return `${Math.floor(h / 24)}d`;
|
|
29191
|
-
}
|
|
29192
|
-
//#endregion
|
|
29193
|
-
//#region src/composites/code-block.tsx
|
|
29194
|
-
function CodeBlock({ children, maxHeight = 300, className }) {
|
|
29195
|
-
const [copied, setCopied] = useState(false);
|
|
29196
|
-
const handleCopy = useCallback(() => {
|
|
29197
|
-
navigator.clipboard.writeText(children).then(() => {
|
|
29198
|
-
setCopied(true);
|
|
29199
|
-
setTimeout(() => setCopied(false), 2e3);
|
|
29200
|
-
});
|
|
29201
|
-
}, [children]);
|
|
29202
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
29203
|
-
className: cn("relative group", className),
|
|
29204
|
-
children: [/* @__PURE__ */ jsx(ScrollArea, {
|
|
29205
|
-
style: { maxHeight },
|
|
29206
|
-
children: /* @__PURE__ */ jsx("pre", {
|
|
29207
|
-
className: "font-mono text-xs bg-surface p-3 rounded-md border border-border-subtle",
|
|
29208
|
-
children: /* @__PURE__ */ jsx("code", { children })
|
|
29209
|
-
})
|
|
29210
|
-
}), /* @__PURE__ */ jsx("div", {
|
|
29211
|
-
className: "absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity",
|
|
29212
|
-
children: /* @__PURE__ */ jsx(IconButton, {
|
|
29213
|
-
icon: copied ? Check : Copy,
|
|
29214
|
-
"aria-label": "Copy code",
|
|
29215
|
-
variant: "ghost",
|
|
29216
|
-
size: "sm",
|
|
29217
|
-
onClick: handleCopy
|
|
29218
|
-
})
|
|
29219
|
-
})]
|
|
29220
|
-
});
|
|
29221
|
-
}
|
|
29222
|
-
//#endregion
|
|
29223
|
-
//#region src/composites/confirm-action-button.tsx
|
|
29224
|
-
/**
|
|
29225
|
-
* ConfirmActionButton — destructive-action button with a modal confirm.
|
|
29226
|
-
*
|
|
29227
|
-
* Two-step UX for any operation operators shouldn't trigger by accident:
|
|
29228
|
-
* reboot device, restart addon, regenerate token, etc. The trigger is a
|
|
29229
|
-
* normal Button that opens a Dialog; confirming runs the async action,
|
|
29230
|
-
* the trigger spinner-disables itself for the duration, and the dialog
|
|
29231
|
-
* closes on success. Errors surface inline at the dialog's bottom.
|
|
29232
|
-
*
|
|
29233
|
-
* Generic on `TResult` so callers don't have to discard the return
|
|
29234
|
-
* value. Pass an `icon` to render it inside the trigger (e.g. RotateCw
|
|
29235
|
-
* for reboot, RefreshCw for restart).
|
|
29236
|
-
*/
|
|
29237
|
-
function ConfirmActionButton({ label, icon: Icon, title = "Confirm action", description, confirmLabel, triggerVariant = "outline", confirmVariant = "danger", size = "sm", disabled, className, action, onSuccess }) {
|
|
29238
|
-
const [open, setOpen] = useState(false);
|
|
29239
|
-
const [running, setRunning] = useState(false);
|
|
29240
|
-
const [error, setError] = useState(null);
|
|
29241
|
-
const handleConfirm = async () => {
|
|
29242
|
-
setError(null);
|
|
29243
|
-
setRunning(true);
|
|
29244
|
-
try {
|
|
29245
|
-
const result = await action();
|
|
29246
|
-
onSuccess?.(result);
|
|
29247
|
-
setOpen(false);
|
|
29248
|
-
} catch (err) {
|
|
29249
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
29250
|
-
} finally {
|
|
29251
|
-
setRunning(false);
|
|
29252
|
-
}
|
|
29253
|
-
};
|
|
29254
|
-
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs(Button, {
|
|
29255
|
-
type: "button",
|
|
29256
|
-
variant: triggerVariant,
|
|
29257
|
-
size,
|
|
29258
|
-
disabled,
|
|
29259
|
-
onClick: () => {
|
|
29260
|
-
setError(null);
|
|
29261
|
-
setOpen(true);
|
|
29262
|
-
},
|
|
29263
|
-
className,
|
|
29264
|
-
children: [Icon && /* @__PURE__ */ jsx(Icon, { className: "h-3.5 w-3.5" }), label]
|
|
29265
|
-
}), /* @__PURE__ */ jsx(Dialog, {
|
|
29266
|
-
open,
|
|
29267
|
-
onOpenChange: (next) => !running && setOpen(next),
|
|
29268
|
-
children: /* @__PURE__ */ jsxs(DialogContent, { children: [
|
|
29269
|
-
/* @__PURE__ */ jsxs(DialogHeader, { children: [/* @__PURE__ */ jsxs(DialogTitle, {
|
|
29270
|
-
className: "flex items-center gap-2",
|
|
29271
|
-
children: [/* @__PURE__ */ jsx(TriangleAlert, { className: "h-4 w-4 text-amber-400" }), title]
|
|
29272
|
-
}), typeof description === "string" ? /* @__PURE__ */ jsx(DialogDescription, { children: description }) : description] }),
|
|
29273
|
-
error && /* @__PURE__ */ jsx("div", {
|
|
29274
|
-
className: cn("mt-2 px-3 py-2 rounded border border-danger/40 bg-danger/10", "text-xs text-danger"),
|
|
29275
|
-
children: error
|
|
29276
|
-
}),
|
|
29277
|
-
/* @__PURE__ */ jsxs(DialogFooter, { children: [/* @__PURE__ */ jsx(Button, {
|
|
29278
|
-
type: "button",
|
|
29279
|
-
variant: "ghost",
|
|
29280
|
-
size,
|
|
29281
|
-
disabled: running,
|
|
29282
|
-
onClick: () => setOpen(false),
|
|
29283
|
-
children: "Cancel"
|
|
29284
|
-
}), /* @__PURE__ */ jsxs(Button, {
|
|
29285
|
-
type: "button",
|
|
29286
|
-
variant: confirmVariant,
|
|
29287
|
-
size,
|
|
29288
|
-
disabled: running,
|
|
29289
|
-
onClick: () => {
|
|
29290
|
-
handleConfirm();
|
|
29291
|
-
},
|
|
29292
|
-
children: [running && /* @__PURE__ */ jsx(LoaderCircle, { className: "h-3.5 w-3.5 animate-spin" }), confirmLabel ?? label]
|
|
29293
|
-
})] })
|
|
29294
|
-
] })
|
|
29295
|
-
})] });
|
|
29296
|
-
}
|
|
29297
|
-
//#endregion
|
|
29298
|
-
//#region src/composites/copy-button.tsx
|
|
29299
|
-
/**
|
|
29300
|
-
* `CopyButton` — a compact button that copies a string `value` to the
|
|
29301
|
-
* clipboard via `navigator.clipboard.writeText`, showing a brief
|
|
29302
|
-
* "copied" check state. Addon-agnostic; used wherever a secret / URL
|
|
29303
|
-
* needs a one-click copy affordance (export setup panels, etc.).
|
|
29304
|
-
*/
|
|
29305
|
-
var COPIED_RESET_MS = 2e3;
|
|
29306
|
-
function CopyButton({ value, label, srLabel, className, disabled }) {
|
|
29307
|
-
const [copied, setCopied] = useState(false);
|
|
29308
|
-
const handleCopy = useCallback(() => {
|
|
29309
|
-
if (!value) return;
|
|
29310
|
-
navigator.clipboard.writeText(value).then(() => {
|
|
29311
|
-
setCopied(true);
|
|
29312
|
-
setTimeout(() => setCopied(false), COPIED_RESET_MS);
|
|
29313
|
-
});
|
|
29314
|
-
}, [value]);
|
|
29315
|
-
return /* @__PURE__ */ jsxs(Button, {
|
|
29316
|
-
size: "sm",
|
|
29317
|
-
variant: "ghost",
|
|
29318
|
-
type: "button",
|
|
29319
|
-
disabled: disabled || value.length === 0,
|
|
29320
|
-
onClick: handleCopy,
|
|
29321
|
-
className: cn(className),
|
|
29322
|
-
"aria-label": copied ? "Copied" : `Copy ${srLabel ?? label ?? "value"}`,
|
|
29323
|
-
children: [copied ? /* @__PURE__ */ jsx(Check, { className: "h-3.5 w-3.5 text-success" }) : /* @__PURE__ */ jsx(Copy, { className: "h-3.5 w-3.5" }), label ? /* @__PURE__ */ jsx("span", {
|
|
29324
|
-
className: "ml-1",
|
|
29325
|
-
children: copied ? "Copied" : label
|
|
29326
|
-
}) : null]
|
|
29327
|
-
});
|
|
29328
|
-
}
|
|
29329
|
-
//#endregion
|
|
29330
|
-
//#region src/composites/data-table-layout.ts
|
|
29331
|
-
/**
|
|
29332
|
-
* At this many columns and above, `auto` switches a narrow viewport to cards.
|
|
29333
|
-
* Three columns still fit a phone at a readable size; four do not.
|
|
29334
|
-
*/
|
|
29335
|
-
var CARD_MODE_MIN_COLUMNS = 4;
|
|
29336
|
-
function resolveTableLayout({ mode, columnCount, isNarrow }) {
|
|
29337
|
-
if (!isNarrow) return "table";
|
|
29338
|
-
if (mode === "scroll") return "table";
|
|
29339
|
-
if (mode === "cards") return "cards";
|
|
29340
|
-
if (columnCount === 0) return "table";
|
|
29341
|
-
return columnCount >= 4 ? "cards" : "table";
|
|
29342
|
-
}
|
|
29343
|
-
//#endregion
|
|
29344
|
-
//#region src/lib/responsive.ts
|
|
29345
|
-
/**
|
|
29346
|
-
* Responsive design tokens — three-tier system.
|
|
29347
|
-
*
|
|
29348
|
-
* The admin-ui targets three width tiers; every component should pick
|
|
29349
|
-
* the right token from this file instead of hand-coding breakpoints.
|
|
29350
|
-
* Adopting the tokens uniformly is what makes the app feel consistent
|
|
29351
|
-
* across operator devices (mobile / tablet / desktop) and what unlocks
|
|
29352
|
-
* targeted tuning (e.g. shrink every label by 1px in one place).
|
|
29353
|
-
*
|
|
29354
|
-
* Tier definitions (Tailwind breakpoints):
|
|
29355
|
-
*
|
|
29356
|
-
* • **L1 — Compact** (`< 640px`, default = mobile portrait)
|
|
29357
|
-
* Single column, tight padding, smallest text, icon-only buttons
|
|
29358
|
-
* where labels can be inferred. The operator can still see every
|
|
29359
|
-
* stat / action — no horizontal scroll, no clipped text. The user
|
|
29360
|
-
* directive: "molta informazione in poco spazio quando la width è
|
|
29361
|
-
* bassa."
|
|
29362
|
-
*
|
|
29363
|
-
* • **L2 — Standard** (`sm` — `640–1023px`, tablet + small laptop)
|
|
29364
|
-
* 2-column grids, side panels become visible, button labels return.
|
|
29365
|
-
* Targets ~768px–1024px viewports — the most common operator sizes.
|
|
29366
|
-
*
|
|
29367
|
-
* • **L3 — Spacious** (`lg+` — `≥ 1024px`, desktop)
|
|
29368
|
-
* 4-column grids, generous padding, full-width header text,
|
|
29369
|
-
* side-by-side composite layouts. The operator has elbow room.
|
|
29370
|
-
*
|
|
29371
|
-
* Conventions:
|
|
29372
|
-
* - **Always start from L1** — the bare class is always the compact
|
|
29373
|
-
* variant, then `sm:` adds room at 640px, `lg:` at 1024px. No
|
|
29374
|
-
* desktop-first breakpoints; no max-width queries.
|
|
29375
|
-
* - **Token strings are static** — kept as `as const` so callers can
|
|
29376
|
-
* compose them via `cn()` / template literals without running a
|
|
29377
|
-
* class generator at runtime.
|
|
29378
|
-
* - **Pair the token with a comment** — the comment in the file
|
|
29379
|
-
* names every breakpoint it crosses, so a reader can predict the
|
|
29380
|
-
* layout without going to Tailwind docs.
|
|
29381
|
-
*/
|
|
29382
|
-
/** Container padding for a card-style section (header + body). */
|
|
29383
|
-
var SECTION_CARD = "rounded-lg border border-border bg-surface overflow-hidden";
|
|
29384
|
-
/** Header strip inside a card (between border-b lines). */
|
|
29385
|
-
var SECTION_HEADER = "border-b border-border px-3 sm:px-4 py-1.5 sm:py-2";
|
|
29386
|
-
/** Body padding for a card-style section. */
|
|
29387
|
-
var SECTION_BODY = "p-2 sm:p-3 lg:p-4";
|
|
29388
|
-
/** Gap between cards in a vertical stack. */
|
|
29389
|
-
var STACK_GAP = "space-y-3 sm:space-y-4";
|
|
29390
|
-
/** Gap between cards in a grid (4 cells / row at lg). */
|
|
29391
|
-
var GRID_GAP = "gap-2 sm:gap-3";
|
|
29392
|
-
/** Row padding inside a divided list (e.g. InfoRow). */
|
|
29393
|
-
var LIST_ROW = "px-3 sm:px-4 py-1.5 sm:py-2";
|
|
29394
|
-
/** 1-up at L1, 2-up at L2, 4-up at L3 — quick-stat row. */
|
|
29395
|
-
var GRID_QUICK_STATS = "grid grid-cols-2 sm:grid-cols-2 lg:grid-cols-4";
|
|
29396
|
-
/** 1-up at L1, 2-up at L2+ — paired panels (motion + detection rules). */
|
|
29397
|
-
var GRID_PAIRED = "grid grid-cols-1 lg:grid-cols-2";
|
|
29398
|
-
/** Side-by-side layout that stacks below md (canvas + side panel). */
|
|
29399
|
-
var SPLIT_PANEL_OUTER = "flex flex-col-reverse md:flex-row";
|
|
29400
|
-
/** Sidebar narrow lane in a SPLIT_PANEL_OUTER layout. */
|
|
29401
|
-
var SPLIT_PANEL_SIDE = "w-full md:w-44 lg:w-52 md:flex-shrink-0 border-b md:border-b-0 md:border-l border-border";
|
|
29402
|
-
/**
|
|
29403
|
-
* The tier boundary at which a settings label/value row stops stacking.
|
|
29404
|
-
*
|
|
29405
|
-
* Below it (L1 — compact) the row is ONE column: label on its own line, value
|
|
29406
|
-
* on the next at full width. Above it the row is label-left / value-right as
|
|
29407
|
-
* before. The two-column form needs `SETTING_ROW_LABEL`'s lane plus a readable
|
|
29408
|
-
* value beside it; a phone has neither, and the failure mode is not "cramped"
|
|
29409
|
-
* but "the value is the part that gets cut" — the one thing the operator
|
|
29410
|
-
* opened the page to read.
|
|
29411
|
-
*/
|
|
29412
|
-
var SETTING_ROW_STACK_BREAKPOINT = "sm";
|
|
29413
|
-
/** Outer row: single column at L1, label/value columns from `sm` up. */
|
|
29414
|
-
var SETTING_ROW = "flex min-w-0 flex-col gap-0.5 py-1.5 sm:flex-row sm:items-center";
|
|
29415
|
-
/** Label lane. Full width (and free to wrap) at L1; a fixed lane from `sm` up. */
|
|
29416
|
-
var SETTING_ROW_LABEL = "min-w-0 text-[11px] leading-tight text-foreground-subtle sm:w-32 sm:shrink-0 sm:pr-2";
|
|
29417
|
-
/**
|
|
29418
|
-
* Value lane — the value text plus its trailing affordances (copy / reveal).
|
|
29419
|
-
* `w-full` at L1 so the value owns the whole row once the label is above it;
|
|
29420
|
-
* `sm:flex-1` makes it share the row again from `sm` up.
|
|
29421
|
-
*/
|
|
29422
|
-
var SETTING_ROW_VALUE = "flex w-full min-w-0 items-center gap-1 sm:flex-1";
|
|
29423
|
-
/**
|
|
29424
|
-
* The value text itself. `break-all` lets an opaque token (a key, a URL) wrap
|
|
29425
|
-
* at L1 instead of being cut mid-word; `sm:truncate` restores the single-line
|
|
29426
|
-
* desktop form, where the row is wide enough for truncation to be a choice
|
|
29427
|
-
* rather than data loss.
|
|
29428
|
-
*/
|
|
29429
|
-
var SETTING_ROW_VALUE_TEXT = "min-w-0 break-all text-xs text-foreground sm:truncate";
|
|
29430
|
-
/** Section header label (uppercase tracking-wider). */
|
|
29431
|
-
var TEXT_SECTION_LABEL = "text-[10px] sm:text-[11px] font-semibold text-foreground uppercase tracking-wider";
|
|
29432
|
-
/** Field label inside a row. */
|
|
29433
|
-
var TEXT_FIELD_LABEL = "text-[11px] sm:text-xs text-foreground";
|
|
29434
|
-
/** Sub-label / hint text. */
|
|
29435
|
-
var TEXT_HINT = "text-[10px] text-foreground-subtle";
|
|
29436
|
-
/** Primary value (a metric, badge, …). */
|
|
29437
|
-
var TEXT_VALUE = "text-[11px] sm:text-xs font-medium text-foreground";
|
|
29438
|
-
/** Big metric value (StatCard). */
|
|
29439
|
-
var TEXT_METRIC = "text-base sm:text-lg font-semibold leading-tight text-foreground";
|
|
29440
|
-
/** Default compact button. */
|
|
29441
|
-
var BTN_COMPACT = "inline-flex items-center justify-center gap-1 rounded border border-border bg-surface px-1.5 py-0.5 sm:px-2 sm:py-1 text-[10px] sm:text-[11px] font-medium text-foreground hover:bg-surface-hover transition-colors disabled:opacity-40";
|
|
29442
|
-
/** Primary compact button. */
|
|
29443
|
-
var BTN_COMPACT_PRIMARY = "inline-flex items-center justify-center gap-1 rounded border border-primary/40 bg-primary/15 px-1.5 py-0.5 sm:px-2 sm:py-1 text-[10px] sm:text-[11px] font-medium text-primary hover:bg-primary/25 transition-colors disabled:opacity-40";
|
|
29444
|
-
/** Danger compact button. */
|
|
29445
|
-
var BTN_COMPACT_DANGER = "inline-flex items-center justify-center gap-1 rounded border border-danger/40 bg-danger/10 px-1.5 py-0.5 sm:px-2 sm:py-1 text-[10px] sm:text-[11px] font-medium text-danger hover:bg-danger/20 transition-colors disabled:opacity-40";
|
|
29446
|
-
/** Warning compact button (e.g. cancel-drawing). */
|
|
29447
|
-
var BTN_COMPACT_WARNING = "inline-flex items-center justify-center gap-1 rounded border border-warning/40 bg-warning/10 px-1.5 py-0.5 sm:px-2 sm:py-1 text-[10px] sm:text-[11px] font-medium text-warning hover:bg-warning/20 transition-colors disabled:opacity-40";
|
|
29448
|
-
/** Compact text/select/number input. */
|
|
29449
|
-
var INPUT_COMPACT = "w-full rounded border border-border bg-background px-1.5 py-0.5 sm:px-2 sm:py-1 text-[11px] sm:text-xs text-foreground focus:border-primary focus:ring-1 focus:ring-primary/30 outline-none";
|
|
29450
|
-
/** Chip-style toggle (zone / class selectors in ZoneRulesEditor). */
|
|
29451
|
-
var CHIP_BASE = "inline-flex items-center gap-1 rounded-full border px-1.5 py-0.5 text-[10px] font-medium transition-colors";
|
|
29452
|
-
var CHIP_ACTIVE = "border-primary/50 bg-primary/15 text-primary";
|
|
29453
|
-
var CHIP_INACTIVE = "border-border bg-surface text-foreground-subtle hover:bg-surface-hover";
|
|
29454
|
-
//#endregion
|
|
29455
|
-
//#region src/composites/setting-row.tsx
|
|
29456
|
-
function SettingRow({ label, children, actions, elements = "plain", className, valueClassName }) {
|
|
29457
|
-
const LabelTag = elements === "description" ? "dt" : "span";
|
|
29458
|
-
const ValueTag = elements === "description" ? "dd" : "div";
|
|
29459
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
29460
|
-
"data-setting-row": "",
|
|
29461
|
-
className: cn(SETTING_ROW, className),
|
|
29462
|
-
children: [/* @__PURE__ */ jsx(LabelTag, {
|
|
29463
|
-
"data-setting-row-label": "",
|
|
29464
|
-
className: SETTING_ROW_LABEL,
|
|
29465
|
-
children: label
|
|
29466
|
-
}), /* @__PURE__ */ jsxs(ValueTag, {
|
|
29467
|
-
"data-setting-row-value": "",
|
|
29468
|
-
className: SETTING_ROW_VALUE,
|
|
29469
|
-
children: [/* @__PURE__ */ jsx("span", {
|
|
29470
|
-
"data-setting-row-value-text": "",
|
|
29471
|
-
className: cn(SETTING_ROW_VALUE_TEXT, valueClassName),
|
|
29472
|
-
children
|
|
29473
|
-
}), actions !== void 0 && /* @__PURE__ */ jsx("span", {
|
|
29474
|
-
className: "flex shrink-0 items-center gap-0.5",
|
|
29475
|
-
children: actions
|
|
29476
|
-
})]
|
|
29477
|
-
})]
|
|
29478
|
-
});
|
|
29479
|
-
}
|
|
29480
|
-
//#endregion
|
|
29481
|
-
//#region src/composites/sort-header-button.tsx
|
|
29482
|
-
function SortHeaderButton({ label, active, dir, onSort }) {
|
|
29483
|
-
return /* @__PURE__ */ jsxs("button", {
|
|
29484
|
-
type: "button",
|
|
29485
|
-
onClick: onSort,
|
|
29486
|
-
className: "group/sort inline-flex items-center gap-0.5 hover:text-foreground transition-colors",
|
|
29487
|
-
children: [/* @__PURE__ */ jsx("span", { children: label }), active ? dir === "asc" ? /* @__PURE__ */ jsx(ChevronUp, { className: "h-3 w-3 flex-shrink-0" }) : /* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3 flex-shrink-0" }) : /* @__PURE__ */ jsx(ChevronUp, { className: cn("h-3 w-3 flex-shrink-0 opacity-0 transition-opacity", "group-hover/sort:opacity-30") })]
|
|
29488
|
-
});
|
|
29489
|
-
}
|
|
29490
|
-
//#endregion
|
|
29491
|
-
//#region src/composites/table-sort.ts
|
|
29492
|
-
/**
|
|
29493
|
-
* The toggle rule, and the only copy of it.
|
|
29494
|
-
*
|
|
29495
|
-
* none → asc → desc → none, and a click on a DIFFERENT column starts that
|
|
29496
|
-
* column fresh at `asc`. `sameColumn` says whether the clicked column is the
|
|
29497
|
-
* one currently sorted; `currentDir` is its direction (`null` when nothing is
|
|
29498
|
-
* sorted). Returns the next direction, or `null` for "unsorted".
|
|
29499
|
-
*
|
|
29500
|
-
* Split out from {@link nextTableSort} so the device list — whose sort state
|
|
29501
|
-
* keys columns by `columnId`, not `columnKey` — can reuse the rule without a
|
|
29502
|
-
* cast and without a second copy of it.
|
|
29503
|
-
*/
|
|
29504
|
-
function nextSortDirection(sameColumn, currentDir) {
|
|
29505
|
-
if (!sameColumn || currentDir === null) return "asc";
|
|
29506
|
-
return currentDir === "asc" ? "desc" : null;
|
|
29507
|
-
}
|
|
29508
|
-
/**
|
|
29509
|
-
* Toggle a column's sort on header click: none → asc → desc → none.
|
|
29510
|
-
* Clicking a DIFFERENT column starts it fresh at `asc`.
|
|
29511
|
-
*/
|
|
29512
|
-
function nextTableSort(current, columnKey) {
|
|
29513
|
-
const dir = nextSortDirection(current !== null && current.columnKey === columnKey, current === null ? null : current.dir);
|
|
29514
|
-
return dir === null ? null : {
|
|
29515
|
-
columnKey,
|
|
29516
|
-
dir
|
|
29517
|
-
};
|
|
29518
|
-
}
|
|
29519
|
-
/**
|
|
29520
|
-
* Ascending comparison of two cell values.
|
|
29521
|
-
*
|
|
29522
|
-
* Numbers compare numerically and strings compare with `localeCompare` — a
|
|
29523
|
-
* numeric column must never be ordered as text ("100" before "9"), and a text
|
|
29524
|
-
* column must respect the locale's collation. A mixed column (one row a
|
|
29525
|
-
* number, another a string) falls back to string comparison of both, which is
|
|
29526
|
-
* the only total order available; it is not a shape any caller should build on
|
|
29527
|
-
* purpose.
|
|
30493
|
+
* The calm sentence for a scene that is sitting the current light out — `null`
|
|
30494
|
+
* whenever it is checking normally.
|
|
29528
30495
|
*
|
|
29529
|
-
*
|
|
29530
|
-
*
|
|
29531
|
-
*
|
|
30496
|
+
* Separate from {@link statusLine} because it is not a verdict and must not
|
|
30497
|
+
* read like one. A paused scene has not failed, has not lost its flag, and
|
|
30498
|
+
* needs nothing from the operator; the only thing worth saying is which light
|
|
30499
|
+
* it is waiting out and that it will pick itself back up.
|
|
29532
30500
|
*/
|
|
29533
|
-
function
|
|
29534
|
-
if (
|
|
29535
|
-
|
|
30501
|
+
function suspensionLine(monitor) {
|
|
30502
|
+
if (monitor.suspendedCondition === null) return null;
|
|
30503
|
+
const light = variantLabel(monitor.suspendedCondition).toLowerCase();
|
|
30504
|
+
return `Paused — nothing was captured in ${light}, so checks are on hold until the light changes back. Capture ${light} if you want it watched around the clock.`;
|
|
29536
30505
|
}
|
|
30506
|
+
var UNAVAILABLE_SENTENCE = {
|
|
30507
|
+
"no-reference-for-condition": "can’t tell — nothing has been captured for this scene yet. Press “Capture daylight” to photograph what normal looks like.",
|
|
30508
|
+
"view-shifted": "can’t tell — the camera view has moved since the reference was captured. Re-capture it.",
|
|
30509
|
+
"no-vision-profile": "can’t tell — the vision model is unreachable, so the change is on hold.",
|
|
30510
|
+
"encoder-model-changed": "can’t tell — the embedding model changed, so the stored references are unreadable. Re-capture them.",
|
|
30511
|
+
"no-snapshot": "can’t tell — this camera did not return a frame."
|
|
30512
|
+
};
|
|
29537
30513
|
/**
|
|
29538
|
-
*
|
|
29539
|
-
*
|
|
29540
|
-
* `valueOf` is the column's `sortValue` — it is called once per row per
|
|
29541
|
-
* comparison, so it must be cheap (read a field, do not format a string).
|
|
29542
|
-
* `sort === null` ⇒ a fresh shallow copy in the incoming order.
|
|
30514
|
+
* The one line under a scene's name. It answers, in order: can it judge, what
|
|
30515
|
+
* does it say, and how long has it said it.
|
|
29543
30516
|
*/
|
|
29544
|
-
function
|
|
29545
|
-
if (
|
|
29546
|
-
|
|
29547
|
-
|
|
29548
|
-
|
|
29549
|
-
|
|
29550
|
-
|
|
29551
|
-
|
|
29552
|
-
|
|
29553
|
-
}
|
|
29554
|
-
return factor * compareValues(av, bv);
|
|
29555
|
-
});
|
|
30517
|
+
function statusLine(monitor, now) {
|
|
30518
|
+
if (monitor.states.length === 0) return "Not armed yet — capture the initial state to start watching.";
|
|
30519
|
+
if (monitor.verdict === "unknown") return monitor.unavailable !== null ? UNAVAILABLE_SENTENCE[monitor.unavailable] : "can’t tell right now.";
|
|
30520
|
+
const since = monitor.verdict === "diverged" ? monitor.divergedAt : monitor.armedAt;
|
|
30521
|
+
const held = since !== null ? ` for ${humanDuration(Math.max(0, now - since))}` : "";
|
|
30522
|
+
if (monitor.latched) return monitor.verdict === "diverged" ? `CHANGED${held} — press Reset when you have dealt with it.` : `Back to normal, but still flagged as changed${held}. Press Reset to clear it.`;
|
|
30523
|
+
const paused = suspensionLine(monitor);
|
|
30524
|
+
if (paused !== null) return paused;
|
|
30525
|
+
return monitor.verdict === "diverged" ? `CHANGED${held}` : `Normal${held}`;
|
|
29556
30526
|
}
|
|
29557
|
-
/**
|
|
29558
|
-
|
|
29559
|
-
|
|
29560
|
-
|
|
30527
|
+
/** The badge word. `latched` wins over `verdict`, because the latch is the
|
|
30528
|
+
* thing the operator asked to be told and it outlives the live state.
|
|
30529
|
+
*
|
|
30530
|
+
* A scene sitting out an uncovered light reads `Paused`, in the NEUTRAL tone —
|
|
30531
|
+
* never `alarm`, and never the `danger` variant the page maps that to. A
|
|
30532
|
+
* missing IR reference must not be able to paint a red badge on a scene that
|
|
30533
|
+
* is doing exactly what it was configured to do. */
|
|
30534
|
+
function badgeFor(monitor) {
|
|
30535
|
+
if (monitor.states.length === 0) return {
|
|
30536
|
+
text: "Not armed",
|
|
30537
|
+
tone: "unknown"
|
|
30538
|
+
};
|
|
30539
|
+
if (monitor.latched) return {
|
|
30540
|
+
text: "Changed",
|
|
30541
|
+
tone: "alarm"
|
|
30542
|
+
};
|
|
30543
|
+
if (monitor.suspendedCondition !== null) return {
|
|
30544
|
+
text: "Paused",
|
|
30545
|
+
tone: "unknown"
|
|
30546
|
+
};
|
|
30547
|
+
if (monitor.verdict === "unknown") return {
|
|
30548
|
+
text: "Can’t tell",
|
|
30549
|
+
tone: "unknown"
|
|
30550
|
+
};
|
|
30551
|
+
return monitor.verdict === "diverged" ? {
|
|
30552
|
+
text: "Changed",
|
|
30553
|
+
tone: "alarm"
|
|
30554
|
+
} : {
|
|
30555
|
+
text: "Normal",
|
|
30556
|
+
tone: "ok"
|
|
30557
|
+
};
|
|
30558
|
+
}
|
|
30559
|
+
/** A rect covering most of the frame is dominated by weather, sky and parked
|
|
30560
|
+
* cars: the bin is 2 % of the pixels and CLIP will not notice it leaving. The
|
|
30561
|
+
* failure is silent INSENSITIVITY, which is the worst kind — hence a warning
|
|
30562
|
+
* rather than a refusal. */
|
|
30563
|
+
var ROI_AREA_WARN_FRACTION = .4;
|
|
30564
|
+
function roiTooLarge(roi) {
|
|
30565
|
+
return roi.width * roi.height > ROI_AREA_WARN_FRACTION;
|
|
30566
|
+
}
|
|
30567
|
+
function humanDuration(ms) {
|
|
30568
|
+
const s = Math.floor(ms / 1e3);
|
|
30569
|
+
if (s < 60) return `${s}s`;
|
|
30570
|
+
const m = Math.floor(s / 60);
|
|
30571
|
+
if (m < 60) return `${m}m`;
|
|
30572
|
+
const h = Math.floor(m / 60);
|
|
30573
|
+
if (h < 48) return `${h}h`;
|
|
30574
|
+
return `${Math.floor(h / 24)}d`;
|
|
29561
30575
|
}
|
|
29562
30576
|
//#endregion
|
|
29563
|
-
//#region src/composites/
|
|
30577
|
+
//#region src/composites/code-block.tsx
|
|
30578
|
+
function CodeBlock({ children, maxHeight = 300, className }) {
|
|
30579
|
+
const [copied, setCopied] = useState(false);
|
|
30580
|
+
const handleCopy = useCallback(() => {
|
|
30581
|
+
navigator.clipboard.writeText(children).then(() => {
|
|
30582
|
+
setCopied(true);
|
|
30583
|
+
setTimeout(() => setCopied(false), 2e3);
|
|
30584
|
+
});
|
|
30585
|
+
}, [children]);
|
|
30586
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
30587
|
+
className: cn("relative group", className),
|
|
30588
|
+
children: [/* @__PURE__ */ jsx(ScrollArea, {
|
|
30589
|
+
style: { maxHeight },
|
|
30590
|
+
children: /* @__PURE__ */ jsx("pre", {
|
|
30591
|
+
className: "font-mono text-xs bg-surface p-3 rounded-md border border-border-subtle",
|
|
30592
|
+
children: /* @__PURE__ */ jsx("code", { children })
|
|
30593
|
+
})
|
|
30594
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
30595
|
+
className: "absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity",
|
|
30596
|
+
children: /* @__PURE__ */ jsx(IconButton, {
|
|
30597
|
+
icon: copied ? Check : Copy,
|
|
30598
|
+
"aria-label": "Copy code",
|
|
30599
|
+
variant: "ghost",
|
|
30600
|
+
size: "sm",
|
|
30601
|
+
onClick: handleCopy
|
|
30602
|
+
})
|
|
30603
|
+
})]
|
|
30604
|
+
});
|
|
30605
|
+
}
|
|
30606
|
+
//#endregion
|
|
30607
|
+
//#region src/composites/confirm-action-button.tsx
|
|
29564
30608
|
/**
|
|
29565
|
-
*
|
|
29566
|
-
*
|
|
29567
|
-
* Recipes baked in:
|
|
29568
|
-
* - `overflow-x-auto` wrapper so narrow viewports don't clip the
|
|
29569
|
-
* right edge (the rounded border survives the scroll).
|
|
29570
|
-
* - `min-w-[Npx]` on the inner table so columns don't squash below
|
|
29571
|
-
* readable width before the scroll kicks in.
|
|
29572
|
-
* - `whitespace-nowrap` on headers — column titles never wrap to
|
|
29573
|
-
* two lines (`AUTO-KILL` was the regression that triggered this).
|
|
29574
|
-
* - Themed `bg-surface` + `border-border` so contrast works in both
|
|
29575
|
-
* light and dark themes.
|
|
29576
|
-
*
|
|
29577
|
-
* Column model: caller supplies `columns: DataColumn<T>[]` and
|
|
29578
|
-
* `rows: T[]`. Each column declares `header`, optional `align`,
|
|
29579
|
-
* optional `nowrap`, and a `render(row) => ReactNode` that produces
|
|
29580
|
-
* the cell content.
|
|
29581
|
-
*
|
|
29582
|
-
* <DataTable
|
|
29583
|
-
* columns={[
|
|
29584
|
-
* { key: 'username', header: 'Username', render: (u) => u.username },
|
|
29585
|
-
* { key: 'role', header: 'Role', render: (u) => <RoleBadge .../> },
|
|
29586
|
-
* { key: 'actions', header: '', align: 'right', render: (u) => <Actions .../> },
|
|
29587
|
-
* ]}
|
|
29588
|
-
* rows={users}
|
|
29589
|
-
* minWidthPx={560}
|
|
29590
|
-
* />
|
|
29591
|
-
*
|
|
29592
|
-
* If finer header markup is needed, pass `headerRender` instead of
|
|
29593
|
-
* `header` (e.g. for a sortable click affordance).
|
|
29594
|
-
*
|
|
29595
|
-
* ── Narrow viewports ──────────────────────────────────────────────────────
|
|
29596
|
-
* The table stays a table. The wrapper is `overflow-x-auto`, so a phone
|
|
29597
|
-
* scrolls the columns sideways INSIDE the table — the page body never moves
|
|
29598
|
-
* sideways, and the cells stay one line (`whitespace-nowrap`). That is the
|
|
29599
|
-
* default (`mobileMode="scroll"`). Cards are opt-in via `mobileMode="cards"`;
|
|
29600
|
-
* `auto` still applies the old column-count rule for a surface that wants it.
|
|
29601
|
-
* See `resolveTableLayout`.
|
|
29602
|
-
*
|
|
29603
|
-
* ── Scrolling the BODY, not the page ──────────────────────────────────────
|
|
29604
|
-
* `maxBodyHeight` caps the table and scrolls its rows INSIDE the container
|
|
29605
|
-
* with the column header pinned. It is ONE prop, not two, because "this list
|
|
29606
|
-
* scrolls internally" and "its header stays visible" are the same decision: a
|
|
29607
|
-
* capped table whose header scrolls away is a list of unlabelled cells.
|
|
30609
|
+
* ConfirmActionButton — destructive-action button with a modal confirm.
|
|
29608
30610
|
*
|
|
29609
|
-
*
|
|
29610
|
-
*
|
|
29611
|
-
*
|
|
29612
|
-
|
|
29613
|
-
|
|
29614
|
-
* The default cap for an internally-scrolling table.
|
|
30611
|
+
* Two-step UX for any operation operators shouldn't trigger by accident:
|
|
30612
|
+
* reboot device, restart addon, regenerate token, etc. The trigger is a
|
|
30613
|
+
* normal Button that opens a Dialog; confirming runs the async action,
|
|
30614
|
+
* the trigger spinner-disables itself for the duration, and the dialog
|
|
30615
|
+
* closes on success. Errors surface inline at the dialog's bottom.
|
|
29615
30616
|
*
|
|
29616
|
-
* `
|
|
29617
|
-
*
|
|
29618
|
-
*
|
|
30617
|
+
* Generic on `TResult` so callers don't have to discard the return
|
|
30618
|
+
* value. Pass an `icon` to render it inside the trigger (e.g. RotateCw
|
|
30619
|
+
* for reboot, RefreshCw for restart).
|
|
29619
30620
|
*/
|
|
29620
|
-
|
|
29621
|
-
|
|
29622
|
-
|
|
29623
|
-
|
|
29624
|
-
|
|
29625
|
-
|
|
29626
|
-
|
|
29627
|
-
|
|
29628
|
-
|
|
29629
|
-
|
|
29630
|
-
|
|
29631
|
-
|
|
29632
|
-
|
|
29633
|
-
|
|
29634
|
-
|
|
29635
|
-
|
|
29636
|
-
|
|
29637
|
-
|
|
29638
|
-
|
|
29639
|
-
|
|
29640
|
-
|
|
29641
|
-
|
|
29642
|
-
|
|
29643
|
-
|
|
29644
|
-
|
|
29645
|
-
|
|
29646
|
-
|
|
29647
|
-
|
|
29648
|
-
|
|
29649
|
-
|
|
29650
|
-
|
|
29651
|
-
|
|
29652
|
-
|
|
29653
|
-
|
|
29654
|
-
|
|
29655
|
-
|
|
29656
|
-
|
|
29657
|
-
|
|
29658
|
-
|
|
29659
|
-
|
|
29660
|
-
|
|
29661
|
-
|
|
29662
|
-
|
|
29663
|
-
|
|
29664
|
-
|
|
29665
|
-
|
|
29666
|
-
|
|
29667
|
-
|
|
29668
|
-
|
|
29669
|
-
|
|
29670
|
-
|
|
29671
|
-
|
|
29672
|
-
|
|
29673
|
-
|
|
29674
|
-
|
|
29675
|
-
|
|
29676
|
-
|
|
29677
|
-
|
|
29678
|
-
|
|
29679
|
-
title: sortable ? void 0 : col.notSortableReason,
|
|
29680
|
-
className: [
|
|
29681
|
-
"px-3 py-2 text-foreground-subtle font-medium bg-surface border-b border-border",
|
|
29682
|
-
scrolls ? "sticky top-0 z-10" : "",
|
|
29683
|
-
ALIGN_CLASS[align],
|
|
29684
|
-
nowrap ? "whitespace-nowrap" : "",
|
|
29685
|
-
col.width ?? ""
|
|
29686
|
-
].filter(Boolean).join(" "),
|
|
29687
|
-
children: sortable ? /* @__PURE__ */ jsx(SortHeaderButton, {
|
|
29688
|
-
label: col.headerRender ? col.headerRender() : col.header,
|
|
29689
|
-
active: isSorted,
|
|
29690
|
-
dir: isSorted && activeSort !== null ? activeSort.dir : "asc",
|
|
29691
|
-
onSort: () => setSort((prev) => nextTableSort(prev, col.key))
|
|
29692
|
-
}) : col.headerRender ? col.headerRender() : col.header
|
|
29693
|
-
}, col.key);
|
|
29694
|
-
}) })
|
|
29695
|
-
}), /* @__PURE__ */ jsx("tbody", { children: displayRows.map((row, rowIndex) => {
|
|
29696
|
-
const key = rowKey ? rowKey(row, rowIndex) : rowIndex;
|
|
29697
|
-
const extra = rowClassName?.(row, rowIndex) ?? "";
|
|
29698
|
-
const interactive = onRowClick !== void 0;
|
|
29699
|
-
return /* @__PURE__ */ jsx("tr", {
|
|
29700
|
-
onClick: interactive ? () => onRowClick(row, rowIndex) : void 0,
|
|
29701
|
-
className: [interactive ? "cursor-pointer hover:bg-primary/5" : "hover:bg-primary/5", extra].filter(Boolean).join(" "),
|
|
29702
|
-
children: columns.map((col) => {
|
|
29703
|
-
const align = col.align ?? "left";
|
|
29704
|
-
const nowrap = col.nowrap ?? true;
|
|
29705
|
-
return /* @__PURE__ */ jsx("td", {
|
|
29706
|
-
className: [
|
|
29707
|
-
"px-3 py-2 text-foreground border-b border-border",
|
|
29708
|
-
ALIGN_CLASS[align],
|
|
29709
|
-
nowrap ? "whitespace-nowrap" : ""
|
|
29710
|
-
].filter(Boolean).join(" "),
|
|
29711
|
-
children: col.render(row, rowIndex)
|
|
29712
|
-
}, col.key);
|
|
29713
|
-
})
|
|
29714
|
-
}, key);
|
|
29715
|
-
}) })]
|
|
29716
|
-
})
|
|
29717
|
-
});
|
|
30621
|
+
function ConfirmActionButton({ label, icon: Icon, title = "Confirm action", description, confirmLabel, triggerVariant = "outline", confirmVariant = "danger", size = "sm", disabled, className, action, onSuccess }) {
|
|
30622
|
+
const [open, setOpen] = useState(false);
|
|
30623
|
+
const [running, setRunning] = useState(false);
|
|
30624
|
+
const [error, setError] = useState(null);
|
|
30625
|
+
const handleConfirm = async () => {
|
|
30626
|
+
setError(null);
|
|
30627
|
+
setRunning(true);
|
|
30628
|
+
try {
|
|
30629
|
+
const result = await action();
|
|
30630
|
+
onSuccess?.(result);
|
|
30631
|
+
setOpen(false);
|
|
30632
|
+
} catch (err) {
|
|
30633
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
30634
|
+
} finally {
|
|
30635
|
+
setRunning(false);
|
|
30636
|
+
}
|
|
30637
|
+
};
|
|
30638
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs(Button, {
|
|
30639
|
+
type: "button",
|
|
30640
|
+
variant: triggerVariant,
|
|
30641
|
+
size,
|
|
30642
|
+
disabled,
|
|
30643
|
+
onClick: () => {
|
|
30644
|
+
setError(null);
|
|
30645
|
+
setOpen(true);
|
|
30646
|
+
},
|
|
30647
|
+
className,
|
|
30648
|
+
children: [Icon && /* @__PURE__ */ jsx(Icon, { className: "h-3.5 w-3.5" }), label]
|
|
30649
|
+
}), /* @__PURE__ */ jsx(Dialog, {
|
|
30650
|
+
open,
|
|
30651
|
+
onOpenChange: (next) => !running && setOpen(next),
|
|
30652
|
+
children: /* @__PURE__ */ jsxs(DialogContent, { children: [
|
|
30653
|
+
/* @__PURE__ */ jsxs(DialogHeader, { children: [/* @__PURE__ */ jsxs(DialogTitle, {
|
|
30654
|
+
className: "flex items-center gap-2",
|
|
30655
|
+
children: [/* @__PURE__ */ jsx(TriangleAlert, { className: "h-4 w-4 text-amber-400" }), title]
|
|
30656
|
+
}), typeof description === "string" ? /* @__PURE__ */ jsx(DialogDescription, { children: description }) : description] }),
|
|
30657
|
+
error && /* @__PURE__ */ jsx("div", {
|
|
30658
|
+
className: cn("mt-2 px-3 py-2 rounded border border-danger/40 bg-danger/10", "text-xs text-danger"),
|
|
30659
|
+
children: error
|
|
30660
|
+
}),
|
|
30661
|
+
/* @__PURE__ */ jsxs(DialogFooter, { children: [/* @__PURE__ */ jsx(Button, {
|
|
30662
|
+
type: "button",
|
|
30663
|
+
variant: "ghost",
|
|
30664
|
+
size,
|
|
30665
|
+
disabled: running,
|
|
30666
|
+
onClick: () => setOpen(false),
|
|
30667
|
+
children: "Cancel"
|
|
30668
|
+
}), /* @__PURE__ */ jsxs(Button, {
|
|
30669
|
+
type: "button",
|
|
30670
|
+
variant: confirmVariant,
|
|
30671
|
+
size,
|
|
30672
|
+
disabled: running,
|
|
30673
|
+
onClick: () => {
|
|
30674
|
+
handleConfirm();
|
|
30675
|
+
},
|
|
30676
|
+
children: [running && /* @__PURE__ */ jsx(LoaderCircle, { className: "h-3.5 w-3.5 animate-spin" }), confirmLabel ?? label]
|
|
30677
|
+
})] })
|
|
30678
|
+
] })
|
|
30679
|
+
})] });
|
|
29718
30680
|
}
|
|
30681
|
+
//#endregion
|
|
30682
|
+
//#region src/composites/copy-button.tsx
|
|
29719
30683
|
/**
|
|
29720
|
-
*
|
|
29721
|
-
*
|
|
29722
|
-
*
|
|
29723
|
-
*
|
|
29724
|
-
* full-width strip at the foot of the card.
|
|
30684
|
+
* `CopyButton` — a compact button that copies a string `value` to the
|
|
30685
|
+
* clipboard via `navigator.clipboard.writeText`, showing a brief
|
|
30686
|
+
* "copied" check state. Addon-agnostic; used wherever a secret / URL
|
|
30687
|
+
* needs a one-click copy affordance (export setup panels, etc.).
|
|
29725
30688
|
*/
|
|
29726
|
-
|
|
29727
|
-
|
|
29728
|
-
const
|
|
29729
|
-
const
|
|
29730
|
-
|
|
29731
|
-
|
|
29732
|
-
|
|
29733
|
-
|
|
29734
|
-
|
|
29735
|
-
|
|
29736
|
-
|
|
29737
|
-
|
|
29738
|
-
|
|
29739
|
-
|
|
29740
|
-
|
|
29741
|
-
|
|
29742
|
-
|
|
29743
|
-
|
|
29744
|
-
|
|
29745
|
-
|
|
29746
|
-
|
|
30689
|
+
var COPIED_RESET_MS = 2e3;
|
|
30690
|
+
function CopyButton({ value, label, srLabel, className, disabled }) {
|
|
30691
|
+
const [copied, setCopied] = useState(false);
|
|
30692
|
+
const handleCopy = useCallback(() => {
|
|
30693
|
+
if (!value) return;
|
|
30694
|
+
navigator.clipboard.writeText(value).then(() => {
|
|
30695
|
+
setCopied(true);
|
|
30696
|
+
setTimeout(() => setCopied(false), COPIED_RESET_MS);
|
|
30697
|
+
});
|
|
30698
|
+
}, [value]);
|
|
30699
|
+
return /* @__PURE__ */ jsxs(Button, {
|
|
30700
|
+
size: "sm",
|
|
30701
|
+
variant: "ghost",
|
|
30702
|
+
type: "button",
|
|
30703
|
+
disabled: disabled || value.length === 0,
|
|
30704
|
+
onClick: handleCopy,
|
|
30705
|
+
className: cn(className),
|
|
30706
|
+
"aria-label": copied ? "Copied" : `Copy ${srLabel ?? label ?? "value"}`,
|
|
30707
|
+
children: [copied ? /* @__PURE__ */ jsx(Check, { className: "h-3.5 w-3.5 text-success" }) : /* @__PURE__ */ jsx(Copy, { className: "h-3.5 w-3.5" }), label ? /* @__PURE__ */ jsx("span", {
|
|
30708
|
+
className: "ml-1",
|
|
30709
|
+
children: copied ? "Copied" : label
|
|
30710
|
+
}) : null]
|
|
29747
30711
|
});
|
|
29748
30712
|
}
|
|
29749
30713
|
//#endregion
|
|
@@ -45801,99 +46765,6 @@ function fuzzyMatch(query, target) {
|
|
|
45801
46765
|
return normalizeForSearch(target).includes(nq);
|
|
45802
46766
|
}
|
|
45803
46767
|
//#endregion
|
|
45804
|
-
//#region src/composites/device-list/pagination.tsx
|
|
45805
|
-
var NEIGHBORS = 2;
|
|
45806
|
-
function computePageButtons(currentPage, totalPages) {
|
|
45807
|
-
if (totalPages <= 1) return [{
|
|
45808
|
-
kind: "page",
|
|
45809
|
-
n: 1
|
|
45810
|
-
}];
|
|
45811
|
-
if (totalPages <= 7) {
|
|
45812
|
-
const all = [];
|
|
45813
|
-
for (let i = 1; i <= totalPages; i++) all.push({
|
|
45814
|
-
kind: "page",
|
|
45815
|
-
n: i
|
|
45816
|
-
});
|
|
45817
|
-
return all;
|
|
45818
|
-
}
|
|
45819
|
-
const result = [];
|
|
45820
|
-
const leftBound = Math.max(2, currentPage - NEIGHBORS);
|
|
45821
|
-
const rightBound = Math.min(totalPages - 1, currentPage + NEIGHBORS);
|
|
45822
|
-
result.push({
|
|
45823
|
-
kind: "page",
|
|
45824
|
-
n: 1
|
|
45825
|
-
});
|
|
45826
|
-
if (leftBound > 2) result.push({
|
|
45827
|
-
kind: "ellipsis",
|
|
45828
|
-
side: "left"
|
|
45829
|
-
});
|
|
45830
|
-
for (let i = leftBound; i <= rightBound; i++) result.push({
|
|
45831
|
-
kind: "page",
|
|
45832
|
-
n: i
|
|
45833
|
-
});
|
|
45834
|
-
if (rightBound < totalPages - 1) result.push({
|
|
45835
|
-
kind: "ellipsis",
|
|
45836
|
-
side: "right"
|
|
45837
|
-
});
|
|
45838
|
-
result.push({
|
|
45839
|
-
kind: "page",
|
|
45840
|
-
n: totalPages
|
|
45841
|
-
});
|
|
45842
|
-
return result;
|
|
45843
|
-
}
|
|
45844
|
-
function Pagination({ currentPage, totalItems, pageSize, onPageChange, itemNoun = "devices" }) {
|
|
45845
|
-
if (totalItems <= pageSize) return null;
|
|
45846
|
-
const totalPages = Math.ceil(totalItems / pageSize);
|
|
45847
|
-
const startIdx = (currentPage - 1) * pageSize + 1;
|
|
45848
|
-
const endIdx = Math.min(currentPage * pageSize, totalItems);
|
|
45849
|
-
const buttons = computePageButtons(currentPage, totalPages);
|
|
45850
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
45851
|
-
className: "flex items-center justify-between gap-4 flex-wrap pt-2 text-[11px] text-foreground-subtle",
|
|
45852
|
-
children: [/* @__PURE__ */ jsxs("span", { children: [
|
|
45853
|
-
"Showing ",
|
|
45854
|
-
startIdx,
|
|
45855
|
-
"–",
|
|
45856
|
-
endIdx,
|
|
45857
|
-
" of ",
|
|
45858
|
-
totalItems,
|
|
45859
|
-
" ",
|
|
45860
|
-
itemNoun
|
|
45861
|
-
] }), /* @__PURE__ */ jsxs("span", {
|
|
45862
|
-
className: "inline-flex items-center gap-1",
|
|
45863
|
-
children: [
|
|
45864
|
-
/* @__PURE__ */ jsx("button", {
|
|
45865
|
-
type: "button",
|
|
45866
|
-
"aria-label": "Previous page",
|
|
45867
|
-
disabled: currentPage === 1,
|
|
45868
|
-
onClick: () => onPageChange(currentPage - 1),
|
|
45869
|
-
className: cn("inline-flex items-center justify-center h-7 w-7 rounded border border-border bg-surface text-foreground-subtle", "hover:text-foreground hover:bg-surface-hover transition-colors", "disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-surface"),
|
|
45870
|
-
children: /* @__PURE__ */ jsx(ChevronLeft, { className: "h-3 w-3" })
|
|
45871
|
-
}),
|
|
45872
|
-
buttons.map((b, i) => {
|
|
45873
|
-
if (b.kind === "ellipsis") return /* @__PURE__ */ jsx("span", {
|
|
45874
|
-
className: "px-1 text-foreground-subtle/60 select-none",
|
|
45875
|
-
children: "…"
|
|
45876
|
-
}, `e-${i}`);
|
|
45877
|
-
return /* @__PURE__ */ jsx("button", {
|
|
45878
|
-
type: "button",
|
|
45879
|
-
onClick: () => onPageChange(b.n),
|
|
45880
|
-
className: cn("inline-flex items-center justify-center h-7 min-w-7 px-1.5 rounded border text-[10.5px] font-medium transition-colors", b.n === currentPage ? "border-primary/40 bg-primary/10 text-primary" : "border-border bg-surface text-foreground-subtle hover:text-foreground hover:bg-surface-hover"),
|
|
45881
|
-
children: b.n
|
|
45882
|
-
}, `p-${b.n}`);
|
|
45883
|
-
}),
|
|
45884
|
-
/* @__PURE__ */ jsx("button", {
|
|
45885
|
-
type: "button",
|
|
45886
|
-
"aria-label": "Next page",
|
|
45887
|
-
disabled: currentPage === totalPages,
|
|
45888
|
-
onClick: () => onPageChange(currentPage + 1),
|
|
45889
|
-
className: cn("inline-flex items-center justify-center h-7 w-7 rounded border border-border bg-surface text-foreground-subtle", "hover:text-foreground hover:bg-surface-hover transition-colors", "disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-surface"),
|
|
45890
|
-
children: /* @__PURE__ */ jsx(ChevronRight, { className: "h-3 w-3" })
|
|
45891
|
-
})
|
|
45892
|
-
]
|
|
45893
|
-
})]
|
|
45894
|
-
});
|
|
45895
|
-
}
|
|
45896
|
-
//#endregion
|
|
45897
46768
|
//#region src/composites/device-list/multi-select.tsx
|
|
45898
46769
|
/**
|
|
45899
46770
|
* Compact multi-select dropdown — a single chip-sized trigger ("Label · N")
|
|
@@ -48877,7 +49748,7 @@ function hlsLevelToInfo(level) {
|
|
|
48877
49748
|
name: level.name.length > 0 ? level.name : void 0
|
|
48878
49749
|
};
|
|
48879
49750
|
}
|
|
48880
|
-
function HlsVideo({ url, authToken, posterUrl, autoPlay = true, className, onQualityOptions, selectedLevel = -1 }) {
|
|
49751
|
+
function HlsVideo({ url, format = "hls", authToken, posterUrl, autoPlay = true, className, onQualityOptions, selectedLevel = -1 }) {
|
|
48881
49752
|
const videoRef = useRef(null);
|
|
48882
49753
|
const hlsRef = useRef(null);
|
|
48883
49754
|
const [error, setError] = useState(null);
|
|
@@ -48890,6 +49761,23 @@ function HlsVideo({ url, authToken, posterUrl, autoPlay = true, className, onQua
|
|
|
48890
49761
|
onQualityOptions?.([]);
|
|
48891
49762
|
const onPlaying = () => setLoading(false);
|
|
48892
49763
|
video.addEventListener("playing", onPlaying);
|
|
49764
|
+
if (format === "mp4") {
|
|
49765
|
+
video.src = url;
|
|
49766
|
+
const onLoaded = () => {
|
|
49767
|
+
if (autoPlay) video.play().catch(() => {});
|
|
49768
|
+
};
|
|
49769
|
+
const onFailed = () => setError("Could not load this clip.");
|
|
49770
|
+
video.addEventListener("loadedmetadata", onLoaded);
|
|
49771
|
+
video.addEventListener("error", onFailed);
|
|
49772
|
+
onQualityOptions?.([]);
|
|
49773
|
+
return () => {
|
|
49774
|
+
video.removeEventListener("loadedmetadata", onLoaded);
|
|
49775
|
+
video.removeEventListener("error", onFailed);
|
|
49776
|
+
video.removeEventListener("playing", onPlaying);
|
|
49777
|
+
video.removeAttribute("src");
|
|
49778
|
+
video.load();
|
|
49779
|
+
};
|
|
49780
|
+
}
|
|
48893
49781
|
if (!("MediaSource" in window) && video.canPlayType("application/vnd.apple.mpegurl")) {
|
|
48894
49782
|
video.src = url;
|
|
48895
49783
|
const onLoaded = () => {
|
|
@@ -48952,6 +49840,7 @@ function HlsVideo({ url, authToken, posterUrl, autoPlay = true, className, onQua
|
|
|
48952
49840
|
};
|
|
48953
49841
|
}, [
|
|
48954
49842
|
url,
|
|
49843
|
+
format,
|
|
48955
49844
|
authToken,
|
|
48956
49845
|
autoPlay,
|
|
48957
49846
|
onQualityOptions
|
|
@@ -51095,7 +51984,7 @@ function ImageOffIcon({ className }) {
|
|
|
51095
51984
|
]
|
|
51096
51985
|
});
|
|
51097
51986
|
}
|
|
51098
|
-
function StreamPanel({ serverUrl, createSession, sendAnswer, handleOffer, getIceServers, addIceCandidate, getIceCandidates, closeSession, getSessionState, getSessionDebug, playbackUrl, playbackAuthToken, onExitPlayback, posterUrl, streams, activeStreamId: controlledStreamId, onStreamChange, deviceName, phase, pipelineMetrics, detections, stationaryObjects, defaultShowDetections = true, defaultShowMotion = false, streamStats, autoPlay = false, showPlayStop = false, snapshotSrc, snapshotLoading = false, onRefreshSnapshot, showStreamStats = false, children, extraOverlay, ptzAvailable = false, ptzShown = false, ptzOverlay, onPtzToggle, intercomAvailable = false, intercomShown = false, onIntercomToggle, chromeless = false, className }) {
|
|
51987
|
+
function StreamPanel({ serverUrl, createSession, sendAnswer, handleOffer, getIceServers, addIceCandidate, getIceCandidates, closeSession, getSessionState, getSessionDebug, playbackUrl, playbackFormat, playbackAuthToken, onExitPlayback, posterUrl, streams, activeStreamId: controlledStreamId, onStreamChange, deviceName, phase, pipelineMetrics, detections, stationaryObjects, defaultShowDetections = true, defaultShowMotion = false, streamStats, autoPlay = false, showPlayStop = false, snapshotSrc, snapshotLoading = false, onRefreshSnapshot, showStreamStats = false, children, extraOverlay, ptzAvailable = false, ptzShown = false, ptzOverlay, onPtzToggle, intercomAvailable = false, intercomShown = false, onIntercomToggle, chromeless = false, className }) {
|
|
51099
51988
|
const registeredButtons = usePlayerToolbarButtons();
|
|
51100
51989
|
const recordedPlayback = useRecordedPlayback();
|
|
51101
51990
|
const [isPlaying, setIsPlaying] = useState(autoPlay);
|
|
@@ -51396,6 +52285,7 @@ function StreamPanel({ serverUrl, createSession, sendAnswer, handleOffer, getIce
|
|
|
51396
52285
|
className: "aspect-video relative",
|
|
51397
52286
|
children: [/* @__PURE__ */ jsx(HlsVideo, {
|
|
51398
52287
|
url: playbackUrl,
|
|
52288
|
+
...playbackFormat ? { format: playbackFormat } : {},
|
|
51399
52289
|
authToken: playbackAuthToken ?? null,
|
|
51400
52290
|
onQualityOptions: handleVodQualityOptions,
|
|
51401
52291
|
selectedLevel: vodLevel,
|
|
@@ -52480,4 +53370,4 @@ var SceneMonitorEditor = lazy(() => import("./SceneMonitorEditor-BvDy3x3k.js").t
|
|
|
52480
53370
|
/** Lazy-wrapped `PrivacyMaskSettings` — code-split off the main bundle. */
|
|
52481
53371
|
var PrivacyMaskSettings = lazy(() => import("./PrivacyMaskSettings-PkYs077O.js").then((m) => ({ default: m.PrivacyMaskSettings })));
|
|
52482
53372
|
//#endregion
|
|
52483
|
-
export { AddonGlobalSettingsForm, AgentStepEditor, AlarmHeroCard, AlarmInlineControl as AlarmPanelInlineControl, AppShell, ArcKnob, AudioClassificationList, AudioLevelWaveform, AudioWaveform, AutomationHeroCard, AutomationInlineControl, AutotrackSection, BTN_COMPACT, BTN_COMPACT_DANGER, BTN_COMPACT_PRIMARY, BTN_COMPACT_WARNING, Badge, BatteryBadge, BottomSheet, Breadcrumb, BrightnessPanel, Button, ButtonControl, ButtonHeroCard, CARD_MODE_MIN_COLUMNS, CENTER, CHIP_ACTIVE, CHIP_BASE, CHIP_INACTIVE, CLASS_COLORS, COLUMN_BREAKPOINT_CLASS, COLUMN_PRIORITY, COMMIT_DEDUPE_TOLERANCE_MS, COMMIT_DEDUPE_WINDOW_MS, CONTROL_CAP_NAMES, CONTROL_FILLS, CameraStreamPlayer, Card, Checkbox, ChildSectionAccordion, ClimatePanel, CodeBlock, CollapsibleCard, ConfigFormBuilder, FormField as ConfigFormField, ConfigSchemaField, ConfirmActionButton, ConfirmDialogProvider, ConsumablesPanel, ContainerChildrenProvider, ContainerPrimaryHero, ControlColumn, ControlHeroCard, ControlInlineControl, ControlPanel, CopyButton, CoverHeroCard, CoverInlineControl, CoverPanel, CustomFieldRenderersProvider, DEFAULT_COLOR, DEVICE_COLUMNS, DEVICE_LIST_PAGE_SIZE_KEY, DEVICE_LIST_PAGE_SIZE_OPTIONS, DEVICE_ROLE_META, DEVICE_TYPE_CONTROL, DEVICE_TYPE_META, DISPLAY_ICON_REGISTRY, DataTable, DetectionCanvas, DetectionOverlay, DetectionResultTree, DevShell, DeviceActivityPanel, DeviceBatchToolbar, DeviceCard, DeviceContextProvider, DeviceExportPanel, DeviceGrid, DeviceItem, DeviceList, DeviceMultiSelectField, DeviceSelectField, DeviceSelectorPicker, DeviceStepMatrix, Dialog, DialogBody, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, DiscoveryPanel, DoorbellRecentPanel, Dropdown, DropdownContent, DropdownItem, DropdownTrigger, DummyHeroCard, DummyInline, EVENT_KIND_ICONS, EmptyState, ErrorBox, EventKindGlyph, EventStream, FILL, FLEET_LIST_GC_TIME_MS, FLEET_LIST_QUERY_KEY, FLEET_LIST_STALE_TIME_MS, FanHeroCard, FanInlineControl, FanPanel, FilterBar, FloatingEventStream, FloatingLogStream, FloatingPanel, FormField$1 as FormField, GRID_GAP, GRID_PAIRED, GRID_QUICK_STATS, GripTrack, GroupedModelSelector, HOST_WIDGETS, HlsVideo, HoverZoomImage, HumidifierHeroCard, HumidifierInlineControl, INPUT_COMPACT, IconAction, IconButton, ImageHeroCard, ImageInlineControl, ImageSelector, InferenceConfigSelector, Input, KebabMenu, KeyValueList, LIST_ROW, Label, LawnMowerHeroCard, LawnMowerInlineControl, LightHeroCard, LightInlineControl, LockHeroCard, LockInlineControl, LockPanel, LogStream, LoginForm, MODE_COLOR, MaskShapeCanvas, MediaPlayerHeroCard, MediaPlayerInlineControl, MediaPlayerPanel, MobileDrawer, ModelPicker, MotionZonesSettings, NAVIGATION_DRIVE_INTERVAL_MS, NavigationPanel, NetworkLinkBadge, NodeMultiSelectField, NodePicker, NodeSelectField, OfflineBadge, PHASE_CONFIG, PRIORITY, PTZOverlay, PageHeader, PhaseIcon, PipelineBuilder, PipelineRuntimeSelector, PipelineStep, PipelineTreeMatrix, PlayerOverlaysProvider, Popover, PopoverContent, PopoverRowAction, PopoverTrigger, PrimaryChildPicker, PrivacyMaskSettings, ProviderBadge, PtzPanel, QrCode, RECONNECT_POLICY, RECORDED_PLAYBACK_MODES, RIGHT, ROI_AREA_WARN_FRACTION, ROLE_DESCRIPTOR, RadialGauge, RecordedPlaybackProvider, RecordingPanel, ResponseLog, SCENE_CAPTURE_VARIANTS, SECTION_BODY, SECTION_CARD, SECTION_HEADER, SETTING_ROW, SETTING_ROW_LABEL, SETTING_ROW_STACK_BREAKPOINT, SETTING_ROW_VALUE, SETTING_ROW_VALUE_TEXT, SNAPSHOT_MEDIA_PATH, SPLIT_PANEL_OUTER, SPLIT_PANEL_SIDE, STACK_GAP, STATE_COLOR, SceneMonitorEditor, ScopePicker, ScriptHeroCard, ScriptInlineControl, ScrollArea, Select, SemanticBadge, SensorHeroCard, SensorInlineControl, SensorValueAtom, Separator, SettingRow, Sidebar, SidebarItem, Skeleton, SlideOverPanel, SlideToggle, SnapshotButton, SortHeaderButton, StatCard, StateValuesStream, StatusBadge, StepTimings, StepTreeMaster, Stepper, StreamBrokerSelector, StreamDebugOverlay, StreamPanel, Switch, SwitchHeroCard, SwitchInlineControl, SwitchPanel, SystemProvider, TABLE_SCROLL_MAX_H, TEXT_FIELD_LABEL, TEXT_HINT, TEXT_METRIC, TEXT_SECTION_LABEL, TEXT_VALUE, TIMEZONES, Tabs, TabsContent, TabsList, TabsTrigger, TapToggle, ThemeProvider, ThermostatHeroCard, ThermostatInlineControl, TimezoneSelector, Tooltip, TooltipContent, TooltipTrigger, VacuumHeroCard, VacuumInlineControl, ValueReadout, ValveHeroCard, ValveInlineControl, VersionBadge, VodPlaybackProvider, WaterHeaterHeroCard, WaterHeaterInlineControl, WeatherHeroCard, WeatherInlineControl, WidgetMetricCard, WidgetPanel, WidgetRegistryProvider, WidgetSlot, ZoneEditingProvider, ImageOff as a, agentColumnKey, allDeviceTypeFilterOptions, applyFleetQueryDefaults, ariaSortForColumn, badgeFor, buildStepTreeFromSchema, Camera as c, childEntityId, childListName, cn, columnsForContext, containerChildToRef, countableDevices, coverHighlight, coverageLine, createSharedContext, createTheme, cursorFractionFor, darkColors, defaultTheme, deriveDeviceKind, deriveWebrtcDebugStats, describeDeviceSelector, deviceMatchesFilter, deviceOptionLabel, deviceRoleMeta, deviceRoleMetaOf, deviceTypeLabel, deviceTypeMeta, deviceTypeMetaOf, devicesToOptions, ensureMfHostInit, eventKindLabel, filterDeviceOptions, findTimezone, formatControlDateTime, formatLastSeen, formatNumeric, freezeVerdict, fuzzyMatch, getClassColor, getPhaseVisual, groupAgentColumns, groupChildrenByLayout, hardwareLabel, hubOrigin, hubPath, hubUrl, hubWsUrl, humanDuration, humidifierTint, Mic as i, initialScrubState, isAbsentProvider, isFieldVisible, createLucideIcon as l, lawnMowerActivityMeta, lightColors, loadRemoteBundle, makeScrubBridge, metadataEntries, metadataString, mirror, mountAddonPage, mountPath, Square as n, nextReconnectAction, nextSort, nextSortDirection, nextTableSort, normalizeForSearch, Grid2x2 as o, openTopLayer, overrideEntityIdFromLink, parseRecordedServerMessage, providerIcons, RotateCcw as r, resetHubMountForTests, resolveContainerPrimary, resolveControlAlign, resolveDeviceControl, resolveDisplayIcon, resolveEffectiveDefaultModel, resolveEffectiveStepModel, resolveEventKindIcon, resolvePrimaryChild, resolveSensorDisplay, resolveStepDefaultModel, resolveTableLayout, roiTooLarge, routerBasename, EyeOff as s, scrubReducer, selectedDeviceOptions, serializeRecordedCommand, shouldCommit, shouldEmit, shouldUseSingleNode, sortRows, sortRowsByColumn, statusIcons, statusLine, stepHasModelForFormat, stepModelOptions, stripParentNamePrefix, Trash2 as t, tabForSelector, tankAlert, themeToCss, toStationaryOverlayBox, trpc, useAccessoriesGetStatus, useAccessoriesSetChildHidden, useAddonPagesListPages, useAddonSettingsGetDeviceSettings, useAddonSettingsGetGlobalSettings, useAddonSettingsGetIntegrationSettings, useAddonSettingsUpdateDeviceSettings, useAddonSettingsUpdateGlobalSettings, useAddonWidgetsListWidgets, useAddonsApplyAutoUpdateToAll, useAddonsCancelJob, useAddonsCustom, useAddonsForceRefresh, useAddonsGetAddonAutoUpdate, useAddonsGetAutoUpdateSettings, useAddonsGetJob, useAddonsGetLastRestart, useAddonsGetLogs, useAddonsGetVersions, useAddonsInstallFromWorkspace, useAddonsInstallPackage, useAddonsIsWorkspaceAvailable, useAddonsList, useAddonsListCapabilityProviders, useAddonsListFrameworkPackages, useAddonsListJobs, useAddonsListPackages, useAddonsListUpdates, useAddonsListWorkspacePackages, useAddonsOnAddonLogs, useAddonsReloadPackages, useAddonsRestartAddon, useAddonsRestartServer, useAddonsRetryLoad, useAddonsRollbackPackage, useAddonsSearchAvailable, useAddonsSetAddonAutoUpdate, useAddonsSetAutoUpdateSettings, useAddonsSetCapabilityProviderEnabled, useAddonsStartJob, useAddonsUninstallPackage, useAddonsUpdatePackage, useAirQualitySensorGetStatus, useAlarmPanelArm, useAlarmPanelDisarm, useAlarmPanelGetStatus, useAlarmPanelTrigger, useAlert, useAlertsDismiss, useAlertsEmit, useAlertsGetUnreadCount, useAlertsList, useAlertsMarkAllRead, useAlertsMarkRead, useAlertsUpdate, useAllWidgets, useAmbientLightSensorGetStatus, useAudioAnalysisApplyDeviceSettingsPatch, useAudioAnalysisGetDeviceLiveContribution, useAudioAnalysisGetDeviceSettingsContribution, useAudioAnalysisResolveDeviceSettings, useAudioAnalyzerAnalyseChunk, useAudioAnalyzerAttachDevice, useAudioAnalyzerClassify, useAudioAnalyzerDetachDevice, useAudioAnalyzerDispose, useAudioAnalyzerIsReady, useAudioAnalyzerReprobeAudioEngine, useAudioCodecCanHandle, useAudioCodecCloseSession, useAudioCodecCreateDecodeSession, useAudioCodecCreateEncodeSession, useAudioCodecFlushEncode, useAudioCodecListActiveSessions, useAudioCodecListSupportedCodecs, useAudioCodecPullEncoded, useAudioCodecPullPcm, useAudioCodecPushEncodedFrame, useAudioCodecPushPcm, useAudioMetricsGetCurrentSnapshot, useAudioMetricsGetHistory, useAutomationControlDisable, useAutomationControlEnable, useAutomationControlGetStatus, useAutomationControlTrigger, useBackupCancel, useBackupDelete, useBackupDeleteSchedule, useBackupGetEntries, useBackupList, useBackupListArchives, useBackupListDestinations, useBackupListLocations, useBackupListRuns, useBackupListSchedules, useBackupPreviewSchedule, useBackupRestore, useBackupTrigger, useBackupUpsertDestinationPolicy, useBackupUpsertSchedule, useBatteryGetStatus, useBatteryWakeForStream, useBinaryGetStatus, useBrightnessGetStatus, useBrightnessSetBrightness, useBrokerAdd, useBrokerGet, useBrokerGetBrokerConfig, useBrokerGetSettings, useBrokerGetSettingsSchema, useBrokerGetState, useBrokerGetStatus, useBrokerList, useBrokerListProviders, useBrokerPublish, useBrokerRemove, useBrokerSetSettings, useBrokerSubscribe, useBrokerTestConnection, useBrokerTestSettings, useBrokerUnsubscribe, useButtonPress, useCameraCredentialsGetCredentials, useCameraCredentialsGetStatus, useCameraGridLayoutApplyDeviceSettingsPatch, useCameraGridLayoutGetDeviceLiveContribution, useCameraGridLayoutGetDeviceSettingsContribution, useCameraGridLayoutGetLayout, useCameraGridLayoutSaveLayout, useCameraPipelineConfigApplyDeviceSettingsPatch, useCameraPipelineConfigGetDeviceLiveContribution, useCameraPipelineConfigGetDeviceSettingsContribution, useCameraStreamsGetBrokerStreams, useCameraStreamsGetCameraStreams, useCameraStreamsGetProfileRtspEntries, useCameraStreamsGetRtspEntries, useCameraStreamsPickStream, useCarbonMonoxideGetStatus, useClimateControlGetStatus, useClimateControlSetFanMode, useClimateControlSetMode, useClimateControlSetPreset, useClimateControlSetSwingHorizontal, useClimateControlSetSwingVertical, useClimateControlSetTarget, useClimateControlSetTargetHumidity, useClimateControlSetTargetRange, useClusterNodes, useClusterTopology, useColorGetStatus, useColorSetColor, useConfirm, useConnectionTestDescribeTest, useConnectionTestTestSettings, useConnectivityGetStatus, useConsumablesGetStatus, useConsumablesReset, useContactGetStatus, useContainerChildren, useControlGetStatus, useControlSetValue, useCoreBlocksCompile, useCoreBlocksCreate, useCoreBlocksDelete, useCoreBlocksGet, useCoreBlocksGetTypeDefs, useCoreBlocksList, useCoreBlocksRestart, useCoreBlocksSetEnabled, useCoreBlocksUpdate, useCoverClose, useCoverGetStatus, useCoverOpen, useCoverSetPosition, useCoverSetTiltPosition, useCoverStop, useCustomFieldRenderer, useDayNightGetOptions, useDayNightGetStatus, useDayNightSetSettings, useDebouncedString, useDecoderCreateSession, useDecoderDestroySession, useDecoderGetFrame, useDecoderGetInfo, useDecoderGetShmStats, useDecoderGetStats, useDecoderListActiveSessions, useDecoderOpenStream, useDecoderPullFrames, useDecoderPullHandles, useDecoderPushPacket, useDecoderReprobeHwaccel, useDecoderSupportsCodec, useDecoderUpdateConfig, useDetectionPipelineApplyDeviceSettingsPatch, useDetectionPipelineGetDeviceLiveContribution, useDetectionPipelineGetDeviceSettingsContribution, useDevShell, useDevice, useDeviceAdoptionAdopt, useDeviceAdoptionGetCandidate, useDeviceAdoptionGetStatus, useDeviceAdoptionListCandidateFilters, useDeviceAdoptionListCandidates, useDeviceAdoptionRefresh, useDeviceAdoptionRelease, useDeviceAdoptionResync, useDeviceAutotrack, useDeviceBattery, useDeviceCapSlice, useDeviceCapability, useDeviceDetections, useDeviceDiscoveryAdoptDevice, useDeviceDiscoveryGetStatus, useDeviceDiscoveryListDiscovered, useDeviceDiscoveryRefreshDiscovery, useDeviceDiscoveryReleaseDevice, useDeviceExportApplyDeviceSettingsPatch, useDeviceExportExposeDevice, useDeviceExportGetDeviceLiveContribution, useDeviceExportGetDeviceSettingsContribution, useDeviceExportGetStatus, useDeviceExportListExposedDevices, useDeviceExportListSupportedDeviceKinds, useDeviceExportUnexposeDevice, useDeviceId, useDeviceListPageSize, useDeviceManagerAddLocation, useDeviceManagerAdoptDevice, useDeviceManagerAdoptionAdopt, useDeviceManagerAdoptionCancelJob, useDeviceManagerAdoptionListCandidateFilters, useDeviceManagerAdoptionListCandidates, useDeviceManagerAdoptionListJobs, useDeviceManagerAdoptionRefresh, useDeviceManagerAdoptionRelease, useDeviceManagerAdoptionResync, useDeviceManagerAdoptionStartJob, useDeviceManagerAllocateDeviceId, useDeviceManagerApplyDeviceSettingsPatch, useDeviceManagerApplyInitialMeta, useDeviceManagerCreateDevice, useDeviceManagerDisable, useDeviceManagerDiscoverAllProviders, useDeviceManagerDiscoverDevices, useDeviceManagerDiscoverProvider, useDeviceManagerDiscoveryProviders, useDeviceManagerEnable, useDeviceManagerGetAllBindings, useDeviceManagerGetBindings, useDeviceManagerGetBindingsBatch, useDeviceManagerGetChildren, useDeviceManagerGetChildrenBatch, useDeviceManagerGetConfigSchema, useDeviceManagerGetCreationSchema, useDeviceManagerGetDevice, useDeviceManagerGetDeviceAggregate, useDeviceManagerGetDeviceLiveContribution, useDeviceManagerGetDeviceLiveInfoAggregate, useDeviceManagerGetDeviceSettingsAggregate, useDeviceManagerGetDeviceSettingsContribution, useDeviceManagerGetDeviceStatusAggregate, useDeviceManagerGetDeviceStatusAggregateBatch, useDeviceManagerGetLinkedDevices, useDeviceManagerGetLinkedDevicesBatch, useDeviceManagerGetRoleDisplayDefaults, useDeviceManagerGetSettingsSchema, useDeviceManagerGetStreamProfileMap, useDeviceManagerGetStreamSources, useDeviceManagerGetWireableFields, useDeviceManagerListAll, useDeviceManagerListBindableCapsForDeviceType, useDeviceManagerListLocationDescriptions, useDeviceManagerListLocationIcons, useDeviceManagerListLocations, useDeviceManagerListPersistedByAddon, useDeviceManagerListSiteZones, useDeviceManagerListWrappersForCap, useDeviceManagerLoadConfig, useDeviceManagerLoadMeta, useDeviceManagerLoadRuntimeState, useDeviceManagerMigrateDevice, useDeviceManagerPersistConfig, useDeviceManagerProbeStreams, useDeviceManagerProviderCreationType, useDeviceManagerProviderDiscoveryParamsSchema, useDeviceManagerRegisterDevice, useDeviceManagerRemove, useDeviceManagerRemoveByIntegration, useDeviceManagerRemoveDevice, useDeviceManagerRemoveLocation, useDeviceManagerRemoveSiteZone, useDeviceManagerRenameLocation, useDeviceManagerRenameSiteZone, useDeviceManagerRunDeviceAction, useDeviceManagerSetChildLayout, useDeviceManagerSetDisabled, useDeviceManagerSetDisplay, useDeviceManagerSetIntegrationId, useDeviceManagerSetLinkDeviceId, useDeviceManagerSetLocation, useDeviceManagerSetLocationDescription, useDeviceManagerSetLocationIcon, useDeviceManagerSetLocationZone, useDeviceManagerSetMetadata, useDeviceManagerSetName, useDeviceManagerSetPrimaryChildEntityId, useDeviceManagerSetRole, useDeviceManagerSetRoleDisplayDefaults, useDeviceManagerSetSiteZone, useDeviceManagerSetStreamProfileMap, useDeviceManagerSetType, useDeviceManagerSetWrapperActive, useDeviceManagerTestCreationField, useDeviceManagerTestField, useDeviceManagerUpdateConfig, useDeviceManagerUpdateDeviceField, useDeviceManagerUpdateDeviceFieldsBatch, useDeviceNetworkLink, useDeviceOpsGetConfigEntries, useDeviceOpsGetRawState, useDeviceOpsGetSettingsSchema, useDeviceOpsGetStreamSources, useDeviceOpsRemoveDevice, useDeviceOpsRunAction, useDeviceOpsSetConfig, useDeviceProviderAdoptDiscoveredDevice, useDeviceProviderCreateDevice, useDeviceProviderDiscoverDevices, useDeviceProviderGetChildCreationSchema, useDeviceProviderGetDevices, useDeviceProviderGetDiscoveryParamsSchema, useDeviceProviderGetManualCreationType, useDeviceProviderGetStatus, useDeviceProviderReloadDevice, useDeviceProviderStart, useDeviceProviderStop, useDeviceProviderSupportsDiscovery, useDeviceProviderSupportsManualCreation, useDeviceProviderTestCreationField, useDeviceProxy, useDeviceSnapshot, useDeviceSnapshotImage, useDeviceState, useDeviceStateGetAllSnapshots, useDeviceStateGetCapSlice, useDeviceStateGetSnapshot, useDeviceStateSetCapSlice, useDeviceStateSlice, useDeviceStatusGetStatus, useDeviceWebrtc, useDevices, useDoorbellApplyDeviceSettingsPatch, useDoorbellEvents, useDoorbellGetDeviceLiveContribution, useDoorbellGetDeviceSettingsContribution, useDoorbellGetStatus, useEnumSensorGetStatus, useEventEmitterGetStatus, useEventInvalidation, useEventStreamLatest, useEventStreamMap, useEventsGetEventClipUrl, useEventsGetEventThumbnail, useEventsGetEvents, useFaceGalleryAssignFace, useFaceGalleryAssignFaces, useFaceGalleryAuditIdentitySamples, useFaceGalleryCreateIdentity, useFaceGalleryDeleteFace, useFaceGalleryDeleteIdentity, useFaceGalleryGetFaceByTrack, useFaceGalleryGetFaceMedia, useFaceGalleryListIdentities, useFaceGalleryListIdentitySamples, useFaceGalleryListRecentFaces, useFaceGalleryRemoveSample, useFaceGalleryRenameIdentity, useFaceGallerySuggestFaceClusters, useFaceGalleryUnassignFace, useFaceGalleryUnassignFaces, useFanControlGetStatus, useFanControlSetDirection, useFanControlSetOscillating, useFanControlSetPercentage, useFanControlSetPreset, useFeatureProbeGetStatus, useFloodGetStatus, useGasGetStatus, useHumidifierGetStatus, useHumidifierSetMode, useHumidifierSetOn, useHumidifierSetTargetHumidity, useHumiditySensorGetStatus, useImageGetStatus, useImageSettingsGetOptions, useImageSettingsGetStatus, useImageSettingsSetSettings, useIntegrationsCreate, useIntegrationsDelete, useIntegrationsGet, useIntegrationsGetAvailableTypes, useIntegrationsGetByAddonId, useIntegrationsGetSettings, useIntegrationsList, useIntegrationsSetSettings, useIntegrationsTestConnection, useIntegrationsUpdate, useIntercomEndTalkSession, useIntercomGetStatus, useIntercomHandleAnswer, useIntercomPushTalkAudio, useIntercomStartSession, useIntercomStartTalkSession, useIntercomStopSession, useIsMidWidth, useIsMobile, useLawnMowerControlDock, useLawnMowerControlGetStatus, useLawnMowerControlPause, useLawnMowerControlStartMowing, useLiveBuffer, useLiveEvent, useLlmCancel, useLlmDeleteModel, useLlmDeleteProfile, useLlmGenerate, useLlmGenerateVision, useLlmGetDefaults, useLlmGetRuntimeStatus, useLlmGetUsage, useLlmInstallModel, useLlmListModelCatalog, useLlmListModels, useLlmListNodeModels, useLlmListProfileKinds, useLlmListProfiles, useLlmListRuntimeNodes, useLlmResolveModelRef, useLlmSetDefault, useLlmStartRuntime, useLlmStopRuntime, useLlmTestProfile, useLlmUpsertProfile, useLocalNetworkDownloadCa, useLocalNetworkGetAllowedAddresses, useLocalNetworkGetConnectionEndpoints, useLocalNetworkGetNotificationEndpoint, useLocalNetworkGetPreferred, useLocalNetworkGetTlsStatus, useLocalNetworkGetViewerEndpoints, useLocalNetworkList, useLocalNetworkRegenerateCertificate, useLocalNetworkResetAllowlistToBestMatch, useLocalNetworkRevertToGeneratedCertificate, useLocalNetworkSetAllowedAddresses, useLocalNetworkSetNotificationEndpoint, useLocalNetworkSetViewerEndpoints, useLocalNetworkUploadCertificate, useLockControlGetStatus, useLockControlLock, useLockControlOpen, useLockControlUnlock, useMediaPlayerGetStatus, useMediaPlayerNext, useMediaPlayerPause, useMediaPlayerPlay, useMediaPlayerPlayMedia, useMediaPlayerPrevious, useMediaPlayerSeek, useMediaPlayerSelectSource, useMediaPlayerSetMute, useMediaPlayerSetRepeat, useMediaPlayerSetShuffle, useMediaPlayerSetVolume, useMediaPlayerStop, useMeshNetworkGetStatus, useMeshNetworkJoin, useMeshNetworkLeave, useMeshNetworkListPeers, useMeshNetworkLogout, useMeshNetworkStartLogin, useMeshNetworkTestConnection, useMetricsProviderCollectSnapshot, useMetricsProviderGetAddonStats, useMetricsProviderGetCached, useMetricsProviderGetCpuTemperature, useMetricsProviderGetCurrent, useMetricsProviderGetDiskSpace, useMetricsProviderGetGpuInfo, useMetricsProviderGetLoadSeries, useMetricsProviderGetProcessStats, useMetricsProviderListAddonInstances, useMetricsProviderListNodeProcesses, useMotionDetectionAnalyze, useMotionDetectionApplyDeviceSettingsPatch, useMotionDetectionGetDeviceLiveContribution, useMotionDetectionGetDeviceSettingsContribution, useMotionDetectionRemoveCamera, useMotionDetectionReset, useMotionGetStatus, useMotionIsDetected, useMotionTriggerGetStatus, useMotionTriggerSetMotionTrigger, useMotionZonesGetOptions, useMotionZonesGetStatus, useMotionZonesSetZone, useMqttBrokerAddBroker, useMqttBrokerGetBrokerConfig, useMqttBrokerGetStatus, useMqttBrokerListBrokers, useMqttBrokerRemoveBroker, useMqttBrokerStartEmbeddedBroker, useMqttBrokerStopEmbeddedBroker, useMqttBrokerTestConnection, useNativeObjectDetectionGetOptions, useNativeObjectDetectionGetStatus, useNativeObjectDetectionSetEnabled, useNativeObjectDetectionSetSettings, useNavigation, useNavigationGetFeatures, useNavigationGetStatus, useNavigationGoToPoint, useNavigationListActions, useNavigationMove, useNavigationPlaySound, useNavigationRunAction, useNavigationSetLightLevel, useNavigationSetLightMode, useNavigationSetLightOn, useNavigationStop, useNetworkAccessGetEndpoint, useNetworkAccessGetStatus, useNetworkAccessListEndpoints, useNetworkAccessStart, useNetworkAccessStop, useNetworkLinkGetStatus, useNetworkQualityGetAllStats, useNetworkQualityGetDeviceStats, useNetworkQualityReportClientStats, useNodesClusterAddonStatus, useNodesDeployAddon, useNodesExecuteQuery, useNodesGetCapUsageGraph, useNodesGetNodeAddons, useNodesRenameNode, useNodesRestartAddon, useNodesRestartNode, useNodesRestartProcess, useNodesSetProcessLogLevel, useNodesShutdownNode, useNodesTopology, useNodesUndeployAddon, useNotificationOutputDeleteTarget, useNotificationOutputDiscoverTargets, useNotificationOutputListTargetKinds, useNotificationOutputListTargets, useNotificationOutputSend, useNotificationOutputSetTargetEnabled, useNotificationOutputTestTarget, useNotificationOutputUpsertTarget, useNotificationRulesCancelSnooze, useNotificationRulesCreateRule, useNotificationRulesCreateSnooze, useNotificationRulesDeleteRule, useNotificationRulesGetAlarmConfig, useNotificationRulesGetClipRetentionAsks, useNotificationRulesGetConditionCatalog, useNotificationRulesGetHistory, useNotificationRulesGetRule, useNotificationRulesListDeviceMutes, useNotificationRulesListRules, useNotificationRulesListSnoozes, useNotificationRulesResolveArtifactUrl, useNotificationRulesSetAlarmConfig, useNotificationRulesSetDeviceMuted, useNotificationRulesSetRuleEnabled, useNotificationRulesTestRule, useNotificationRulesUpdateRule, useNotifierCancel, useNotifierGetStatus, useNotifierSend, useNumericSensorGetStatus, useOptimisticSlice, useOptionalSystem, useOptionalWidgetRegistry, useOsdGetStatus, useOsdManagerClearSlotBinding, useOsdManagerCopyDeviceConfiguration, useOsdManagerGetConditionSupport, useOsdManagerGetDeviceOsd, useOsdManagerGetSourceCatalog, useOsdManagerPreviewSlot, useOsdManagerRenderDevice, useOsdManagerSetSlotBinding, useOsdSetOverlay, usePTZ, usePetFeederCallPet, usePetFeederCancelFeed, usePetFeederFeed, usePetFeederGetStatus, usePetFeederMarkFoodReplenished, usePetFeederPlaySound, usePetFeederResetDesiccant, usePetFeederSetChildLock, usePetFeederSetFeedSound, usePetFeederSetIndicatorLight, usePetFeederSetVolume, usePipelineAnalyticsApplyDeviceSettingsPatch, usePipelineAnalyticsCancelRelocateMedia, usePipelineAnalyticsCancelStorageMigrationMove, usePipelineAnalyticsClearTracks, usePipelineAnalyticsCompleteRetrainTrack, usePipelineAnalyticsCountRelocatableMedia, usePipelineAnalyticsCountUnstampedEventMedia, usePipelineAnalyticsDeleteDeviceEvents, usePipelineAnalyticsDeleteTracks, usePipelineAnalyticsDeselectRetrainFrame, usePipelineAnalyticsGetActiveTracks, usePipelineAnalyticsGetAudioEvents, usePipelineAnalyticsGetDeviceLiveContribution, usePipelineAnalyticsGetDeviceSettingsContribution, usePipelineAnalyticsGetEventDensity, usePipelineAnalyticsGetEventDensityBatch, usePipelineAnalyticsGetEventMedia, usePipelineAnalyticsGetEventMediaFootprintByKind, usePipelineAnalyticsGetEventStoreFootprint, usePipelineAnalyticsGetKeyEvents, usePipelineAnalyticsGetKeyEventsBatch, usePipelineAnalyticsGetMediaReclaimStatus, usePipelineAnalyticsGetMotionEvents, usePipelineAnalyticsGetObjectEmbeddingRebuildStatus, usePipelineAnalyticsGetObjectEvents, usePipelineAnalyticsGetRetrainExportUrl, usePipelineAnalyticsGetRetrainFrameImage, usePipelineAnalyticsGetSensorEvents, usePipelineAnalyticsGetStorageMigrationMoveStatus, usePipelineAnalyticsGetSummary, usePipelineAnalyticsGetTrack, usePipelineAnalyticsGetTrackMedia, usePipelineAnalyticsGetTrainingExportSummary, usePipelineAnalyticsGetTrainingExportUrl, usePipelineAnalyticsListArchivedDebugNotes, usePipelineAnalyticsListBirthDecisions, usePipelineAnalyticsListEventKinds, usePipelineAnalyticsListEventKindsBatch, usePipelineAnalyticsListEventMedia, usePipelineAnalyticsListOpsLog, usePipelineAnalyticsListRecentSummaries, usePipelineAnalyticsListRecentTracks, usePipelineAnalyticsListRelocateMediaJobs, usePipelineAnalyticsListRetrainAnnotations, usePipelineAnalyticsListRetrainFrames, usePipelineAnalyticsListRetrainStaging, usePipelineAnalyticsListSummaries, usePipelineAnalyticsListTrackMedia, usePipelineAnalyticsListTracks, usePipelineAnalyticsPauseForStorageMigration, usePipelineAnalyticsProposeRetrainAnnotations, usePipelineAnalyticsPruneEvents, usePipelineAnalyticsPruneEventsBefore, usePipelineAnalyticsPruneTracksBefore, usePipelineAnalyticsReadLongTermSeries, usePipelineAnalyticsRebuildObjectEmbeddings, usePipelineAnalyticsReclaimDebugMedia, usePipelineAnalyticsReconcileFromDisk, usePipelineAnalyticsRefreshStorageLocationsForMigration, usePipelineAnalyticsRelocateMedia, usePipelineAnalyticsRestageRetrainTrack, usePipelineAnalyticsResumeForStorageMigration, usePipelineAnalyticsRunReplayFrameProcessor, usePipelineAnalyticsSaveRetrainAnnotations, usePipelineAnalyticsSearchObjectEvents, usePipelineAnalyticsSelectRetrainFrames, usePipelineAnalyticsSetTrackFlags, usePipelineAnalyticsStartStorageMigrationMove, usePipelineAnalyticsWipeAllAnalytics, usePipelineAnalyticsWipeObjectEmbeddings, usePipelineExecutorCacheFrameInPool, usePipelineExecutorClearDeviceOverrides, usePipelineExecutorDeleteModel, usePipelineExecutorDeleteTemplate, usePipelineExecutorDownloadModel, usePipelineExecutorGetAddonModels, usePipelineExecutorGetAudioCapabilities, usePipelineExecutorGetAvailableEngines, usePipelineExecutorGetCapabilities, usePipelineExecutorGetDefaultSteps, usePipelineExecutorGetDetectionConfigSchema, usePipelineExecutorGetEffectiveTuning, usePipelineExecutorGetEngineProvisioning, usePipelineExecutorGetGlobalPipelineConfig, usePipelineExecutorGetGlobalSteps, usePipelineExecutorGetInferenceDeviceHealth, usePipelineExecutorGetOrchestratorConfigSchema, usePipelineExecutorGetReferenceAudio, usePipelineExecutorGetReferenceAudioFiles, usePipelineExecutorGetReferenceImage, usePipelineExecutorGetSchema, usePipelineExecutorGetSelectedEngine, usePipelineExecutorGetVideoPipelineSteps, usePipelineExecutorInferCached, usePipelineExecutorKillEngine, usePipelineExecutorListLoadedEngines, usePipelineExecutorListReferenceImages, usePipelineExecutorListTemplates, usePipelineExecutorRearmInferenceDevice, usePipelineExecutorRunAudioTest, usePipelineExecutorRunPipeline, usePipelineExecutorRunPipelineBatch, usePipelineExecutorSaveTemplate, usePipelineExecutorSetVideoPipelineSteps, usePipelineExecutorSpinEngine, usePipelineExecutorUncacheFrame, usePipelineExecutorUpdateTemplate, usePipelineExecutorValidatePipeline, usePipelineOrchestratorApplyDeviceSettingsPatch, usePipelineOrchestratorAssignAudio, usePipelineOrchestratorAssignPipeline, usePipelineOrchestratorDeleteTemplate, usePipelineOrchestratorGetAgentLoad, usePipelineOrchestratorGetAgentSettings, usePipelineOrchestratorGetAudioAssignment, usePipelineOrchestratorGetAudioAssignments, usePipelineOrchestratorGetAudioNodeLoad, usePipelineOrchestratorGetCameraMetrics, usePipelineOrchestratorGetCameraSettings, usePipelineOrchestratorGetCameraStatus, usePipelineOrchestratorGetCameraStatuses, usePipelineOrchestratorGetCameraStepOverrides, usePipelineOrchestratorGetCameraSwitches, usePipelineOrchestratorGetCapabilityBindings, usePipelineOrchestratorGetDeviceLiveContribution, usePipelineOrchestratorGetDeviceSettingsContribution, usePipelineOrchestratorGetGlobalMetrics, usePipelineOrchestratorGetIngestOwner, usePipelineOrchestratorGetNodeInferenceDevices, usePipelineOrchestratorGetPipelineAssignment, usePipelineOrchestratorGetPipelineAssignments, usePipelineOrchestratorGetPipelineDevicePin, usePipelineOrchestratorGetReconcileFromDiskStatus, usePipelineOrchestratorListAgentSettings, usePipelineOrchestratorListTemplates, usePipelineOrchestratorPauseForStorageMigration, usePipelineOrchestratorRebalance, usePipelineOrchestratorReconcileFromDisk, usePipelineOrchestratorRemoveAgentSettings, usePipelineOrchestratorResetNodePipelineDefaults, usePipelineOrchestratorResolvePipeline, usePipelineOrchestratorResumeForStorageMigration, usePipelineOrchestratorSaveTemplate, usePipelineOrchestratorSetAgentCapabilities, usePipelineOrchestratorSetAgentDecodeLimits, usePipelineOrchestratorSetAgentDetectWeight, usePipelineOrchestratorSetAgentInferenceDevices, usePipelineOrchestratorSetAgentMaxCameras, usePipelineOrchestratorSetAgentReachableHost, usePipelineOrchestratorSetCameraPipelineForAgent, usePipelineOrchestratorSetCameraStepOverride, usePipelineOrchestratorSetCameraStepToggle, usePipelineOrchestratorSetCameraSwitch, usePipelineOrchestratorSetCapabilityBinding, usePipelineOrchestratorSetPipelineDevicePin, usePipelineOrchestratorUnassignAudio, usePipelineOrchestratorUnassignPipeline, usePipelineOrchestratorUpdateTemplate, usePipelineRunnerAttachCamera, usePipelineRunnerDetachCamera, usePipelineRunnerGetAllCameraMetrics, usePipelineRunnerGetCameraMetrics, usePipelineRunnerGetLocalCameras, usePipelineRunnerGetLocalLoad, usePipelineRunnerGetLocalMetrics, usePipelineRunnerGetNativeCrop, usePipelineRunnerGetParkedTrackFrame, usePipelineRunnerParkTrackFrame, usePipelineRunnerReleaseParkedTrackFrames, usePipelineRunnerReportMotion, usePipelineRunnerRequestOccupancyBurst, usePipelineRunnerRunDetailSubtree, usePipelineRunnerRunStatelessStep, usePlateGalleryAssignPlate, usePlateGalleryAssignPlates, usePlateGalleryCorrectPlateText, usePlateGalleryCreateVehicle, usePlateGalleryDeletePlate, usePlateGalleryDeleteVehicle, usePlateGalleryGetPlateByTrack, usePlateGalleryGetPlateMedia, usePlateGalleryListPlates, usePlateGalleryListVehicleSamples, usePlateGalleryListVehicles, usePlateGalleryRemoveVehicleSample, usePlateGalleryRenameVehicle, usePlateGallerySearchPlates, usePlateGallerySuggestPlateClusters, usePlateGalleryUnassignPlate, usePlateGalleryUnassignPlates, usePlayerOverlayLayer, usePlayerOverlayLayers, usePlayerToolbarButton, usePlayerToolbarButtons, usePowerMeterGetStatus, usePresenceGetStatus, usePressureSensorGetStatus, usePrivacyMaskGetOptions, usePrivacyMaskGetStatus, usePrivacyMaskSetAudioEnabled, usePrivacyMaskSetMask, usePrompt, usePtzAutotrackGetSettings, usePtzAutotrackGetStatus, usePtzAutotrackSetEnabled, usePtzAutotrackSetSettings, usePtzCaptureHomeHere, usePtzContinuousMove, usePtzDeletePreset, usePtzGetHomePreset, usePtzGetHomeReturn, usePtzGetOptions, usePtzGetPosition, usePtzGetPresets, usePtzGetStatus, usePtzGoHome, usePtzGoToPreset, usePtzMove, usePtzSavePreset, usePtzSetAutofocus, usePtzSetHomePreset, usePtzSetHomeReturn, usePtzStop, useRebootReboot, useRecordedPlayback, useRecordingApplyDeviceSettingsPatch, useRecordingCancelRelocateJob, useRecordingCancelStorageMigrationMove, useRecordingDeleteFootprint, useRecordingExportCancelExport, useRecordingExportCreateExport, useRecordingExportDeleteExport, useRecordingExportGetDownloadUrl, useRecordingExportGetExport, useRecordingExportListExports, useRecordingExportReadExportBytes, useRecordingGetAvailability, useRecordingGetAvailabilityBatch, useRecordingGetDaysWithRecordings, useRecordingGetDaysWithRecordingsBatch, useRecordingGetDeviceConfig, useRecordingGetDeviceLiveContribution, useRecordingGetDeviceSettingsContribution, useRecordingGetPlacement, useRecordingGetPlaybackManifest, useRecordingGetRelocateResidue, useRecordingGetStatus, useRecordingGetStorageMigrationMoveStatus, useRecordingGetStorageUsage, useRecordingListOpsLog, useRecordingListRelocateJobs, useRecordingLocateSegment, useRecordingPauseForStorageMigration, useRecordingPlanStorageRebalance, useRecordingPruneFootage, useRecordingReadGopBytes, useRecordingReadSegmentBytes, useRecordingReadWindowBytes, useRecordingReconcileLedgerAgainstDisk, useRecordingRefreshStorageLocationsForMigration, useRecordingRelocateFootage, useRecordingRenderClip, useRecordingRenderGif, useRecordingRescanStorage, useRecordingResumeForStorageMigration, useRecordingSetDeviceConfig, useRecordingSetDevicePlacement, useRecordingSignalGetStatus, useRecordingStartStorageMigrationMove, useRecordingStartStorageRebalance, useRemoteComponent, useSceneMonitorCaptureReference, useSceneMonitorCreateScene, useSceneMonitorDeleteReference, useSceneMonitorDeleteScene, useSceneMonitorGetStatus, useSceneMonitorListScenes, useSceneMonitorListScenesBatch, useSceneMonitorRecheckNow, useSceneMonitorResetScene, useSceneMonitorUpdateScene, useScriptRunnerGetStatus, useScriptRunnerRun, useScriptRunnerStop, useScrubController, useServerManagementApplyServerUpdate, useServerManagementCheckServerUpdate, useServerManagementGetServerPackageStatus, useServerManagementRestartServer, useServerManagementRollbackServerUpdate, useSettingsStoreAggregate, useSettingsStoreCount, useSettingsStoreDeclareCollection, useSettingsStoreDelete, useSettingsStoreDeleteWhere, useSettingsStoreGet, useSettingsStoreHistogram, useSettingsStoreInsert, useSettingsStoreInsertMany, useSettingsStoreIsEmpty, useSettingsStoreQuery, useSettingsStoreSet, useSettingsStoreUpdate, useSettingsStoreUpdateWhere, useSettingsStoreUpsert, useSmokeGetStatus, useSnapshotApplyDeviceSettingsPatch, useSnapshotGetDebugState, useSnapshotGetDeviceLiveContribution, useSnapshotGetDeviceSettingsContribution, useSnapshotGetSnapshot, useSnapshotGetSnapshotLinks, useSnapshotGetSnapshotOverview, useSnapshotGetStatus, useSnapshotInvalidateCache, useStationaryOverlay, useStorageAbortUpload, useStorageBeginDownload, useStorageBeginUpload, useStorageDelete, useStorageDeleteLocation, useStorageEndDownload, useStorageExists, useStorageFinalizeUpload, useStorageGetAvailableSpace, useStorageList, useStorageListDrainProgress, useStorageListLocationDeclarations, useStorageListLocations, useStorageListProviders, useStorageMigrationCancel, useStorageMigrationCleanupCancel, useStorageMigrationCleanupStart, useStorageMigrationCleanupStatus, useStorageMigrationDrain, useStorageMigrationHistory, useStorageMigrationMovers, useStorageMigrationPlan, useStorageMigrationResidue, useStorageMigrationStart, useStorageMigrationStatus, useStorageRead, useStorageReadChunk, useStorageResolve, useStorageTestConfig, useStorageTestLocation, useStorageUpsertLocation, useStorageWrite, useStorageWriteChunk, useStreamBrokerAcquireEgressTranscode, useStreamBrokerApplyDeviceSettingsPatch, useStreamBrokerAssignProfile, useStreamBrokerFetchEventMedia, useStreamBrokerForgetDeviceHardware, useStreamBrokerGetAllRtspEntries, useStreamBrokerGetBrokerStats, useStreamBrokerGetDeviceAudioMute, useStreamBrokerGetDeviceLiveContribution, useStreamBrokerGetDeviceSettingsContribution, useStreamBrokerGetPreBufferInfo, useStreamBrokerGetRtspEntry, useStreamBrokerGetRtspPort, useStreamBrokerGetStreamUrl, useStreamBrokerGetStreamWithCodec, useStreamBrokerIsRtspEnabled, useStreamBrokerKillClient, useStreamBrokerListAllCameraStreams, useStreamBrokerListAllProfileSlots, useStreamBrokerListClients, useStreamBrokerProbeStream, useStreamBrokerProduceEventMedia, useStreamBrokerPublishCameraStream, useStreamBrokerPullAudioChunks, useStreamBrokerPullFrameHandles, useStreamBrokerRegenerateRtspToken, useStreamBrokerReleaseEgressTranscode, useStreamBrokerReleaseStreamWithCodec, useStreamBrokerRenderPreBufferClip, useStreamBrokerReportStreamSignal, useStreamBrokerRestartProfile, useStreamBrokerRetractCameraStream, useStreamBrokerSetDeviceAudioMute, useStreamBrokerSetPreBufferDuration, useStreamBrokerSetRtspEnabled, useStreamBrokerSubscribeAudioChunks, useStreamBrokerSubscribeFrames, useStreamBrokerUnassignProfile, useStreamBrokerUnsubscribeAudioChunks, useStreamBrokerUnsubscribeFrames, useStreamCatalogGetCatalog, useStreamParamsGetConfigSchema, useStreamParamsGetOptions, useStreamParamsGetStatus, useStreamParamsSetProfile, useStreamSignalsListSignals, useSwitchGetStatus, useSwitchSetState, useSystem, useSystemDetectSiteLocation, useSystemFeatureFlags, useSystemForceRetentionCleanup, useSystemGetFailureContributions, useSystemGetLoadContributions, useSystemGetLoggingSettings, useSystemGetRequestCensus, useSystemGetRetentionConfig, useSystemGetSiteLocation, useSystemHealth, useSystemInfo, useSystemMutation, useSystemNetworkAddresses, useSystemQuery, useSystemSetLoggingSettings, useSystemSetRetentionConfig, useSystemSetSiteLocation, useTamperGetStatus, useTemperatureSensorGetStatus, useTerminalSessionAdoptLegacyMonitor, useTerminalSessionClose, useTerminalSessionCreateInstance, useTerminalSessionDeleteInstance, useTerminalSessionListInstances, useTerminalSessionListLegacyCameras, useTerminalSessionListProfiles, useTerminalSessionListSessions, useTerminalSessionOpenSession, useTerminalSessionPullOutput, useTerminalSessionResize, useTerminalSessionSetInstanceEnabled, useTerminalSessionUpdateInstance, useTerminalSessionWriteInput, useThemeMode, useToastOnToast, useTurnProviderGetTurnServers, useUpdateGetStatus, useUpdateInstallUpdate, useUserManagementConfirmTotp, useUserManagementCreateApiKey, useUserManagementCreateScopedToken, useUserManagementCreateUser, useUserManagementDeleteUser, useUserManagementDisableTotp, useUserManagementGetTotpStatus, useUserManagementListApiKeys, useUserManagementListOauthSessions, useUserManagementListScopedTokens, useUserManagementListUsers, useUserManagementOauthExchangeCode, useUserManagementOauthIssueCode, useUserManagementOauthRefresh, useUserManagementOauthVerifyAccessToken, useUserManagementResetPassword, useUserManagementRevokeApiKey, useUserManagementRevokeOauthSession, useUserManagementRevokeScopedToken, useUserManagementSetUserScopes, useUserManagementSetupTotp, useUserManagementUpdateUser, useUserManagementValidateApiKey, useUserManagementValidateCredentials, useUserManagementValidateScopedToken, useUserManagementVerifyTotp, useVacuumControlGetStatus, useVacuumControlLocate, useVacuumControlPause, useVacuumControlReturnToBase, useVacuumControlSetFanSpeed, useVacuumControlStart, useVacuumControlStop, useValveClose, useValveGetStatus, useValveOpen, useValveSetPosition, useValveStop, useVibrationGetStatus, useVideoclipsGetClipPlayback, useVideoclipsListClips, useVodPlayback, useWaterHeaterGetStatus, useWaterHeaterSetAway, useWaterHeaterSetOperationMode, useWaterHeaterSetTargetTemp, useWeatherGetStatus, useWebrtcSessionAddIceCandidate, useWebrtcSessionCloseSession, useWebrtcSessionCreateSession, useWebrtcSessionGetIceCandidates, useWebrtcSessionGetSessionDebug, useWebrtcSessionGetSessionState, useWebrtcSessionHandleAnswer, useWebrtcSessionHandleOffer, useWebrtcSessionHasAdaptiveBitrate, useWebrtcSessionListStreams, useWidget, useWidgetMetadata, useWidgetRegistry, useZoneAnalyticsGetCameraHistory, useZoneAnalyticsGetCurrentSnapshot, useZoneAnalyticsGetCurrentSnapshotBatch, useZoneAnalyticsGetUnzonedHistory, useZoneAnalyticsGetZoneHistory, useZoneEditing, useZoneRulesListRules, useZoneRulesSetRules, useZonesAddZone, useZonesListZones, useZonesRemoveZone, useZonesUpdateZone, vacuumStateMeta, validateScopes, valveStateMeta, variantLabel, waterHeaterPhase, waterHeaterTint, weatherConditionMeta, weatherTint };
|
|
53373
|
+
export { AddonGlobalSettingsForm, AgentStepEditor, AlarmHeroCard, AlarmInlineControl as AlarmPanelInlineControl, AppShell, ArcKnob, AudioClassificationList, AudioLevelWaveform, AudioWaveform, AutomationHeroCard, AutomationInlineControl, AutotrackSection, BTN_COMPACT, BTN_COMPACT_DANGER, BTN_COMPACT_PRIMARY, BTN_COMPACT_WARNING, Badge, BatteryBadge, BottomSheet, Breadcrumb, BrightnessPanel, Button, ButtonControl, ButtonHeroCard, CARD_MODE_MIN_COLUMNS, CENTER, CHIP_ACTIVE, CHIP_BASE, CHIP_INACTIVE, CLASS_COLORS, COLUMN_BREAKPOINT_CLASS, COLUMN_PRIORITY, COMMIT_DEDUPE_TOLERANCE_MS, COMMIT_DEDUPE_WINDOW_MS, CONTROL_CAP_NAMES, CONTROL_FILLS, CameraStreamPlayer, Card, Checkbox, ChildSectionAccordion, ClimatePanel, CodeBlock, CollapsibleCard, ConfigFormBuilder, FormField as ConfigFormField, ConfigSchemaField, ConfirmActionButton, ConfirmDialogProvider, ConsumablesPanel, ContainerChildrenProvider, ContainerPrimaryHero, ControlColumn, ControlHeroCard, ControlInlineControl, ControlPanel, CopyButton, CoverHeroCard, CoverInlineControl, CoverPanel, CustomFieldRenderersProvider, DEFAULT_COLOR, DEVICE_COLUMNS, DEVICE_LIST_PAGE_SIZE_KEY, DEVICE_LIST_PAGE_SIZE_OPTIONS, DEVICE_ROLE_META, DEVICE_TYPE_CONTROL, DEVICE_TYPE_META, DISPLAY_ICON_REGISTRY, DataTable, DetectionCanvas, DetectionOverlay, DetectionResultTree, DevShell, DeviceActivityPanel, DeviceBatchToolbar, DeviceCard, DeviceContextProvider, DeviceExportPanel, DeviceGrid, DeviceItem, DeviceList, DeviceMultiSelectField, DeviceSelectField, DeviceSelectorPicker, DeviceStepMatrix, Dialog, DialogBody, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, DiscoveryPanel, DoorbellRecentPanel, Dropdown, DropdownContent, DropdownItem, DropdownTrigger, DummyHeroCard, DummyInline, EVENT_KIND_ICONS, EmptyState, ErrorBox, EventKindGlyph, EventStream, FILL, FLEET_LIST_GC_TIME_MS, FLEET_LIST_QUERY_KEY, FLEET_LIST_STALE_TIME_MS, FanHeroCard, FanInlineControl, FanPanel, FilterBar, FloatingEventStream, FloatingLogStream, FloatingPanel, FormField$1 as FormField, GRID_GAP, GRID_PAIRED, GRID_QUICK_STATS, GripTrack, GroupedModelSelector, HOST_WIDGETS, HlsVideo, HoverZoomImage, HumidifierHeroCard, HumidifierInlineControl, INPUT_COMPACT, IconAction, IconButton, ImageHeroCard, ImageInlineControl, ImageSelector, InferenceConfigSelector, Input, KebabMenu, KeyValueList, LIST_ROW, Label, LawnMowerHeroCard, LawnMowerInlineControl, LightHeroCard, LightInlineControl, LockHeroCard, LockInlineControl, LockPanel, LogStream, LoginForm, MODE_COLOR, MaskShapeCanvas, MediaPlayerHeroCard, MediaPlayerInlineControl, MediaPlayerPanel, MobileDrawer, ModelPicker, MotionZonesSettings, NAVIGATION_DRIVE_INTERVAL_MS, NavigationPanel, NetworkLinkBadge, NodeMultiSelectField, NodePicker, NodeSelectField, OfflineBadge, PHASE_CONFIG, PRIORITY, PTZOverlay, PageHeader, PhaseIcon, PipelineBuilder, PipelineRuntimeSelector, PipelineStep, PipelineTreeMatrix, PlayerOverlaysProvider, Popover, PopoverContent, PopoverRowAction, PopoverTrigger, PrimaryChildPicker, PrivacyMaskSettings, ProviderBadge, PtzPanel, QrCode, RECONNECT_POLICY, RECORDED_PLAYBACK_MODES, RIGHT, ROI_AREA_WARN_FRACTION, ROLE_DESCRIPTOR, RadialGauge, RecordedPlaybackProvider, RecordingPanel, ResponseLog, SCENE_CAPTURE_VARIANTS, SECTION_BODY, SECTION_CARD, SECTION_HEADER, SETTING_ROW, SETTING_ROW_LABEL, SETTING_ROW_STACK_BREAKPOINT, SETTING_ROW_VALUE, SETTING_ROW_VALUE_TEXT, SNAPSHOT_MEDIA_PATH, SPLIT_PANEL_OUTER, SPLIT_PANEL_SIDE, STACK_GAP, STATE_COLOR, SceneMonitorEditor, ScopePicker, ScriptHeroCard, ScriptInlineControl, ScrollArea, Select, SemanticBadge, SensorHeroCard, SensorInlineControl, SensorValueAtom, Separator, SettingRow, Sidebar, SidebarItem, Skeleton, SlideOverPanel, SlideToggle, SnapshotButton, SortHeaderButton, StatCard, StateValuesStream, StatusBadge, StepTimings, StepTreeMaster, Stepper, StreamBrokerSelector, StreamDebugOverlay, StreamPanel, Switch, SwitchHeroCard, SwitchInlineControl, SwitchPanel, SystemProvider, TABLE_SCROLL_MAX_H, TEXT_FIELD_LABEL, TEXT_HINT, TEXT_METRIC, TEXT_SECTION_LABEL, TEXT_VALUE, TIMEZONES, Tabs, TabsContent, TabsList, TabsTrigger, TapToggle, ThemeProvider, ThermostatHeroCard, ThermostatInlineControl, TimezoneSelector, Tooltip, TooltipContent, TooltipTrigger, VacuumHeroCard, VacuumInlineControl, ValueReadout, ValveHeroCard, ValveInlineControl, VersionBadge, VodPlaybackProvider, WaterHeaterHeroCard, WaterHeaterInlineControl, WeatherHeroCard, WeatherInlineControl, WidgetMetricCard, WidgetPanel, WidgetRegistryProvider, WidgetSlot, ZoneEditingProvider, ImageOff as a, agentColumnKey, allDeviceTypeFilterOptions, applyFleetQueryDefaults, ariaSortForColumn, badgeFor, buildStepTreeFromSchema, Camera as c, childEntityId, childListName, classifyCapability, cn, columnsForContext, containerChildToRef, countableDevices, coverHighlight, coverageLine, createSharedContext, createTheme, cursorFractionFor, darkColors, defaultTheme, deriveDeviceKind, deriveWebrtcDebugStats, describeDeviceSelector, deviceMatchesFilter, deviceOptionLabel, deviceRoleMeta, deviceRoleMetaOf, deviceTypeLabel, deviceTypeMeta, deviceTypeMetaOf, devicesToOptions, ensureMfHostInit, eventKindLabel, filterDeviceOptions, findTimezone, formatControlDateTime, formatLastSeen, formatNumeric, freezeVerdict, fuzzyMatch, getClassColor, getPhaseVisual, groupAgentColumns, groupChildrenByLayout, hardwareLabel, hubOrigin, hubPath, hubUrl, hubWsUrl, humanDuration, humidifierTint, Mic as i, initialScrubState, isAbsentProvider, isFieldVisible, createLucideIcon as l, lawnMowerActivityMeta, lightColors, loadRemoteBundle, makeScrubBridge, metadataEntries, metadataString, mirror, mountAddonPage, mountPath, Square as n, nextReconnectAction, nextSort, nextSortDirection, nextTableSort, normalizeForSearch, Grid2x2 as o, openTopLayer, overrideEntityIdFromLink, parseRecordedServerMessage, providerIcons, RotateCcw as r, resetHubMountForTests, resolveContainerPrimary, resolveControlAlign, resolveDeviceControl, resolveDisplayIcon, resolveEffectiveDefaultModel, resolveEffectiveStepModel, resolveEventKindIcon, resolvePrimaryChild, resolveSensorDisplay, resolveStepDefaultModel, resolveTableLayout, roiTooLarge, routerBasename, EyeOff as s, scrubReducer, selectedDeviceOptions, serializeRecordedCommand, shouldCommit, shouldEmit, shouldUseSingleNode, sortRows, sortRowsByColumn, statusIcons, statusLine, stepHasModelForFormat, stepModelOptions, stripParentNamePrefix, Trash2 as t, tabForSelector, tankAlert, themeToCss, toStationaryOverlayBox, trpc, useAccessoriesGetStatus, useAccessoriesSetChildHidden, useAddonPagesListPages, useAddonSettingsGetDeviceSettings, useAddonSettingsGetGlobalSettings, useAddonSettingsGetIntegrationSettings, useAddonSettingsUpdateDeviceSettings, useAddonSettingsUpdateGlobalSettings, useAddonWidgetsListWidgets, useAddonsApplyAutoUpdateToAll, useAddonsCancelJob, useAddonsCustom, useAddonsForceRefresh, useAddonsGetAddonAutoUpdate, useAddonsGetAutoUpdateSettings, useAddonsGetJob, useAddonsGetLastRestart, useAddonsGetLogs, useAddonsGetVersions, useAddonsInstallFromWorkspace, useAddonsInstallPackage, useAddonsIsWorkspaceAvailable, useAddonsList, useAddonsListCapabilityProviders, useAddonsListFrameworkPackages, useAddonsListJobs, useAddonsListPackages, useAddonsListUpdates, useAddonsListWorkspacePackages, useAddonsOnAddonLogs, useAddonsReloadPackages, useAddonsRestartAddon, useAddonsRestartServer, useAddonsRetryLoad, useAddonsRollbackPackage, useAddonsSearchAvailable, useAddonsSetAddonAutoUpdate, useAddonsSetAutoUpdateSettings, useAddonsSetCapabilityProviderEnabled, useAddonsStartJob, useAddonsUninstallPackage, useAddonsUpdatePackage, useAirQualitySensorGetStatus, useAlarmPanelArm, useAlarmPanelDisarm, useAlarmPanelGetStatus, useAlarmPanelTrigger, useAlert, useAlertsDismiss, useAlertsEmit, useAlertsGetUnreadCount, useAlertsList, useAlertsMarkAllRead, useAlertsMarkRead, useAlertsUpdate, useAllWidgets, useAmbientLightSensorGetStatus, useAudioAnalysisApplyDeviceSettingsPatch, useAudioAnalysisGetDeviceLiveContribution, useAudioAnalysisGetDeviceSettingsContribution, useAudioAnalysisResolveDeviceSettings, useAudioAnalyzerAnalyseChunk, useAudioAnalyzerAttachDevice, useAudioAnalyzerClassify, useAudioAnalyzerDetachDevice, useAudioAnalyzerDispose, useAudioAnalyzerIsReady, useAudioAnalyzerReprobeAudioEngine, useAudioCodecCanHandle, useAudioCodecCloseSession, useAudioCodecCreateDecodeSession, useAudioCodecCreateEncodeSession, useAudioCodecFlushEncode, useAudioCodecListActiveSessions, useAudioCodecListSupportedCodecs, useAudioCodecPullEncoded, useAudioCodecPullPcm, useAudioCodecPushEncodedFrame, useAudioCodecPushPcm, useAudioMetricsGetCurrentSnapshot, useAudioMetricsGetHistory, useAutomationControlDisable, useAutomationControlEnable, useAutomationControlGetStatus, useAutomationControlTrigger, useBackupCancel, useBackupDelete, useBackupDeleteSchedule, useBackupGetEntries, useBackupList, useBackupListArchives, useBackupListDestinations, useBackupListLocations, useBackupListRuns, useBackupListSchedules, useBackupPreviewSchedule, useBackupRestore, useBackupTrigger, useBackupUpsertDestinationPolicy, useBackupUpsertSchedule, useBatteryGetStatus, useBatteryWakeForStream, useBinaryGetStatus, useBrightnessGetStatus, useBrightnessSetBrightness, useBrokerAdd, useBrokerGet, useBrokerGetBrokerConfig, useBrokerGetSettings, useBrokerGetSettingsSchema, useBrokerGetState, useBrokerGetStatus, useBrokerList, useBrokerListProviders, useBrokerPublish, useBrokerRemove, useBrokerSetSettings, useBrokerSubscribe, useBrokerTestConnection, useBrokerTestSettings, useBrokerUnsubscribe, useButtonPress, useCameraCredentialsGetCredentials, useCameraCredentialsGetStatus, useCameraGridLayoutApplyDeviceSettingsPatch, useCameraGridLayoutGetDeviceLiveContribution, useCameraGridLayoutGetDeviceSettingsContribution, useCameraGridLayoutGetLayout, useCameraGridLayoutSaveLayout, useCameraPipelineConfigApplyDeviceSettingsPatch, useCameraPipelineConfigGetDeviceLiveContribution, useCameraPipelineConfigGetDeviceSettingsContribution, useCameraStreamsGetBrokerStreams, useCameraStreamsGetCameraStreams, useCameraStreamsGetProfileRtspEntries, useCameraStreamsGetRtspEntries, useCameraStreamsPickStream, useCapabilityPresence, useCarbonMonoxideGetStatus, useClimateControlGetStatus, useClimateControlSetFanMode, useClimateControlSetMode, useClimateControlSetPreset, useClimateControlSetSwingHorizontal, useClimateControlSetSwingVertical, useClimateControlSetTarget, useClimateControlSetTargetHumidity, useClimateControlSetTargetRange, useClusterNodes, useClusterTopology, useColorGetStatus, useColorSetColor, useConfirm, useConnectionTestDescribeTest, useConnectionTestTestSettings, useConnectivityGetStatus, useConsumablesGetStatus, useConsumablesReset, useContactGetStatus, useContainerChildren, useControlGetStatus, useControlSetValue, useCoreBlocksCompile, useCoreBlocksCreate, useCoreBlocksDelete, useCoreBlocksGet, useCoreBlocksGetTypeDefs, useCoreBlocksList, useCoreBlocksRestart, useCoreBlocksSetEnabled, useCoreBlocksUpdate, useCoverClose, useCoverGetStatus, useCoverOpen, useCoverSetPosition, useCoverSetTiltPosition, useCoverStop, useCustomFieldRenderer, useDayNightGetOptions, useDayNightGetStatus, useDayNightSetSettings, useDebouncedString, useDecoderCreateSession, useDecoderDestroySession, useDecoderGetFrame, useDecoderGetInfo, useDecoderGetShmStats, useDecoderGetStats, useDecoderListActiveSessions, useDecoderOpenStream, useDecoderPullFrames, useDecoderPullHandles, useDecoderPushPacket, useDecoderReprobeHwaccel, useDecoderSupportsCodec, useDecoderUpdateConfig, useDetectionPipelineApplyDeviceSettingsPatch, useDetectionPipelineGetDeviceLiveContribution, useDetectionPipelineGetDeviceSettingsContribution, useDevShell, useDevice, useDeviceAdoptionAdopt, useDeviceAdoptionGetCandidate, useDeviceAdoptionGetStatus, useDeviceAdoptionListCandidateFilters, useDeviceAdoptionListCandidates, useDeviceAdoptionRefresh, useDeviceAdoptionRelease, useDeviceAdoptionResync, useDeviceAutotrack, useDeviceBattery, useDeviceCapSlice, useDeviceCapability, useDeviceDetections, useDeviceDiscoveryAdoptDevice, useDeviceDiscoveryGetStatus, useDeviceDiscoveryListDiscovered, useDeviceDiscoveryRefreshDiscovery, useDeviceDiscoveryReleaseDevice, useDeviceExportApplyDeviceSettingsPatch, useDeviceExportExposeDevice, useDeviceExportGetDeviceLiveContribution, useDeviceExportGetDeviceSettingsContribution, useDeviceExportGetStatus, useDeviceExportListExposedDevices, useDeviceExportListSupportedDeviceKinds, useDeviceExportUnexposeDevice, useDeviceId, useDeviceListPageSize, useDeviceManagerAddLocation, useDeviceManagerAdoptDevice, useDeviceManagerAdoptionAdopt, useDeviceManagerAdoptionCancelJob, useDeviceManagerAdoptionListCandidateFilters, useDeviceManagerAdoptionListCandidates, useDeviceManagerAdoptionListJobs, useDeviceManagerAdoptionRefresh, useDeviceManagerAdoptionRelease, useDeviceManagerAdoptionResync, useDeviceManagerAdoptionStartJob, useDeviceManagerAllocateDeviceId, useDeviceManagerApplyDeviceSettingsPatch, useDeviceManagerApplyInitialMeta, useDeviceManagerCreateDevice, useDeviceManagerDisable, useDeviceManagerDiscoverAllProviders, useDeviceManagerDiscoverDevices, useDeviceManagerDiscoverProvider, useDeviceManagerDiscoveryProviders, useDeviceManagerEnable, useDeviceManagerGetAllBindings, useDeviceManagerGetBindings, useDeviceManagerGetBindingsBatch, useDeviceManagerGetChildren, useDeviceManagerGetChildrenBatch, useDeviceManagerGetConfigSchema, useDeviceManagerGetCreationSchema, useDeviceManagerGetDevice, useDeviceManagerGetDeviceAggregate, useDeviceManagerGetDeviceLiveContribution, useDeviceManagerGetDeviceLiveInfoAggregate, useDeviceManagerGetDeviceSettingsAggregate, useDeviceManagerGetDeviceSettingsContribution, useDeviceManagerGetDeviceStatusAggregate, useDeviceManagerGetDeviceStatusAggregateBatch, useDeviceManagerGetLinkedDevices, useDeviceManagerGetLinkedDevicesBatch, useDeviceManagerGetRoleDisplayDefaults, useDeviceManagerGetSettingsSchema, useDeviceManagerGetStreamProfileMap, useDeviceManagerGetStreamSources, useDeviceManagerGetWireableFields, useDeviceManagerListAll, useDeviceManagerListBindableCapsForDeviceType, useDeviceManagerListLocationDescriptions, useDeviceManagerListLocationIcons, useDeviceManagerListLocations, useDeviceManagerListPersistedByAddon, useDeviceManagerListSiteZones, useDeviceManagerListWrappersForCap, useDeviceManagerLoadConfig, useDeviceManagerLoadMeta, useDeviceManagerLoadRuntimeState, useDeviceManagerMigrateDevice, useDeviceManagerPersistConfig, useDeviceManagerProbeStreams, useDeviceManagerProviderCreationType, useDeviceManagerProviderDiscoveryParamsSchema, useDeviceManagerRegisterDevice, useDeviceManagerRemove, useDeviceManagerRemoveByIntegration, useDeviceManagerRemoveDevice, useDeviceManagerRemoveLocation, useDeviceManagerRemoveSiteZone, useDeviceManagerRenameLocation, useDeviceManagerRenameSiteZone, useDeviceManagerRunDeviceAction, useDeviceManagerSetChildLayout, useDeviceManagerSetDisabled, useDeviceManagerSetDisplay, useDeviceManagerSetIntegrationId, useDeviceManagerSetLinkDeviceId, useDeviceManagerSetLocation, useDeviceManagerSetLocationDescription, useDeviceManagerSetLocationIcon, useDeviceManagerSetLocationZone, useDeviceManagerSetMetadata, useDeviceManagerSetName, useDeviceManagerSetPrimaryChildEntityId, useDeviceManagerSetRole, useDeviceManagerSetRoleDisplayDefaults, useDeviceManagerSetSiteZone, useDeviceManagerSetStreamProfileMap, useDeviceManagerSetType, useDeviceManagerSetWrapperActive, useDeviceManagerTestCreationField, useDeviceManagerTestField, useDeviceManagerUpdateConfig, useDeviceManagerUpdateDeviceField, useDeviceManagerUpdateDeviceFieldsBatch, useDeviceNetworkLink, useDeviceOpsGetConfigEntries, useDeviceOpsGetRawState, useDeviceOpsGetSettingsSchema, useDeviceOpsGetStreamSources, useDeviceOpsRemoveDevice, useDeviceOpsRunAction, useDeviceOpsSetConfig, useDeviceProviderAdoptDiscoveredDevice, useDeviceProviderCreateDevice, useDeviceProviderDiscoverDevices, useDeviceProviderGetChildCreationSchema, useDeviceProviderGetDevices, useDeviceProviderGetDiscoveryParamsSchema, useDeviceProviderGetManualCreationType, useDeviceProviderGetStatus, useDeviceProviderReloadDevice, useDeviceProviderStart, useDeviceProviderStop, useDeviceProviderSupportsDiscovery, useDeviceProviderSupportsManualCreation, useDeviceProviderTestCreationField, useDeviceProxy, useDeviceSnapshot, useDeviceSnapshotImage, useDeviceState, useDeviceStateGetAllSnapshots, useDeviceStateGetCapSlice, useDeviceStateGetSnapshot, useDeviceStateSetCapSlice, useDeviceStateSlice, useDeviceStatusGetStatus, useDeviceWebrtc, useDevices, useDoorbellApplyDeviceSettingsPatch, useDoorbellEvents, useDoorbellGetDeviceLiveContribution, useDoorbellGetDeviceSettingsContribution, useDoorbellGetStatus, useEnumSensorGetStatus, useEventEmitterGetStatus, useEventInvalidation, useEventStreamLatest, useEventStreamMap, useEventsGetEventClipUrl, useEventsGetEventThumbnail, useEventsGetEvents, useFaceGalleryAssignFace, useFaceGalleryAssignFaces, useFaceGalleryAuditIdentitySamples, useFaceGalleryCreateIdentity, useFaceGalleryDeleteFace, useFaceGalleryDeleteIdentity, useFaceGalleryGetFaceByTrack, useFaceGalleryGetFaceMedia, useFaceGalleryListIdentities, useFaceGalleryListIdentitySamples, useFaceGalleryListRecentFaces, useFaceGalleryRemoveSample, useFaceGalleryRenameIdentity, useFaceGallerySuggestFaceClusters, useFaceGalleryUnassignFace, useFaceGalleryUnassignFaces, useFanControlGetStatus, useFanControlSetDirection, useFanControlSetOscillating, useFanControlSetPercentage, useFanControlSetPreset, useFeatureProbeGetStatus, useFloodGetStatus, useGasGetStatus, useHumidifierGetStatus, useHumidifierSetMode, useHumidifierSetOn, useHumidifierSetTargetHumidity, useHumiditySensorGetStatus, useImageGetStatus, useImageSettingsGetOptions, useImageSettingsGetStatus, useImageSettingsSetSettings, useIntegrationsCreate, useIntegrationsDelete, useIntegrationsGet, useIntegrationsGetAvailableTypes, useIntegrationsGetByAddonId, useIntegrationsGetSettings, useIntegrationsList, useIntegrationsSetSettings, useIntegrationsTestConnection, useIntegrationsUpdate, useIntercomEndTalkSession, useIntercomGetStatus, useIntercomHandleAnswer, useIntercomPushTalkAudio, useIntercomStartSession, useIntercomStartTalkSession, useIntercomStopSession, useIsMidWidth, useIsMobile, useLawnMowerControlDock, useLawnMowerControlGetStatus, useLawnMowerControlPause, useLawnMowerControlStartMowing, useLiveBuffer, useLiveEvent, useLlmCancel, useLlmDeleteModel, useLlmDeleteProfile, useLlmGenerate, useLlmGenerateVision, useLlmGetDefaults, useLlmGetRuntimeStatus, useLlmGetUsage, useLlmInstallModel, useLlmListModelCatalog, useLlmListModels, useLlmListNodeModels, useLlmListProfileKinds, useLlmListProfiles, useLlmListRuntimeNodes, useLlmResolveModelRef, useLlmSetDefault, useLlmStartRuntime, useLlmStopRuntime, useLlmTestProfile, useLlmUpsertProfile, useLocalNetworkDownloadCa, useLocalNetworkGetAllowedAddresses, useLocalNetworkGetConnectionEndpoints, useLocalNetworkGetNotificationEndpoint, useLocalNetworkGetPreferred, useLocalNetworkGetTlsStatus, useLocalNetworkGetViewerEndpoints, useLocalNetworkList, useLocalNetworkRegenerateCertificate, useLocalNetworkResetAllowlistToBestMatch, useLocalNetworkRevertToGeneratedCertificate, useLocalNetworkSetAllowedAddresses, useLocalNetworkSetNotificationEndpoint, useLocalNetworkSetViewerEndpoints, useLocalNetworkUploadCertificate, useLockControlGetStatus, useLockControlLock, useLockControlOpen, useLockControlUnlock, useMediaPlayerGetStatus, useMediaPlayerNext, useMediaPlayerPause, useMediaPlayerPlay, useMediaPlayerPlayMedia, useMediaPlayerPrevious, useMediaPlayerSeek, useMediaPlayerSelectSource, useMediaPlayerSetMute, useMediaPlayerSetRepeat, useMediaPlayerSetShuffle, useMediaPlayerSetVolume, useMediaPlayerStop, useMeshNetworkGetStatus, useMeshNetworkJoin, useMeshNetworkLeave, useMeshNetworkListPeers, useMeshNetworkLogout, useMeshNetworkStartLogin, useMeshNetworkTestConnection, useMetricsProviderCollectSnapshot, useMetricsProviderGetAddonStats, useMetricsProviderGetCached, useMetricsProviderGetCpuTemperature, useMetricsProviderGetCurrent, useMetricsProviderGetDiskSpace, useMetricsProviderGetGpuInfo, useMetricsProviderGetLoadSeries, useMetricsProviderGetProcessStats, useMetricsProviderListAddonInstances, useMetricsProviderListNodeProcesses, useMotionDetectionAnalyze, useMotionDetectionApplyDeviceSettingsPatch, useMotionDetectionGetDeviceLiveContribution, useMotionDetectionGetDeviceSettingsContribution, useMotionDetectionRemoveCamera, useMotionDetectionReset, useMotionGetStatus, useMotionIsDetected, useMotionTriggerGetStatus, useMotionTriggerSetMotionTrigger, useMotionZonesGetOptions, useMotionZonesGetStatus, useMotionZonesSetZone, useMqttBrokerAddBroker, useMqttBrokerGetBrokerConfig, useMqttBrokerGetStatus, useMqttBrokerListBrokers, useMqttBrokerRemoveBroker, useMqttBrokerStartEmbeddedBroker, useMqttBrokerStopEmbeddedBroker, useMqttBrokerTestConnection, useNativeObjectDetectionGetOptions, useNativeObjectDetectionGetStatus, useNativeObjectDetectionSetEnabled, useNativeObjectDetectionSetSettings, useNavigation, useNavigationGetFeatures, useNavigationGetStatus, useNavigationGoToPoint, useNavigationListActions, useNavigationMove, useNavigationPlaySound, useNavigationRunAction, useNavigationSetLightLevel, useNavigationSetLightMode, useNavigationSetLightOn, useNavigationStop, useNetworkAccessGetEndpoint, useNetworkAccessGetStatus, useNetworkAccessListEndpoints, useNetworkAccessStart, useNetworkAccessStop, useNetworkLinkGetStatus, useNetworkQualityGetAllStats, useNetworkQualityGetDeviceStats, useNetworkQualityReportClientStats, useNodesClusterAddonStatus, useNodesDeployAddon, useNodesExecuteQuery, useNodesGetCapUsageGraph, useNodesGetNodeAddons, useNodesRenameNode, useNodesRestartAddon, useNodesRestartNode, useNodesRestartProcess, useNodesSetProcessLogLevel, useNodesShutdownNode, useNodesTopology, useNodesUndeployAddon, useNotificationOutputDeleteTarget, useNotificationOutputDiscoverTargets, useNotificationOutputListTargetKinds, useNotificationOutputListTargets, useNotificationOutputSend, useNotificationOutputSetTargetEnabled, useNotificationOutputTestTarget, useNotificationOutputUpsertTarget, useNotificationRulesCancelSnooze, useNotificationRulesCreateRule, useNotificationRulesCreateSnooze, useNotificationRulesDeleteRule, useNotificationRulesGetAlarmConfig, useNotificationRulesGetClipRetentionAsks, useNotificationRulesGetConditionCatalog, useNotificationRulesGetHistory, useNotificationRulesGetRule, useNotificationRulesListDeviceMutes, useNotificationRulesListRules, useNotificationRulesListSnoozes, useNotificationRulesResolveArtifactUrl, useNotificationRulesSetAlarmConfig, useNotificationRulesSetDeviceMuted, useNotificationRulesSetRuleEnabled, useNotificationRulesTestRule, useNotificationRulesUpdateRule, useNotifierCancel, useNotifierGetStatus, useNotifierSend, useNumericSensorGetStatus, useOptimisticSlice, useOptionalSystem, useOptionalWidgetRegistry, useOsdGetStatus, useOsdManagerClearSlotBinding, useOsdManagerCopyDeviceConfiguration, useOsdManagerGetConditionSupport, useOsdManagerGetDeviceOsd, useOsdManagerGetSourceCatalog, useOsdManagerPreviewSlot, useOsdManagerRenderDevice, useOsdManagerSetSlotBinding, useOsdSetOverlay, usePTZ, usePetFeederCallPet, usePetFeederCancelFeed, usePetFeederFeed, usePetFeederGetStatus, usePetFeederMarkFoodReplenished, usePetFeederPlaySound, usePetFeederResetDesiccant, usePetFeederSetChildLock, usePetFeederSetFeedSound, usePetFeederSetIndicatorLight, usePetFeederSetVolume, usePipelineAnalyticsApplyDeviceSettingsPatch, usePipelineAnalyticsCancelRelocateMedia, usePipelineAnalyticsCancelStorageMigrationMove, usePipelineAnalyticsClearTracks, usePipelineAnalyticsCompleteRetrainTrack, usePipelineAnalyticsCountRelocatableMedia, usePipelineAnalyticsCountUnstampedEventMedia, usePipelineAnalyticsDeleteDeviceEvents, usePipelineAnalyticsDeleteTracks, usePipelineAnalyticsDeselectRetrainFrame, usePipelineAnalyticsGetActiveTracks, usePipelineAnalyticsGetAudioEvents, usePipelineAnalyticsGetDeviceLiveContribution, usePipelineAnalyticsGetDeviceSettingsContribution, usePipelineAnalyticsGetEventDensity, usePipelineAnalyticsGetEventDensityBatch, usePipelineAnalyticsGetEventMedia, usePipelineAnalyticsGetEventMediaFootprintByKind, usePipelineAnalyticsGetEventStoreFootprint, usePipelineAnalyticsGetKeyEvents, usePipelineAnalyticsGetKeyEventsBatch, usePipelineAnalyticsGetMediaReclaimStatus, usePipelineAnalyticsGetMotionEvents, usePipelineAnalyticsGetObjectEmbeddingRebuildStatus, usePipelineAnalyticsGetObjectEvents, usePipelineAnalyticsGetRetrainExportUrl, usePipelineAnalyticsGetRetrainFrameImage, usePipelineAnalyticsGetSensorEvents, usePipelineAnalyticsGetStorageMigrationMoveStatus, usePipelineAnalyticsGetSummary, usePipelineAnalyticsGetTrack, usePipelineAnalyticsGetTrackMedia, usePipelineAnalyticsGetTrainingExportSummary, usePipelineAnalyticsGetTrainingExportUrl, usePipelineAnalyticsListArchivedDebugNotes, usePipelineAnalyticsListBirthDecisions, usePipelineAnalyticsListEventKinds, usePipelineAnalyticsListEventKindsBatch, usePipelineAnalyticsListEventMedia, usePipelineAnalyticsListOpsLog, usePipelineAnalyticsListRecentSummaries, usePipelineAnalyticsListRecentTracks, usePipelineAnalyticsListRelocateMediaJobs, usePipelineAnalyticsListRetrainAnnotations, usePipelineAnalyticsListRetrainFrames, usePipelineAnalyticsListRetrainStaging, usePipelineAnalyticsListSummaries, usePipelineAnalyticsListTrackMedia, usePipelineAnalyticsListTracks, usePipelineAnalyticsPauseForStorageMigration, usePipelineAnalyticsProposeRetrainAnnotations, usePipelineAnalyticsPruneEvents, usePipelineAnalyticsPruneEventsBefore, usePipelineAnalyticsPruneTracksBefore, usePipelineAnalyticsReadLongTermSeries, usePipelineAnalyticsRebuildObjectEmbeddings, usePipelineAnalyticsReclaimDebugMedia, usePipelineAnalyticsReconcileFromDisk, usePipelineAnalyticsRefreshStorageLocationsForMigration, usePipelineAnalyticsRelocateMedia, usePipelineAnalyticsRestageRetrainTrack, usePipelineAnalyticsResumeForStorageMigration, usePipelineAnalyticsRunReplayFrameProcessor, usePipelineAnalyticsSaveRetrainAnnotations, usePipelineAnalyticsSearchObjectEvents, usePipelineAnalyticsSelectRetrainFrames, usePipelineAnalyticsSetTrackFlags, usePipelineAnalyticsStartStorageMigrationMove, usePipelineAnalyticsWipeAllAnalytics, usePipelineAnalyticsWipeObjectEmbeddings, usePipelineExecutorCacheFrameInPool, usePipelineExecutorClearDeviceOverrides, usePipelineExecutorDeleteModel, usePipelineExecutorDeleteTemplate, usePipelineExecutorDownloadModel, usePipelineExecutorGetAddonModels, usePipelineExecutorGetAudioCapabilities, usePipelineExecutorGetAvailableEngines, usePipelineExecutorGetCapabilities, usePipelineExecutorGetDefaultSteps, usePipelineExecutorGetDetectionConfigSchema, usePipelineExecutorGetEffectiveTuning, usePipelineExecutorGetEngineProvisioning, usePipelineExecutorGetGlobalPipelineConfig, usePipelineExecutorGetGlobalSteps, usePipelineExecutorGetInferenceDeviceHealth, usePipelineExecutorGetOrchestratorConfigSchema, usePipelineExecutorGetReferenceAudio, usePipelineExecutorGetReferenceAudioFiles, usePipelineExecutorGetReferenceImage, usePipelineExecutorGetSchema, usePipelineExecutorGetSelectedEngine, usePipelineExecutorGetVideoPipelineSteps, usePipelineExecutorInferCached, usePipelineExecutorKillEngine, usePipelineExecutorListLoadedEngines, usePipelineExecutorListReferenceImages, usePipelineExecutorListTemplates, usePipelineExecutorRearmInferenceDevice, usePipelineExecutorRunAudioTest, usePipelineExecutorRunPipeline, usePipelineExecutorRunPipelineBatch, usePipelineExecutorSaveTemplate, usePipelineExecutorSetVideoPipelineSteps, usePipelineExecutorSpinEngine, usePipelineExecutorUncacheFrame, usePipelineExecutorUpdateTemplate, usePipelineExecutorValidatePipeline, usePipelineOrchestratorApplyDeviceSettingsPatch, usePipelineOrchestratorAssignAudio, usePipelineOrchestratorAssignPipeline, usePipelineOrchestratorDeleteTemplate, usePipelineOrchestratorGetAgentLoad, usePipelineOrchestratorGetAgentSettings, usePipelineOrchestratorGetAudioAssignment, usePipelineOrchestratorGetAudioAssignments, usePipelineOrchestratorGetAudioNodeLoad, usePipelineOrchestratorGetCameraMetrics, usePipelineOrchestratorGetCameraSettings, usePipelineOrchestratorGetCameraStatus, usePipelineOrchestratorGetCameraStatuses, usePipelineOrchestratorGetCameraStepOverrides, usePipelineOrchestratorGetCameraSwitches, usePipelineOrchestratorGetCapabilityBindings, usePipelineOrchestratorGetDeviceLiveContribution, usePipelineOrchestratorGetDeviceSettingsContribution, usePipelineOrchestratorGetGlobalMetrics, usePipelineOrchestratorGetIngestOwner, usePipelineOrchestratorGetNodeInferenceDevices, usePipelineOrchestratorGetPipelineAssignment, usePipelineOrchestratorGetPipelineAssignments, usePipelineOrchestratorGetPipelineDevicePin, usePipelineOrchestratorGetReconcileFromDiskStatus, usePipelineOrchestratorListAgentSettings, usePipelineOrchestratorListTemplates, usePipelineOrchestratorPauseForStorageMigration, usePipelineOrchestratorRebalance, usePipelineOrchestratorReconcileFromDisk, usePipelineOrchestratorRemoveAgentSettings, usePipelineOrchestratorResetNodePipelineDefaults, usePipelineOrchestratorResolvePipeline, usePipelineOrchestratorResumeForStorageMigration, usePipelineOrchestratorSaveTemplate, usePipelineOrchestratorSetAgentCapabilities, usePipelineOrchestratorSetAgentDecodeLimits, usePipelineOrchestratorSetAgentDetectWeight, usePipelineOrchestratorSetAgentInferenceDevices, usePipelineOrchestratorSetAgentMaxCameras, usePipelineOrchestratorSetAgentReachableHost, usePipelineOrchestratorSetCameraPipelineForAgent, usePipelineOrchestratorSetCameraStepOverride, usePipelineOrchestratorSetCameraStepToggle, usePipelineOrchestratorSetCameraSwitch, usePipelineOrchestratorSetCapabilityBinding, usePipelineOrchestratorSetPipelineDevicePin, usePipelineOrchestratorUnassignAudio, usePipelineOrchestratorUnassignPipeline, usePipelineOrchestratorUpdateTemplate, usePipelineRunnerAttachCamera, usePipelineRunnerDetachCamera, usePipelineRunnerGetAllCameraMetrics, usePipelineRunnerGetCameraMetrics, usePipelineRunnerGetLocalCameras, usePipelineRunnerGetLocalLoad, usePipelineRunnerGetLocalMetrics, usePipelineRunnerGetNativeCrop, usePipelineRunnerGetParkedTrackFrame, usePipelineRunnerParkTrackFrame, usePipelineRunnerReleaseParkedTrackFrames, usePipelineRunnerReportMotion, usePipelineRunnerRequestOccupancyBurst, usePipelineRunnerRunDetailSubtree, usePipelineRunnerRunStatelessStep, usePlateGalleryAssignPlate, usePlateGalleryAssignPlates, usePlateGalleryCorrectPlateText, usePlateGalleryCreateVehicle, usePlateGalleryDeletePlate, usePlateGalleryDeleteVehicle, usePlateGalleryGetPlateByTrack, usePlateGalleryGetPlateMedia, usePlateGalleryListPlates, usePlateGalleryListVehicleSamples, usePlateGalleryListVehicles, usePlateGalleryRemoveVehicleSample, usePlateGalleryRenameVehicle, usePlateGallerySearchPlates, usePlateGallerySuggestPlateClusters, usePlateGalleryUnassignPlate, usePlateGalleryUnassignPlates, usePlayerOverlayLayer, usePlayerOverlayLayers, usePlayerToolbarButton, usePlayerToolbarButtons, usePowerMeterGetStatus, usePresenceGetStatus, usePressureSensorGetStatus, usePrivacyMaskGetOptions, usePrivacyMaskGetStatus, usePrivacyMaskSetAudioEnabled, usePrivacyMaskSetMask, usePrompt, usePtzAutotrackGetSettings, usePtzAutotrackGetStatus, usePtzAutotrackSetEnabled, usePtzAutotrackSetSettings, usePtzCaptureHomeHere, usePtzContinuousMove, usePtzDeletePreset, usePtzGetHomePreset, usePtzGetHomeReturn, usePtzGetOptions, usePtzGetPosition, usePtzGetPresets, usePtzGetStatus, usePtzGoHome, usePtzGoToPreset, usePtzMove, usePtzSavePreset, usePtzSetAutofocus, usePtzSetHomePreset, usePtzSetHomeReturn, usePtzStop, useRebootReboot, useRecordedPlayback, useRecordingApplyDeviceSettingsPatch, useRecordingCancelRelocateJob, useRecordingCancelStorageMigrationMove, useRecordingDeleteFootprint, useRecordingExportCancelExport, useRecordingExportCreateExport, useRecordingExportDeleteExport, useRecordingExportGetDownloadUrl, useRecordingExportGetExport, useRecordingExportListExports, useRecordingExportReadExportBytes, useRecordingGetAvailability, useRecordingGetAvailabilityBatch, useRecordingGetDaysWithRecordings, useRecordingGetDaysWithRecordingsBatch, useRecordingGetDeviceConfig, useRecordingGetDeviceLiveContribution, useRecordingGetDeviceSettingsContribution, useRecordingGetPlacement, useRecordingGetPlaybackManifest, useRecordingGetRelocateResidue, useRecordingGetStatus, useRecordingGetStorageMigrationMoveStatus, useRecordingGetStorageUsage, useRecordingListOpsLog, useRecordingListRelocateJobs, useRecordingLocateSegment, useRecordingPauseForStorageMigration, useRecordingPlanStorageRebalance, useRecordingPruneFootage, useRecordingReadGopBytes, useRecordingReadSegmentBytes, useRecordingReadWindowBytes, useRecordingReconcileLedgerAgainstDisk, useRecordingRefreshStorageLocationsForMigration, useRecordingRelocateFootage, useRecordingRenderClip, useRecordingRenderGif, useRecordingRescanStorage, useRecordingResumeForStorageMigration, useRecordingSetDeviceConfig, useRecordingSetDevicePlacement, useRecordingSignalGetStatus, useRecordingStartStorageMigrationMove, useRecordingStartStorageRebalance, useRemoteComponent, useSceneMonitorCaptureReference, useSceneMonitorCreateScene, useSceneMonitorDeleteReference, useSceneMonitorDeleteScene, useSceneMonitorGetStatus, useSceneMonitorListScenes, useSceneMonitorListScenesBatch, useSceneMonitorRecheckNow, useSceneMonitorResetScene, useSceneMonitorUpdateScene, useScriptRunnerGetStatus, useScriptRunnerRun, useScriptRunnerStop, useScrubController, useServerManagementApplyServerUpdate, useServerManagementCheckServerUpdate, useServerManagementGetServerPackageStatus, useServerManagementRestartServer, useServerManagementRollbackServerUpdate, useSettingsStoreAggregate, useSettingsStoreCount, useSettingsStoreDeclareCollection, useSettingsStoreDelete, useSettingsStoreDeleteWhere, useSettingsStoreGet, useSettingsStoreHistogram, useSettingsStoreInsert, useSettingsStoreInsertMany, useSettingsStoreIsEmpty, useSettingsStoreQuery, useSettingsStoreSet, useSettingsStoreUpdate, useSettingsStoreUpdateWhere, useSettingsStoreUpsert, useSmokeGetStatus, useSnapshotApplyDeviceSettingsPatch, useSnapshotGetDebugState, useSnapshotGetDeviceLiveContribution, useSnapshotGetDeviceSettingsContribution, useSnapshotGetSnapshot, useSnapshotGetSnapshotLinks, useSnapshotGetSnapshotOverview, useSnapshotGetStatus, useSnapshotInvalidateCache, useStationaryOverlay, useStorageAbortUpload, useStorageBeginDownload, useStorageBeginUpload, useStorageDelete, useStorageDeleteLocation, useStorageEndDownload, useStorageExists, useStorageFinalizeUpload, useStorageGetAvailableSpace, useStorageList, useStorageListDrainProgress, useStorageListLocationDeclarations, useStorageListLocations, useStorageListProviders, useStorageMigrationCancel, useStorageMigrationCleanupCancel, useStorageMigrationCleanupStart, useStorageMigrationCleanupStatus, useStorageMigrationDrain, useStorageMigrationHistory, useStorageMigrationMovers, useStorageMigrationPlan, useStorageMigrationResidue, useStorageMigrationStart, useStorageMigrationStatus, useStorageRead, useStorageReadChunk, useStorageResolve, useStorageTestConfig, useStorageTestLocation, useStorageUpsertLocation, useStorageWrite, useStorageWriteChunk, useStreamBrokerAcquireEgressTranscode, useStreamBrokerApplyDeviceSettingsPatch, useStreamBrokerAssignProfile, useStreamBrokerFetchEventMedia, useStreamBrokerForgetDeviceHardware, useStreamBrokerGetAllRtspEntries, useStreamBrokerGetBrokerStats, useStreamBrokerGetDeviceAudioMute, useStreamBrokerGetDeviceLiveContribution, useStreamBrokerGetDeviceSettingsContribution, useStreamBrokerGetPreBufferInfo, useStreamBrokerGetRtspEntry, useStreamBrokerGetRtspPort, useStreamBrokerGetStreamUrl, useStreamBrokerGetStreamWithCodec, useStreamBrokerIsRtspEnabled, useStreamBrokerKillClient, useStreamBrokerListAllCameraStreams, useStreamBrokerListAllProfileSlots, useStreamBrokerListClients, useStreamBrokerProbeStream, useStreamBrokerProduceEventMedia, useStreamBrokerPublishCameraStream, useStreamBrokerPullAudioChunks, useStreamBrokerPullFrameHandles, useStreamBrokerRegenerateRtspToken, useStreamBrokerReleaseEgressTranscode, useStreamBrokerReleaseStreamWithCodec, useStreamBrokerRenderPreBufferClip, useStreamBrokerReportStreamSignal, useStreamBrokerRestartProfile, useStreamBrokerRetractCameraStream, useStreamBrokerSetDeviceAudioMute, useStreamBrokerSetPreBufferDuration, useStreamBrokerSetRtspEnabled, useStreamBrokerSubscribeAudioChunks, useStreamBrokerSubscribeFrames, useStreamBrokerUnassignProfile, useStreamBrokerUnsubscribeAudioChunks, useStreamBrokerUnsubscribeFrames, useStreamCatalogGetCatalog, useStreamParamsGetConfigSchema, useStreamParamsGetOptions, useStreamParamsGetStatus, useStreamParamsSetProfile, useStreamSignalsListSignals, useSwitchGetStatus, useSwitchSetState, useSystem, useSystemDetectSiteLocation, useSystemFeatureFlags, useSystemForceRetentionCleanup, useSystemGetFailureContributions, useSystemGetLoadContributions, useSystemGetLoggingSettings, useSystemGetRequestCensus, useSystemGetRetentionConfig, useSystemGetSiteLocation, useSystemHealth, useSystemInfo, useSystemMutation, useSystemNetworkAddresses, useSystemQuery, useSystemSetLoggingSettings, useSystemSetRetentionConfig, useSystemSetSiteLocation, useTamperGetStatus, useTemperatureSensorGetStatus, useTerminalSessionAdoptLegacyMonitor, useTerminalSessionClose, useTerminalSessionCreateInstance, useTerminalSessionDeleteInstance, useTerminalSessionListInstances, useTerminalSessionListLegacyCameras, useTerminalSessionListProfiles, useTerminalSessionListSessions, useTerminalSessionOpenSession, useTerminalSessionPullOutput, useTerminalSessionResize, useTerminalSessionSetInstanceEnabled, useTerminalSessionUpdateInstance, useTerminalSessionWriteInput, useThemeMode, useToastOnToast, useTurnProviderGetTurnServers, useUpdateGetStatus, useUpdateInstallUpdate, useUserManagementConfirmTotp, useUserManagementCreateApiKey, useUserManagementCreateScopedToken, useUserManagementCreateUser, useUserManagementDeleteUser, useUserManagementDisableTotp, useUserManagementGetTotpStatus, useUserManagementListApiKeys, useUserManagementListOauthSessions, useUserManagementListScopedTokens, useUserManagementListUsers, useUserManagementOauthExchangeCode, useUserManagementOauthIssueCode, useUserManagementOauthRefresh, useUserManagementOauthVerifyAccessToken, useUserManagementResetPassword, useUserManagementRevokeApiKey, useUserManagementRevokeOauthSession, useUserManagementRevokeScopedToken, useUserManagementSetUserScopes, useUserManagementSetupTotp, useUserManagementUpdateUser, useUserManagementValidateApiKey, useUserManagementValidateCredentials, useUserManagementValidateScopedToken, useUserManagementVerifyTotp, useVacuumControlGetStatus, useVacuumControlLocate, useVacuumControlPause, useVacuumControlReturnToBase, useVacuumControlSetFanSpeed, useVacuumControlStart, useVacuumControlStop, useValveClose, useValveGetStatus, useValveOpen, useValveSetPosition, useValveStop, useVibrationGetStatus, useVideoclipsGetClipPlayback, useVideoclipsListClips, useVideoclipsListSources, useVodPlayback, useWaterHeaterGetStatus, useWaterHeaterSetAway, useWaterHeaterSetOperationMode, useWaterHeaterSetTargetTemp, useWeatherGetStatus, useWebrtcSessionAddIceCandidate, useWebrtcSessionCloseSession, useWebrtcSessionCreateSession, useWebrtcSessionGetIceCandidates, useWebrtcSessionGetSessionDebug, useWebrtcSessionGetSessionState, useWebrtcSessionHandleAnswer, useWebrtcSessionHandleOffer, useWebrtcSessionHasAdaptiveBitrate, useWebrtcSessionListStreams, useWidget, useWidgetMetadata, useWidgetRegistry, useZoneAnalyticsGetCameraHistory, useZoneAnalyticsGetCurrentSnapshot, useZoneAnalyticsGetCurrentSnapshotBatch, useZoneAnalyticsGetUnzonedHistory, useZoneAnalyticsGetZoneHistory, useZoneEditing, useZoneRulesListRules, useZoneRulesSetRules, useZonesAddZone, useZonesListZones, useZonesRemoveZone, useZonesUpdateZone, vacuumStateMeta, validateScopes, valveStateMeta, variantLabel, waterHeaterPhase, waterHeaterTint, weatherConditionMeta, weatherTint };
|