@camstack/ui-library 1.2.190 → 1.2.192
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/ClipBandBar.d.ts +16 -0
- package/dist/composites/clips/ClipsSection.d.ts +4 -0
- package/dist/composites/clips/clip-band.d.ts +58 -0
- package/dist/composites/clips/clip-day-window.d.ts +21 -22
- package/dist/composites/clips/clip-still-retry.d.ts +50 -0
- package/dist/composites/clips/clip-tile-media.d.ts +25 -7
- package/dist/composites/clips/clip-view-state.d.ts +15 -4
- package/dist/generated/system-hooks.d.ts +2 -0
- package/dist/index.cjs +2368 -1871
- package/dist/index.js +2369 -1873
- package/dist/lib/format-duration.d.ts +14 -0
- 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/composites/clips/ClipSilentSources.d.ts +0 -4
- package/dist/composites/clips/ClipSourceRow.d.ts +0 -9
- package/dist/composites/clips/ClipsBrowser.d.ts +0 -4
package/dist/index.js
CHANGED
|
@@ -13,7 +13,7 @@ import * as __mfHostReactDOMClient from "react-dom/client";
|
|
|
13
13
|
import { createRoot } from "react-dom/client";
|
|
14
14
|
import * as __mfHostTrpcClient from "@trpc/client";
|
|
15
15
|
import { createTRPCClient, createWSClient, httpLink, splitLink, wsLink } from "@trpc/client";
|
|
16
|
-
import { ALL_CAPABILITY_DEFINITIONS, CAM_PROFILE_ORDER, CONTAINER_CHILD_PRIORITY, DEFAULT_EVENTS_BAND_BUFFER_SEC, DEFAULT_EVENT_COLOR, DEVICE_TYPE_CONTROL_KIND, DeviceExportStatusSchema, DeviceFeature, DeviceRole, DeviceType, EVENT_TAXONOMY, EventCategory, ExposedDeviceSchema, FIRST_LEVEL_MACRO_CLASSES, KNOWN_CAP_NAMES, MAX_SENSOR_TRIGGER_DEVICES, MODEL_PROVIDER_IDS, NAVIGATION_LIGHT_LEVEL_MAX, NAVIGATION_LIGHT_LEVEL_MIN, PET_FEEDER_MANUAL_FEED_MAX, PET_FEEDER_MANUAL_FEED_MIN, RECORDING_AUDIO_TRIGGER_OFFERED_DBFS, RecordingConfigSchema, RecordingStatusSchema, SOURCE_CAP_ACTIVE_FIELD, STATIONARY_OVERLAY_STYLE, TIMEZONES, buildModelVariantGroups, canConvertUnit, clusterStepSettingFieldsFor, createDeviceProxy, createLazyTrpcSource, createSliceHandle, deriveRecordingMode, describeModelVariant, findTimezone, formatParkedFor, resolveContainerPrimaryChild, resolveDeviceControlKind, resolveRecordingProfiles, resolveVariantModelId, resolveViewableDeviceIds, selectAssignedProfileSlots, tryConvertUnit } from "@camstack/types";
|
|
16
|
+
import { ALL_CAPABILITY_DEFINITIONS, CAM_PROFILE_ORDER, CLIP_STILL_DISPOSITION_HEADER, CLIP_STILL_REASON_CODE_HEADER, CLIP_STILL_REASON_HEADER, CONTAINER_CHILD_PRIORITY, DEFAULT_EVENTS_BAND_BUFFER_SEC, DEFAULT_EVENT_COLOR, DEVICE_TYPE_CONTROL_KIND, DeviceExportStatusSchema, DeviceFeature, DeviceRole, DeviceType, EVENT_TAXONOMY, EventCategory, ExposedDeviceSchema, FIRST_LEVEL_MACRO_CLASSES, KNOWN_CAP_NAMES, MAX_SENSOR_TRIGGER_DEVICES, MODEL_PROVIDER_IDS, NAVIGATION_LIGHT_LEVEL_MAX, NAVIGATION_LIGHT_LEVEL_MIN, PET_FEEDER_MANUAL_FEED_MAX, PET_FEEDER_MANUAL_FEED_MIN, RECORDING_AUDIO_TRIGGER_OFFERED_DBFS, RecordingConfigSchema, RecordingStatusSchema, SOURCE_CAP_ACTIVE_FIELD, STATIONARY_OVERLAY_STYLE, TIMEZONES, buildModelVariantGroups, canConvertUnit, clusterStepSettingFieldsFor, createDeviceProxy, createLazyTrpcSource, createSliceHandle, deriveRecordingMode, describeModelVariant, findTimezone, formatParkedFor, resolveContainerPrimaryChild, resolveDeviceControlKind, resolveRecordingProfiles, resolveVariantModelId, resolveViewableDeviceIds, selectAssignedProfileSlots, tryConvertUnit } from "@camstack/types";
|
|
17
17
|
import { createSystem } from "@camstack/sdk";
|
|
18
18
|
import { z } from "zod";
|
|
19
19
|
//#region ../../node_modules/lucide-react/dist/esm/shared/src/utils/mergeClasses.js
|
|
@@ -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",
|
|
@@ -6359,6 +6383,8 @@ var usePipelineAnalyticsGetTrackMedia = trpc.pipelineAnalytics.getTrackMedia.use
|
|
|
6359
6383
|
var usePipelineAnalyticsListTrackMedia = trpc.pipelineAnalytics.listTrackMedia.useQuery;
|
|
6360
6384
|
/** Generated alias around `trpc.pipelineAnalytics.listEventMedia.useQuery`. */
|
|
6361
6385
|
var usePipelineAnalyticsListEventMedia = trpc.pipelineAnalytics.listEventMedia.useQuery;
|
|
6386
|
+
/** Generated alias around `trpc.pipelineAnalytics.ownersWithMedia.useQuery`. */
|
|
6387
|
+
var usePipelineAnalyticsOwnersWithMedia = trpc.pipelineAnalytics.ownersWithMedia.useQuery;
|
|
6362
6388
|
/** Generated alias around `trpc.pipelineAnalytics.searchObjectEvents.useQuery`. */
|
|
6363
6389
|
var usePipelineAnalyticsSearchObjectEvents = trpc.pipelineAnalytics.searchObjectEvents.useQuery;
|
|
6364
6390
|
/** Generated alias around `trpc.pipelineAnalytics.wipeObjectEmbeddings.useMutation`. */
|
|
@@ -17581,334 +17607,1630 @@ function useCapabilityPresence(capabilityName) {
|
|
|
17581
17607
|
return classifyCapability(query.data);
|
|
17582
17608
|
}
|
|
17583
17609
|
//#endregion
|
|
17584
|
-
//#region
|
|
17610
|
+
//#region ../../node_modules/class-variance-authority/dist/index.mjs
|
|
17585
17611
|
/**
|
|
17586
|
-
*
|
|
17587
|
-
* widget/section (the Recording panel) and the hero StreamPanel.
|
|
17612
|
+
* Copyright 2022 Joe Bell. All rights reserved.
|
|
17588
17613
|
*
|
|
17589
|
-
*
|
|
17590
|
-
*
|
|
17591
|
-
*
|
|
17592
|
-
* returns the hero to live.
|
|
17614
|
+
* This file is licensed to you under the Apache License, Version 2.0
|
|
17615
|
+
* (the "License"); you may not use this file except in compliance with the
|
|
17616
|
+
* License. You may obtain a copy of the License at
|
|
17593
17617
|
*
|
|
17594
|
-
*
|
|
17595
|
-
*
|
|
17596
|
-
*
|
|
17597
|
-
* the
|
|
17598
|
-
*
|
|
17599
|
-
*
|
|
17600
|
-
*
|
|
17601
|
-
*/
|
|
17602
|
-
var
|
|
17603
|
-
|
|
17604
|
-
|
|
17605
|
-
|
|
17618
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
17619
|
+
*
|
|
17620
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
17621
|
+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
17622
|
+
* WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or implied. See the
|
|
17623
|
+
* License for the specific language governing permissions and limitations under
|
|
17624
|
+
* the License.
|
|
17625
|
+
*/ var falsyToString = (value) => typeof value === "boolean" ? `${value}` : value === 0 ? "0" : value;
|
|
17626
|
+
var cx = clsx;
|
|
17627
|
+
var cva = (base, config) => (props) => {
|
|
17628
|
+
var _config_compoundVariants;
|
|
17629
|
+
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);
|
|
17630
|
+
const { variants, defaultVariants } = config;
|
|
17631
|
+
const getVariantClassNames = Object.keys(variants).map((variant) => {
|
|
17632
|
+
const variantProp = props === null || props === void 0 ? void 0 : props[variant];
|
|
17633
|
+
const defaultVariantProp = defaultVariants === null || defaultVariants === void 0 ? void 0 : defaultVariants[variant];
|
|
17634
|
+
if (variantProp === null) return null;
|
|
17635
|
+
const variantKey = falsyToString(variantProp) || falsyToString(defaultVariantProp);
|
|
17636
|
+
return variants[variant][variantKey];
|
|
17637
|
+
});
|
|
17638
|
+
const propsWithoutUndefined = props && Object.entries(props).reduce((acc, param) => {
|
|
17639
|
+
let [key, value] = param;
|
|
17640
|
+
if (value === void 0) return acc;
|
|
17641
|
+
acc[key] = value;
|
|
17642
|
+
return acc;
|
|
17643
|
+
}, {});
|
|
17644
|
+
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) => {
|
|
17645
|
+
let { class: cvClass, className: cvClassName, ...compoundVariantOptions } = param;
|
|
17646
|
+
return Object.entries(compoundVariantOptions).every((param) => {
|
|
17647
|
+
let [key, value] = param;
|
|
17648
|
+
return Array.isArray(value) ? value.includes({
|
|
17649
|
+
...defaultVariants,
|
|
17650
|
+
...propsWithoutUndefined
|
|
17651
|
+
}[key]) : {
|
|
17652
|
+
...defaultVariants,
|
|
17653
|
+
...propsWithoutUndefined
|
|
17654
|
+
}[key] === value;
|
|
17655
|
+
}) ? [
|
|
17656
|
+
...acc,
|
|
17657
|
+
cvClass,
|
|
17658
|
+
cvClassName
|
|
17659
|
+
] : acc;
|
|
17660
|
+
}, []), props === null || props === void 0 ? void 0 : props.class, props === null || props === void 0 ? void 0 : props.className);
|
|
17661
|
+
};
|
|
17662
|
+
//#endregion
|
|
17663
|
+
//#region src/primitives/badge.tsx
|
|
17664
|
+
var badgeVariants = cva("inline-flex items-center rounded-full text-xs font-medium px-2 py-0.5", {
|
|
17665
|
+
variants: {
|
|
17666
|
+
variant: {
|
|
17667
|
+
default: "bg-surface-hover text-foreground",
|
|
17668
|
+
success: "bg-success/15 text-success",
|
|
17669
|
+
warning: "bg-warning/15 text-warning",
|
|
17670
|
+
danger: "bg-danger/15 text-danger",
|
|
17671
|
+
info: "bg-info/15 text-info"
|
|
17672
|
+
},
|
|
17673
|
+
styleVariant: {
|
|
17674
|
+
solid: "",
|
|
17675
|
+
outline: "border bg-transparent"
|
|
17676
|
+
}
|
|
17677
|
+
},
|
|
17678
|
+
defaultVariants: {
|
|
17679
|
+
variant: "default",
|
|
17680
|
+
styleVariant: "solid"
|
|
17681
|
+
}
|
|
17606
17682
|
});
|
|
17607
|
-
|
|
17608
|
-
|
|
17609
|
-
|
|
17610
|
-
|
|
17611
|
-
|
|
17612
|
-
|
|
17613
|
-
|
|
17614
|
-
|
|
17615
|
-
value,
|
|
17616
|
-
children
|
|
17683
|
+
var Badge = forwardRef(({ className, variant, style, ...props }, ref) => {
|
|
17684
|
+
return /* @__PURE__ */ jsx("span", {
|
|
17685
|
+
ref,
|
|
17686
|
+
className: cn(badgeVariants({
|
|
17687
|
+
variant,
|
|
17688
|
+
styleVariant: style
|
|
17689
|
+
}), className),
|
|
17690
|
+
...props
|
|
17617
17691
|
});
|
|
17618
|
-
}
|
|
17619
|
-
|
|
17620
|
-
function useVodPlayback() {
|
|
17621
|
-
return useContext(VodPlaybackContext);
|
|
17622
|
-
}
|
|
17692
|
+
});
|
|
17693
|
+
Badge.displayName = "Badge";
|
|
17623
17694
|
//#endregion
|
|
17624
|
-
//#region src/
|
|
17695
|
+
//#region src/primitives/button.tsx
|
|
17696
|
+
var buttonVariants = cva("inline-flex items-center justify-center rounded-md font-medium transition-colors cursor-pointer disabled:opacity-50 disabled:pointer-events-none", {
|
|
17697
|
+
variants: {
|
|
17698
|
+
variant: {
|
|
17699
|
+
primary: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
17700
|
+
secondary: "bg-surface text-foreground hover:bg-surface-hover",
|
|
17701
|
+
ghost: "hover:bg-surface-hover text-foreground-muted",
|
|
17702
|
+
danger: "bg-danger text-white hover:bg-danger/90",
|
|
17703
|
+
outline: "border border-border bg-transparent text-foreground hover:bg-surface-hover"
|
|
17704
|
+
},
|
|
17705
|
+
size: {
|
|
17706
|
+
sm: "h-7 px-2.5 text-xs gap-1.5",
|
|
17707
|
+
md: "h-8 px-3 text-sm gap-2",
|
|
17708
|
+
lg: "h-9 px-4 text-sm gap-2"
|
|
17709
|
+
}
|
|
17710
|
+
},
|
|
17711
|
+
defaultVariants: {
|
|
17712
|
+
variant: "primary",
|
|
17713
|
+
size: "sm"
|
|
17714
|
+
}
|
|
17715
|
+
});
|
|
17716
|
+
var Button = forwardRef(({ className, variant, size, ...props }, ref) => {
|
|
17717
|
+
return /* @__PURE__ */ jsx("button", {
|
|
17718
|
+
ref,
|
|
17719
|
+
className: cn(buttonVariants({
|
|
17720
|
+
variant,
|
|
17721
|
+
size
|
|
17722
|
+
}), className),
|
|
17723
|
+
...props
|
|
17724
|
+
});
|
|
17725
|
+
});
|
|
17726
|
+
Button.displayName = "Button";
|
|
17727
|
+
//#endregion
|
|
17728
|
+
//#region src/primitives/select.tsx
|
|
17625
17729
|
/**
|
|
17626
|
-
*
|
|
17627
|
-
*
|
|
17628
|
-
* **No second player.** `getClipPlayback` answers `{ playbackUrl, format,
|
|
17629
|
-
* served }` and the panel hands it to the SAME hero the recordings timeline
|
|
17630
|
-
* drives: `vod.play({ url, format })` → `StreamPanel playbackUrl/playbackFormat`
|
|
17631
|
-
* → `HlsVideo`'s `mp4` branch (`video.src`, no hls.js). Building a clip player
|
|
17632
|
-
* here is the thing D549 decision 22 exists to refuse.
|
|
17633
|
-
*
|
|
17634
|
-
* **`format` travels.** An MP4 handed to hls.js is parsed as a manifest and
|
|
17635
|
-
* fails, so the format the provider named is forwarded verbatim and never
|
|
17636
|
-
* defaulted here.
|
|
17730
|
+
* A native `<select>` styled to match the rest of the field set.
|
|
17637
17731
|
*
|
|
17638
|
-
*
|
|
17639
|
-
* it
|
|
17640
|
-
*
|
|
17641
|
-
*
|
|
17642
|
-
*
|
|
17732
|
+
* `appearance-none` plus an explicit `bg-surface` / `text-foreground` is what
|
|
17733
|
+
* makes it match — and it is also what USED to make a disabled one a lie. A
|
|
17734
|
+
* browser greys a disabled control by rendering its own background and text
|
|
17735
|
+
* colour; all three of those are overridden here, so a `disabled` dropdown
|
|
17736
|
+
* looked exactly like a live one and simply ignored the click. That is the
|
|
17737
|
+
* whole of the "the language selector does not open" report of 2026-08-15:
|
|
17738
|
+
* the AI section's switch was off, every control under it was correctly inert,
|
|
17739
|
+
* and nothing on screen said so.
|
|
17643
17740
|
*
|
|
17644
|
-
*
|
|
17645
|
-
*
|
|
17646
|
-
* the
|
|
17741
|
+
* `disabled:` classes belong HERE and not on each call site: there are dozens
|
|
17742
|
+
* of `<Select disabled>` in this app and the one that gets forgotten is the one
|
|
17743
|
+
* that generates the ticket. Guarded by `disabled-affordance.test.tsx`.
|
|
17647
17744
|
*/
|
|
17648
|
-
var
|
|
17649
|
-
"low",
|
|
17650
|
-
"mid",
|
|
17651
|
-
"high"
|
|
17652
|
-
];
|
|
17653
|
-
function ClipPanel({ deviceId, clip, profile, onProfileChange }) {
|
|
17654
|
-
const vod = useVodPlayback();
|
|
17655
|
-
const playback = useVideoclipsGetClipPlayback({
|
|
17656
|
-
deviceId,
|
|
17657
|
-
clipId: clip.id,
|
|
17658
|
-
profile
|
|
17659
|
-
});
|
|
17660
|
-
const data = playback.data;
|
|
17661
|
-
const label = `${new Date(clip.timeRange.startMs).toLocaleString()} · ${clip.source}`;
|
|
17662
|
-
useEffect(() => {
|
|
17663
|
-
if (data === void 0) return;
|
|
17664
|
-
vod.play({
|
|
17665
|
-
url: hubPath(data.playbackUrl),
|
|
17666
|
-
...data.format === void 0 ? {} : { format: data.format },
|
|
17667
|
-
label
|
|
17668
|
-
});
|
|
17669
|
-
}, [data, label]);
|
|
17745
|
+
var Select = forwardRef(({ className, options, disabled, ...props }, ref) => {
|
|
17670
17746
|
return /* @__PURE__ */ jsxs("div", {
|
|
17671
|
-
className: "
|
|
17672
|
-
children: [
|
|
17673
|
-
|
|
17674
|
-
|
|
17675
|
-
|
|
17676
|
-
|
|
17677
|
-
/* @__PURE__ */
|
|
17678
|
-
|
|
17679
|
-
children:
|
|
17680
|
-
|
|
17681
|
-
|
|
17682
|
-
|
|
17683
|
-
|
|
17684
|
-
|
|
17685
|
-
onClick: () => onProfileChange(option),
|
|
17686
|
-
className: `rounded px-2 py-0.5 text-xs ${option === profile ? "bg-primary text-primary-foreground" : "bg-muted"}`,
|
|
17687
|
-
children: option
|
|
17688
|
-
}, option))]
|
|
17689
|
-
}),
|
|
17690
|
-
playback.status === "pending" ? /* @__PURE__ */ jsx("div", {
|
|
17691
|
-
role: "status",
|
|
17692
|
-
children: "Preparing the clip — the file is fetched and muxed once…"
|
|
17693
|
-
}) : playback.status === "error" ? /* @__PURE__ */ jsxs("div", {
|
|
17694
|
-
role: "alert",
|
|
17695
|
-
children: ["This clip could not be prepared: ", playback.error?.message ?? "no reason given"]
|
|
17696
|
-
}) : data === void 0 ? null : /* @__PURE__ */ jsxs("div", {
|
|
17697
|
-
className: "space-y-1",
|
|
17698
|
-
children: [data.served === void 0 || data.served === profile ? null : /* @__PURE__ */ jsxs("div", {
|
|
17699
|
-
className: "text-xs text-amber-600 dark:text-amber-400",
|
|
17700
|
-
children: [
|
|
17701
|
-
"Served the ",
|
|
17702
|
-
/* @__PURE__ */ jsx("strong", { children: data.served }),
|
|
17703
|
-
" stream — this clip has no ",
|
|
17704
|
-
profile,
|
|
17705
|
-
" twin."
|
|
17706
|
-
]
|
|
17707
|
-
}), /* @__PURE__ */ jsx("a", {
|
|
17708
|
-
href: hubPath(data.playbackUrl),
|
|
17709
|
-
download: true,
|
|
17710
|
-
className: "text-xs underline",
|
|
17711
|
-
children: "Download this clip"
|
|
17712
|
-
})]
|
|
17713
|
-
})
|
|
17714
|
-
]
|
|
17747
|
+
className: "relative w-full",
|
|
17748
|
+
children: [/* @__PURE__ */ jsx("select", {
|
|
17749
|
+
ref,
|
|
17750
|
+
disabled,
|
|
17751
|
+
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),
|
|
17752
|
+
...props,
|
|
17753
|
+
children: options.map((option) => /* @__PURE__ */ jsx("option", {
|
|
17754
|
+
value: option.value,
|
|
17755
|
+
children: option.label
|
|
17756
|
+
}, option.value))
|
|
17757
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
17758
|
+
className: cn("absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none text-foreground-muted", disabled === true ? "opacity-50" : ""),
|
|
17759
|
+
children: /* @__PURE__ */ jsx(ChevronDown, { className: "h-3.5 w-3.5" })
|
|
17760
|
+
})]
|
|
17715
17761
|
});
|
|
17716
|
-
}
|
|
17762
|
+
});
|
|
17763
|
+
Select.displayName = "Select";
|
|
17717
17764
|
//#endregion
|
|
17718
|
-
//#region src/
|
|
17719
|
-
|
|
17720
|
-
|
|
17721
|
-
|
|
17722
|
-
className: "
|
|
17723
|
-
|
|
17724
|
-
/* @__PURE__ */ jsx("span", {
|
|
17725
|
-
className: "font-medium",
|
|
17726
|
-
children: notice.label
|
|
17727
|
-
}),
|
|
17728
|
-
/* @__PURE__ */ jsxs("span", {
|
|
17729
|
-
className: "text-muted-foreground",
|
|
17730
|
-
children: [
|
|
17731
|
-
" (",
|
|
17732
|
-
notice.source,
|
|
17733
|
-
")"
|
|
17734
|
-
]
|
|
17735
|
-
}),
|
|
17736
|
-
/* @__PURE__ */ jsx("div", { children: notice.headline }),
|
|
17737
|
-
notice.detail === null ? null : /* @__PURE__ */ jsx("div", {
|
|
17738
|
-
className: "text-muted-foreground break-words",
|
|
17739
|
-
children: notice.detail
|
|
17740
|
-
})
|
|
17741
|
-
] }, notice.source))
|
|
17765
|
+
//#region src/primitives/skeleton.tsx
|
|
17766
|
+
var Skeleton = forwardRef(({ className, ...props }, ref) => {
|
|
17767
|
+
return /* @__PURE__ */ jsx("div", {
|
|
17768
|
+
ref,
|
|
17769
|
+
className: cn("animate-pulse bg-surface-hover rounded-md h-4 w-full", className),
|
|
17770
|
+
...props
|
|
17742
17771
|
});
|
|
17743
|
-
}
|
|
17772
|
+
});
|
|
17773
|
+
Skeleton.displayName = "Skeleton";
|
|
17744
17774
|
//#endregion
|
|
17745
|
-
//#region src/composites/
|
|
17746
|
-
|
|
17747
|
-
|
|
17748
|
-
|
|
17749
|
-
|
|
17750
|
-
|
|
17751
|
-
|
|
17752
|
-
|
|
17753
|
-
|
|
17754
|
-
|
|
17755
|
-
|
|
17756
|
-
|
|
17757
|
-
|
|
17758
|
-
|
|
17759
|
-
|
|
17760
|
-
|
|
17761
|
-
|
|
17762
|
-
|
|
17763
|
-
|
|
17764
|
-
|
|
17765
|
-
|
|
17766
|
-
|
|
17767
|
-
|
|
17768
|
-
|
|
17769
|
-
|
|
17770
|
-
|
|
17771
|
-
|
|
17772
|
-
|
|
17773
|
-
|
|
17774
|
-
|
|
17775
|
-
|
|
17776
|
-
|
|
17777
|
-
|
|
17778
|
-
|
|
17779
|
-
|
|
17780
|
-
|
|
17781
|
-
|
|
17782
|
-
|
|
17783
|
-
|
|
17784
|
-
|
|
17785
|
-
|
|
17786
|
-
|
|
17787
|
-
|
|
17788
|
-
|
|
17789
|
-
|
|
17790
|
-
|
|
17775
|
+
//#region src/composites/cap-settings/recording-timeline-model.ts
|
|
17776
|
+
/** Tie-break precedence for a bucket's dominant kind (first wins on equal count). */
|
|
17777
|
+
var KIND_PRECEDENCE = [
|
|
17778
|
+
"object",
|
|
17779
|
+
"motion",
|
|
17780
|
+
"audio"
|
|
17781
|
+
];
|
|
17782
|
+
/**
|
|
17783
|
+
* Convert server-side density rows into `DensityBucket[]` for the timeline
|
|
17784
|
+
* heatmap. Rows with total === 0 are skipped (defensive; server already omits
|
|
17785
|
+
* them). Returns a sparse, ascending-by-index array — EventHeatmap positions
|
|
17786
|
+
* each bar by `b.startMs` rather than array index, so gaps are fine.
|
|
17787
|
+
*/
|
|
17788
|
+
function densityRowsToBuckets(rows, windowFrom, bucketMs) {
|
|
17789
|
+
const buckets = [];
|
|
17790
|
+
for (const row of rows) {
|
|
17791
|
+
const counts = {
|
|
17792
|
+
motion: row.motion,
|
|
17793
|
+
object: row.object,
|
|
17794
|
+
audio: row.audio
|
|
17795
|
+
};
|
|
17796
|
+
const total = counts.motion + counts.object + counts.audio;
|
|
17797
|
+
if (total === 0) continue;
|
|
17798
|
+
const index = Math.floor((row.bucketStart - windowFrom) / bucketMs);
|
|
17799
|
+
let dominant = null;
|
|
17800
|
+
let best = -1;
|
|
17801
|
+
for (const kind of KIND_PRECEDENCE) if (counts[kind] > best) {
|
|
17802
|
+
best = counts[kind];
|
|
17803
|
+
dominant = kind;
|
|
17804
|
+
}
|
|
17805
|
+
buckets.push({
|
|
17806
|
+
index,
|
|
17807
|
+
startMs: row.bucketStart,
|
|
17808
|
+
endMs: row.bucketStart + bucketMs,
|
|
17809
|
+
counts,
|
|
17810
|
+
total,
|
|
17811
|
+
dominant
|
|
17812
|
+
});
|
|
17813
|
+
}
|
|
17814
|
+
return buckets.toSorted((a, b) => a.index - b.index);
|
|
17815
|
+
}
|
|
17816
|
+
/** Distance from `ms` to the interval `[startMs, endMs]` (0 if inside). */
|
|
17817
|
+
function distanceToSpan(ms, span) {
|
|
17818
|
+
if (ms < span.startMs) return span.startMs - ms;
|
|
17819
|
+
if (ms > span.endMs) return ms - span.endMs;
|
|
17820
|
+
return 0;
|
|
17791
17821
|
}
|
|
17792
|
-
//#endregion
|
|
17793
|
-
//#region src/composites/clips/clip-tile-media.ts
|
|
17794
17822
|
/**
|
|
17795
|
-
*
|
|
17796
|
-
*
|
|
17823
|
+
* Resolve a playback window for a click at `clickMs` against recorded `spans`.
|
|
17824
|
+
* Inside a span → play from the click to that span's end. In a gap / outside →
|
|
17825
|
+
* snap to the WHOLE nearest span. No spans → null (nothing to play).
|
|
17826
|
+
* `spans` are expected ascending by start (as `mergeAvailabilitySpans` returns).
|
|
17797
17827
|
*/
|
|
17798
|
-
|
|
17799
|
-
|
|
17800
|
-
|
|
17801
|
-
|
|
17802
|
-
|
|
17803
|
-
|
|
17804
|
-
"no-catalog-row": "This clip is no longer in the catalog"
|
|
17805
|
-
};
|
|
17806
|
-
function refusal(reason) {
|
|
17807
|
-
return {
|
|
17808
|
-
kind: "unavailable",
|
|
17809
|
-
reason,
|
|
17810
|
-
text: REFUSAL_TEXT[reason] ?? `Refused: ${reason}`
|
|
17828
|
+
function seekTargetForSpans(clickMs, spans) {
|
|
17829
|
+
if (spans.length === 0) return null;
|
|
17830
|
+
const containing = spans.find((s) => clickMs >= s.startMs && clickMs <= s.endMs);
|
|
17831
|
+
if (containing) return {
|
|
17832
|
+
fromMs: clickMs,
|
|
17833
|
+
toMs: containing.endMs
|
|
17811
17834
|
};
|
|
17835
|
+
let nearest = null;
|
|
17836
|
+
let bestDist = Infinity;
|
|
17837
|
+
for (const s of spans) {
|
|
17838
|
+
const d = distanceToSpan(clickMs, s);
|
|
17839
|
+
if (d < bestDist) {
|
|
17840
|
+
bestDist = d;
|
|
17841
|
+
nearest = s;
|
|
17842
|
+
}
|
|
17843
|
+
}
|
|
17844
|
+
return nearest ? {
|
|
17845
|
+
fromMs: nearest.startMs,
|
|
17846
|
+
toMs: nearest.endMs
|
|
17847
|
+
} : null;
|
|
17812
17848
|
}
|
|
17813
|
-
/**
|
|
17814
|
-
|
|
17815
|
-
|
|
17816
|
-
|
|
17817
|
-
|
|
17818
|
-
|
|
17819
|
-
if (
|
|
17820
|
-
|
|
17821
|
-
|
|
17822
|
-
|
|
17823
|
-
|
|
17824
|
-
return {
|
|
17825
|
-
kind: "none",
|
|
17826
|
-
text: "This clip carries no still and offered none"
|
|
17827
|
-
};
|
|
17849
|
+
/**
|
|
17850
|
+
* Position `ms` inside the window `[fromMs, toMs]` as a clamped percentage
|
|
17851
|
+
* (0–100). A zero/negative-width window yields 0. Callers use this for `left:`.
|
|
17852
|
+
*/
|
|
17853
|
+
function pctForMs(ms, fromMs, toMs) {
|
|
17854
|
+
const span = toMs - fromMs;
|
|
17855
|
+
if (span <= 0) return 0;
|
|
17856
|
+
const pct = (ms - fromMs) / span * 100;
|
|
17857
|
+
if (pct < 0) return 0;
|
|
17858
|
+
if (pct > 100) return 100;
|
|
17859
|
+
return pct;
|
|
17828
17860
|
}
|
|
17829
17861
|
/**
|
|
17830
|
-
*
|
|
17831
|
-
*
|
|
17832
|
-
*
|
|
17833
|
-
*
|
|
17834
|
-
*
|
|
17835
|
-
* firmware inventing a refusal must not become a blank tile.
|
|
17862
|
+
* Build a DayWindow for the given calendar `date` using local time.
|
|
17863
|
+
* For any day strictly in the past: `fromMs` = local midnight, `toMs` =
|
|
17864
|
+
* next local midnight (exactly 24 h).
|
|
17865
|
+
* For today (or any future day that contains `nowMs`): `toMs = nowMs` so the
|
|
17866
|
+
* window never extends into the future.
|
|
17836
17867
|
*/
|
|
17837
|
-
function
|
|
17838
|
-
|
|
17868
|
+
function dayWindow(date, nowMs) {
|
|
17869
|
+
const fromMs = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0, 0).getTime();
|
|
17870
|
+
const nextMidnight = new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1, 0, 0, 0, 0).getTime();
|
|
17871
|
+
return {
|
|
17872
|
+
fromMs,
|
|
17873
|
+
toMs: Math.max(fromMs, Math.min(nextMidnight, nowMs))
|
|
17874
|
+
};
|
|
17839
17875
|
}
|
|
17840
|
-
|
|
17841
|
-
|
|
17876
|
+
/** Available zoom levels in ms, from widest (24h) to narrowest (15 min). */
|
|
17877
|
+
var ZOOM_LEVELS = [
|
|
17878
|
+
24 * 36e5,
|
|
17879
|
+
12 * 36e5,
|
|
17880
|
+
6 * 36e5,
|
|
17881
|
+
36e5,
|
|
17882
|
+
15 * 6e4
|
|
17883
|
+
];
|
|
17842
17884
|
/**
|
|
17843
|
-
*
|
|
17844
|
-
*
|
|
17845
|
-
*
|
|
17846
|
-
*
|
|
17847
|
-
*
|
|
17848
|
-
* a 204 fires `onerror` with nothing to say, turning a named refusal into an
|
|
17849
|
-
* anonymous broken tile. So a mint goes through `fetch`.
|
|
17850
|
-
*
|
|
17851
|
-
* **Nothing is appended to the URL.** Both data-plane routes are registered
|
|
17852
|
-
* `access: 'authenticated'` and the hub's `/addon/<id>/<prefix>` proxy takes
|
|
17853
|
-
* the session cookie the admin UI sets at login, so the URL is never a
|
|
17854
|
-
* credential (D549 decision 22) — it may sit in history or a `Referer` and
|
|
17855
|
-
* open nothing.
|
|
17885
|
+
* Produce a `levelMs`-wide window whose RIGHT edge stays anchored at
|
|
17886
|
+
* `rightEdgeMs` (clamped within `dayBounds`), rather than centring on the view
|
|
17887
|
+
* midpoint. On today's timeline the view's right edge is "now", so zooming this
|
|
17888
|
+
* way keeps the most recent footage pinned to the right instead of drifting to
|
|
17889
|
+
* a midday range that no longer contains now. Panning then moves back/forward.
|
|
17856
17890
|
*
|
|
17857
|
-
*
|
|
17858
|
-
*
|
|
17891
|
+
* Falls back to `dayBounds` when `levelMs` meets or exceeds the day span (can't
|
|
17892
|
+
* show a wider window than the day holds — e.g. early morning with little
|
|
17893
|
+
* elapsed time).
|
|
17859
17894
|
*/
|
|
17895
|
+
function zoomWindowAnchoredRight(dayBounds, levelMs, rightEdgeMs) {
|
|
17896
|
+
if (levelMs >= dayBounds.toMs - dayBounds.fromMs) return dayBounds;
|
|
17897
|
+
const toMs = Math.min(Math.max(rightEdgeMs, dayBounds.fromMs + levelMs), dayBounds.toMs);
|
|
17898
|
+
return {
|
|
17899
|
+
fromMs: toMs - levelMs,
|
|
17900
|
+
toMs
|
|
17901
|
+
};
|
|
17902
|
+
}
|
|
17860
17903
|
/**
|
|
17861
|
-
*
|
|
17862
|
-
*
|
|
17863
|
-
* Never rejects: a thrown fetch is a refusal like any other, because a tile
|
|
17864
|
-
* that throws is a tile that renders nothing and says nothing — the exact
|
|
17865
|
-
* failure this module exists to prevent.
|
|
17904
|
+
* Shift `win` by `deltaMs`, preserving width, clamped within `dayBounds`.
|
|
17866
17905
|
*/
|
|
17867
|
-
|
|
17868
|
-
|
|
17869
|
-
|
|
17870
|
-
|
|
17871
|
-
|
|
17872
|
-
|
|
17906
|
+
function panWindow(win, dayBounds, deltaMs) {
|
|
17907
|
+
const width = win.toMs - win.fromMs;
|
|
17908
|
+
let fromMs = win.fromMs + deltaMs;
|
|
17909
|
+
let toMs = win.toMs + deltaMs;
|
|
17910
|
+
if (fromMs < dayBounds.fromMs) {
|
|
17911
|
+
fromMs = dayBounds.fromMs;
|
|
17912
|
+
toMs = fromMs + width;
|
|
17873
17913
|
}
|
|
17874
|
-
if (
|
|
17875
|
-
|
|
17876
|
-
|
|
17877
|
-
return {
|
|
17878
|
-
kind: "image",
|
|
17879
|
-
blob: await response.blob()
|
|
17880
|
-
};
|
|
17881
|
-
} catch (err) {
|
|
17882
|
-
return latchMintRefusal(err instanceof Error ? err.message : String(err));
|
|
17914
|
+
if (toMs > dayBounds.toMs) {
|
|
17915
|
+
toMs = dayBounds.toMs;
|
|
17916
|
+
fromMs = toMs - width;
|
|
17883
17917
|
}
|
|
17918
|
+
return {
|
|
17919
|
+
fromMs,
|
|
17920
|
+
toMs
|
|
17921
|
+
};
|
|
17884
17922
|
}
|
|
17885
|
-
|
|
17886
|
-
|
|
17923
|
+
/** Candidate bucket sizes in ms (coarsest first). */
|
|
17924
|
+
var BUCKET_CANDIDATES = [
|
|
17925
|
+
15 * 6e4,
|
|
17926
|
+
5 * 6e4,
|
|
17927
|
+
6e4,
|
|
17928
|
+
3e4,
|
|
17929
|
+
15e3,
|
|
17930
|
+
5e3
|
|
17931
|
+
];
|
|
17887
17932
|
/**
|
|
17888
|
-
*
|
|
17889
|
-
*
|
|
17890
|
-
* The picture is resolved by `resolveTileMedia` — vouched JPEG, an offer, or
|
|
17891
|
-
* the reason there is none (D549 decision 5). An offer is fetched rather than
|
|
17892
|
-
* dropped into an `<img src>`, because the route's **204 + x-camstack-reason**
|
|
17893
|
-
* is invisible to an `<img>`: it fires `onerror` with nothing to say. The
|
|
17894
|
-
* refusal is LATCHED for the life of the tile and never retried behind the
|
|
17895
|
-
* operator's back.
|
|
17896
|
-
*
|
|
17897
|
-
* The row also carries what the camera itself said: its native types, the
|
|
17898
|
-
* behaviours beside the class, which storages hold it, and — for a row that
|
|
17899
|
-
* is an EVENT sitting inside no file — `playable: false` with its reason
|
|
17900
|
-
* (measured: 1 of 29 hub events on 3628). Such a row is SHOWN, never dropped
|
|
17901
|
-
* and never offered as playable-then-failing (D549 decision 19).
|
|
17933
|
+
* Choose a sensible heatmap bucket size for a visible span of `spanMs`,
|
|
17934
|
+
* targeting ~96-120 buckets. Always picks from `BUCKET_CANDIDATES`.
|
|
17902
17935
|
*/
|
|
17903
|
-
function
|
|
17904
|
-
|
|
17905
|
-
|
|
17936
|
+
function bucketMsForSpan(spanMs) {
|
|
17937
|
+
const targetBucket = spanMs / 100;
|
|
17938
|
+
for (const candidate of BUCKET_CANDIDATES) if (candidate <= targetBucket) return candidate;
|
|
17939
|
+
return BUCKET_CANDIDATES[BUCKET_CANDIDATES.length - 1] ?? 5e3;
|
|
17940
|
+
}
|
|
17941
|
+
/**
|
|
17942
|
+
* Produce `count` evenly-spaced axis ticks spanning [fromMs, toMs].
|
|
17943
|
+
* - `pct` is via pctForMs (first=0, last=100).
|
|
17944
|
+
* - `label` is a localised HH:MM clock string.
|
|
17945
|
+
*/
|
|
17946
|
+
function axisTicks(fromMs, toMs, count) {
|
|
17947
|
+
if (count <= 1) return [{
|
|
17948
|
+
pct: 0,
|
|
17949
|
+
label: new Date(fromMs).toLocaleTimeString(void 0, {
|
|
17950
|
+
hour: "2-digit",
|
|
17951
|
+
minute: "2-digit"
|
|
17952
|
+
})
|
|
17953
|
+
}];
|
|
17954
|
+
const ticks = [];
|
|
17955
|
+
for (let i = 0; i < count; i++) {
|
|
17956
|
+
const ms = fromMs + i / (count - 1) * (toMs - fromMs);
|
|
17957
|
+
const pct = pctForMs(ms, fromMs, toMs);
|
|
17958
|
+
const label = new Date(ms).toLocaleTimeString(void 0, {
|
|
17959
|
+
hour: "2-digit",
|
|
17960
|
+
minute: "2-digit"
|
|
17961
|
+
});
|
|
17962
|
+
ticks.push({
|
|
17963
|
+
pct,
|
|
17964
|
+
label
|
|
17965
|
+
});
|
|
17966
|
+
}
|
|
17967
|
+
return ticks;
|
|
17968
|
+
}
|
|
17969
|
+
//#endregion
|
|
17970
|
+
//#region src/hooks/use-is-mobile.ts
|
|
17971
|
+
var MOBILE_QUERY = "(max-width: 767px)";
|
|
17972
|
+
/**
|
|
17973
|
+
* Mid breakpoint — below this width the sidebar collapses to an
|
|
17974
|
+
* icons-only rail to reclaim canvas space. Above it (and outside
|
|
17975
|
+
* mobile) the full sidebar renders. Threshold matches Tailwind `lg`
|
|
17976
|
+
* so sidebar fans out at the same point grids switch to multi-column.
|
|
17977
|
+
*/
|
|
17978
|
+
var MID_QUERY = "(min-width: 768px) and (max-width: 1023px)";
|
|
17979
|
+
/**
|
|
17980
|
+
* `matchMedia` is universal in browsers but absent in bare DOM environments.
|
|
17981
|
+
* These hooks are called from shared primitives (`DataTable` picks its narrow
|
|
17982
|
+
* layout with one), so throwing here takes down the whole page that merely
|
|
17983
|
+
* rendered a table. Where the capability is missing, report the desktop
|
|
17984
|
+
* layout — a widescreen rendering is wrong-looking; a crashed page is gone.
|
|
17985
|
+
*/
|
|
17986
|
+
function matchQuery(query) {
|
|
17987
|
+
if (typeof window.matchMedia !== "function") return null;
|
|
17988
|
+
return window.matchMedia(query);
|
|
17989
|
+
}
|
|
17990
|
+
function subscribeQuery(query) {
|
|
17991
|
+
return (callback) => {
|
|
17992
|
+
const mql = matchQuery(query);
|
|
17993
|
+
if (mql === null) return () => {};
|
|
17994
|
+
mql.addEventListener("change", callback);
|
|
17995
|
+
return () => mql.removeEventListener("change", callback);
|
|
17996
|
+
};
|
|
17997
|
+
}
|
|
17998
|
+
function getSnapshot(query) {
|
|
17999
|
+
return () => matchQuery(query)?.matches ?? false;
|
|
18000
|
+
}
|
|
18001
|
+
function getServerSnapshot() {
|
|
18002
|
+
return false;
|
|
18003
|
+
}
|
|
18004
|
+
function useIsMobile() {
|
|
18005
|
+
return useSyncExternalStore(subscribeQuery(MOBILE_QUERY), getSnapshot(MOBILE_QUERY), getServerSnapshot);
|
|
18006
|
+
}
|
|
18007
|
+
/**
|
|
18008
|
+
* True when the viewport is in the medium (`md`) Tailwind range —
|
|
18009
|
+
* 768px ≤ width < 1024px. Use to drive layouts that should pack denser
|
|
18010
|
+
* than full-desktop but more legibly than mobile (e.g. icons-only
|
|
18011
|
+
* sidebar rail, narrower stat tiles).
|
|
18012
|
+
*/
|
|
18013
|
+
function useIsMidWidth() {
|
|
18014
|
+
return useSyncExternalStore(subscribeQuery(MID_QUERY), getSnapshot(MID_QUERY), getServerSnapshot);
|
|
18015
|
+
}
|
|
18016
|
+
//#endregion
|
|
18017
|
+
//#region src/composites/data-table-layout.ts
|
|
18018
|
+
/**
|
|
18019
|
+
* At this many columns and above, `auto` switches a narrow viewport to cards.
|
|
18020
|
+
* Three columns still fit a phone at a readable size; four do not.
|
|
18021
|
+
*/
|
|
18022
|
+
var CARD_MODE_MIN_COLUMNS = 4;
|
|
18023
|
+
function resolveTableLayout({ mode, columnCount, isNarrow }) {
|
|
18024
|
+
if (!isNarrow) return "table";
|
|
18025
|
+
if (mode === "scroll") return "table";
|
|
18026
|
+
if (mode === "cards") return "cards";
|
|
18027
|
+
if (columnCount === 0) return "table";
|
|
18028
|
+
return columnCount >= 4 ? "cards" : "table";
|
|
18029
|
+
}
|
|
18030
|
+
//#endregion
|
|
18031
|
+
//#region src/lib/responsive.ts
|
|
18032
|
+
/**
|
|
18033
|
+
* Responsive design tokens — three-tier system.
|
|
18034
|
+
*
|
|
18035
|
+
* The admin-ui targets three width tiers; every component should pick
|
|
18036
|
+
* the right token from this file instead of hand-coding breakpoints.
|
|
18037
|
+
* Adopting the tokens uniformly is what makes the app feel consistent
|
|
18038
|
+
* across operator devices (mobile / tablet / desktop) and what unlocks
|
|
18039
|
+
* targeted tuning (e.g. shrink every label by 1px in one place).
|
|
18040
|
+
*
|
|
18041
|
+
* Tier definitions (Tailwind breakpoints):
|
|
18042
|
+
*
|
|
18043
|
+
* • **L1 — Compact** (`< 640px`, default = mobile portrait)
|
|
18044
|
+
* Single column, tight padding, smallest text, icon-only buttons
|
|
18045
|
+
* where labels can be inferred. The operator can still see every
|
|
18046
|
+
* stat / action — no horizontal scroll, no clipped text. The user
|
|
18047
|
+
* directive: "molta informazione in poco spazio quando la width è
|
|
18048
|
+
* bassa."
|
|
18049
|
+
*
|
|
18050
|
+
* • **L2 — Standard** (`sm` — `640–1023px`, tablet + small laptop)
|
|
18051
|
+
* 2-column grids, side panels become visible, button labels return.
|
|
18052
|
+
* Targets ~768px–1024px viewports — the most common operator sizes.
|
|
18053
|
+
*
|
|
18054
|
+
* • **L3 — Spacious** (`lg+` — `≥ 1024px`, desktop)
|
|
18055
|
+
* 4-column grids, generous padding, full-width header text,
|
|
18056
|
+
* side-by-side composite layouts. The operator has elbow room.
|
|
18057
|
+
*
|
|
18058
|
+
* Conventions:
|
|
18059
|
+
* - **Always start from L1** — the bare class is always the compact
|
|
18060
|
+
* variant, then `sm:` adds room at 640px, `lg:` at 1024px. No
|
|
18061
|
+
* desktop-first breakpoints; no max-width queries.
|
|
18062
|
+
* - **Token strings are static** — kept as `as const` so callers can
|
|
18063
|
+
* compose them via `cn()` / template literals without running a
|
|
18064
|
+
* class generator at runtime.
|
|
18065
|
+
* - **Pair the token with a comment** — the comment in the file
|
|
18066
|
+
* names every breakpoint it crosses, so a reader can predict the
|
|
18067
|
+
* layout without going to Tailwind docs.
|
|
18068
|
+
*/
|
|
18069
|
+
/** Container padding for a card-style section (header + body). */
|
|
18070
|
+
var SECTION_CARD = "rounded-lg border border-border bg-surface overflow-hidden";
|
|
18071
|
+
/** Header strip inside a card (between border-b lines). */
|
|
18072
|
+
var SECTION_HEADER = "border-b border-border px-3 sm:px-4 py-1.5 sm:py-2";
|
|
18073
|
+
/** Body padding for a card-style section. */
|
|
18074
|
+
var SECTION_BODY = "p-2 sm:p-3 lg:p-4";
|
|
18075
|
+
/** Gap between cards in a vertical stack. */
|
|
18076
|
+
var STACK_GAP = "space-y-3 sm:space-y-4";
|
|
18077
|
+
/** Gap between cards in a grid (4 cells / row at lg). */
|
|
18078
|
+
var GRID_GAP = "gap-2 sm:gap-3";
|
|
18079
|
+
/** Row padding inside a divided list (e.g. InfoRow). */
|
|
18080
|
+
var LIST_ROW = "px-3 sm:px-4 py-1.5 sm:py-2";
|
|
18081
|
+
/** 1-up at L1, 2-up at L2, 4-up at L3 — quick-stat row. */
|
|
18082
|
+
var GRID_QUICK_STATS = "grid grid-cols-2 sm:grid-cols-2 lg:grid-cols-4";
|
|
18083
|
+
/** 1-up at L1, 2-up at L2+ — paired panels (motion + detection rules). */
|
|
18084
|
+
var GRID_PAIRED = "grid grid-cols-1 lg:grid-cols-2";
|
|
18085
|
+
/** Side-by-side layout that stacks below md (canvas + side panel). */
|
|
18086
|
+
var SPLIT_PANEL_OUTER = "flex flex-col-reverse md:flex-row";
|
|
18087
|
+
/** Sidebar narrow lane in a SPLIT_PANEL_OUTER layout. */
|
|
18088
|
+
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";
|
|
18089
|
+
/**
|
|
18090
|
+
* The tier boundary at which a settings label/value row stops stacking.
|
|
18091
|
+
*
|
|
18092
|
+
* Below it (L1 — compact) the row is ONE column: label on its own line, value
|
|
18093
|
+
* on the next at full width. Above it the row is label-left / value-right as
|
|
18094
|
+
* before. The two-column form needs `SETTING_ROW_LABEL`'s lane plus a readable
|
|
18095
|
+
* value beside it; a phone has neither, and the failure mode is not "cramped"
|
|
18096
|
+
* but "the value is the part that gets cut" — the one thing the operator
|
|
18097
|
+
* opened the page to read.
|
|
18098
|
+
*/
|
|
18099
|
+
var SETTING_ROW_STACK_BREAKPOINT = "sm";
|
|
18100
|
+
/** Outer row: single column at L1, label/value columns from `sm` up. */
|
|
18101
|
+
var SETTING_ROW = "flex min-w-0 flex-col gap-0.5 py-1.5 sm:flex-row sm:items-center";
|
|
18102
|
+
/** Label lane. Full width (and free to wrap) at L1; a fixed lane from `sm` up. */
|
|
18103
|
+
var SETTING_ROW_LABEL = "min-w-0 text-[11px] leading-tight text-foreground-subtle sm:w-32 sm:shrink-0 sm:pr-2";
|
|
18104
|
+
/**
|
|
18105
|
+
* Value lane — the value text plus its trailing affordances (copy / reveal).
|
|
18106
|
+
* `w-full` at L1 so the value owns the whole row once the label is above it;
|
|
18107
|
+
* `sm:flex-1` makes it share the row again from `sm` up.
|
|
18108
|
+
*/
|
|
18109
|
+
var SETTING_ROW_VALUE = "flex w-full min-w-0 items-center gap-1 sm:flex-1";
|
|
18110
|
+
/**
|
|
18111
|
+
* The value text itself. `break-all` lets an opaque token (a key, a URL) wrap
|
|
18112
|
+
* at L1 instead of being cut mid-word; `sm:truncate` restores the single-line
|
|
18113
|
+
* desktop form, where the row is wide enough for truncation to be a choice
|
|
18114
|
+
* rather than data loss.
|
|
18115
|
+
*/
|
|
18116
|
+
var SETTING_ROW_VALUE_TEXT = "min-w-0 break-all text-xs text-foreground sm:truncate";
|
|
18117
|
+
/** Section header label (uppercase tracking-wider). */
|
|
18118
|
+
var TEXT_SECTION_LABEL = "text-[10px] sm:text-[11px] font-semibold text-foreground uppercase tracking-wider";
|
|
18119
|
+
/** Field label inside a row. */
|
|
18120
|
+
var TEXT_FIELD_LABEL = "text-[11px] sm:text-xs text-foreground";
|
|
18121
|
+
/** Sub-label / hint text. */
|
|
18122
|
+
var TEXT_HINT = "text-[10px] text-foreground-subtle";
|
|
18123
|
+
/** Primary value (a metric, badge, …). */
|
|
18124
|
+
var TEXT_VALUE = "text-[11px] sm:text-xs font-medium text-foreground";
|
|
18125
|
+
/** Big metric value (StatCard). */
|
|
18126
|
+
var TEXT_METRIC = "text-base sm:text-lg font-semibold leading-tight text-foreground";
|
|
18127
|
+
/** Default compact button. */
|
|
18128
|
+
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";
|
|
18129
|
+
/** Primary compact button. */
|
|
18130
|
+
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";
|
|
18131
|
+
/** Danger compact button. */
|
|
18132
|
+
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";
|
|
18133
|
+
/** Warning compact button (e.g. cancel-drawing). */
|
|
18134
|
+
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";
|
|
18135
|
+
/** Compact text/select/number input. */
|
|
18136
|
+
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";
|
|
18137
|
+
/** Chip-style toggle (zone / class selectors in ZoneRulesEditor). */
|
|
18138
|
+
var CHIP_BASE = "inline-flex items-center gap-1 rounded-full border px-1.5 py-0.5 text-[10px] font-medium transition-colors";
|
|
18139
|
+
var CHIP_ACTIVE = "border-primary/50 bg-primary/15 text-primary";
|
|
18140
|
+
var CHIP_INACTIVE = "border-border bg-surface text-foreground-subtle hover:bg-surface-hover";
|
|
18141
|
+
//#endregion
|
|
18142
|
+
//#region src/composites/setting-row.tsx
|
|
18143
|
+
function SettingRow({ label, children, actions, elements = "plain", className, valueClassName }) {
|
|
18144
|
+
const LabelTag = elements === "description" ? "dt" : "span";
|
|
18145
|
+
const ValueTag = elements === "description" ? "dd" : "div";
|
|
18146
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
18147
|
+
"data-setting-row": "",
|
|
18148
|
+
className: cn(SETTING_ROW, className),
|
|
18149
|
+
children: [/* @__PURE__ */ jsx(LabelTag, {
|
|
18150
|
+
"data-setting-row-label": "",
|
|
18151
|
+
className: SETTING_ROW_LABEL,
|
|
18152
|
+
children: label
|
|
18153
|
+
}), /* @__PURE__ */ jsxs(ValueTag, {
|
|
18154
|
+
"data-setting-row-value": "",
|
|
18155
|
+
className: SETTING_ROW_VALUE,
|
|
18156
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
18157
|
+
"data-setting-row-value-text": "",
|
|
18158
|
+
className: cn(SETTING_ROW_VALUE_TEXT, valueClassName),
|
|
18159
|
+
children
|
|
18160
|
+
}), actions !== void 0 && /* @__PURE__ */ jsx("span", {
|
|
18161
|
+
className: "flex shrink-0 items-center gap-0.5",
|
|
18162
|
+
children: actions
|
|
18163
|
+
})]
|
|
18164
|
+
})]
|
|
18165
|
+
});
|
|
18166
|
+
}
|
|
18167
|
+
//#endregion
|
|
18168
|
+
//#region src/composites/sort-header-button.tsx
|
|
18169
|
+
function SortHeaderButton({ label, active, dir, onSort }) {
|
|
18170
|
+
return /* @__PURE__ */ jsxs("button", {
|
|
18171
|
+
type: "button",
|
|
18172
|
+
onClick: onSort,
|
|
18173
|
+
className: "group/sort inline-flex items-center gap-0.5 hover:text-foreground transition-colors",
|
|
18174
|
+
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") })]
|
|
18175
|
+
});
|
|
18176
|
+
}
|
|
18177
|
+
//#endregion
|
|
18178
|
+
//#region src/composites/table-sort.ts
|
|
18179
|
+
/**
|
|
18180
|
+
* The toggle rule, and the only copy of it.
|
|
18181
|
+
*
|
|
18182
|
+
* none → asc → desc → none, and a click on a DIFFERENT column starts that
|
|
18183
|
+
* column fresh at `asc`. `sameColumn` says whether the clicked column is the
|
|
18184
|
+
* one currently sorted; `currentDir` is its direction (`null` when nothing is
|
|
18185
|
+
* sorted). Returns the next direction, or `null` for "unsorted".
|
|
18186
|
+
*
|
|
18187
|
+
* Split out from {@link nextTableSort} so the device list — whose sort state
|
|
18188
|
+
* keys columns by `columnId`, not `columnKey` — can reuse the rule without a
|
|
18189
|
+
* cast and without a second copy of it.
|
|
18190
|
+
*/
|
|
18191
|
+
function nextSortDirection(sameColumn, currentDir) {
|
|
18192
|
+
if (!sameColumn || currentDir === null) return "asc";
|
|
18193
|
+
return currentDir === "asc" ? "desc" : null;
|
|
18194
|
+
}
|
|
18195
|
+
/**
|
|
18196
|
+
* Toggle a column's sort on header click: none → asc → desc → none.
|
|
18197
|
+
* Clicking a DIFFERENT column starts it fresh at `asc`.
|
|
18198
|
+
*/
|
|
18199
|
+
function nextTableSort(current, columnKey) {
|
|
18200
|
+
const dir = nextSortDirection(current !== null && current.columnKey === columnKey, current === null ? null : current.dir);
|
|
18201
|
+
return dir === null ? null : {
|
|
18202
|
+
columnKey,
|
|
18203
|
+
dir
|
|
18204
|
+
};
|
|
18205
|
+
}
|
|
18206
|
+
/**
|
|
18207
|
+
* Ascending comparison of two cell values.
|
|
18208
|
+
*
|
|
18209
|
+
* Numbers compare numerically and strings compare with `localeCompare` — a
|
|
18210
|
+
* numeric column must never be ordered as text ("100" before "9"), and a text
|
|
18211
|
+
* column must respect the locale's collation. A mixed column (one row a
|
|
18212
|
+
* number, another a string) falls back to string comparison of both, which is
|
|
18213
|
+
* the only total order available; it is not a shape any caller should build on
|
|
18214
|
+
* purpose.
|
|
18215
|
+
*
|
|
18216
|
+
* `null` is NOT handled here — {@link sortRowsByColumn} pulls it out first,
|
|
18217
|
+
* because its placement is direction-independent and a comparator that is
|
|
18218
|
+
* multiplied by the direction factor cannot express that.
|
|
18219
|
+
*/
|
|
18220
|
+
function compareValues(a, b) {
|
|
18221
|
+
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
18222
|
+
return String(a).localeCompare(String(b));
|
|
18223
|
+
}
|
|
18224
|
+
/**
|
|
18225
|
+
* Return a NEW array of `rows` ordered by `sort`.
|
|
18226
|
+
*
|
|
18227
|
+
* `valueOf` is the column's `sortValue` — it is called once per row per
|
|
18228
|
+
* comparison, so it must be cheap (read a field, do not format a string).
|
|
18229
|
+
* `sort === null` ⇒ a fresh shallow copy in the incoming order.
|
|
18230
|
+
*/
|
|
18231
|
+
function sortRowsByColumn(rows, sort, valueOf) {
|
|
18232
|
+
if (sort === null) return [...rows];
|
|
18233
|
+
const factor = sort.dir === "asc" ? 1 : -1;
|
|
18234
|
+
return rows.toSorted((a, b) => {
|
|
18235
|
+
const av = valueOf(a);
|
|
18236
|
+
const bv = valueOf(b);
|
|
18237
|
+
if (av === null || bv === null) {
|
|
18238
|
+
if (av === bv) return 0;
|
|
18239
|
+
return av === null ? 1 : -1;
|
|
18240
|
+
}
|
|
18241
|
+
return factor * compareValues(av, bv);
|
|
18242
|
+
});
|
|
18243
|
+
}
|
|
18244
|
+
/** Derive the `<th>` `aria-sort` for `columnKey` from the table's sort state. */
|
|
18245
|
+
function ariaSortForColumn(sort, columnKey) {
|
|
18246
|
+
if (sort === null || sort.columnKey !== columnKey) return "none";
|
|
18247
|
+
return sort.dir === "asc" ? "ascending" : "descending";
|
|
18248
|
+
}
|
|
18249
|
+
//#endregion
|
|
18250
|
+
//#region src/composites/data-table.tsx
|
|
18251
|
+
/**
|
|
18252
|
+
* DataTable — shared responsive table for admin pages.
|
|
18253
|
+
*
|
|
18254
|
+
* Recipes baked in:
|
|
18255
|
+
* - `overflow-x-auto` wrapper so narrow viewports don't clip the
|
|
18256
|
+
* right edge (the rounded border survives the scroll).
|
|
18257
|
+
* - `min-w-[Npx]` on the inner table so columns don't squash below
|
|
18258
|
+
* readable width before the scroll kicks in.
|
|
18259
|
+
* - `whitespace-nowrap` on headers — column titles never wrap to
|
|
18260
|
+
* two lines (`AUTO-KILL` was the regression that triggered this).
|
|
18261
|
+
* - Themed `bg-surface` + `border-border` so contrast works in both
|
|
18262
|
+
* light and dark themes.
|
|
18263
|
+
*
|
|
18264
|
+
* Column model: caller supplies `columns: DataColumn<T>[]` and
|
|
18265
|
+
* `rows: T[]`. Each column declares `header`, optional `align`,
|
|
18266
|
+
* optional `nowrap`, and a `render(row) => ReactNode` that produces
|
|
18267
|
+
* the cell content.
|
|
18268
|
+
*
|
|
18269
|
+
* <DataTable
|
|
18270
|
+
* columns={[
|
|
18271
|
+
* { key: 'username', header: 'Username', render: (u) => u.username },
|
|
18272
|
+
* { key: 'role', header: 'Role', render: (u) => <RoleBadge .../> },
|
|
18273
|
+
* { key: 'actions', header: '', align: 'right', render: (u) => <Actions .../> },
|
|
18274
|
+
* ]}
|
|
18275
|
+
* rows={users}
|
|
18276
|
+
* minWidthPx={560}
|
|
18277
|
+
* />
|
|
18278
|
+
*
|
|
18279
|
+
* If finer header markup is needed, pass `headerRender` instead of
|
|
18280
|
+
* `header` (e.g. for a sortable click affordance).
|
|
18281
|
+
*
|
|
18282
|
+
* ── Narrow viewports ──────────────────────────────────────────────────────
|
|
18283
|
+
* The table stays a table. The wrapper is `overflow-x-auto`, so a phone
|
|
18284
|
+
* scrolls the columns sideways INSIDE the table — the page body never moves
|
|
18285
|
+
* sideways, and the cells stay one line (`whitespace-nowrap`). That is the
|
|
18286
|
+
* default (`mobileMode="scroll"`). Cards are opt-in via `mobileMode="cards"`;
|
|
18287
|
+
* `auto` still applies the old column-count rule for a surface that wants it.
|
|
18288
|
+
* See `resolveTableLayout`.
|
|
18289
|
+
*
|
|
18290
|
+
* ── Scrolling the BODY, not the page ──────────────────────────────────────
|
|
18291
|
+
* `maxBodyHeight` caps the table and scrolls its rows INSIDE the container
|
|
18292
|
+
* with the column header pinned. It is ONE prop, not two, because "this list
|
|
18293
|
+
* scrolls internally" and "its header stays visible" are the same decision: a
|
|
18294
|
+
* capped table whose header scrolls away is a list of unlabelled cells.
|
|
18295
|
+
*
|
|
18296
|
+
* Use it wherever the row count is open-ended (rules, targets, delivery
|
|
18297
|
+
* history) so the page keeps its own header, filters and actions in view while
|
|
18298
|
+
* the operator moves through the rows.
|
|
18299
|
+
*/
|
|
18300
|
+
/**
|
|
18301
|
+
* The default cap for an internally-scrolling table.
|
|
18302
|
+
*
|
|
18303
|
+
* `60vh` leaves the page header, its filters and its actions on screen at
|
|
18304
|
+
* every viewport while still showing ~10 rows — and it is a fraction, so it
|
|
18305
|
+
* does not need a breakpoint of its own.
|
|
18306
|
+
*/
|
|
18307
|
+
var TABLE_SCROLL_MAX_H = "max-h-[60vh]";
|
|
18308
|
+
var ALIGN_CLASS = {
|
|
18309
|
+
left: "text-left",
|
|
18310
|
+
right: "text-right",
|
|
18311
|
+
center: "text-center"
|
|
18312
|
+
};
|
|
18313
|
+
function DataTable({ columns, rows, rowKey, onRowClick, minWidthPx = 480, emptyMessage, className, bordered = true, rowClassName, mobileMode = "scroll", maxBodyHeight, defaultSort }) {
|
|
18314
|
+
const isNarrow = useIsMobile();
|
|
18315
|
+
const [sort, setSort] = useState(defaultSort ?? null);
|
|
18316
|
+
const layout = resolveTableLayout({
|
|
18317
|
+
mode: mobileMode,
|
|
18318
|
+
columnCount: columns.length,
|
|
18319
|
+
isNarrow
|
|
18320
|
+
});
|
|
18321
|
+
const scrolls = maxBodyHeight !== void 0;
|
|
18322
|
+
const sortedColumn = useMemo(() => sort === null ? void 0 : columns.find((col) => col.key === sort.columnKey && col.sortValue !== void 0), [columns, sort]);
|
|
18323
|
+
const sortValue = sortedColumn?.sortValue;
|
|
18324
|
+
const displayRows = useMemo(() => sortValue === void 0 ? rows : sortRowsByColumn(rows, sort, sortValue), [
|
|
18325
|
+
rows,
|
|
18326
|
+
sort,
|
|
18327
|
+
sortValue
|
|
18328
|
+
]);
|
|
18329
|
+
const activeSort = sortedColumn === void 0 ? null : sort;
|
|
18330
|
+
if (rows.length === 0 && emptyMessage) return /* @__PURE__ */ jsx("div", {
|
|
18331
|
+
className: `rounded-lg ${bordered ? "border border-border" : ""} bg-surface px-3 py-4 text-xs text-foreground-subtle text-center ${className ?? ""}`,
|
|
18332
|
+
children: emptyMessage
|
|
18333
|
+
});
|
|
18334
|
+
if (rows.length === 0) return null;
|
|
18335
|
+
if (layout === "cards") return /* @__PURE__ */ jsx("div", {
|
|
18336
|
+
className: `space-y-2 ${scrolls ? `${maxBodyHeight} overflow-y-auto` : ""} ${className ?? ""}`,
|
|
18337
|
+
children: displayRows.map((row, rowIndex) => /* @__PURE__ */ jsx(DataTableCard, {
|
|
18338
|
+
row,
|
|
18339
|
+
rowIndex,
|
|
18340
|
+
columns,
|
|
18341
|
+
bordered,
|
|
18342
|
+
onRowClick,
|
|
18343
|
+
rowClassName
|
|
18344
|
+
}, rowKey ? rowKey(row, rowIndex) : rowIndex))
|
|
18345
|
+
});
|
|
18346
|
+
return /* @__PURE__ */ jsx("div", {
|
|
18347
|
+
className: [
|
|
18348
|
+
"rounded-lg",
|
|
18349
|
+
bordered ? "border border-border" : "",
|
|
18350
|
+
"bg-surface overflow-x-auto",
|
|
18351
|
+
scrolls ? `${maxBodyHeight} overflow-y-auto` : "",
|
|
18352
|
+
className ?? ""
|
|
18353
|
+
].filter(Boolean).join(" "),
|
|
18354
|
+
children: /* @__PURE__ */ jsxs("table", {
|
|
18355
|
+
className: `w-full text-xs ${scrolls ? "border-separate border-spacing-0" : ""}`,
|
|
18356
|
+
style: { minWidth: `${minWidthPx}px` },
|
|
18357
|
+
children: [/* @__PURE__ */ jsx("thead", {
|
|
18358
|
+
className: "bg-surface",
|
|
18359
|
+
children: /* @__PURE__ */ jsx("tr", { children: columns.map((col) => {
|
|
18360
|
+
const align = col.align ?? "left";
|
|
18361
|
+
const nowrap = col.nowrap ?? true;
|
|
18362
|
+
const sortable = col.sortValue !== void 0;
|
|
18363
|
+
const isSorted = activeSort !== null && activeSort.columnKey === col.key;
|
|
18364
|
+
return /* @__PURE__ */ jsx("th", {
|
|
18365
|
+
"aria-sort": sortable ? ariaSortForColumn(activeSort, col.key) : void 0,
|
|
18366
|
+
title: sortable ? void 0 : col.notSortableReason,
|
|
18367
|
+
className: [
|
|
18368
|
+
"px-3 py-2 text-foreground-subtle font-medium bg-surface border-b border-border",
|
|
18369
|
+
scrolls ? "sticky top-0 z-10" : "",
|
|
18370
|
+
ALIGN_CLASS[align],
|
|
18371
|
+
nowrap ? "whitespace-nowrap" : "",
|
|
18372
|
+
col.width ?? ""
|
|
18373
|
+
].filter(Boolean).join(" "),
|
|
18374
|
+
children: sortable ? /* @__PURE__ */ jsx(SortHeaderButton, {
|
|
18375
|
+
label: col.headerRender ? col.headerRender() : col.header,
|
|
18376
|
+
active: isSorted,
|
|
18377
|
+
dir: isSorted && activeSort !== null ? activeSort.dir : "asc",
|
|
18378
|
+
onSort: () => setSort((prev) => nextTableSort(prev, col.key))
|
|
18379
|
+
}) : col.headerRender ? col.headerRender() : col.header
|
|
18380
|
+
}, col.key);
|
|
18381
|
+
}) })
|
|
18382
|
+
}), /* @__PURE__ */ jsx("tbody", { children: displayRows.map((row, rowIndex) => {
|
|
18383
|
+
const key = rowKey ? rowKey(row, rowIndex) : rowIndex;
|
|
18384
|
+
const extra = rowClassName?.(row, rowIndex) ?? "";
|
|
18385
|
+
const interactive = onRowClick !== void 0;
|
|
18386
|
+
return /* @__PURE__ */ jsx("tr", {
|
|
18387
|
+
onClick: interactive ? () => onRowClick(row, rowIndex) : void 0,
|
|
18388
|
+
className: [interactive ? "cursor-pointer hover:bg-primary/5" : "hover:bg-primary/5", extra].filter(Boolean).join(" "),
|
|
18389
|
+
children: columns.map((col) => {
|
|
18390
|
+
const align = col.align ?? "left";
|
|
18391
|
+
const nowrap = col.nowrap ?? true;
|
|
18392
|
+
return /* @__PURE__ */ jsx("td", {
|
|
18393
|
+
className: [
|
|
18394
|
+
"px-3 py-2 text-foreground border-b border-border",
|
|
18395
|
+
ALIGN_CLASS[align],
|
|
18396
|
+
nowrap ? "whitespace-nowrap" : ""
|
|
18397
|
+
].filter(Boolean).join(" "),
|
|
18398
|
+
children: col.render(row, rowIndex)
|
|
18399
|
+
}, col.key);
|
|
18400
|
+
})
|
|
18401
|
+
}, key);
|
|
18402
|
+
}) })]
|
|
18403
|
+
})
|
|
18404
|
+
});
|
|
18405
|
+
}
|
|
18406
|
+
/**
|
|
18407
|
+
* One row as a card. Columns that carry a plain-text `header` become labelled
|
|
18408
|
+
* `SettingRow`s — the label is the column title and the value gets the full
|
|
18409
|
+
* card width. Columns with no text header (an actions column, a
|
|
18410
|
+
* `headerRender`-only column) have no label to show, so they render as a
|
|
18411
|
+
* full-width strip at the foot of the card.
|
|
18412
|
+
*/
|
|
18413
|
+
function DataTableCard({ row, rowIndex, columns, bordered, onRowClick, rowClassName }) {
|
|
18414
|
+
const labelled = columns.filter((col) => col.header !== void 0 && col.header !== "");
|
|
18415
|
+
const unlabelled = columns.filter((col) => col.header === void 0 || col.header === "");
|
|
18416
|
+
const interactive = onRowClick !== void 0;
|
|
18417
|
+
const extra = rowClassName?.(row, rowIndex) ?? "";
|
|
18418
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
18419
|
+
"data-data-table-card": "",
|
|
18420
|
+
onClick: interactive ? () => onRowClick(row, rowIndex) : void 0,
|
|
18421
|
+
className: [
|
|
18422
|
+
"rounded-lg bg-surface px-3 py-1.5 divide-y divide-border-subtle",
|
|
18423
|
+
bordered ? "border border-border" : "",
|
|
18424
|
+
interactive ? "cursor-pointer hover:bg-primary/5" : "",
|
|
18425
|
+
extra
|
|
18426
|
+
].filter(Boolean).join(" "),
|
|
18427
|
+
children: [labelled.map((col) => /* @__PURE__ */ jsx(SettingRow, {
|
|
18428
|
+
label: col.header,
|
|
18429
|
+
children: col.render(row, rowIndex)
|
|
18430
|
+
}, col.key)), unlabelled.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
18431
|
+
className: "flex flex-wrap items-center justify-end gap-2 py-1.5",
|
|
18432
|
+
children: unlabelled.map((col) => /* @__PURE__ */ jsx("div", { children: col.render(row, rowIndex) }, col.key))
|
|
18433
|
+
})]
|
|
18434
|
+
});
|
|
18435
|
+
}
|
|
18436
|
+
//#endregion
|
|
18437
|
+
//#region src/composites/device-list/pagination.tsx
|
|
18438
|
+
var NEIGHBORS = 2;
|
|
18439
|
+
function computePageButtons(currentPage, totalPages) {
|
|
18440
|
+
if (totalPages <= 1) return [{
|
|
18441
|
+
kind: "page",
|
|
18442
|
+
n: 1
|
|
18443
|
+
}];
|
|
18444
|
+
if (totalPages <= 7) {
|
|
18445
|
+
const all = [];
|
|
18446
|
+
for (let i = 1; i <= totalPages; i++) all.push({
|
|
18447
|
+
kind: "page",
|
|
18448
|
+
n: i
|
|
18449
|
+
});
|
|
18450
|
+
return all;
|
|
18451
|
+
}
|
|
18452
|
+
const result = [];
|
|
18453
|
+
const leftBound = Math.max(2, currentPage - NEIGHBORS);
|
|
18454
|
+
const rightBound = Math.min(totalPages - 1, currentPage + NEIGHBORS);
|
|
18455
|
+
result.push({
|
|
18456
|
+
kind: "page",
|
|
18457
|
+
n: 1
|
|
18458
|
+
});
|
|
18459
|
+
if (leftBound > 2) result.push({
|
|
18460
|
+
kind: "ellipsis",
|
|
18461
|
+
side: "left"
|
|
18462
|
+
});
|
|
18463
|
+
for (let i = leftBound; i <= rightBound; i++) result.push({
|
|
18464
|
+
kind: "page",
|
|
18465
|
+
n: i
|
|
18466
|
+
});
|
|
18467
|
+
if (rightBound < totalPages - 1) result.push({
|
|
18468
|
+
kind: "ellipsis",
|
|
18469
|
+
side: "right"
|
|
18470
|
+
});
|
|
18471
|
+
result.push({
|
|
18472
|
+
kind: "page",
|
|
18473
|
+
n: totalPages
|
|
18474
|
+
});
|
|
18475
|
+
return result;
|
|
18476
|
+
}
|
|
18477
|
+
function Pagination({ currentPage, totalItems, pageSize, onPageChange, itemNoun = "devices" }) {
|
|
18478
|
+
if (totalItems <= pageSize) return null;
|
|
18479
|
+
const totalPages = Math.ceil(totalItems / pageSize);
|
|
18480
|
+
const startIdx = (currentPage - 1) * pageSize + 1;
|
|
18481
|
+
const endIdx = Math.min(currentPage * pageSize, totalItems);
|
|
18482
|
+
const buttons = computePageButtons(currentPage, totalPages);
|
|
18483
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
18484
|
+
className: "flex items-center justify-between gap-4 flex-wrap pt-2 text-[11px] text-foreground-subtle",
|
|
18485
|
+
children: [/* @__PURE__ */ jsxs("span", { children: [
|
|
18486
|
+
"Showing ",
|
|
18487
|
+
startIdx,
|
|
18488
|
+
"–",
|
|
18489
|
+
endIdx,
|
|
18490
|
+
" of ",
|
|
18491
|
+
totalItems,
|
|
18492
|
+
" ",
|
|
18493
|
+
itemNoun
|
|
18494
|
+
] }), /* @__PURE__ */ jsxs("span", {
|
|
18495
|
+
className: "inline-flex items-center gap-1",
|
|
18496
|
+
children: [
|
|
18497
|
+
/* @__PURE__ */ jsx("button", {
|
|
18498
|
+
type: "button",
|
|
18499
|
+
"aria-label": "Previous page",
|
|
18500
|
+
disabled: currentPage === 1,
|
|
18501
|
+
onClick: () => onPageChange(currentPage - 1),
|
|
18502
|
+
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"),
|
|
18503
|
+
children: /* @__PURE__ */ jsx(ChevronLeft, { className: "h-3 w-3" })
|
|
18504
|
+
}),
|
|
18505
|
+
buttons.map((b, i) => {
|
|
18506
|
+
if (b.kind === "ellipsis") return /* @__PURE__ */ jsx("span", {
|
|
18507
|
+
className: "px-1 text-foreground-subtle/60 select-none",
|
|
18508
|
+
children: "…"
|
|
18509
|
+
}, `e-${i}`);
|
|
18510
|
+
return /* @__PURE__ */ jsx("button", {
|
|
18511
|
+
type: "button",
|
|
18512
|
+
onClick: () => onPageChange(b.n),
|
|
18513
|
+
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"),
|
|
18514
|
+
children: b.n
|
|
18515
|
+
}, `p-${b.n}`);
|
|
18516
|
+
}),
|
|
18517
|
+
/* @__PURE__ */ jsx("button", {
|
|
18518
|
+
type: "button",
|
|
18519
|
+
"aria-label": "Next page",
|
|
18520
|
+
disabled: currentPage === totalPages,
|
|
18521
|
+
onClick: () => onPageChange(currentPage + 1),
|
|
18522
|
+
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"),
|
|
18523
|
+
children: /* @__PURE__ */ jsx(ChevronRight, { className: "h-3 w-3" })
|
|
18524
|
+
})
|
|
18525
|
+
]
|
|
18526
|
+
})]
|
|
18527
|
+
});
|
|
18528
|
+
}
|
|
18529
|
+
//#endregion
|
|
18530
|
+
//#region src/composites/empty-state.tsx
|
|
18531
|
+
function EmptyState({ icon: Icon, title, description, action, className }) {
|
|
18532
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
18533
|
+
className: cn("flex flex-col items-center justify-center gap-3 py-12", className),
|
|
18534
|
+
children: [
|
|
18535
|
+
Icon && /* @__PURE__ */ jsx(Icon, {
|
|
18536
|
+
className: "h-12 w-12 text-foreground-subtle",
|
|
18537
|
+
"aria-hidden": "true"
|
|
18538
|
+
}),
|
|
18539
|
+
/* @__PURE__ */ jsxs("div", {
|
|
18540
|
+
className: "flex flex-col items-center gap-1 text-center",
|
|
18541
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
18542
|
+
className: "text-foreground-muted text-sm font-medium",
|
|
18543
|
+
children: title
|
|
18544
|
+
}), description && /* @__PURE__ */ jsx("p", {
|
|
18545
|
+
className: "text-foreground-subtle text-xs max-w-xs",
|
|
18546
|
+
children: description
|
|
18547
|
+
})]
|
|
18548
|
+
}),
|
|
18549
|
+
action && /* @__PURE__ */ jsx("div", {
|
|
18550
|
+
className: "mt-1",
|
|
18551
|
+
children: action
|
|
18552
|
+
})
|
|
18553
|
+
]
|
|
18554
|
+
});
|
|
18555
|
+
}
|
|
18556
|
+
//#endregion
|
|
18557
|
+
//#region src/composites/error-box.tsx
|
|
18558
|
+
var VARIANT_STYLES$1 = {
|
|
18559
|
+
danger: {
|
|
18560
|
+
box: "border-danger/40 bg-danger/10",
|
|
18561
|
+
icon: "text-danger",
|
|
18562
|
+
title: "text-danger",
|
|
18563
|
+
message: "text-danger"
|
|
18564
|
+
},
|
|
18565
|
+
warning: {
|
|
18566
|
+
box: "border-amber-500/40 bg-amber-500/10",
|
|
18567
|
+
icon: "text-amber-600 dark:text-amber-400",
|
|
18568
|
+
title: "text-amber-700 dark:text-amber-300",
|
|
18569
|
+
message: "text-amber-700 dark:text-amber-300"
|
|
18570
|
+
}
|
|
18571
|
+
};
|
|
18572
|
+
function ErrorBox({ message, title, onRetry, action, variant = "danger", className }) {
|
|
18573
|
+
const styles = VARIANT_STYLES$1[variant];
|
|
18574
|
+
const Icon = variant === "warning" ? TriangleAlert : CircleAlert;
|
|
18575
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
18576
|
+
role: "alert",
|
|
18577
|
+
className: cn("flex items-start gap-2.5 rounded border px-3 py-2.5", styles.box, className),
|
|
18578
|
+
children: [
|
|
18579
|
+
/* @__PURE__ */ jsx(Icon, {
|
|
18580
|
+
className: cn("h-4 w-4 mt-0.5 shrink-0", styles.icon),
|
|
18581
|
+
"aria-hidden": "true"
|
|
18582
|
+
}),
|
|
18583
|
+
/* @__PURE__ */ jsxs("div", {
|
|
18584
|
+
className: "flex-1 min-w-0",
|
|
18585
|
+
children: [title && /* @__PURE__ */ jsx("p", {
|
|
18586
|
+
className: cn("text-xs font-semibold", styles.title),
|
|
18587
|
+
children: title
|
|
18588
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
18589
|
+
className: cn("text-xs break-words", styles.message, title ? "mt-0.5" : ""),
|
|
18590
|
+
children: message
|
|
18591
|
+
})]
|
|
18592
|
+
}),
|
|
18593
|
+
action ?? (onRetry && /* @__PURE__ */ jsxs("button", {
|
|
18594
|
+
type: "button",
|
|
18595
|
+
onClick: onRetry,
|
|
18596
|
+
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),
|
|
18597
|
+
children: [/* @__PURE__ */ jsx(RefreshCw, {
|
|
18598
|
+
className: "h-3 w-3",
|
|
18599
|
+
"aria-hidden": "true"
|
|
18600
|
+
}), "Retry"]
|
|
18601
|
+
}))
|
|
18602
|
+
]
|
|
18603
|
+
});
|
|
18604
|
+
}
|
|
18605
|
+
//#endregion
|
|
18606
|
+
//#region src/composites/cap-settings/EventHeatmap.tsx
|
|
18607
|
+
var KIND_BG = {
|
|
18608
|
+
motion: "bg-emerald-500",
|
|
18609
|
+
object: "bg-violet-500",
|
|
18610
|
+
audio: "bg-orange-500"
|
|
18611
|
+
};
|
|
18612
|
+
function EventHeatmap({ windowFrom, windowTo, buckets, selectedIndex, onSelectBucket }) {
|
|
18613
|
+
const peak = buckets.reduce((m, b) => Math.max(m, b.total), 0);
|
|
18614
|
+
const firstBucket = buckets[0];
|
|
18615
|
+
const bucketWidthPct = firstBucket ? pctForMs(firstBucket.endMs, windowFrom, windowTo) - pctForMs(firstBucket.startMs, windowFrom, windowTo) : 0;
|
|
18616
|
+
return /* @__PURE__ */ jsx("div", {
|
|
18617
|
+
className: "relative h-6 rounded bg-background/40",
|
|
18618
|
+
children: buckets.map((b) => {
|
|
18619
|
+
if (b.total === 0 || b.dominant === null) return null;
|
|
18620
|
+
const heightPct = peak > 0 ? Math.max(15, b.total / peak * 100) : 0;
|
|
18621
|
+
const left = pctForMs(b.startMs, windowFrom, windowTo);
|
|
18622
|
+
const selected = b.index === selectedIndex;
|
|
18623
|
+
return /* @__PURE__ */ jsx("button", {
|
|
18624
|
+
type: "button",
|
|
18625
|
+
"data-testid": "heatmap-bar",
|
|
18626
|
+
title: `${new Date(b.startMs).toLocaleTimeString(void 0, {
|
|
18627
|
+
hour: "2-digit",
|
|
18628
|
+
minute: "2-digit"
|
|
18629
|
+
})} · motion ${b.counts.motion} · object ${b.counts.object}`,
|
|
18630
|
+
onClick: () => onSelectBucket(b.index),
|
|
18631
|
+
className: `absolute bottom-0 rounded-t-sm transition-opacity ${KIND_BG[b.dominant]} ${selected ? "opacity-100 ring-1 ring-amber-400" : "opacity-80 hover:opacity-100"}`,
|
|
18632
|
+
style: {
|
|
18633
|
+
left: `${left}%`,
|
|
18634
|
+
width: `${Math.max(.6, bucketWidthPct)}%`,
|
|
18635
|
+
height: `${heightPct}%`
|
|
18636
|
+
}
|
|
18637
|
+
}, b.index);
|
|
18638
|
+
})
|
|
18639
|
+
});
|
|
18640
|
+
}
|
|
18641
|
+
//#endregion
|
|
18642
|
+
//#region src/composites/cap-settings/TimelineControls.tsx
|
|
18643
|
+
/** Format a Date as "YYYY-MM-DD" for <input type="date">. */
|
|
18644
|
+
function formatDateInput(date) {
|
|
18645
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
|
18646
|
+
}
|
|
18647
|
+
/** Parse "YYYY-MM-DD" from <input type="date"> into a local-midnight Date. */
|
|
18648
|
+
function parseDateInput(value) {
|
|
18649
|
+
const parts = value.split("-");
|
|
18650
|
+
const y = parts[0] !== void 0 ? parseInt(parts[0], 10) : NaN;
|
|
18651
|
+
const m = parts[1] !== void 0 ? parseInt(parts[1], 10) : NaN;
|
|
18652
|
+
const d = parts[2] !== void 0 ? parseInt(parts[2], 10) : NaN;
|
|
18653
|
+
if (isNaN(y) || isNaN(m) || isNaN(d)) return null;
|
|
18654
|
+
return new Date(y, m - 1, d);
|
|
18655
|
+
}
|
|
18656
|
+
/**
|
|
18657
|
+
* Date selector + prev/next pan + zoom-in / zoom-out buttons for the recording
|
|
18658
|
+
* timeline. Zoom cycles through ZOOM_LEVELS (widest → narrowest), anchored on
|
|
18659
|
+
* the view's RIGHT edge — so on today's timeline "now" stays pinned to the right
|
|
18660
|
+
* instead of drifting to a midday range. The ◀ / ▶ buttons step the visible
|
|
18661
|
+
* window earlier/later within the day (the discoverable equivalent of the
|
|
18662
|
+
* horizontal wheel-pan wired in RecordingTimeline), so a zoomed-in day can be
|
|
18663
|
+
* navigated back and forth without a mouse wheel.
|
|
18664
|
+
*/
|
|
18665
|
+
function TimelineControls({ day, onDayChange, view, dayBounds, onViewChange }) {
|
|
18666
|
+
const handleDateChange = (e) => {
|
|
18667
|
+
const parsed = parseDateInput(e.target.value);
|
|
18668
|
+
if (parsed !== null) onDayChange(parsed);
|
|
18669
|
+
};
|
|
18670
|
+
const viewSpan = view.toMs - view.fromMs;
|
|
18671
|
+
const currentLevelIdx = (() => {
|
|
18672
|
+
let best = 0;
|
|
18673
|
+
let bestDiff = Infinity;
|
|
18674
|
+
for (let i = 0; i < ZOOM_LEVELS.length; i++) {
|
|
18675
|
+
const level = ZOOM_LEVELS[i];
|
|
18676
|
+
if (level === void 0) continue;
|
|
18677
|
+
const diff = Math.abs(level - viewSpan);
|
|
18678
|
+
if (diff < bestDiff) {
|
|
18679
|
+
bestDiff = diff;
|
|
18680
|
+
best = i;
|
|
18681
|
+
}
|
|
18682
|
+
}
|
|
18683
|
+
return best;
|
|
18684
|
+
})();
|
|
18685
|
+
const rightEdgeMs = view.toMs;
|
|
18686
|
+
const handleZoomIn = () => {
|
|
18687
|
+
const nextLevel = ZOOM_LEVELS[Math.min(currentLevelIdx + 1, ZOOM_LEVELS.length - 1)];
|
|
18688
|
+
if (nextLevel === void 0) return;
|
|
18689
|
+
onViewChange(zoomWindowAnchoredRight(dayBounds, nextLevel, rightEdgeMs));
|
|
18690
|
+
};
|
|
18691
|
+
const handleZoomOut = () => {
|
|
18692
|
+
const nextLevel = ZOOM_LEVELS[Math.max(currentLevelIdx - 1, 0)];
|
|
18693
|
+
if (nextLevel === void 0) return;
|
|
18694
|
+
onViewChange(zoomWindowAnchoredRight(dayBounds, nextLevel, rightEdgeMs));
|
|
18695
|
+
};
|
|
18696
|
+
const canZoomIn = currentLevelIdx < ZOOM_LEVELS.length - 1;
|
|
18697
|
+
const canZoomOut = currentLevelIdx > 0;
|
|
18698
|
+
const panStepMs = Math.max(1, Math.round(viewSpan * .9));
|
|
18699
|
+
const canPanPrev = view.fromMs > dayBounds.fromMs;
|
|
18700
|
+
const canPanNext = view.toMs < dayBounds.toMs;
|
|
18701
|
+
const handlePanPrev = () => {
|
|
18702
|
+
onViewChange(panWindow(view, dayBounds, -panStepMs));
|
|
18703
|
+
};
|
|
18704
|
+
const handlePanNext = () => {
|
|
18705
|
+
onViewChange(panWindow(view, dayBounds, panStepMs));
|
|
18706
|
+
};
|
|
18707
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
18708
|
+
className: "flex items-center gap-2",
|
|
18709
|
+
children: [
|
|
18710
|
+
/* @__PURE__ */ jsx("input", {
|
|
18711
|
+
type: "date",
|
|
18712
|
+
value: formatDateInput(day),
|
|
18713
|
+
max: formatDateInput(/* @__PURE__ */ new Date()),
|
|
18714
|
+
onChange: handleDateChange,
|
|
18715
|
+
className: "rounded border border-border bg-background px-2 py-0.5 text-[11px] text-foreground focus:outline-none focus:ring-1 focus:ring-primary",
|
|
18716
|
+
"aria-label": "Select recording day"
|
|
18717
|
+
}),
|
|
18718
|
+
/* @__PURE__ */ jsxs("div", {
|
|
18719
|
+
className: "ml-auto flex items-center gap-1",
|
|
18720
|
+
children: [
|
|
18721
|
+
/* @__PURE__ */ jsx("button", {
|
|
18722
|
+
type: "button",
|
|
18723
|
+
onClick: handlePanPrev,
|
|
18724
|
+
disabled: !canPanPrev,
|
|
18725
|
+
title: "Earlier (pan back)",
|
|
18726
|
+
"aria-label": "Pan earlier",
|
|
18727
|
+
className: "rounded p-1 text-foreground-subtle transition-colors hover:bg-background/60 hover:text-foreground disabled:cursor-not-allowed disabled:opacity-40",
|
|
18728
|
+
children: /* @__PURE__ */ jsx(ChevronLeft, { size: 14 })
|
|
18729
|
+
}),
|
|
18730
|
+
/* @__PURE__ */ jsx("button", {
|
|
18731
|
+
type: "button",
|
|
18732
|
+
onClick: handlePanNext,
|
|
18733
|
+
disabled: !canPanNext,
|
|
18734
|
+
title: "Later (pan forward)",
|
|
18735
|
+
"aria-label": "Pan later",
|
|
18736
|
+
className: "rounded p-1 text-foreground-subtle transition-colors hover:bg-background/60 hover:text-foreground disabled:cursor-not-allowed disabled:opacity-40",
|
|
18737
|
+
children: /* @__PURE__ */ jsx(ChevronRight, { size: 14 })
|
|
18738
|
+
}),
|
|
18739
|
+
/* @__PURE__ */ jsx("span", {
|
|
18740
|
+
className: "mx-0.5 h-3 w-px bg-border",
|
|
18741
|
+
"aria-hidden": "true"
|
|
18742
|
+
}),
|
|
18743
|
+
/* @__PURE__ */ jsx("button", {
|
|
18744
|
+
type: "button",
|
|
18745
|
+
onClick: handleZoomOut,
|
|
18746
|
+
disabled: !canZoomOut,
|
|
18747
|
+
title: "Zoom out",
|
|
18748
|
+
"aria-label": "Zoom out",
|
|
18749
|
+
className: "rounded p-1 text-foreground-subtle transition-colors hover:bg-background/60 hover:text-foreground disabled:cursor-not-allowed disabled:opacity-40",
|
|
18750
|
+
children: /* @__PURE__ */ jsx(ZoomOut, { size: 13 })
|
|
18751
|
+
}),
|
|
18752
|
+
/* @__PURE__ */ jsx("button", {
|
|
18753
|
+
type: "button",
|
|
18754
|
+
onClick: handleZoomIn,
|
|
18755
|
+
disabled: !canZoomIn,
|
|
18756
|
+
title: "Zoom in",
|
|
18757
|
+
"aria-label": "Zoom in",
|
|
18758
|
+
className: "rounded p-1 text-foreground-subtle transition-colors hover:bg-background/60 hover:text-foreground disabled:cursor-not-allowed disabled:opacity-40",
|
|
18759
|
+
children: /* @__PURE__ */ jsx(ZoomIn, { size: 13 })
|
|
18760
|
+
})
|
|
18761
|
+
]
|
|
18762
|
+
}),
|
|
18763
|
+
/* @__PURE__ */ jsx("span", {
|
|
18764
|
+
className: "text-[9px] text-foreground-subtle",
|
|
18765
|
+
children: zoomLabel(viewSpan)
|
|
18766
|
+
})
|
|
18767
|
+
]
|
|
18768
|
+
});
|
|
18769
|
+
}
|
|
18770
|
+
function zoomLabel(spanMs) {
|
|
18771
|
+
const h = spanMs / 36e5;
|
|
18772
|
+
if (h >= 24) return "24h";
|
|
18773
|
+
if (h >= 1) return `${Math.round(h)}h`;
|
|
18774
|
+
const m = spanMs / 6e4;
|
|
18775
|
+
return `${Math.round(m)}m`;
|
|
18776
|
+
}
|
|
18777
|
+
//#endregion
|
|
18778
|
+
//#region src/composites/clips/ClipBandBar.tsx
|
|
18779
|
+
/**
|
|
18780
|
+
* The clips section's timebar — the recording section's own controls, driving
|
|
18781
|
+
* a clip list instead of a player (D564).
|
|
18782
|
+
*
|
|
18783
|
+
* *"Possiamo utilizzare lo stesso componente della sezione recording con la
|
|
18784
|
+
* timebar dove selezionare un periodo e mostrare le clip solo di quella banda
|
|
18785
|
+
* selezionata, espandibile."* — the operator, 2026-09-20. So this is not a
|
|
18786
|
+
* second timebar: it is `TimelineControls` and `EventHeatmap`, the two pieces
|
|
18787
|
+
* `RecordingTimeline` is itself built from, over the same
|
|
18788
|
+
* `recording-timeline-model` (`dayWindow`, `panWindow`,
|
|
18789
|
+
* `zoomWindowAnchoredRight`, `bucketMsForSpan`, `axisTicks`). One
|
|
18790
|
+
* implementation, one model, two surfaces.
|
|
18791
|
+
*
|
|
18792
|
+
* ## What it does NOT take from the recording timeline
|
|
18793
|
+
*
|
|
18794
|
+
* `CoverageTrack` stays behind. There it is the seek surface — a click drives
|
|
18795
|
+
* the hero player through the `camstack-control` channel or an HLS VOD
|
|
18796
|
+
* request. A copy here would either be a dead bar or a SECOND thing claiming
|
|
18797
|
+
* to move the playhead, and two components holding two ideas of the same
|
|
18798
|
+
* position is the defect this repo keeps paying for. The clips section plays a
|
|
18799
|
+
* clip through `ClipPanel`, which is its own, already-shipped path.
|
|
18800
|
+
*
|
|
18801
|
+
* ## This component is presentational
|
|
18802
|
+
*
|
|
18803
|
+
* It holds no state. The day, the visible window and the picked bucket all
|
|
18804
|
+
* live in `ClipsSection`, because that is where the band is turned into the
|
|
18805
|
+
* `listClips` argument — a bar that kept its own copy of the selection would
|
|
18806
|
+
* be the second authority again, one component down.
|
|
18807
|
+
*/
|
|
18808
|
+
/**
|
|
18809
|
+
* How many ms one pixel of horizontal wheel delta pans.
|
|
18810
|
+
*
|
|
18811
|
+
* The same calibration `RecordingTimeline` uses — a standard wheel tick
|
|
18812
|
+
* (~100 px of `deltaX`) pans ~30 min. Two timebars that panned at different
|
|
18813
|
+
* rates would read as two different controls.
|
|
18814
|
+
*/
|
|
18815
|
+
var PAN_MS_PER_PX$1 = 18e3 / 100;
|
|
18816
|
+
function ClipBandBar({ day, onDayChange, view, dayBounds, onViewChange, buckets, selectedBucketIndex, onSelectBucket, densityLoading }) {
|
|
18817
|
+
const trackAreaRef = useRef(null);
|
|
18818
|
+
const handleWheel = useCallback((e) => {
|
|
18819
|
+
const deltaX = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : e.shiftKey ? e.deltaY : 0;
|
|
18820
|
+
if (deltaX === 0) return;
|
|
18821
|
+
e.preventDefault();
|
|
18822
|
+
onViewChange(panWindow(view, dayBounds, deltaX * PAN_MS_PER_PX$1));
|
|
18823
|
+
}, [
|
|
18824
|
+
view,
|
|
18825
|
+
dayBounds,
|
|
18826
|
+
onViewChange
|
|
18827
|
+
]);
|
|
18828
|
+
const ticks = axisTicks(view.fromMs, view.toMs, 5);
|
|
18829
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
18830
|
+
className: "space-y-1",
|
|
18831
|
+
children: [/* @__PURE__ */ jsx(TimelineControls, {
|
|
18832
|
+
day,
|
|
18833
|
+
onDayChange,
|
|
18834
|
+
view,
|
|
18835
|
+
dayBounds,
|
|
18836
|
+
onViewChange
|
|
18837
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
18838
|
+
ref: trackAreaRef,
|
|
18839
|
+
onWheel: handleWheel,
|
|
18840
|
+
className: "space-y-1",
|
|
18841
|
+
children: [
|
|
18842
|
+
/* @__PURE__ */ jsx("div", {
|
|
18843
|
+
className: "text-[10px] font-semibold uppercase tracking-wider text-foreground-subtle",
|
|
18844
|
+
children: "Events · density"
|
|
18845
|
+
}),
|
|
18846
|
+
densityLoading ? /* @__PURE__ */ jsx("div", { className: "h-6 animate-pulse rounded bg-background/40" }) : /* @__PURE__ */ jsx(EventHeatmap, {
|
|
18847
|
+
windowFrom: view.fromMs,
|
|
18848
|
+
windowTo: view.toMs,
|
|
18849
|
+
buckets,
|
|
18850
|
+
selectedIndex: selectedBucketIndex,
|
|
18851
|
+
onSelectBucket
|
|
18852
|
+
}),
|
|
18853
|
+
/* @__PURE__ */ jsx("div", {
|
|
18854
|
+
className: "relative h-3",
|
|
18855
|
+
children: ticks.map((tick) => /* @__PURE__ */ jsx("span", {
|
|
18856
|
+
className: "absolute -translate-x-1/2 text-[9px] text-foreground-subtle",
|
|
18857
|
+
style: { left: `${String(tick.pct)}%` },
|
|
18858
|
+
children: tick.label
|
|
18859
|
+
}, tick.pct))
|
|
18860
|
+
})
|
|
18861
|
+
]
|
|
18862
|
+
})]
|
|
18863
|
+
});
|
|
18864
|
+
}
|
|
18865
|
+
//#endregion
|
|
18866
|
+
//#region src/contexts/vod-playback.tsx
|
|
18867
|
+
/**
|
|
18868
|
+
* VodPlayback — shared context coordinating recorded (VOD) playback between a
|
|
18869
|
+
* widget/section (the Recording panel) and the hero StreamPanel.
|
|
18870
|
+
*
|
|
18871
|
+
* A CONTROLLER (the Recording widget) calls `play({ url, label })`; the hero
|
|
18872
|
+
* StreamPanel CONSUMES `request` and switches into VOD mode (HLS playback
|
|
18873
|
+
* through the hub data-plane) instead of the live WebRTC stream. `clear()`
|
|
18874
|
+
* returns the hero to live.
|
|
18875
|
+
*
|
|
18876
|
+
* Built on `createSharedContext` (NOT a plain `createContext`) so it crosses
|
|
18877
|
+
* the Module-Federation boundary: an addon-shipped widget consuming
|
|
18878
|
+
* `useVodPlayback()` resolves the SAME provider instance the host mounted —
|
|
18879
|
+
* the same mechanism `player-overlays` / `zone-editing` use to let federated
|
|
18880
|
+
* widgets (zone-editor, privacy-mask) drive the live player. Consumers used
|
|
18881
|
+
* outside a provider get the inert default (no-op), so a StreamPanel mounted
|
|
18882
|
+
* on a page without the Recording widget stays live-only without crashing.
|
|
18883
|
+
*/
|
|
18884
|
+
var VodPlaybackContext = createSharedContext("camstack:vod-playback", {
|
|
18885
|
+
request: null,
|
|
18886
|
+
play: () => {},
|
|
18887
|
+
clear: () => {}
|
|
18888
|
+
});
|
|
18889
|
+
function VodPlaybackProvider({ children }) {
|
|
18890
|
+
const [request, setRequest] = useState(null);
|
|
18891
|
+
const value = useMemo(() => ({
|
|
18892
|
+
request,
|
|
18893
|
+
play: (req) => setRequest(req),
|
|
18894
|
+
clear: () => setRequest(null)
|
|
18895
|
+
}), [request]);
|
|
18896
|
+
return /* @__PURE__ */ jsx(VodPlaybackContext.Provider, {
|
|
18897
|
+
value,
|
|
18898
|
+
children
|
|
18899
|
+
});
|
|
18900
|
+
}
|
|
18901
|
+
/** @returns the VOD playback controller (inert no-op outside a provider). */
|
|
18902
|
+
function useVodPlayback() {
|
|
18903
|
+
return useContext(VodPlaybackContext);
|
|
18904
|
+
}
|
|
18905
|
+
//#endregion
|
|
18906
|
+
//#region src/primitives/card.tsx
|
|
18907
|
+
var cardVariants = cva("rounded-lg p-3", {
|
|
18908
|
+
variants: { variant: {
|
|
18909
|
+
flat: "bg-surface",
|
|
18910
|
+
bordered: "bg-surface border border-border"
|
|
18911
|
+
} },
|
|
18912
|
+
defaultVariants: { variant: "bordered" }
|
|
18913
|
+
});
|
|
18914
|
+
var Card = forwardRef(({ className, variant, ...props }, ref) => {
|
|
18915
|
+
return /* @__PURE__ */ jsx("div", {
|
|
18916
|
+
ref,
|
|
18917
|
+
className: cn(cardVariants({ variant }), className),
|
|
18918
|
+
...props
|
|
18919
|
+
});
|
|
18920
|
+
});
|
|
18921
|
+
Card.displayName = "Card";
|
|
18922
|
+
//#endregion
|
|
18923
|
+
//#region src/composites/clips/ClipPanel.tsx
|
|
18924
|
+
/**
|
|
18925
|
+
* The selected clip — what it is, how it plays, and how it is saved.
|
|
18926
|
+
*
|
|
18927
|
+
* **No second player.** `getClipPlayback` answers `{ playbackUrl, format,
|
|
18928
|
+
* served }` and the panel hands it to the SAME hero the recordings timeline
|
|
18929
|
+
* drives: `vod.play({ url, format })` → `StreamPanel playbackUrl/playbackFormat`
|
|
18930
|
+
* → `HlsVideo`'s `mp4` branch (`video.src`, no hls.js). Building a clip player
|
|
18931
|
+
* here is the thing D549 decision 22 exists to refuse.
|
|
18932
|
+
*
|
|
18933
|
+
* **`format` travels.** An MP4 handed to hls.js is parsed as a manifest and
|
|
18934
|
+
* fails, so the format the provider named is forwarded verbatim and never
|
|
18935
|
+
* defaulted here.
|
|
18936
|
+
*
|
|
18937
|
+
* **`served` is shown.** A clip that holds only one twin answers with the one
|
|
18938
|
+
* it has, and the surface SAYS so — a missing main twin must never be served
|
|
18939
|
+
* silently as if it were the asked-for quality (D549 decision 15). Both ids
|
|
18940
|
+
* are already on the row, so changing quality re-asks the hub and never
|
|
18941
|
+
* re-searches the camera.
|
|
18942
|
+
*
|
|
18943
|
+
* **Nothing is appended to the URL.** The route is `access:'authenticated'`
|
|
18944
|
+
* and the session cookie authenticates it (D549 decision 22); the same URL is
|
|
18945
|
+
* the download.
|
|
18946
|
+
*/
|
|
18947
|
+
var PROFILES = [
|
|
18948
|
+
"low",
|
|
18949
|
+
"mid",
|
|
18950
|
+
"high"
|
|
18951
|
+
];
|
|
18952
|
+
function ClipPanel({ deviceId, clip, profile, onProfileChange }) {
|
|
18953
|
+
const vod = useVodPlayback();
|
|
18954
|
+
const playback = useVideoclipsGetClipPlayback({
|
|
18955
|
+
deviceId,
|
|
18956
|
+
clipId: clip.id,
|
|
18957
|
+
profile
|
|
18958
|
+
});
|
|
18959
|
+
const data = playback.data;
|
|
18960
|
+
const label = `${new Date(clip.timeRange.startMs).toLocaleString()} · ${clip.source}`;
|
|
18961
|
+
useEffect(() => {
|
|
18962
|
+
if (data === void 0) return;
|
|
18963
|
+
vod.play({
|
|
18964
|
+
url: hubPath(data.playbackUrl),
|
|
18965
|
+
...data.format === void 0 ? {} : { format: data.format },
|
|
18966
|
+
label
|
|
18967
|
+
});
|
|
18968
|
+
}, [data, label]);
|
|
18969
|
+
return /* @__PURE__ */ jsxs(Card, {
|
|
18970
|
+
className: "space-y-2 text-sm",
|
|
18971
|
+
children: [
|
|
18972
|
+
/* @__PURE__ */ jsx("div", {
|
|
18973
|
+
className: "font-medium text-foreground",
|
|
18974
|
+
children: label
|
|
18975
|
+
}),
|
|
18976
|
+
/* @__PURE__ */ jsxs("div", {
|
|
18977
|
+
className: "flex items-center gap-1",
|
|
18978
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
18979
|
+
className: "mr-1 text-[10px] uppercase tracking-wide text-foreground-subtle",
|
|
18980
|
+
children: "Quality"
|
|
18981
|
+
}), PROFILES.map((option) => /* @__PURE__ */ jsx(Button, {
|
|
18982
|
+
variant: option === profile ? "primary" : "secondary",
|
|
18983
|
+
"aria-pressed": option === profile,
|
|
18984
|
+
onClick: () => onProfileChange(option),
|
|
18985
|
+
children: option
|
|
18986
|
+
}, option))]
|
|
18987
|
+
}),
|
|
18988
|
+
playback.status === "pending" ? /* @__PURE__ */ jsxs("div", {
|
|
18989
|
+
className: "space-y-1",
|
|
18990
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
18991
|
+
role: "status",
|
|
18992
|
+
className: "text-xs text-foreground-subtle",
|
|
18993
|
+
children: "Preparing the clip — the file is fetched and muxed once…"
|
|
18994
|
+
}), /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-48" })]
|
|
18995
|
+
}) : playback.status === "error" ? /* @__PURE__ */ jsx(ErrorBox, {
|
|
18996
|
+
title: "This clip could not be prepared",
|
|
18997
|
+
message: playback.error?.message ?? "no reason given"
|
|
18998
|
+
}) : data === void 0 ? null : /* @__PURE__ */ jsxs("div", {
|
|
18999
|
+
className: "space-y-2",
|
|
19000
|
+
children: [data.served === void 0 || data.served === profile ? null : /* @__PURE__ */ jsx(ErrorBox, {
|
|
19001
|
+
variant: "warning",
|
|
19002
|
+
message: /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
19003
|
+
"Served the ",
|
|
19004
|
+
/* @__PURE__ */ jsx("strong", { children: data.served }),
|
|
19005
|
+
" stream — this clip has no ",
|
|
19006
|
+
profile,
|
|
19007
|
+
" ",
|
|
19008
|
+
"twin."
|
|
19009
|
+
] })
|
|
19010
|
+
}), /* @__PURE__ */ jsxs("a", {
|
|
19011
|
+
href: hubPath(data.playbackUrl),
|
|
19012
|
+
download: true,
|
|
19013
|
+
className: "inline-flex items-center gap-1.5 text-xs font-medium text-primary hover:underline",
|
|
19014
|
+
children: [/* @__PURE__ */ jsx(Download, {
|
|
19015
|
+
className: "h-3.5 w-3.5",
|
|
19016
|
+
"aria-hidden": "true"
|
|
19017
|
+
}), "Download this clip"]
|
|
19018
|
+
})]
|
|
19019
|
+
})
|
|
19020
|
+
]
|
|
19021
|
+
});
|
|
19022
|
+
}
|
|
19023
|
+
//#endregion
|
|
19024
|
+
//#region src/lib/format-duration.ts
|
|
19025
|
+
/**
|
|
19026
|
+
* How long something lasted, the way this product says it.
|
|
19027
|
+
*
|
|
19028
|
+
* Promoted verbatim from `composites/cap-settings/RecordingPanel.tsx`, which
|
|
19029
|
+
* had been the only place answering this question about MEDIA. It is now the
|
|
19030
|
+
* only place, so a clip's length and a recording's retention read the same.
|
|
19031
|
+
*
|
|
19032
|
+
* Two other private formatters stay where they are, on purpose:
|
|
19033
|
+
* `config-form-field.tsx` formats a config VALUE (sub-second `ms`, `—` for a
|
|
19034
|
+
* value that is not a number), and `scene-monitor-copy.ts`'s `humanDuration`
|
|
19035
|
+
* is deliberately single-unit and coarse for a sentence. Neither is a media
|
|
19036
|
+
* length, and folding them in here would change what they say.
|
|
19037
|
+
*/
|
|
19038
|
+
function formatDurationMs(ms) {
|
|
19039
|
+
const totalSec = Math.round(ms / 1e3);
|
|
19040
|
+
const h = Math.floor(totalSec / 3600);
|
|
19041
|
+
const m = Math.floor(totalSec % 3600 / 60);
|
|
19042
|
+
const s = totalSec % 60;
|
|
19043
|
+
if (h > 0) return `${h}h ${m}m`;
|
|
19044
|
+
if (m > 0) return `${m}m ${s}s`;
|
|
19045
|
+
return `${s}s`;
|
|
19046
|
+
}
|
|
19047
|
+
//#endregion
|
|
19048
|
+
//#region src/composites/clips/clip-tile-media.ts
|
|
19049
|
+
/**
|
|
19050
|
+
* Why no still exists, in words, keyed by the refusal CODE.
|
|
19051
|
+
*
|
|
19052
|
+
* A code this table does not know falls back to the provider's own prose, so a
|
|
19053
|
+
* firmware inventing a refusal reaches the operator rather than becoming a
|
|
19054
|
+
* blank tile.
|
|
19055
|
+
*/
|
|
19056
|
+
var REFUSAL_TEXT = {
|
|
19057
|
+
sleeping: () => "The camera is asleep — a still would cost a wake",
|
|
19058
|
+
"camera-refused": (reason) => `The camera refused — ${reason}`,
|
|
19059
|
+
"no-keyframe": () => "No decodable frame inside this window",
|
|
19060
|
+
unsupported: () => "This source cannot produce stills",
|
|
19061
|
+
"queue-full": () => "Waiting for the camera — its still queue is full",
|
|
19062
|
+
"camera-backoff": () => "The camera is resting after refusing — trying again shortly",
|
|
19063
|
+
"no-catalog-row": () => "This clip is no longer in the catalog",
|
|
19064
|
+
"unknown-device": () => "This camera is not served here",
|
|
19065
|
+
"uid-missing": () => "The camera’s id is not known yet"
|
|
19066
|
+
};
|
|
19067
|
+
function wordsFor(code, reason) {
|
|
19068
|
+
const known = code === void 0 ? void 0 : REFUSAL_TEXT[code];
|
|
19069
|
+
return known === void 0 ? reason : known(reason);
|
|
19070
|
+
}
|
|
19071
|
+
function refusal(reason, code) {
|
|
19072
|
+
return {
|
|
19073
|
+
kind: "unavailable",
|
|
19074
|
+
reason,
|
|
19075
|
+
text: wordsFor(code, reason)
|
|
19076
|
+
};
|
|
19077
|
+
}
|
|
19078
|
+
/** What to draw for one clip's tile. */
|
|
19079
|
+
function resolveTileMedia(clip) {
|
|
19080
|
+
if (clip.thumbnail !== void 0) return {
|
|
19081
|
+
kind: "vouched",
|
|
19082
|
+
url: clip.thumbnail
|
|
19083
|
+
};
|
|
19084
|
+
if (clip.thumbnailUnavailable !== void 0) return refusal(clip.thumbnailUnavailable.reason, clip.thumbnailUnavailable.reason);
|
|
19085
|
+
if (clip.thumbnailMint !== void 0) return {
|
|
19086
|
+
kind: "mint",
|
|
19087
|
+
url: clip.thumbnailMint
|
|
19088
|
+
};
|
|
19089
|
+
return {
|
|
19090
|
+
kind: "none",
|
|
19091
|
+
text: "This clip carries no still and offered none"
|
|
19092
|
+
};
|
|
19093
|
+
}
|
|
19094
|
+
/**
|
|
19095
|
+
* A 204, as something the tile can draw.
|
|
19096
|
+
*
|
|
19097
|
+
* `deferred` is the ONLY answer that comes back, and only because the server
|
|
19098
|
+
* said so. Everything else — an unknown disposition, an absent one, a code
|
|
19099
|
+
* this build has never heard of — is final, so the failure mode of a
|
|
19100
|
+
* vocabulary that moves is a tile that stops early, never one that asks for
|
|
19101
|
+
* ever.
|
|
19102
|
+
*/
|
|
19103
|
+
function describeRefusal(headers) {
|
|
19104
|
+
if (headers.disposition === "deferred") return {
|
|
19105
|
+
kind: "deferred",
|
|
19106
|
+
reason: headers.reason,
|
|
19107
|
+
text: wordsFor(headers.code, headers.reason),
|
|
19108
|
+
retryAfterMs: headers.retryAfterMs ?? null
|
|
19109
|
+
};
|
|
19110
|
+
return refusal(headers.reason, headers.code);
|
|
19111
|
+
}
|
|
19112
|
+
//#endregion
|
|
19113
|
+
//#region src/composites/clips/clip-thumbnail-fetch.ts
|
|
19114
|
+
/**
|
|
19115
|
+
* Asking a `thumbnailMint` route for a still — and hearing what it says.
|
|
19116
|
+
*
|
|
19117
|
+
* The route answers **200** with a JPEG or **204** with a refusal. An
|
|
19118
|
+
* `<img src>` cannot read a 204's headers: it fires `onerror` with nothing to
|
|
19119
|
+
* say, turning a named refusal into an anonymous broken tile. So a mint goes
|
|
19120
|
+
* through `fetch`.
|
|
19121
|
+
*
|
|
19122
|
+
* **Nothing is appended to the URL.** Both data-plane routes are registered
|
|
19123
|
+
* `access: 'authenticated'` and the hub's `/addon/<id>/<prefix>` proxy takes
|
|
19124
|
+
* the session cookie the admin UI sets at login, so the URL is never a
|
|
19125
|
+
* credential (D549 decision 22) — it may sit in history or a `Referer` and
|
|
19126
|
+
* open nothing.
|
|
19127
|
+
*
|
|
19128
|
+
* ## Three headers, one of which decides everything (D563)
|
|
19129
|
+
*
|
|
19130
|
+
* `x-camstack-reason-code` is the token the tile keys its words on,
|
|
19131
|
+
* `x-camstack-reason` is the camera's own prose, and
|
|
19132
|
+
* `x-camstack-disposition` says whether to come back. This module reads them;
|
|
19133
|
+
* it does not classify them. A response with no disposition — every
|
|
19134
|
+
* provider-reolink build before D563 — is FINAL, which is precisely the
|
|
19135
|
+
* behaviour that shipped.
|
|
19136
|
+
*
|
|
19137
|
+
* A thrown fetch is DEFERRED: nothing refused anything, one request lost a
|
|
19138
|
+
* race, and a tile that goes permanently blank for that is the same defect
|
|
19139
|
+
* with a different cause.
|
|
19140
|
+
*/
|
|
19141
|
+
/**
|
|
19142
|
+
* Ask a mint route for one still.
|
|
19143
|
+
*
|
|
19144
|
+
* Never rejects: a thrown fetch is a refusal like any other, because a tile
|
|
19145
|
+
* that throws is a tile that renders nothing and says nothing — the exact
|
|
19146
|
+
* failure this module exists to prevent.
|
|
19147
|
+
*/
|
|
19148
|
+
async function fetchMintedThumbnail(url, fetcher) {
|
|
19149
|
+
let response;
|
|
19150
|
+
try {
|
|
19151
|
+
response = await fetcher(url, { credentials: "same-origin" });
|
|
19152
|
+
} catch (err) {
|
|
19153
|
+
return deferredNetworkFailure(err);
|
|
19154
|
+
}
|
|
19155
|
+
if (response.status === 204) return describeRefusal(refusalHeadersOf(response));
|
|
19156
|
+
if (!response.ok) return describeRefusal({
|
|
19157
|
+
code: void 0,
|
|
19158
|
+
reason: `HTTP ${String(response.status)}`,
|
|
19159
|
+
disposition: void 0
|
|
19160
|
+
});
|
|
19161
|
+
try {
|
|
19162
|
+
return {
|
|
19163
|
+
kind: "image",
|
|
19164
|
+
blob: await response.blob()
|
|
19165
|
+
};
|
|
19166
|
+
} catch (err) {
|
|
19167
|
+
return deferredNetworkFailure(err);
|
|
19168
|
+
}
|
|
19169
|
+
}
|
|
19170
|
+
function refusalHeadersOf(response) {
|
|
19171
|
+
const retryAfter = response.headers.get("retry-after");
|
|
19172
|
+
const seconds = retryAfter === null ? NaN : Number(retryAfter);
|
|
19173
|
+
return {
|
|
19174
|
+
code: response.headers.get(CLIP_STILL_REASON_CODE_HEADER) ?? void 0,
|
|
19175
|
+
reason: response.headers.get(CLIP_STILL_REASON_HEADER) ?? "no reason given",
|
|
19176
|
+
disposition: response.headers.get(CLIP_STILL_DISPOSITION_HEADER) ?? void 0,
|
|
19177
|
+
retryAfterMs: Number.isFinite(seconds) && seconds > 0 ? seconds * 1e3 : null
|
|
19178
|
+
};
|
|
19179
|
+
}
|
|
19180
|
+
function deferredNetworkFailure(err) {
|
|
19181
|
+
return describeRefusal({
|
|
19182
|
+
code: void 0,
|
|
19183
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
19184
|
+
disposition: "deferred"
|
|
19185
|
+
});
|
|
19186
|
+
}
|
|
19187
|
+
/** First wait, doubling from there. */
|
|
19188
|
+
var STILL_RETRY_BASE_MS = 4e3;
|
|
19189
|
+
/** No single wait is longer than this, whatever the server hints. */
|
|
19190
|
+
var STILL_RETRY_MAX_WAIT_MS = 3e4;
|
|
19191
|
+
/** ±25 %: enough to break a synchronised herd, not enough to stop being a
|
|
19192
|
+
* schedule. */
|
|
19193
|
+
var JITTER_FRACTION = .25;
|
|
19194
|
+
/** The next wait, or `null` when the budget is spent. */
|
|
19195
|
+
function planStillRetry(input) {
|
|
19196
|
+
if (input.attempt >= 4 || input.attempt < 0) return null;
|
|
19197
|
+
const curve = STILL_RETRY_BASE_MS * 2 ** input.attempt;
|
|
19198
|
+
const chosen = Math.max(curve, input.serverHintMs ?? 0);
|
|
19199
|
+
const bounded = Math.min(chosen, STILL_RETRY_MAX_WAIT_MS);
|
|
19200
|
+
const jitter = 1 + (input.random() * 2 - 1) * JITTER_FRACTION;
|
|
19201
|
+
return { waitMs: Math.round(bounded * jitter) };
|
|
19202
|
+
}
|
|
19203
|
+
//#endregion
|
|
19204
|
+
//#region src/composites/clips/ClipTile.tsx
|
|
19205
|
+
/**
|
|
19206
|
+
* One clip, as a tile.
|
|
19207
|
+
*
|
|
19208
|
+
* The picture is resolved by `resolveTileMedia` — vouched JPEG, an offer, or
|
|
19209
|
+
* the reason there is none (D549 decision 5). An offer is fetched rather than
|
|
19210
|
+
* dropped into an `<img src>`, because the route's **204 + x-camstack-reason**
|
|
19211
|
+
* is invisible to an `<img>`: it fires `onerror` with nothing to say. The
|
|
19212
|
+
* refusal is LATCHED for the life of the tile and never retried behind the
|
|
19213
|
+
* operator's back.
|
|
19214
|
+
*
|
|
19215
|
+
* The row also carries what the camera itself said: its native types, the
|
|
19216
|
+
* behaviours beside the class, which storages hold it, and — for a row that
|
|
19217
|
+
* is an EVENT sitting inside no file — `playable: false` with its reason
|
|
19218
|
+
* (measured: 1 of 29 hub events on 3628). Such a row is SHOWN, never dropped
|
|
19219
|
+
* and never offered as playable-then-failing (D549 decision 19).
|
|
19220
|
+
*
|
|
19221
|
+
* The CHROME is the library's: a `Card` for the tile, `Badge` for every label
|
|
19222
|
+
* the camera gave it, `Skeleton` while a mint is in flight. Only the ladder
|
|
19223
|
+
* above is this file's own, because only it is a decision.
|
|
19224
|
+
*/
|
|
19225
|
+
function clockOf(ms) {
|
|
19226
|
+
return new Date(ms).toLocaleTimeString(void 0, {
|
|
19227
|
+
hour: "2-digit",
|
|
17906
19228
|
minute: "2-digit",
|
|
17907
19229
|
second: "2-digit"
|
|
17908
19230
|
});
|
|
17909
19231
|
}
|
|
17910
19232
|
/**
|
|
17911
|
-
* Ask a mint route
|
|
19233
|
+
* Ask a mint route, and come back only for the answers that say to.
|
|
17912
19234
|
*
|
|
17913
19235
|
* The offer is memoised on the three fields it is derived from, not rebuilt
|
|
17914
19236
|
* per render: `resolveTileMedia` returns a fresh object every call, and an
|
|
@@ -17916,8 +19238,12 @@ function clockOf(ms) {
|
|
|
17916
19238
|
* effect sets state, is an infinite render loop. It was one here, and it hung
|
|
17917
19239
|
* the suite rather than failing it.
|
|
17918
19240
|
*
|
|
17919
|
-
*
|
|
17920
|
-
* "
|
|
19241
|
+
* A FINAL refusal is latched for the life of the tile: a camera that just said
|
|
19242
|
+
* "asleep" is not asked again behind the operator's back. A DEFERRED one is
|
|
19243
|
+
* not a refusal at all — the still is queued — so the tile keeps showing
|
|
19244
|
+
* loading and re-asks on `planStillRetry`'s bounded, jittered schedule. When
|
|
19245
|
+
* that budget runs out the deferred answer becomes what the tile SAYS, so the
|
|
19246
|
+
* end state is words, never a blank box and never another request (D563).
|
|
17921
19247
|
*/
|
|
17922
19248
|
function useMintedStill(clip) {
|
|
17923
19249
|
const offered = useMemo(() => resolveTileMedia(clip), [
|
|
@@ -17932,7 +19258,8 @@ function useMintedStill(clip) {
|
|
|
17932
19258
|
if (mintUrl === null || typeof fetch !== "function") return void 0;
|
|
17933
19259
|
let objectUrl = null;
|
|
17934
19260
|
let cancelled = false;
|
|
17935
|
-
|
|
19261
|
+
let timer;
|
|
19262
|
+
const ask = async (attempt) => {
|
|
17936
19263
|
const answer = await fetchMintedThumbnail(hubPath(mintUrl), fetch);
|
|
17937
19264
|
if (cancelled) return;
|
|
17938
19265
|
if (answer.kind === "image") {
|
|
@@ -17943,10 +19270,31 @@ function useMintedStill(clip) {
|
|
|
17943
19270
|
});
|
|
17944
19271
|
return;
|
|
17945
19272
|
}
|
|
17946
|
-
|
|
17947
|
-
|
|
19273
|
+
if (answer.kind !== "deferred") {
|
|
19274
|
+
setLatched(answer);
|
|
19275
|
+
return;
|
|
19276
|
+
}
|
|
19277
|
+
const plan = planStillRetry({
|
|
19278
|
+
attempt,
|
|
19279
|
+
serverHintMs: answer.retryAfterMs,
|
|
19280
|
+
random: Math.random
|
|
19281
|
+
});
|
|
19282
|
+
if (plan === null) {
|
|
19283
|
+
setLatched({
|
|
19284
|
+
kind: "unavailable",
|
|
19285
|
+
reason: answer.reason,
|
|
19286
|
+
text: answer.text
|
|
19287
|
+
});
|
|
19288
|
+
return;
|
|
19289
|
+
}
|
|
19290
|
+
timer = setTimeout(() => {
|
|
19291
|
+
ask(attempt + 1);
|
|
19292
|
+
}, plan.waitMs);
|
|
19293
|
+
};
|
|
19294
|
+
ask(0);
|
|
17948
19295
|
return () => {
|
|
17949
19296
|
cancelled = true;
|
|
19297
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
17950
19298
|
if (objectUrl !== null) URL.revokeObjectURL(objectUrl);
|
|
17951
19299
|
};
|
|
17952
19300
|
}, [mintUrl]);
|
|
@@ -17956,51 +19304,90 @@ function ClipTile({ clip, selected, onSelect }) {
|
|
|
17956
19304
|
const media = useMintedStill(clip);
|
|
17957
19305
|
const playable = clip.playable !== false;
|
|
17958
19306
|
const labels = [...clip.labels ?? [], ...clip.behaviours ?? []];
|
|
17959
|
-
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */
|
|
19307
|
+
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("button", {
|
|
17960
19308
|
type: "button",
|
|
17961
19309
|
onClick: () => onSelect(clip),
|
|
17962
19310
|
disabled: !playable,
|
|
17963
19311
|
"aria-current": selected ? "true" : void 0,
|
|
17964
|
-
className:
|
|
17965
|
-
children:
|
|
17966
|
-
className:
|
|
17967
|
-
children:
|
|
17968
|
-
|
|
17969
|
-
|
|
17970
|
-
|
|
17971
|
-
|
|
17972
|
-
|
|
17973
|
-
|
|
17974
|
-
|
|
17975
|
-
|
|
17976
|
-
|
|
17977
|
-
|
|
17978
|
-
/* @__PURE__ */ jsx("
|
|
17979
|
-
className: "
|
|
17980
|
-
children:
|
|
17981
|
-
}),
|
|
17982
|
-
/* @__PURE__ */ jsx("div", {
|
|
17983
|
-
className: "text-xs text-muted-foreground",
|
|
17984
|
-
children: clip.source
|
|
17985
|
-
}),
|
|
17986
|
-
labels.length === 0 ? null : /* @__PURE__ */ jsx("div", {
|
|
17987
|
-
className: "text-xs",
|
|
17988
|
-
children: labels.join(" · ")
|
|
17989
|
-
}),
|
|
17990
|
-
clip.nativeTypes === void 0 || clip.nativeTypes.length === 0 ? null : /* @__PURE__ */ jsx("div", {
|
|
17991
|
-
className: "text-[10px] text-muted-foreground",
|
|
17992
|
-
children: clip.nativeTypes.join(", ")
|
|
17993
|
-
}),
|
|
17994
|
-
playable ? null : /* @__PURE__ */ jsxs("div", {
|
|
17995
|
-
className: "text-xs text-amber-600 dark:text-amber-400",
|
|
17996
|
-
children: ["Not playable — ", clip.unplayableReason ?? "the source gave no reason"]
|
|
19312
|
+
className: "block w-full text-left disabled:cursor-not-allowed",
|
|
19313
|
+
children: /* @__PURE__ */ jsxs(Card, {
|
|
19314
|
+
className: `flex gap-3 transition-colors ${selected ? "border-primary" : ""} ${playable ? "hover:bg-surface-hover" : "opacity-70"}`,
|
|
19315
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
19316
|
+
className: "flex h-16 w-28 shrink-0 items-center justify-center overflow-hidden rounded bg-surface-hover text-[10px] text-foreground-subtle",
|
|
19317
|
+
children: media.kind === "vouched" ? /* @__PURE__ */ jsx("img", {
|
|
19318
|
+
src: media.url,
|
|
19319
|
+
alt: "",
|
|
19320
|
+
className: "h-full w-full object-cover"
|
|
19321
|
+
}) : media.kind === "mint" || media.kind === "deferred" ? /* @__PURE__ */ jsx("span", {
|
|
19322
|
+
role: "status",
|
|
19323
|
+
"aria-label": media.kind === "mint" ? "Loading the still" : media.text,
|
|
19324
|
+
className: "h-full w-full",
|
|
19325
|
+
children: /* @__PURE__ */ jsx(Skeleton, { className: "h-full w-full rounded-none" })
|
|
19326
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
19327
|
+
className: "px-1 text-center leading-tight",
|
|
19328
|
+
children: media.text
|
|
17997
19329
|
})
|
|
17998
|
-
|
|
17999
|
-
|
|
19330
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
19331
|
+
className: "min-w-0 flex-1 space-y-1",
|
|
19332
|
+
children: [
|
|
19333
|
+
/* @__PURE__ */ jsxs("div", {
|
|
19334
|
+
className: "flex items-baseline gap-2",
|
|
19335
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
19336
|
+
className: "text-sm font-medium text-foreground",
|
|
19337
|
+
children: clockOf(clip.timeRange.startMs)
|
|
19338
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
19339
|
+
className: "text-[11px] text-foreground-subtle",
|
|
19340
|
+
children: clip.inProgress === true ? "Recording…" : formatDurationMs(clip.timeRange.endMs - clip.timeRange.startMs)
|
|
19341
|
+
})]
|
|
19342
|
+
}),
|
|
19343
|
+
/* @__PURE__ */ jsx("div", {
|
|
19344
|
+
className: "text-[10px] text-foreground-subtle",
|
|
19345
|
+
children: clip.source
|
|
19346
|
+
}),
|
|
19347
|
+
labels.length === 0 && clip.nativeTypes === void 0 ? null : /* @__PURE__ */ jsxs("div", {
|
|
19348
|
+
className: "flex flex-wrap gap-1",
|
|
19349
|
+
children: [labels.map((label) => /* @__PURE__ */ jsx(Badge, {
|
|
19350
|
+
variant: "info",
|
|
19351
|
+
children: label
|
|
19352
|
+
}, `label:${label}`)), (clip.nativeTypes ?? []).map((native) => /* @__PURE__ */ jsx(Badge, {
|
|
19353
|
+
style: "outline",
|
|
19354
|
+
children: native
|
|
19355
|
+
}, `native:${native}`))]
|
|
19356
|
+
}),
|
|
19357
|
+
playable ? null : /* @__PURE__ */ jsxs("div", {
|
|
19358
|
+
className: "text-[11px] text-warning",
|
|
19359
|
+
children: ["Not playable — ", clip.unplayableReason ?? "the source gave no reason"]
|
|
19360
|
+
})
|
|
19361
|
+
]
|
|
19362
|
+
})]
|
|
19363
|
+
})
|
|
18000
19364
|
}) });
|
|
18001
19365
|
}
|
|
18002
19366
|
//#endregion
|
|
18003
19367
|
//#region src/composites/clips/clip-day-window.ts
|
|
19368
|
+
/**
|
|
19369
|
+
* LOCAL calendar-day arithmetic for the clips browser.
|
|
19370
|
+
*
|
|
19371
|
+
* The browser no longer NAVIGATES days — it follows a band selected on the
|
|
19372
|
+
* recording timebar (D564) — but the local day survived the move as the unit
|
|
19373
|
+
* the COST is counted in: a Reolink search is clamped to one camera-local day
|
|
19374
|
+
* per call, so `clip-band.ts` counts a band's local days to decide whether it
|
|
19375
|
+
* may be asked for. The window builder this file used to own went with the day
|
|
19376
|
+
* picker; `recording-timeline-model.ts`'s `dayWindow(date, nowMs)` is the one
|
|
19377
|
+
* that builds windows now, and one directory holding two functions of that
|
|
19378
|
+
* name was the confusion worth deleting.
|
|
19379
|
+
*
|
|
19380
|
+
* Two properties are pinned by spec because both are invisible until they are
|
|
19381
|
+
* wrong:
|
|
19382
|
+
*
|
|
19383
|
+
* - Deriving the day in UTC shifts every band by the browser's offset, and an
|
|
19384
|
+
* operator in Berlin would see a 22:00 clip filed under the next day.
|
|
19385
|
+
* - Walking days by adding 86 400 000 ms repeats or skips a calendar day
|
|
19386
|
+
* across a DST change. The walk goes through `Date`'s own day arithmetic.
|
|
19387
|
+
*
|
|
19388
|
+
* A day id is `YYYY-MM-DD` — it orders lexicographically, which is what lets
|
|
19389
|
+
* the band's day walk terminate on a comparison rather than on a count.
|
|
19390
|
+
*/
|
|
18004
19391
|
function pad2(value) {
|
|
18005
19392
|
return value < 10 ? `0${String(value)}` : String(value);
|
|
18006
19393
|
}
|
|
@@ -18013,16 +19400,6 @@ function startOfDay(dayId) {
|
|
|
18013
19400
|
function toDayId(instant) {
|
|
18014
19401
|
return `${String(instant.getFullYear())}-${pad2(instant.getMonth() + 1)}-${pad2(instant.getDate())}`;
|
|
18015
19402
|
}
|
|
18016
|
-
/** The half-open local-wall-clock window of one day. */
|
|
18017
|
-
function dayWindow$1(dayId) {
|
|
18018
|
-
const start = startOfDay(dayId);
|
|
18019
|
-
const end = startOfDay(dayId);
|
|
18020
|
-
end.setDate(end.getDate() + 1);
|
|
18021
|
-
return {
|
|
18022
|
-
since: start.getTime(),
|
|
18023
|
-
until: end.getTime()
|
|
18024
|
-
};
|
|
18025
|
-
}
|
|
18026
19403
|
/** Walk `days` calendar days from `dayId` — DST-safe, because `Date` does it. */
|
|
18027
19404
|
function shiftDay(dayId, days) {
|
|
18028
19405
|
const moved = startOfDay(dayId);
|
|
@@ -18030,11 +19407,12 @@ function shiftDay(dayId, days) {
|
|
|
18030
19407
|
return toDayId(moved);
|
|
18031
19408
|
}
|
|
18032
19409
|
/**
|
|
18033
|
-
* The day, spelled out
|
|
19410
|
+
* The day, spelled out — the calendar half of a band's label.
|
|
18034
19411
|
*
|
|
18035
|
-
* An empty list has to say WHICH
|
|
18036
|
-
* beside it is the ambiguity this surface exists to remove
|
|
18037
|
-
*
|
|
19412
|
+
* An empty list has to say WHICH period it is empty for; "no clips" with no
|
|
19413
|
+
* date beside it is the ambiguity this surface exists to remove. `formatBandLabel`
|
|
19414
|
+
* puts the times around it, and every state renders the result, not only the
|
|
19415
|
+
* populated one.
|
|
18038
19416
|
*/
|
|
18039
19417
|
function formatDayLabel(dayId) {
|
|
18040
19418
|
return startOfDay(dayId).toLocaleDateString(void 0, {
|
|
@@ -18044,6 +19422,77 @@ function formatDayLabel(dayId) {
|
|
|
18044
19422
|
day: "numeric"
|
|
18045
19423
|
});
|
|
18046
19424
|
}
|
|
19425
|
+
/** Defensive ceiling on the day walk so an inverted band can never loop. */
|
|
19426
|
+
var MAX_WALK_DAYS = 366;
|
|
19427
|
+
/**
|
|
19428
|
+
* How many LOCAL calendar days the half-open band touches.
|
|
19429
|
+
*
|
|
19430
|
+
* The end instant is exclusive, so a band that ends exactly at the next local
|
|
19431
|
+
* midnight is one day, not two — otherwise every whole-day band would report
|
|
19432
|
+
* double what it costs.
|
|
19433
|
+
*/
|
|
19434
|
+
function localDaysInBand(band) {
|
|
19435
|
+
const lastInstant = Math.max(band.fromMs, band.toMs - 1);
|
|
19436
|
+
const endDay = toDayId(new Date(lastInstant));
|
|
19437
|
+
let cursor = toDayId(new Date(band.fromMs));
|
|
19438
|
+
let days = 0;
|
|
19439
|
+
while (cursor <= endDay && days < MAX_WALK_DAYS) {
|
|
19440
|
+
days += 1;
|
|
19441
|
+
cursor = shiftDay(cursor, 1);
|
|
19442
|
+
}
|
|
19443
|
+
return days;
|
|
19444
|
+
}
|
|
19445
|
+
/** Whether this band may be asked for, and what it costs, in one value. */
|
|
19446
|
+
function planClipBand(band) {
|
|
19447
|
+
const localDays = localDaysInBand(band);
|
|
19448
|
+
if (localDays > 2) return {
|
|
19449
|
+
kind: "too-wide",
|
|
19450
|
+
band,
|
|
19451
|
+
localDays,
|
|
19452
|
+
maxLocalDays: 2
|
|
19453
|
+
};
|
|
19454
|
+
return {
|
|
19455
|
+
kind: "askable",
|
|
19456
|
+
band,
|
|
19457
|
+
localDays
|
|
19458
|
+
};
|
|
19459
|
+
}
|
|
19460
|
+
/**
|
|
19461
|
+
* The band for the current selection: the picked bucket, or — when nothing is
|
|
19462
|
+
* picked — the whole visible window.
|
|
19463
|
+
*
|
|
19464
|
+
* "Nothing picked means everything visible" is safe here and only here,
|
|
19465
|
+
* because the visible window is itself bounded by the timebar. It is never
|
|
19466
|
+
* "everything the camera holds" (D382's unstated ask).
|
|
19467
|
+
*/
|
|
19468
|
+
function bandForSelection(view, bucket) {
|
|
19469
|
+
if (bucket === null) return {
|
|
19470
|
+
fromMs: view.fromMs,
|
|
19471
|
+
toMs: view.toMs
|
|
19472
|
+
};
|
|
19473
|
+
return {
|
|
19474
|
+
fromMs: Math.max(bucket.startMs, view.fromMs),
|
|
19475
|
+
toMs: Math.min(bucket.endMs, view.toMs)
|
|
19476
|
+
};
|
|
19477
|
+
}
|
|
19478
|
+
function formatTime$2(instant) {
|
|
19479
|
+
return new Date(instant).toLocaleTimeString(void 0, {
|
|
19480
|
+
hour: "2-digit",
|
|
19481
|
+
minute: "2-digit"
|
|
19482
|
+
});
|
|
19483
|
+
}
|
|
19484
|
+
/**
|
|
19485
|
+
* The band, spelled out.
|
|
19486
|
+
*
|
|
19487
|
+
* Every state renders it — an empty list has to say WHICH period it is empty
|
|
19488
|
+
* for, the same rule the day label carried before the band replaced it.
|
|
19489
|
+
*/
|
|
19490
|
+
function formatBandLabel(band) {
|
|
19491
|
+
const fromDay = toDayId(new Date(band.fromMs));
|
|
19492
|
+
const toDay = toDayId(new Date(Math.max(band.fromMs, band.toMs - 1)));
|
|
19493
|
+
if (fromDay === toDay) return `${formatDayLabel(fromDay)}, ${formatTime$2(band.fromMs)} – ${formatTime$2(band.toMs)}`;
|
|
19494
|
+
return `${formatDayLabel(fromDay)} ${formatTime$2(band.fromMs)} – ${formatDayLabel(toDay)} ${formatTime$2(band.toMs)}`;
|
|
19495
|
+
}
|
|
18047
19496
|
//#endregion
|
|
18048
19497
|
//#region src/composites/clips/clip-source-state.ts
|
|
18049
19498
|
var STATE_COPY = {
|
|
@@ -18105,13 +19554,13 @@ function formatCatalogAge(catalogAsOf, now) {
|
|
|
18105
19554
|
}
|
|
18106
19555
|
//#endregion
|
|
18107
19556
|
//#region src/composites/clips/clip-view-state.ts
|
|
18108
|
-
/** Keep only the rows of selected sources; an absent selection keeps them all. */
|
|
18109
|
-
function applySelection(clips, selected) {
|
|
18110
|
-
if (selected === void 0) return clips;
|
|
18111
|
-
const allowed = new Set(selected);
|
|
18112
|
-
return clips.filter((clip) => allowed.has(clip.source));
|
|
18113
|
-
}
|
|
18114
19557
|
function resolveClipsView(input) {
|
|
19558
|
+
if (input.plan.kind === "too-wide") return {
|
|
19559
|
+
kind: "refused",
|
|
19560
|
+
band: input.plan.band,
|
|
19561
|
+
localDays: input.plan.localDays,
|
|
19562
|
+
maxLocalDays: input.plan.maxLocalDays
|
|
19563
|
+
};
|
|
18115
19564
|
if (input.clipsStatus === "error") return {
|
|
18116
19565
|
kind: "failed",
|
|
18117
19566
|
message: input.clipsError ?? "The clip list could not be read"
|
|
@@ -18124,10 +19573,10 @@ function resolveClipsView(input) {
|
|
|
18124
19573
|
const notices = input.sources.map(describeSource);
|
|
18125
19574
|
const answered = notices.filter((notice) => notice.answered);
|
|
18126
19575
|
const silent = notices.filter((notice) => !notice.answered);
|
|
18127
|
-
const clips =
|
|
19576
|
+
const clips = input.clips;
|
|
18128
19577
|
if (clips.length === 0) return {
|
|
18129
19578
|
kind: "empty",
|
|
18130
|
-
|
|
19579
|
+
band: input.plan.band,
|
|
18131
19580
|
answered,
|
|
18132
19581
|
silent,
|
|
18133
19582
|
nobodyAnswered: answered.length === 0
|
|
@@ -18140,174 +19589,415 @@ function resolveClipsView(input) {
|
|
|
18140
19589
|
};
|
|
18141
19590
|
}
|
|
18142
19591
|
//#endregion
|
|
18143
|
-
//#region src/composites/clips/
|
|
19592
|
+
//#region src/composites/clips/ClipsSection.tsx
|
|
18144
19593
|
/**
|
|
18145
|
-
* `host/clips-browser` — a camera's clips, from EVERY source it has
|
|
19594
|
+
* `host/clips-browser` — a camera's clips, from EVERY source it has, as the
|
|
19595
|
+
* LAST SECTION of the Recording tab.
|
|
18146
19596
|
*
|
|
18147
19597
|
* Mounted by the framework, not by hand: `videoclips.cap.ts` declares
|
|
18148
19598
|
* `deviceConfig.ui = { kind:'widget', widgetId:'host/clips-browser',
|
|
18149
|
-
* tab:'
|
|
18150
|
-
* section (D14). `DeviceDetail.tsx`
|
|
18151
|
-
*
|
|
18152
|
-
*
|
|
18153
|
-
*
|
|
18154
|
-
*
|
|
18155
|
-
*
|
|
18156
|
-
*
|
|
18157
|
-
*
|
|
18158
|
-
*
|
|
18159
|
-
*
|
|
19599
|
+
* tab:'recording', topTab:true, order:100 }` and
|
|
19600
|
+
* `deriveDeviceConfigContribution` emits the section (D14). `DeviceDetail.tsx`
|
|
19601
|
+
* is not edited — placement is data. Because the admin Recordings page renders
|
|
19602
|
+
* every `location:'top-tab'` + `tab:'recording'` section behind its camera
|
|
19603
|
+
* picker (`CameraRecordingSettingsSection`), the same declaration puts this on
|
|
19604
|
+
* both surfaces with no second wiring.
|
|
19605
|
+
*
|
|
19606
|
+
* ## Built from the library, not from scratch
|
|
19607
|
+
*
|
|
19608
|
+
* It shipped as its own top tab made of five bespoke components, and the
|
|
19609
|
+
* operator rejected both: *"La UI è orribile, non segue la nostra UI affatto …
|
|
19610
|
+
* Io in realtà utilizzerei la stessa tab recordings, lì abbiamo già tutto il
|
|
19611
|
+
* necessario, una nuova sezione alla fine per le clips dove possiamo
|
|
19612
|
+
* selezionare le source con un multiselect common, non componenti nuovi
|
|
19613
|
+
* anonimi."* So every piece of chrome here is a component this repo already
|
|
19614
|
+
* has: `Select`, `DataTable`, `EmptyState`, `ErrorBox`, `Pagination`, `Card`,
|
|
19615
|
+
* `Badge`, `Button`, `Skeleton`. What the chrome CARRIES is unchanged, and it
|
|
19616
|
+
* is the feature:
|
|
18160
19617
|
*
|
|
18161
19618
|
* - **The source list is complete, always.** Every source the camera has,
|
|
18162
|
-
* with its availability and the age of its catalog
|
|
18163
|
-
*
|
|
19619
|
+
* with its availability and the age of its catalog, whichever provider is
|
|
19620
|
+
* being asked. A source never leaves the table.
|
|
18164
19621
|
* - **Nothing is deduped.** A clip held in two storages appears twice, once
|
|
18165
|
-
* per source, and every
|
|
18166
|
-
* D549 so it is not "fixed" later.
|
|
19622
|
+
* per source, and every tile says which (D549).
|
|
18167
19623
|
* - **Four states are tellable apart**: loading, empty-with-a-cause,
|
|
18168
19624
|
* degraded-per-source, and each silent source in its own words. Every one
|
|
18169
|
-
* of them used to render as "no clips", which is the defect this whole
|
|
18170
|
-
*
|
|
18171
|
-
*
|
|
19625
|
+
* of them used to render as "no clips", which is the defect this whole line
|
|
19626
|
+
* of work is about — a camera with no SD card (640) is not quiet, and a
|
|
19627
|
+
* camera whose own index disagrees with its own calendar (618, 6–20
|
|
18172
19628
|
* September) is not quiet either.
|
|
18173
19629
|
*
|
|
18174
|
-
* ##
|
|
18175
|
-
*
|
|
18176
|
-
*
|
|
18177
|
-
*
|
|
18178
|
-
*
|
|
18179
|
-
*
|
|
18180
|
-
*
|
|
18181
|
-
*
|
|
18182
|
-
*
|
|
18183
|
-
*
|
|
18184
|
-
*
|
|
18185
|
-
*
|
|
18186
|
-
*
|
|
19630
|
+
* ## The period comes from the recording section's timebar, not a day picker
|
|
19631
|
+
*
|
|
19632
|
+
* It shipped with its own `‹ day ›` pair and the operator replaced it the same
|
|
19633
|
+
* way he replaced the chrome: *"Possiamo utilizzare lo stesso componente della
|
|
19634
|
+
* sezione recording con la timebar dove selezionare un periodo e mostrare le
|
|
19635
|
+
* clip solo di quella banda selezionata, espandibile."* So the list follows a
|
|
19636
|
+
* BAND (D564) — `TimelineControls` for the day, the zoom and the pan,
|
|
19637
|
+
* `EventHeatmap` for picking a narrower window out of it, both of them the
|
|
19638
|
+
* pieces `RecordingTimeline` is itself built from, over the same
|
|
19639
|
+
* `recording-timeline-model`. Three things that are decisions, not details:
|
|
19640
|
+
*
|
|
19641
|
+
* - **The band lives HERE, once.** `ClipBandBar` is presentational. Two
|
|
19642
|
+
* components holding two ideas of the selected period is the defect this
|
|
19643
|
+
* repo keeps paying for, and the period is what the query is made of.
|
|
19644
|
+
* - **`CoverageTrack` stayed behind**, because there it is the seek surface
|
|
19645
|
+
* that moves the hero player's playhead. A copy here would be a dead bar or
|
|
19646
|
+
* a second claimant on that position.
|
|
19647
|
+
* - **A band is asked for only if it is affordable.** `clip-band.ts` counts
|
|
19648
|
+
* the LOCAL days it touches — the camera's own search is clamped to one per
|
|
19649
|
+
* call, and a day listing is 4.3–6.0 s on 592 — and refuses past the bound
|
|
19650
|
+
* by name rather than asking and rendering whichever days answered.
|
|
19651
|
+
*
|
|
19652
|
+
* ## The picker WRITES nothing, and there is nothing for it to write
|
|
19653
|
+
*
|
|
19654
|
+
* A clip source is not enabled or disabled — it EXISTS, because of a
|
|
19655
|
+
* configuration that lives somewhere else. The operator put it exactly:
|
|
19656
|
+
* *"non è affatto necessario lo stato di abilitato o meno delle sources. Loro
|
|
19657
|
+
* vivono lì, ad esempio hksv c'è se la camera è esportata su homekit, reolink
|
|
19658
|
+
* la dà di suo così come il camstack."* HKSV is there if the camera is
|
|
19659
|
+
* exported to HomeKit; the Reolink source is there because Reolink is that
|
|
19660
|
+
* camera's provider; CamStack's is always there. None of that is a decision to
|
|
19661
|
+
* make in a clips list, so this surface offers no switch and writes no
|
|
19662
|
+
* authority (D557, amended). A directory-wide guard in
|
|
19663
|
+
* `clips-section-sources.test.tsx` keeps it that way.
|
|
19664
|
+
*
|
|
19665
|
+
* ## Why the picker is a SELECT and not a multiselect
|
|
19666
|
+
*
|
|
19667
|
+
* The operator asked for *"un multiselect common"* while `listClips` still
|
|
19668
|
+
* took `sources?: string[]`. It now takes ONE `provider` — *"il get videoclips
|
|
19669
|
+
* prendere un provider opzionale, di default va su camstack … l'utilizzatore è
|
|
19670
|
+
* uno solo"* — and a multi-pick control over a one-at-a-time argument offers a
|
|
19671
|
+
* sum the hub cannot answer. So the control is the shared single-choice one,
|
|
19672
|
+
* `Select`: the same component the Recordings page's own camera picker uses,
|
|
19673
|
+
* which is what *"segue la nostra UI"* means here. Absent means the camera's
|
|
19674
|
+
* BOUND provider, and the option that carries it is labelled with that
|
|
19675
|
+
* provider's own sources rather than the word "default".
|
|
19676
|
+
*
|
|
19677
|
+
* ## One provider is a fleet STATE
|
|
19678
|
+
*
|
|
19679
|
+
* `listSources` derives from `deviceManager.getBindings`, and today nothing
|
|
19680
|
+
* binds `provider-reolink` for `videoclips`, so a real camera answers with one
|
|
19681
|
+
* provider. The selector is then inert and SAYS why. An empty or silently
|
|
19682
|
+
* single-entry control would read as a defect; the binding store learning to
|
|
19683
|
+
* hold several providers for a collection cap is what changes this, and
|
|
19684
|
+
* nothing here needs to change with it.
|
|
18187
19685
|
*/
|
|
18188
19686
|
/** Newest-first, and bounded: a busy day on 592 lists 546 clips. */
|
|
18189
19687
|
var CLIP_PAGE_LIMIT = 500;
|
|
18190
|
-
|
|
18191
|
-
|
|
18192
|
-
|
|
19688
|
+
/** Tiles per page. The rest are a `Pagination` click away, never dropped. */
|
|
19689
|
+
var TILES_PER_PAGE = 60;
|
|
19690
|
+
var TONE_BADGE = {
|
|
19691
|
+
ok: "success",
|
|
19692
|
+
warn: "warning",
|
|
19693
|
+
error: "danger"
|
|
19694
|
+
};
|
|
19695
|
+
var SOURCE_COLUMNS = [
|
|
19696
|
+
{
|
|
19697
|
+
key: "source",
|
|
19698
|
+
header: "Source",
|
|
19699
|
+
render: (row) => /* @__PURE__ */ jsxs("div", {
|
|
19700
|
+
className: "min-w-0",
|
|
19701
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
19702
|
+
className: "font-medium text-foreground",
|
|
19703
|
+
children: row.notice.label
|
|
19704
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
19705
|
+
className: "text-[10px] text-foreground-subtle",
|
|
19706
|
+
children: row.notice.source
|
|
19707
|
+
})]
|
|
19708
|
+
})
|
|
19709
|
+
},
|
|
19710
|
+
{
|
|
19711
|
+
key: "state",
|
|
19712
|
+
header: "State",
|
|
19713
|
+
nowrap: false,
|
|
19714
|
+
render: (row) => /* @__PURE__ */ jsx(Badge, {
|
|
19715
|
+
variant: TONE_BADGE[row.notice.tone],
|
|
19716
|
+
children: row.notice.headline
|
|
19717
|
+
})
|
|
19718
|
+
},
|
|
19719
|
+
{
|
|
19720
|
+
key: "reason",
|
|
19721
|
+
header: "Reason",
|
|
19722
|
+
nowrap: false,
|
|
19723
|
+
render: (row) => row.notice.detail === null ? /* @__PURE__ */ jsx("span", {
|
|
19724
|
+
className: "text-foreground-subtle",
|
|
19725
|
+
children: "—"
|
|
19726
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
19727
|
+
className: "break-words",
|
|
19728
|
+
children: row.notice.detail
|
|
19729
|
+
})
|
|
19730
|
+
},
|
|
19731
|
+
{
|
|
19732
|
+
key: "read",
|
|
19733
|
+
header: "Catalog read",
|
|
19734
|
+
render: (row) => /* @__PURE__ */ jsx("span", {
|
|
19735
|
+
className: "text-foreground-subtle",
|
|
19736
|
+
children: row.catalogAge ?? "live"
|
|
19737
|
+
})
|
|
19738
|
+
}
|
|
19739
|
+
];
|
|
19740
|
+
/** The sources that contributed NOTHING to what is on screen, and why. */
|
|
19741
|
+
function SilentSources({ silent }) {
|
|
19742
|
+
if (silent.length === 0) return null;
|
|
19743
|
+
return /* @__PURE__ */ jsx(ErrorBox, {
|
|
19744
|
+
variant: "warning",
|
|
19745
|
+
title: silent.length === 1 ? "One source contributed nothing to this list" : `${String(silent.length)} sources contributed nothing to this list`,
|
|
19746
|
+
message: /* @__PURE__ */ jsx("div", {
|
|
19747
|
+
className: "space-y-1",
|
|
19748
|
+
children: silent.map((notice) => /* @__PURE__ */ jsxs("div", { children: [
|
|
19749
|
+
/* @__PURE__ */ jsx("span", {
|
|
19750
|
+
className: "font-medium",
|
|
19751
|
+
children: notice.label
|
|
19752
|
+
}),
|
|
19753
|
+
/* @__PURE__ */ jsxs("span", {
|
|
19754
|
+
className: "opacity-70",
|
|
19755
|
+
children: [
|
|
19756
|
+
" (",
|
|
19757
|
+
notice.source,
|
|
19758
|
+
")"
|
|
19759
|
+
]
|
|
19760
|
+
}),
|
|
19761
|
+
/* @__PURE__ */ jsx("div", { children: notice.headline }),
|
|
19762
|
+
notice.detail === null ? null : /* @__PURE__ */ jsx("div", {
|
|
19763
|
+
className: "opacity-80 break-words",
|
|
19764
|
+
children: notice.detail
|
|
19765
|
+
})
|
|
19766
|
+
] }, notice.source))
|
|
19767
|
+
})
|
|
19768
|
+
});
|
|
19769
|
+
}
|
|
19770
|
+
function ClipsSection({ deviceId }) {
|
|
19771
|
+
const [day, setDay] = useState(() => /* @__PURE__ */ new Date());
|
|
19772
|
+
const [view, setView] = useState(() => dayWindow(/* @__PURE__ */ new Date(), Date.now()));
|
|
19773
|
+
const [selectedBucket, setSelectedBucket] = useState(null);
|
|
19774
|
+
const [provider, setProvider] = useState("");
|
|
19775
|
+
const [page, setPage] = useState(1);
|
|
18193
19776
|
const [selectedClip, setSelectedClip] = useState(null);
|
|
18194
19777
|
const [profile, setProfile] = useState("low");
|
|
18195
19778
|
const presence = useCapabilityPresence("videoclips");
|
|
18196
|
-
const
|
|
19779
|
+
const dayBounds = useMemo(() => dayWindow(day, Date.now()), [day]);
|
|
19780
|
+
const bucketMs = bucketMsForSpan(view.toMs - view.fromMs);
|
|
19781
|
+
const density = usePipelineAnalyticsGetEventDensity({
|
|
19782
|
+
deviceId,
|
|
19783
|
+
since: view.fromMs,
|
|
19784
|
+
until: view.toMs,
|
|
19785
|
+
bucketMs
|
|
19786
|
+
});
|
|
19787
|
+
const buckets = useMemo(() => densityRowsToBuckets(density.data ?? [], view.fromMs, bucketMs), [
|
|
19788
|
+
density.data,
|
|
19789
|
+
view.fromMs,
|
|
19790
|
+
bucketMs
|
|
19791
|
+
]);
|
|
19792
|
+
const selected = selectedBucket === null ? void 0 : buckets.find((b) => b.index === selectedBucket);
|
|
19793
|
+
const band = useMemo(() => bandForSelection(view, selected === void 0 ? null : {
|
|
19794
|
+
startMs: selected.startMs,
|
|
19795
|
+
endMs: selected.endMs
|
|
19796
|
+
}), [view, selected]);
|
|
19797
|
+
const plan = planClipBand(band);
|
|
18197
19798
|
const sources = useVideoclipsListSources({ deviceId });
|
|
18198
19799
|
const clips = useVideoclipsListClips({
|
|
18199
19800
|
deviceId,
|
|
18200
|
-
since:
|
|
18201
|
-
until:
|
|
18202
|
-
limit: CLIP_PAGE_LIMIT
|
|
18203
|
-
|
|
19801
|
+
since: band.fromMs,
|
|
19802
|
+
until: band.toMs,
|
|
19803
|
+
limit: CLIP_PAGE_LIMIT,
|
|
19804
|
+
...provider === "" ? {} : { provider }
|
|
19805
|
+
}, { enabled: plan.kind === "askable" });
|
|
18204
19806
|
const sourceRows = sources.data ?? [];
|
|
18205
|
-
const
|
|
18206
|
-
|
|
18207
|
-
dayId,
|
|
19807
|
+
const clipsView = resolveClipsView({
|
|
19808
|
+
plan,
|
|
18208
19809
|
clipsStatus: clips.status,
|
|
18209
19810
|
clips: clips.data ?? [],
|
|
18210
19811
|
sourcesStatus: sources.status,
|
|
18211
19812
|
sources: sourceRows,
|
|
18212
|
-
...hidden.length === 0 ? {} : { selected },
|
|
18213
19813
|
...clips.error ? { clipsError: clips.error.message } : {},
|
|
18214
19814
|
...sources.error ? { sourcesError: sources.error.message } : {}
|
|
18215
19815
|
});
|
|
18216
19816
|
const now = Date.now();
|
|
18217
|
-
const
|
|
18218
|
-
|
|
18219
|
-
|
|
18220
|
-
|
|
19817
|
+
const options = useMemo(() => {
|
|
19818
|
+
const byAddon = /* @__PURE__ */ new Map();
|
|
19819
|
+
for (const row of sourceRows) {
|
|
19820
|
+
const addonId = row.addonId;
|
|
19821
|
+
if (addonId === void 0) continue;
|
|
19822
|
+
const labels = byAddon.get(addonId);
|
|
19823
|
+
if (labels === void 0) byAddon.set(addonId, [row.label]);
|
|
19824
|
+
else labels.push(row.label);
|
|
19825
|
+
}
|
|
19826
|
+
const rest = [...byAddon].map(([value, labels]) => ({
|
|
19827
|
+
value,
|
|
19828
|
+
label: labels.join(" · ")
|
|
19829
|
+
}));
|
|
19830
|
+
const first = rest[0];
|
|
19831
|
+
return [{
|
|
19832
|
+
value: "",
|
|
19833
|
+
label: first === void 0 ? "The camera’s bound provider" : first.label
|
|
19834
|
+
}, ...rest.slice(1)];
|
|
19835
|
+
}, [sourceRows]);
|
|
19836
|
+
const tableRows = useMemo(() => sourceRows.map((row) => ({
|
|
19837
|
+
notice: describeSource(row),
|
|
19838
|
+
catalogAge: formatCatalogAge(row.availability.catalogAsOf, now)
|
|
19839
|
+
})), [sourceRows]);
|
|
19840
|
+
useEffect(() => {
|
|
19841
|
+
setPage(1);
|
|
19842
|
+
}, [band.fromMs, band.toMs]);
|
|
19843
|
+
const handleDayChange = (next) => {
|
|
19844
|
+
setDay(next);
|
|
19845
|
+
setView(dayWindow(next, Date.now()));
|
|
19846
|
+
setSelectedBucket(null);
|
|
19847
|
+
};
|
|
19848
|
+
const handleViewChange = (next) => {
|
|
19849
|
+
setView(next);
|
|
19850
|
+
setSelectedBucket(null);
|
|
19851
|
+
};
|
|
19852
|
+
const handleSelectBucket = (index) => {
|
|
19853
|
+
setSelectedBucket((current) => current === index ? null : index);
|
|
19854
|
+
};
|
|
19855
|
+
const pick = (next) => {
|
|
19856
|
+
setProvider(next);
|
|
19857
|
+
setPage(1);
|
|
19858
|
+
};
|
|
19859
|
+
if (presence === "pending") return /* @__PURE__ */ jsxs("div", {
|
|
19860
|
+
className: "space-y-2",
|
|
19861
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
19862
|
+
role: "status",
|
|
19863
|
+
className: "text-xs text-foreground-subtle",
|
|
19864
|
+
children: "Asking the registry whether this cluster serves clips…"
|
|
19865
|
+
}), /* @__PURE__ */ jsx(Skeleton, { className: "h-24 w-full" })]
|
|
18221
19866
|
});
|
|
18222
|
-
if (presence === "unknown") return /* @__PURE__ */ jsx(
|
|
18223
|
-
|
|
18224
|
-
|
|
19867
|
+
if (presence === "unknown") return /* @__PURE__ */ jsx(ErrorBox, {
|
|
19868
|
+
variant: "warning",
|
|
19869
|
+
title: "Clip sources unknown",
|
|
19870
|
+
message: "The capability registry could not be reached, so whether this camera has clip sources is unknown — not absent."
|
|
18225
19871
|
});
|
|
18226
|
-
if (presence === "absent") return /* @__PURE__ */
|
|
19872
|
+
if (presence === "absent") return /* @__PURE__ */ jsx("div", {
|
|
18227
19873
|
role: "status",
|
|
18228
|
-
children:
|
|
18229
|
-
|
|
18230
|
-
|
|
18231
|
-
" is installed
|
|
18232
|
-
|
|
19874
|
+
children: /* @__PURE__ */ jsx(EmptyState, {
|
|
19875
|
+
icon: Clapperboard,
|
|
19876
|
+
title: "No provider of videoclips in this cluster",
|
|
19877
|
+
description: "Nothing is installed that could list a camera's stored clips."
|
|
19878
|
+
})
|
|
18233
19879
|
});
|
|
19880
|
+
const visible = clipsView.kind === "listed" ? clipsView.clips : [];
|
|
19881
|
+
const pageClips = visible.slice((page - 1) * TILES_PER_PAGE, page * TILES_PER_PAGE);
|
|
18234
19882
|
return /* @__PURE__ */ jsxs("div", {
|
|
18235
|
-
className: "space-y-
|
|
19883
|
+
className: "space-y-3",
|
|
18236
19884
|
children: [
|
|
18237
|
-
/* @__PURE__ */ jsxs("
|
|
18238
|
-
|
|
18239
|
-
|
|
18240
|
-
|
|
18241
|
-
|
|
18242
|
-
|
|
18243
|
-
|
|
18244
|
-
children: "
|
|
18245
|
-
})
|
|
18246
|
-
|
|
18247
|
-
|
|
18248
|
-
|
|
18249
|
-
|
|
18250
|
-
|
|
18251
|
-
|
|
18252
|
-
|
|
18253
|
-
}, row.source)) })]
|
|
18254
|
-
}),
|
|
18255
|
-
/* @__PURE__ */ jsxs("section", {
|
|
18256
|
-
"aria-label": "Day",
|
|
18257
|
-
className: "flex items-center gap-2",
|
|
19885
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
19886
|
+
/* @__PURE__ */ jsx("h3", {
|
|
19887
|
+
className: "text-sm font-semibold text-foreground",
|
|
19888
|
+
children: "Clips"
|
|
19889
|
+
}),
|
|
19890
|
+
/* @__PURE__ */ jsx("p", {
|
|
19891
|
+
className: "mt-1 text-[11px] text-foreground-subtle",
|
|
19892
|
+
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."
|
|
19893
|
+
}),
|
|
19894
|
+
/* @__PURE__ */ jsx("p", {
|
|
19895
|
+
className: "mt-1 text-[11px] text-foreground-subtle",
|
|
19896
|
+
children: "The list follows the period selected on the timebar below — the recording section's own control. Zoom out to widen it, pan or pick a day to move it, click a density bar to narrow it to that window."
|
|
19897
|
+
})
|
|
19898
|
+
] }),
|
|
19899
|
+
/* @__PURE__ */ jsxs("div", {
|
|
19900
|
+
className: "flex flex-wrap items-center gap-2",
|
|
18258
19901
|
children: [
|
|
18259
|
-
/* @__PURE__ */ jsx("
|
|
18260
|
-
|
|
18261
|
-
|
|
18262
|
-
|
|
18263
|
-
children: "‹"
|
|
18264
|
-
}),
|
|
18265
|
-
/* @__PURE__ */ jsx("span", {
|
|
18266
|
-
className: "font-medium",
|
|
18267
|
-
children: formatDayLabel(dayId)
|
|
19902
|
+
/* @__PURE__ */ jsx("label", {
|
|
19903
|
+
htmlFor: "clips-provider",
|
|
19904
|
+
className: "text-[10px] uppercase tracking-wide text-foreground-subtle",
|
|
19905
|
+
children: "Source"
|
|
18268
19906
|
}),
|
|
18269
|
-
/* @__PURE__ */ jsx("
|
|
18270
|
-
|
|
18271
|
-
|
|
18272
|
-
|
|
18273
|
-
|
|
19907
|
+
/* @__PURE__ */ jsx("div", {
|
|
19908
|
+
className: "w-60",
|
|
19909
|
+
children: /* @__PURE__ */ jsx(Select, {
|
|
19910
|
+
id: "clips-provider",
|
|
19911
|
+
options,
|
|
19912
|
+
value: provider,
|
|
19913
|
+
disabled: options.length < 2,
|
|
19914
|
+
onChange: (e) => pick(e.target.value)
|
|
19915
|
+
})
|
|
18274
19916
|
}),
|
|
18275
|
-
/* @__PURE__ */ jsx("
|
|
18276
|
-
|
|
18277
|
-
|
|
18278
|
-
|
|
19917
|
+
options.length < 2 ? /* @__PURE__ */ jsx("span", {
|
|
19918
|
+
className: "text-[11px] text-foreground-subtle",
|
|
19919
|
+
children: "This camera is bound to one clip provider today."
|
|
19920
|
+
}) : null,
|
|
19921
|
+
/* @__PURE__ */ jsxs("div", {
|
|
19922
|
+
className: "ml-auto flex items-center gap-1",
|
|
19923
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
19924
|
+
className: "px-1 text-xs font-medium text-foreground",
|
|
19925
|
+
children: formatBandLabel(band)
|
|
19926
|
+
}), selectedBucket === null ? null : /* @__PURE__ */ jsx(Button, {
|
|
19927
|
+
variant: "outline",
|
|
19928
|
+
onClick: () => setSelectedBucket(null),
|
|
19929
|
+
children: "Expand to the visible window"
|
|
19930
|
+
})]
|
|
18279
19931
|
})
|
|
18280
19932
|
]
|
|
18281
19933
|
}),
|
|
19934
|
+
/* @__PURE__ */ jsx(ClipBandBar, {
|
|
19935
|
+
day,
|
|
19936
|
+
onDayChange: handleDayChange,
|
|
19937
|
+
view,
|
|
19938
|
+
dayBounds,
|
|
19939
|
+
onViewChange: handleViewChange,
|
|
19940
|
+
buckets,
|
|
19941
|
+
selectedBucketIndex: selectedBucket,
|
|
19942
|
+
onSelectBucket: handleSelectBucket,
|
|
19943
|
+
densityLoading: density.status === "pending"
|
|
19944
|
+
}),
|
|
19945
|
+
/* @__PURE__ */ jsx("section", {
|
|
19946
|
+
"aria-label": "Clip sources",
|
|
19947
|
+
className: "space-y-1",
|
|
19948
|
+
children: sources.status === "pending" ? /* @__PURE__ */ jsx(Skeleton, { className: "h-16 w-full" }) : tableRows.length === 0 ? /* @__PURE__ */ jsx(EmptyState, {
|
|
19949
|
+
icon: Film,
|
|
19950
|
+
title: "No provider answered for this camera",
|
|
19951
|
+
description: "Nothing claims to hold clips for it."
|
|
19952
|
+
}) : /* @__PURE__ */ jsx(DataTable, {
|
|
19953
|
+
columns: SOURCE_COLUMNS,
|
|
19954
|
+
rows: [...tableRows],
|
|
19955
|
+
minWidthPx: 520
|
|
19956
|
+
})
|
|
19957
|
+
}),
|
|
18282
19958
|
/* @__PURE__ */ jsx("section", {
|
|
18283
19959
|
"aria-label": "Clips",
|
|
18284
|
-
|
|
19960
|
+
className: "space-y-2",
|
|
19961
|
+
children: clipsView.kind === "refused" ? /* @__PURE__ */ jsx(ErrorBox, {
|
|
19962
|
+
variant: "warning",
|
|
19963
|
+
title: "That period is too wide to list",
|
|
19964
|
+
message: `${formatBandLabel(clipsView.band)} covers ${String(clipsView.localDays)} local days, and a clip listing is asked one local day at a time — at most ${String(clipsView.maxLocalDays)}. Nothing was asked for; narrow the band and the list follows it.`
|
|
19965
|
+
}) : clipsView.kind === "loading" ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
|
|
18285
19966
|
role: "status",
|
|
19967
|
+
className: "text-xs text-foreground-subtle",
|
|
18286
19968
|
children: [
|
|
18287
19969
|
"Loading clips for ",
|
|
18288
|
-
|
|
19970
|
+
formatBandLabel(band),
|
|
18289
19971
|
"…"
|
|
18290
19972
|
]
|
|
18291
|
-
}) :
|
|
18292
|
-
|
|
18293
|
-
|
|
18294
|
-
}) :
|
|
18295
|
-
|
|
18296
|
-
children:
|
|
18297
|
-
|
|
18298
|
-
|
|
18299
|
-
|
|
18300
|
-
|
|
18301
|
-
|
|
18302
|
-
|
|
18303
|
-
|
|
18304
|
-
|
|
19973
|
+
}), /* @__PURE__ */ jsx(Skeleton, { className: "h-24 w-full" })] }) : clipsView.kind === "failed" ? /* @__PURE__ */ jsx(ErrorBox, {
|
|
19974
|
+
title: "The clip list could not be read",
|
|
19975
|
+
message: clipsView.message
|
|
19976
|
+
}) : clipsView.kind === "empty" ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
|
|
19977
|
+
role: "status",
|
|
19978
|
+
children: /* @__PURE__ */ jsx(EmptyState, {
|
|
19979
|
+
icon: Film,
|
|
19980
|
+
title: clipsView.nobodyAnswered ? `No source could answer for ${formatBandLabel(clipsView.band)}` : `No clips in ${formatBandLabel(clipsView.band)}`,
|
|
19981
|
+
...clipsView.nobodyAnswered ? {} : { description: `Asked ${clipsView.answered.map((notice) => notice.label).join(", ")}.` }
|
|
19982
|
+
})
|
|
19983
|
+
}), /* @__PURE__ */ jsx(SilentSources, { silent: clipsView.silent })] }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
19984
|
+
/* @__PURE__ */ jsx(SilentSources, { silent: clipsView.silent }),
|
|
19985
|
+
/* @__PURE__ */ jsx("ul", {
|
|
19986
|
+
className: "grid grid-cols-1 gap-2 sm:grid-cols-2 xl:grid-cols-3",
|
|
19987
|
+
children: pageClips.map((clip) => /* @__PURE__ */ jsx(ClipTile, {
|
|
18305
19988
|
clip,
|
|
18306
19989
|
selected: selectedClip?.id === clip.id,
|
|
18307
19990
|
onSelect: setSelectedClip
|
|
18308
19991
|
}, `${clip.source}:${clip.id}`))
|
|
18309
|
-
})
|
|
18310
|
-
|
|
19992
|
+
}),
|
|
19993
|
+
/* @__PURE__ */ jsx(Pagination, {
|
|
19994
|
+
currentPage: page,
|
|
19995
|
+
totalItems: visible.length,
|
|
19996
|
+
pageSize: TILES_PER_PAGE,
|
|
19997
|
+
onPageChange: setPage,
|
|
19998
|
+
itemNoun: "clips"
|
|
19999
|
+
})
|
|
20000
|
+
] })
|
|
18311
20001
|
}),
|
|
18312
20002
|
selectedClip === null ? null : /* @__PURE__ */ jsx(ClipPanel, {
|
|
18313
20003
|
deviceId,
|
|
@@ -20438,82 +22128,6 @@ function useScrubController(sink, isPlayerLive = () => false) {
|
|
|
20438
22128
|
};
|
|
20439
22129
|
}
|
|
20440
22130
|
//#endregion
|
|
20441
|
-
//#region src/composites/empty-state.tsx
|
|
20442
|
-
function EmptyState({ icon: Icon, title, description, action, className }) {
|
|
20443
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
20444
|
-
className: cn("flex flex-col items-center justify-center gap-3 py-12", className),
|
|
20445
|
-
children: [
|
|
20446
|
-
Icon && /* @__PURE__ */ jsx(Icon, {
|
|
20447
|
-
className: "h-12 w-12 text-foreground-subtle",
|
|
20448
|
-
"aria-hidden": "true"
|
|
20449
|
-
}),
|
|
20450
|
-
/* @__PURE__ */ jsxs("div", {
|
|
20451
|
-
className: "flex flex-col items-center gap-1 text-center",
|
|
20452
|
-
children: [/* @__PURE__ */ jsx("p", {
|
|
20453
|
-
className: "text-foreground-muted text-sm font-medium",
|
|
20454
|
-
children: title
|
|
20455
|
-
}), description && /* @__PURE__ */ jsx("p", {
|
|
20456
|
-
className: "text-foreground-subtle text-xs max-w-xs",
|
|
20457
|
-
children: description
|
|
20458
|
-
})]
|
|
20459
|
-
}),
|
|
20460
|
-
action && /* @__PURE__ */ jsx("div", {
|
|
20461
|
-
className: "mt-1",
|
|
20462
|
-
children: action
|
|
20463
|
-
})
|
|
20464
|
-
]
|
|
20465
|
-
});
|
|
20466
|
-
}
|
|
20467
|
-
//#endregion
|
|
20468
|
-
//#region src/composites/error-box.tsx
|
|
20469
|
-
var VARIANT_STYLES$1 = {
|
|
20470
|
-
danger: {
|
|
20471
|
-
box: "border-danger/40 bg-danger/10",
|
|
20472
|
-
icon: "text-danger",
|
|
20473
|
-
title: "text-danger",
|
|
20474
|
-
message: "text-danger"
|
|
20475
|
-
},
|
|
20476
|
-
warning: {
|
|
20477
|
-
box: "border-amber-500/40 bg-amber-500/10",
|
|
20478
|
-
icon: "text-amber-600 dark:text-amber-400",
|
|
20479
|
-
title: "text-amber-700 dark:text-amber-300",
|
|
20480
|
-
message: "text-amber-700 dark:text-amber-300"
|
|
20481
|
-
}
|
|
20482
|
-
};
|
|
20483
|
-
function ErrorBox({ message, title, onRetry, action, variant = "danger", className }) {
|
|
20484
|
-
const styles = VARIANT_STYLES$1[variant];
|
|
20485
|
-
const Icon = variant === "warning" ? TriangleAlert : CircleAlert;
|
|
20486
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
20487
|
-
role: "alert",
|
|
20488
|
-
className: cn("flex items-start gap-2.5 rounded border px-3 py-2.5", styles.box, className),
|
|
20489
|
-
children: [
|
|
20490
|
-
/* @__PURE__ */ jsx(Icon, {
|
|
20491
|
-
className: cn("h-4 w-4 mt-0.5 shrink-0", styles.icon),
|
|
20492
|
-
"aria-hidden": "true"
|
|
20493
|
-
}),
|
|
20494
|
-
/* @__PURE__ */ jsxs("div", {
|
|
20495
|
-
className: "flex-1 min-w-0",
|
|
20496
|
-
children: [title && /* @__PURE__ */ jsx("p", {
|
|
20497
|
-
className: cn("text-xs font-semibold", styles.title),
|
|
20498
|
-
children: title
|
|
20499
|
-
}), /* @__PURE__ */ jsx("div", {
|
|
20500
|
-
className: cn("text-xs break-words", styles.message, title ? "mt-0.5" : ""),
|
|
20501
|
-
children: message
|
|
20502
|
-
})]
|
|
20503
|
-
}),
|
|
20504
|
-
action ?? (onRetry && /* @__PURE__ */ jsxs("button", {
|
|
20505
|
-
type: "button",
|
|
20506
|
-
onClick: onRetry,
|
|
20507
|
-
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),
|
|
20508
|
-
children: [/* @__PURE__ */ jsx(RefreshCw, {
|
|
20509
|
-
className: "h-3 w-3",
|
|
20510
|
-
"aria-hidden": "true"
|
|
20511
|
-
}), "Retry"]
|
|
20512
|
-
}))
|
|
20513
|
-
]
|
|
20514
|
-
});
|
|
20515
|
-
}
|
|
20516
|
-
//#endregion
|
|
20517
22131
|
//#region src/composites/cap-settings/recording-spans.ts
|
|
20518
22132
|
/**
|
|
20519
22133
|
* Merge availability ranges (across every profile) into the union of distinct
|
|
@@ -20563,201 +22177,6 @@ function formatBytes(bytes) {
|
|
|
20563
22177
|
return `${value >= 100 ? Math.round(value).toString() : value.toFixed(1)} ${unit}`;
|
|
20564
22178
|
}
|
|
20565
22179
|
//#endregion
|
|
20566
|
-
//#region src/composites/cap-settings/recording-timeline-model.ts
|
|
20567
|
-
/** Tie-break precedence for a bucket's dominant kind (first wins on equal count). */
|
|
20568
|
-
var KIND_PRECEDENCE = [
|
|
20569
|
-
"object",
|
|
20570
|
-
"motion",
|
|
20571
|
-
"audio"
|
|
20572
|
-
];
|
|
20573
|
-
/**
|
|
20574
|
-
* Convert server-side density rows into `DensityBucket[]` for the timeline
|
|
20575
|
-
* heatmap. Rows with total === 0 are skipped (defensive; server already omits
|
|
20576
|
-
* them). Returns a sparse, ascending-by-index array — EventHeatmap positions
|
|
20577
|
-
* each bar by `b.startMs` rather than array index, so gaps are fine.
|
|
20578
|
-
*/
|
|
20579
|
-
function densityRowsToBuckets(rows, windowFrom, bucketMs) {
|
|
20580
|
-
const buckets = [];
|
|
20581
|
-
for (const row of rows) {
|
|
20582
|
-
const counts = {
|
|
20583
|
-
motion: row.motion,
|
|
20584
|
-
object: row.object,
|
|
20585
|
-
audio: row.audio
|
|
20586
|
-
};
|
|
20587
|
-
const total = counts.motion + counts.object + counts.audio;
|
|
20588
|
-
if (total === 0) continue;
|
|
20589
|
-
const index = Math.floor((row.bucketStart - windowFrom) / bucketMs);
|
|
20590
|
-
let dominant = null;
|
|
20591
|
-
let best = -1;
|
|
20592
|
-
for (const kind of KIND_PRECEDENCE) if (counts[kind] > best) {
|
|
20593
|
-
best = counts[kind];
|
|
20594
|
-
dominant = kind;
|
|
20595
|
-
}
|
|
20596
|
-
buckets.push({
|
|
20597
|
-
index,
|
|
20598
|
-
startMs: row.bucketStart,
|
|
20599
|
-
endMs: row.bucketStart + bucketMs,
|
|
20600
|
-
counts,
|
|
20601
|
-
total,
|
|
20602
|
-
dominant
|
|
20603
|
-
});
|
|
20604
|
-
}
|
|
20605
|
-
return buckets.toSorted((a, b) => a.index - b.index);
|
|
20606
|
-
}
|
|
20607
|
-
/** Distance from `ms` to the interval `[startMs, endMs]` (0 if inside). */
|
|
20608
|
-
function distanceToSpan(ms, span) {
|
|
20609
|
-
if (ms < span.startMs) return span.startMs - ms;
|
|
20610
|
-
if (ms > span.endMs) return ms - span.endMs;
|
|
20611
|
-
return 0;
|
|
20612
|
-
}
|
|
20613
|
-
/**
|
|
20614
|
-
* Resolve a playback window for a click at `clickMs` against recorded `spans`.
|
|
20615
|
-
* Inside a span → play from the click to that span's end. In a gap / outside →
|
|
20616
|
-
* snap to the WHOLE nearest span. No spans → null (nothing to play).
|
|
20617
|
-
* `spans` are expected ascending by start (as `mergeAvailabilitySpans` returns).
|
|
20618
|
-
*/
|
|
20619
|
-
function seekTargetForSpans(clickMs, spans) {
|
|
20620
|
-
if (spans.length === 0) return null;
|
|
20621
|
-
const containing = spans.find((s) => clickMs >= s.startMs && clickMs <= s.endMs);
|
|
20622
|
-
if (containing) return {
|
|
20623
|
-
fromMs: clickMs,
|
|
20624
|
-
toMs: containing.endMs
|
|
20625
|
-
};
|
|
20626
|
-
let nearest = null;
|
|
20627
|
-
let bestDist = Infinity;
|
|
20628
|
-
for (const s of spans) {
|
|
20629
|
-
const d = distanceToSpan(clickMs, s);
|
|
20630
|
-
if (d < bestDist) {
|
|
20631
|
-
bestDist = d;
|
|
20632
|
-
nearest = s;
|
|
20633
|
-
}
|
|
20634
|
-
}
|
|
20635
|
-
return nearest ? {
|
|
20636
|
-
fromMs: nearest.startMs,
|
|
20637
|
-
toMs: nearest.endMs
|
|
20638
|
-
} : null;
|
|
20639
|
-
}
|
|
20640
|
-
/**
|
|
20641
|
-
* Position `ms` inside the window `[fromMs, toMs]` as a clamped percentage
|
|
20642
|
-
* (0–100). A zero/negative-width window yields 0. Callers use this for `left:`.
|
|
20643
|
-
*/
|
|
20644
|
-
function pctForMs(ms, fromMs, toMs) {
|
|
20645
|
-
const span = toMs - fromMs;
|
|
20646
|
-
if (span <= 0) return 0;
|
|
20647
|
-
const pct = (ms - fromMs) / span * 100;
|
|
20648
|
-
if (pct < 0) return 0;
|
|
20649
|
-
if (pct > 100) return 100;
|
|
20650
|
-
return pct;
|
|
20651
|
-
}
|
|
20652
|
-
/**
|
|
20653
|
-
* Build a DayWindow for the given calendar `date` using local time.
|
|
20654
|
-
* For any day strictly in the past: `fromMs` = local midnight, `toMs` =
|
|
20655
|
-
* next local midnight (exactly 24 h).
|
|
20656
|
-
* For today (or any future day that contains `nowMs`): `toMs = nowMs` so the
|
|
20657
|
-
* window never extends into the future.
|
|
20658
|
-
*/
|
|
20659
|
-
function dayWindow(date, nowMs) {
|
|
20660
|
-
const fromMs = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0, 0).getTime();
|
|
20661
|
-
const nextMidnight = new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1, 0, 0, 0, 0).getTime();
|
|
20662
|
-
return {
|
|
20663
|
-
fromMs,
|
|
20664
|
-
toMs: Math.max(fromMs, Math.min(nextMidnight, nowMs))
|
|
20665
|
-
};
|
|
20666
|
-
}
|
|
20667
|
-
/** Available zoom levels in ms, from widest (24h) to narrowest (15 min). */
|
|
20668
|
-
var ZOOM_LEVELS = [
|
|
20669
|
-
24 * 36e5,
|
|
20670
|
-
12 * 36e5,
|
|
20671
|
-
6 * 36e5,
|
|
20672
|
-
36e5,
|
|
20673
|
-
15 * 6e4
|
|
20674
|
-
];
|
|
20675
|
-
/**
|
|
20676
|
-
* Produce a `levelMs`-wide window whose RIGHT edge stays anchored at
|
|
20677
|
-
* `rightEdgeMs` (clamped within `dayBounds`), rather than centring on the view
|
|
20678
|
-
* midpoint. On today's timeline the view's right edge is "now", so zooming this
|
|
20679
|
-
* way keeps the most recent footage pinned to the right instead of drifting to
|
|
20680
|
-
* a midday range that no longer contains now. Panning then moves back/forward.
|
|
20681
|
-
*
|
|
20682
|
-
* Falls back to `dayBounds` when `levelMs` meets or exceeds the day span (can't
|
|
20683
|
-
* show a wider window than the day holds — e.g. early morning with little
|
|
20684
|
-
* elapsed time).
|
|
20685
|
-
*/
|
|
20686
|
-
function zoomWindowAnchoredRight(dayBounds, levelMs, rightEdgeMs) {
|
|
20687
|
-
if (levelMs >= dayBounds.toMs - dayBounds.fromMs) return dayBounds;
|
|
20688
|
-
const toMs = Math.min(Math.max(rightEdgeMs, dayBounds.fromMs + levelMs), dayBounds.toMs);
|
|
20689
|
-
return {
|
|
20690
|
-
fromMs: toMs - levelMs,
|
|
20691
|
-
toMs
|
|
20692
|
-
};
|
|
20693
|
-
}
|
|
20694
|
-
/**
|
|
20695
|
-
* Shift `win` by `deltaMs`, preserving width, clamped within `dayBounds`.
|
|
20696
|
-
*/
|
|
20697
|
-
function panWindow(win, dayBounds, deltaMs) {
|
|
20698
|
-
const width = win.toMs - win.fromMs;
|
|
20699
|
-
let fromMs = win.fromMs + deltaMs;
|
|
20700
|
-
let toMs = win.toMs + deltaMs;
|
|
20701
|
-
if (fromMs < dayBounds.fromMs) {
|
|
20702
|
-
fromMs = dayBounds.fromMs;
|
|
20703
|
-
toMs = fromMs + width;
|
|
20704
|
-
}
|
|
20705
|
-
if (toMs > dayBounds.toMs) {
|
|
20706
|
-
toMs = dayBounds.toMs;
|
|
20707
|
-
fromMs = toMs - width;
|
|
20708
|
-
}
|
|
20709
|
-
return {
|
|
20710
|
-
fromMs,
|
|
20711
|
-
toMs
|
|
20712
|
-
};
|
|
20713
|
-
}
|
|
20714
|
-
/** Candidate bucket sizes in ms (coarsest first). */
|
|
20715
|
-
var BUCKET_CANDIDATES = [
|
|
20716
|
-
15 * 6e4,
|
|
20717
|
-
5 * 6e4,
|
|
20718
|
-
6e4,
|
|
20719
|
-
3e4,
|
|
20720
|
-
15e3,
|
|
20721
|
-
5e3
|
|
20722
|
-
];
|
|
20723
|
-
/**
|
|
20724
|
-
* Choose a sensible heatmap bucket size for a visible span of `spanMs`,
|
|
20725
|
-
* targeting ~96-120 buckets. Always picks from `BUCKET_CANDIDATES`.
|
|
20726
|
-
*/
|
|
20727
|
-
function bucketMsForSpan(spanMs) {
|
|
20728
|
-
const targetBucket = spanMs / 100;
|
|
20729
|
-
for (const candidate of BUCKET_CANDIDATES) if (candidate <= targetBucket) return candidate;
|
|
20730
|
-
return BUCKET_CANDIDATES[BUCKET_CANDIDATES.length - 1] ?? 5e3;
|
|
20731
|
-
}
|
|
20732
|
-
/**
|
|
20733
|
-
* Produce `count` evenly-spaced axis ticks spanning [fromMs, toMs].
|
|
20734
|
-
* - `pct` is via pctForMs (first=0, last=100).
|
|
20735
|
-
* - `label` is a localised HH:MM clock string.
|
|
20736
|
-
*/
|
|
20737
|
-
function axisTicks(fromMs, toMs, count) {
|
|
20738
|
-
if (count <= 1) return [{
|
|
20739
|
-
pct: 0,
|
|
20740
|
-
label: new Date(fromMs).toLocaleTimeString(void 0, {
|
|
20741
|
-
hour: "2-digit",
|
|
20742
|
-
minute: "2-digit"
|
|
20743
|
-
})
|
|
20744
|
-
}];
|
|
20745
|
-
const ticks = [];
|
|
20746
|
-
for (let i = 0; i < count; i++) {
|
|
20747
|
-
const ms = fromMs + i / (count - 1) * (toMs - fromMs);
|
|
20748
|
-
const pct = pctForMs(ms, fromMs, toMs);
|
|
20749
|
-
const label = new Date(ms).toLocaleTimeString(void 0, {
|
|
20750
|
-
hour: "2-digit",
|
|
20751
|
-
minute: "2-digit"
|
|
20752
|
-
});
|
|
20753
|
-
ticks.push({
|
|
20754
|
-
pct,
|
|
20755
|
-
label
|
|
20756
|
-
});
|
|
20757
|
-
}
|
|
20758
|
-
return ticks;
|
|
20759
|
-
}
|
|
20760
|
-
//#endregion
|
|
20761
22180
|
//#region src/composites/cap-settings/CoverageTrack.tsx
|
|
20762
22181
|
function CoverageTrack({ windowFrom, windowTo, spans, playheadMs, onSeek, onScrubStart, onScrubMove, onScrubEnd }) {
|
|
20763
22182
|
const [hoverInfo, setHoverInfo] = useState(null);
|
|
@@ -20868,42 +22287,6 @@ function CoverageTrack({ windowFrom, windowTo, spans, playheadMs, onSeek, onScru
|
|
|
20868
22287
|
});
|
|
20869
22288
|
}
|
|
20870
22289
|
//#endregion
|
|
20871
|
-
//#region src/composites/cap-settings/EventHeatmap.tsx
|
|
20872
|
-
var KIND_BG = {
|
|
20873
|
-
motion: "bg-emerald-500",
|
|
20874
|
-
object: "bg-violet-500",
|
|
20875
|
-
audio: "bg-orange-500"
|
|
20876
|
-
};
|
|
20877
|
-
function EventHeatmap({ windowFrom, windowTo, buckets, selectedIndex, onSelectBucket }) {
|
|
20878
|
-
const peak = buckets.reduce((m, b) => Math.max(m, b.total), 0);
|
|
20879
|
-
const firstBucket = buckets[0];
|
|
20880
|
-
const bucketWidthPct = firstBucket ? pctForMs(firstBucket.endMs, windowFrom, windowTo) - pctForMs(firstBucket.startMs, windowFrom, windowTo) : 0;
|
|
20881
|
-
return /* @__PURE__ */ jsx("div", {
|
|
20882
|
-
className: "relative h-6 rounded bg-background/40",
|
|
20883
|
-
children: buckets.map((b) => {
|
|
20884
|
-
if (b.total === 0 || b.dominant === null) return null;
|
|
20885
|
-
const heightPct = peak > 0 ? Math.max(15, b.total / peak * 100) : 0;
|
|
20886
|
-
const left = pctForMs(b.startMs, windowFrom, windowTo);
|
|
20887
|
-
const selected = b.index === selectedIndex;
|
|
20888
|
-
return /* @__PURE__ */ jsx("button", {
|
|
20889
|
-
type: "button",
|
|
20890
|
-
"data-testid": "heatmap-bar",
|
|
20891
|
-
title: `${new Date(b.startMs).toLocaleTimeString(void 0, {
|
|
20892
|
-
hour: "2-digit",
|
|
20893
|
-
minute: "2-digit"
|
|
20894
|
-
})} · motion ${b.counts.motion} · object ${b.counts.object}`,
|
|
20895
|
-
onClick: () => onSelectBucket(b.index),
|
|
20896
|
-
className: `absolute bottom-0 rounded-t-sm transition-opacity ${KIND_BG[b.dominant]} ${selected ? "opacity-100 ring-1 ring-amber-400" : "opacity-80 hover:opacity-100"}`,
|
|
20897
|
-
style: {
|
|
20898
|
-
left: `${left}%`,
|
|
20899
|
-
width: `${Math.max(.6, bucketWidthPct)}%`,
|
|
20900
|
-
height: `${heightPct}%`
|
|
20901
|
-
}
|
|
20902
|
-
}, b.index);
|
|
20903
|
-
})
|
|
20904
|
-
});
|
|
20905
|
-
}
|
|
20906
|
-
//#endregion
|
|
20907
22290
|
//#region src/composites/cap-settings/event-thumb.tsx
|
|
20908
22291
|
function EventThumb({ mediaUrl, kind }) {
|
|
20909
22292
|
const [failed, setFailed] = useState(false);
|
|
@@ -21028,142 +22411,6 @@ function EventBucketStrip({ clips, loading, busyKey, onPlay, enabledKinds, onTog
|
|
|
21028
22411
|
});
|
|
21029
22412
|
}
|
|
21030
22413
|
//#endregion
|
|
21031
|
-
//#region src/composites/cap-settings/TimelineControls.tsx
|
|
21032
|
-
/** Format a Date as "YYYY-MM-DD" for <input type="date">. */
|
|
21033
|
-
function formatDateInput(date) {
|
|
21034
|
-
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
|
21035
|
-
}
|
|
21036
|
-
/** Parse "YYYY-MM-DD" from <input type="date"> into a local-midnight Date. */
|
|
21037
|
-
function parseDateInput(value) {
|
|
21038
|
-
const parts = value.split("-");
|
|
21039
|
-
const y = parts[0] !== void 0 ? parseInt(parts[0], 10) : NaN;
|
|
21040
|
-
const m = parts[1] !== void 0 ? parseInt(parts[1], 10) : NaN;
|
|
21041
|
-
const d = parts[2] !== void 0 ? parseInt(parts[2], 10) : NaN;
|
|
21042
|
-
if (isNaN(y) || isNaN(m) || isNaN(d)) return null;
|
|
21043
|
-
return new Date(y, m - 1, d);
|
|
21044
|
-
}
|
|
21045
|
-
/**
|
|
21046
|
-
* Date selector + prev/next pan + zoom-in / zoom-out buttons for the recording
|
|
21047
|
-
* timeline. Zoom cycles through ZOOM_LEVELS (widest → narrowest), anchored on
|
|
21048
|
-
* the view's RIGHT edge — so on today's timeline "now" stays pinned to the right
|
|
21049
|
-
* instead of drifting to a midday range. The ◀ / ▶ buttons step the visible
|
|
21050
|
-
* window earlier/later within the day (the discoverable equivalent of the
|
|
21051
|
-
* horizontal wheel-pan wired in RecordingTimeline), so a zoomed-in day can be
|
|
21052
|
-
* navigated back and forth without a mouse wheel.
|
|
21053
|
-
*/
|
|
21054
|
-
function TimelineControls({ day, onDayChange, view, dayBounds, onViewChange }) {
|
|
21055
|
-
const handleDateChange = (e) => {
|
|
21056
|
-
const parsed = parseDateInput(e.target.value);
|
|
21057
|
-
if (parsed !== null) onDayChange(parsed);
|
|
21058
|
-
};
|
|
21059
|
-
const viewSpan = view.toMs - view.fromMs;
|
|
21060
|
-
const currentLevelIdx = (() => {
|
|
21061
|
-
let best = 0;
|
|
21062
|
-
let bestDiff = Infinity;
|
|
21063
|
-
for (let i = 0; i < ZOOM_LEVELS.length; i++) {
|
|
21064
|
-
const level = ZOOM_LEVELS[i];
|
|
21065
|
-
if (level === void 0) continue;
|
|
21066
|
-
const diff = Math.abs(level - viewSpan);
|
|
21067
|
-
if (diff < bestDiff) {
|
|
21068
|
-
bestDiff = diff;
|
|
21069
|
-
best = i;
|
|
21070
|
-
}
|
|
21071
|
-
}
|
|
21072
|
-
return best;
|
|
21073
|
-
})();
|
|
21074
|
-
const rightEdgeMs = view.toMs;
|
|
21075
|
-
const handleZoomIn = () => {
|
|
21076
|
-
const nextLevel = ZOOM_LEVELS[Math.min(currentLevelIdx + 1, ZOOM_LEVELS.length - 1)];
|
|
21077
|
-
if (nextLevel === void 0) return;
|
|
21078
|
-
onViewChange(zoomWindowAnchoredRight(dayBounds, nextLevel, rightEdgeMs));
|
|
21079
|
-
};
|
|
21080
|
-
const handleZoomOut = () => {
|
|
21081
|
-
const nextLevel = ZOOM_LEVELS[Math.max(currentLevelIdx - 1, 0)];
|
|
21082
|
-
if (nextLevel === void 0) return;
|
|
21083
|
-
onViewChange(zoomWindowAnchoredRight(dayBounds, nextLevel, rightEdgeMs));
|
|
21084
|
-
};
|
|
21085
|
-
const canZoomIn = currentLevelIdx < ZOOM_LEVELS.length - 1;
|
|
21086
|
-
const canZoomOut = currentLevelIdx > 0;
|
|
21087
|
-
const panStepMs = Math.max(1, Math.round(viewSpan * .9));
|
|
21088
|
-
const canPanPrev = view.fromMs > dayBounds.fromMs;
|
|
21089
|
-
const canPanNext = view.toMs < dayBounds.toMs;
|
|
21090
|
-
const handlePanPrev = () => {
|
|
21091
|
-
onViewChange(panWindow(view, dayBounds, -panStepMs));
|
|
21092
|
-
};
|
|
21093
|
-
const handlePanNext = () => {
|
|
21094
|
-
onViewChange(panWindow(view, dayBounds, panStepMs));
|
|
21095
|
-
};
|
|
21096
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
21097
|
-
className: "flex items-center gap-2",
|
|
21098
|
-
children: [
|
|
21099
|
-
/* @__PURE__ */ jsx("input", {
|
|
21100
|
-
type: "date",
|
|
21101
|
-
value: formatDateInput(day),
|
|
21102
|
-
max: formatDateInput(/* @__PURE__ */ new Date()),
|
|
21103
|
-
onChange: handleDateChange,
|
|
21104
|
-
className: "rounded border border-border bg-background px-2 py-0.5 text-[11px] text-foreground focus:outline-none focus:ring-1 focus:ring-primary",
|
|
21105
|
-
"aria-label": "Select recording day"
|
|
21106
|
-
}),
|
|
21107
|
-
/* @__PURE__ */ jsxs("div", {
|
|
21108
|
-
className: "ml-auto flex items-center gap-1",
|
|
21109
|
-
children: [
|
|
21110
|
-
/* @__PURE__ */ jsx("button", {
|
|
21111
|
-
type: "button",
|
|
21112
|
-
onClick: handlePanPrev,
|
|
21113
|
-
disabled: !canPanPrev,
|
|
21114
|
-
title: "Earlier (pan back)",
|
|
21115
|
-
"aria-label": "Pan earlier",
|
|
21116
|
-
className: "rounded p-1 text-foreground-subtle transition-colors hover:bg-background/60 hover:text-foreground disabled:cursor-not-allowed disabled:opacity-40",
|
|
21117
|
-
children: /* @__PURE__ */ jsx(ChevronLeft, { size: 14 })
|
|
21118
|
-
}),
|
|
21119
|
-
/* @__PURE__ */ jsx("button", {
|
|
21120
|
-
type: "button",
|
|
21121
|
-
onClick: handlePanNext,
|
|
21122
|
-
disabled: !canPanNext,
|
|
21123
|
-
title: "Later (pan forward)",
|
|
21124
|
-
"aria-label": "Pan later",
|
|
21125
|
-
className: "rounded p-1 text-foreground-subtle transition-colors hover:bg-background/60 hover:text-foreground disabled:cursor-not-allowed disabled:opacity-40",
|
|
21126
|
-
children: /* @__PURE__ */ jsx(ChevronRight, { size: 14 })
|
|
21127
|
-
}),
|
|
21128
|
-
/* @__PURE__ */ jsx("span", {
|
|
21129
|
-
className: "mx-0.5 h-3 w-px bg-border",
|
|
21130
|
-
"aria-hidden": "true"
|
|
21131
|
-
}),
|
|
21132
|
-
/* @__PURE__ */ jsx("button", {
|
|
21133
|
-
type: "button",
|
|
21134
|
-
onClick: handleZoomOut,
|
|
21135
|
-
disabled: !canZoomOut,
|
|
21136
|
-
title: "Zoom out",
|
|
21137
|
-
"aria-label": "Zoom out",
|
|
21138
|
-
className: "rounded p-1 text-foreground-subtle transition-colors hover:bg-background/60 hover:text-foreground disabled:cursor-not-allowed disabled:opacity-40",
|
|
21139
|
-
children: /* @__PURE__ */ jsx(ZoomOut, { size: 13 })
|
|
21140
|
-
}),
|
|
21141
|
-
/* @__PURE__ */ jsx("button", {
|
|
21142
|
-
type: "button",
|
|
21143
|
-
onClick: handleZoomIn,
|
|
21144
|
-
disabled: !canZoomIn,
|
|
21145
|
-
title: "Zoom in",
|
|
21146
|
-
"aria-label": "Zoom in",
|
|
21147
|
-
className: "rounded p-1 text-foreground-subtle transition-colors hover:bg-background/60 hover:text-foreground disabled:cursor-not-allowed disabled:opacity-40",
|
|
21148
|
-
children: /* @__PURE__ */ jsx(ZoomIn, { size: 13 })
|
|
21149
|
-
})
|
|
21150
|
-
]
|
|
21151
|
-
}),
|
|
21152
|
-
/* @__PURE__ */ jsx("span", {
|
|
21153
|
-
className: "text-[9px] text-foreground-subtle",
|
|
21154
|
-
children: zoomLabel(viewSpan)
|
|
21155
|
-
})
|
|
21156
|
-
]
|
|
21157
|
-
});
|
|
21158
|
-
}
|
|
21159
|
-
function zoomLabel(spanMs) {
|
|
21160
|
-
const h = spanMs / 36e5;
|
|
21161
|
-
if (h >= 24) return "24h";
|
|
21162
|
-
if (h >= 1) return `${Math.round(h)}h`;
|
|
21163
|
-
const m = spanMs / 6e4;
|
|
21164
|
-
return `${Math.round(m)}m`;
|
|
21165
|
-
}
|
|
21166
|
-
//#endregion
|
|
21167
22414
|
//#region src/composites/cap-settings/RecordingTimeline.tsx
|
|
21168
22415
|
var ALL_KINDS = [
|
|
21169
22416
|
"motion",
|
|
@@ -21559,15 +22806,6 @@ function RecordingTimeline({ deviceId }) {
|
|
|
21559
22806
|
//#endregion
|
|
21560
22807
|
//#region src/composites/cap-settings/RecordingPanel.tsx
|
|
21561
22808
|
var DAY_MS$1 = 1440 * 60 * 1e3;
|
|
21562
|
-
function formatDuration$1(ms) {
|
|
21563
|
-
const totalSec = Math.round(ms / 1e3);
|
|
21564
|
-
const h = Math.floor(totalSec / 3600);
|
|
21565
|
-
const m = Math.floor(totalSec % 3600 / 60);
|
|
21566
|
-
const s = totalSec % 60;
|
|
21567
|
-
if (h > 0) return `${h}h ${m}m`;
|
|
21568
|
-
if (m > 0) return `${m}m ${s}s`;
|
|
21569
|
-
return `${s}s`;
|
|
21570
|
-
}
|
|
21571
22809
|
function RecordingPanel({ deviceId }) {
|
|
21572
22810
|
const queryClient = useQueryClient();
|
|
21573
22811
|
const statusQuery = useRecordingGetStatus({ deviceId });
|
|
@@ -21667,7 +22905,7 @@ function RecordingPanel({ deviceId }) {
|
|
|
21667
22905
|
label: "Recorded (24h)",
|
|
21668
22906
|
icon: /* @__PURE__ */ jsx(Clock, { className: "h-3.5 w-3.5" }),
|
|
21669
22907
|
footer: `${ranges.length} segment range${ranges.length === 1 ? "" : "s"}`,
|
|
21670
|
-
children: availabilityQuery.isLoading ? "…" :
|
|
22908
|
+
children: availabilityQuery.isLoading ? "…" : formatDurationMs(recordedMs)
|
|
21671
22909
|
})
|
|
21672
22910
|
]
|
|
21673
22911
|
}),
|
|
@@ -21697,7 +22935,7 @@ var HOST_WIDGETS = {
|
|
|
21697
22935
|
"host/ptz-autotrack": AutotrackSection,
|
|
21698
22936
|
"host/recording-panel": RecordingPanel,
|
|
21699
22937
|
"host/camera-grid-layout": CameraGridLayoutEditor,
|
|
21700
|
-
"host/clips-browser":
|
|
22938
|
+
"host/clips-browser": ClipsSection
|
|
21701
22939
|
};
|
|
21702
22940
|
//#endregion
|
|
21703
22941
|
//#region src/composites/widget-slot.tsx
|
|
@@ -26004,92 +27242,6 @@ function AgentStepEditor(props) {
|
|
|
26004
27242
|
});
|
|
26005
27243
|
}
|
|
26006
27244
|
//#endregion
|
|
26007
|
-
//#region ../../node_modules/class-variance-authority/dist/index.mjs
|
|
26008
|
-
/**
|
|
26009
|
-
* Copyright 2022 Joe Bell. All rights reserved.
|
|
26010
|
-
*
|
|
26011
|
-
* This file is licensed to you under the Apache License, Version 2.0
|
|
26012
|
-
* (the "License"); you may not use this file except in compliance with the
|
|
26013
|
-
* License. You may obtain a copy of the License at
|
|
26014
|
-
*
|
|
26015
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
26016
|
-
*
|
|
26017
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
26018
|
-
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
26019
|
-
* WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or implied. See the
|
|
26020
|
-
* License for the specific language governing permissions and limitations under
|
|
26021
|
-
* the License.
|
|
26022
|
-
*/ var falsyToString = (value) => typeof value === "boolean" ? `${value}` : value === 0 ? "0" : value;
|
|
26023
|
-
var cx = clsx;
|
|
26024
|
-
var cva = (base, config) => (props) => {
|
|
26025
|
-
var _config_compoundVariants;
|
|
26026
|
-
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);
|
|
26027
|
-
const { variants, defaultVariants } = config;
|
|
26028
|
-
const getVariantClassNames = Object.keys(variants).map((variant) => {
|
|
26029
|
-
const variantProp = props === null || props === void 0 ? void 0 : props[variant];
|
|
26030
|
-
const defaultVariantProp = defaultVariants === null || defaultVariants === void 0 ? void 0 : defaultVariants[variant];
|
|
26031
|
-
if (variantProp === null) return null;
|
|
26032
|
-
const variantKey = falsyToString(variantProp) || falsyToString(defaultVariantProp);
|
|
26033
|
-
return variants[variant][variantKey];
|
|
26034
|
-
});
|
|
26035
|
-
const propsWithoutUndefined = props && Object.entries(props).reduce((acc, param) => {
|
|
26036
|
-
let [key, value] = param;
|
|
26037
|
-
if (value === void 0) return acc;
|
|
26038
|
-
acc[key] = value;
|
|
26039
|
-
return acc;
|
|
26040
|
-
}, {});
|
|
26041
|
-
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) => {
|
|
26042
|
-
let { class: cvClass, className: cvClassName, ...compoundVariantOptions } = param;
|
|
26043
|
-
return Object.entries(compoundVariantOptions).every((param) => {
|
|
26044
|
-
let [key, value] = param;
|
|
26045
|
-
return Array.isArray(value) ? value.includes({
|
|
26046
|
-
...defaultVariants,
|
|
26047
|
-
...propsWithoutUndefined
|
|
26048
|
-
}[key]) : {
|
|
26049
|
-
...defaultVariants,
|
|
26050
|
-
...propsWithoutUndefined
|
|
26051
|
-
}[key] === value;
|
|
26052
|
-
}) ? [
|
|
26053
|
-
...acc,
|
|
26054
|
-
cvClass,
|
|
26055
|
-
cvClassName
|
|
26056
|
-
] : acc;
|
|
26057
|
-
}, []), props === null || props === void 0 ? void 0 : props.class, props === null || props === void 0 ? void 0 : props.className);
|
|
26058
|
-
};
|
|
26059
|
-
//#endregion
|
|
26060
|
-
//#region src/primitives/button.tsx
|
|
26061
|
-
var buttonVariants = cva("inline-flex items-center justify-center rounded-md font-medium transition-colors cursor-pointer disabled:opacity-50 disabled:pointer-events-none", {
|
|
26062
|
-
variants: {
|
|
26063
|
-
variant: {
|
|
26064
|
-
primary: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
26065
|
-
secondary: "bg-surface text-foreground hover:bg-surface-hover",
|
|
26066
|
-
ghost: "hover:bg-surface-hover text-foreground-muted",
|
|
26067
|
-
danger: "bg-danger text-white hover:bg-danger/90",
|
|
26068
|
-
outline: "border border-border bg-transparent text-foreground hover:bg-surface-hover"
|
|
26069
|
-
},
|
|
26070
|
-
size: {
|
|
26071
|
-
sm: "h-7 px-2.5 text-xs gap-1.5",
|
|
26072
|
-
md: "h-8 px-3 text-sm gap-2",
|
|
26073
|
-
lg: "h-9 px-4 text-sm gap-2"
|
|
26074
|
-
}
|
|
26075
|
-
},
|
|
26076
|
-
defaultVariants: {
|
|
26077
|
-
variant: "primary",
|
|
26078
|
-
size: "sm"
|
|
26079
|
-
}
|
|
26080
|
-
});
|
|
26081
|
-
var Button = forwardRef(({ className, variant, size, ...props }, ref) => {
|
|
26082
|
-
return /* @__PURE__ */ jsx("button", {
|
|
26083
|
-
ref,
|
|
26084
|
-
className: cn(buttonVariants({
|
|
26085
|
-
variant,
|
|
26086
|
-
size
|
|
26087
|
-
}), className),
|
|
26088
|
-
...props
|
|
26089
|
-
});
|
|
26090
|
-
});
|
|
26091
|
-
Button.displayName = "Button";
|
|
26092
|
-
//#endregion
|
|
26093
27245
|
//#region src/primitives/icon-button.tsx
|
|
26094
27246
|
var iconButtonVariants = cva("inline-flex items-center justify-center rounded-md font-medium transition-colors cursor-pointer disabled:opacity-50 disabled:pointer-events-none", {
|
|
26095
27247
|
variants: {
|
|
@@ -26124,55 +27276,6 @@ var IconButton = forwardRef(({ className, variant, size, icon: Icon, ...props },
|
|
|
26124
27276
|
});
|
|
26125
27277
|
IconButton.displayName = "IconButton";
|
|
26126
27278
|
//#endregion
|
|
26127
|
-
//#region src/primitives/badge.tsx
|
|
26128
|
-
var badgeVariants = cva("inline-flex items-center rounded-full text-xs font-medium px-2 py-0.5", {
|
|
26129
|
-
variants: {
|
|
26130
|
-
variant: {
|
|
26131
|
-
default: "bg-surface-hover text-foreground",
|
|
26132
|
-
success: "bg-success/15 text-success",
|
|
26133
|
-
warning: "bg-warning/15 text-warning",
|
|
26134
|
-
danger: "bg-danger/15 text-danger",
|
|
26135
|
-
info: "bg-info/15 text-info"
|
|
26136
|
-
},
|
|
26137
|
-
styleVariant: {
|
|
26138
|
-
solid: "",
|
|
26139
|
-
outline: "border bg-transparent"
|
|
26140
|
-
}
|
|
26141
|
-
},
|
|
26142
|
-
defaultVariants: {
|
|
26143
|
-
variant: "default",
|
|
26144
|
-
styleVariant: "solid"
|
|
26145
|
-
}
|
|
26146
|
-
});
|
|
26147
|
-
var Badge = forwardRef(({ className, variant, style, ...props }, ref) => {
|
|
26148
|
-
return /* @__PURE__ */ jsx("span", {
|
|
26149
|
-
ref,
|
|
26150
|
-
className: cn(badgeVariants({
|
|
26151
|
-
variant,
|
|
26152
|
-
styleVariant: style
|
|
26153
|
-
}), className),
|
|
26154
|
-
...props
|
|
26155
|
-
});
|
|
26156
|
-
});
|
|
26157
|
-
Badge.displayName = "Badge";
|
|
26158
|
-
//#endregion
|
|
26159
|
-
//#region src/primitives/card.tsx
|
|
26160
|
-
var cardVariants = cva("rounded-lg p-3", {
|
|
26161
|
-
variants: { variant: {
|
|
26162
|
-
flat: "bg-surface",
|
|
26163
|
-
bordered: "bg-surface border border-border"
|
|
26164
|
-
} },
|
|
26165
|
-
defaultVariants: { variant: "bordered" }
|
|
26166
|
-
});
|
|
26167
|
-
var Card = forwardRef(({ className, variant, ...props }, ref) => {
|
|
26168
|
-
return /* @__PURE__ */ jsx("div", {
|
|
26169
|
-
ref,
|
|
26170
|
-
className: cn(cardVariants({ variant }), className),
|
|
26171
|
-
...props
|
|
26172
|
-
});
|
|
26173
|
-
});
|
|
26174
|
-
Card.displayName = "Card";
|
|
26175
|
-
//#endregion
|
|
26176
27279
|
//#region src/primitives/label.tsx
|
|
26177
27280
|
var Label = forwardRef(({ className, ...props }, ref) => {
|
|
26178
27281
|
return /* @__PURE__ */ jsx("label", {
|
|
@@ -26201,16 +27304,6 @@ var Separator = forwardRef(({ className, orientation, ...props }, ref) => {
|
|
|
26201
27304
|
});
|
|
26202
27305
|
Separator.displayName = "Separator";
|
|
26203
27306
|
//#endregion
|
|
26204
|
-
//#region src/primitives/skeleton.tsx
|
|
26205
|
-
var Skeleton = forwardRef(({ className, ...props }, ref) => {
|
|
26206
|
-
return /* @__PURE__ */ jsx("div", {
|
|
26207
|
-
ref,
|
|
26208
|
-
className: cn("animate-pulse bg-surface-hover rounded-md h-4 w-full", className),
|
|
26209
|
-
...props
|
|
26210
|
-
});
|
|
26211
|
-
});
|
|
26212
|
-
Skeleton.displayName = "Skeleton";
|
|
26213
|
-
//#endregion
|
|
26214
27307
|
//#region src/primitives/input.tsx
|
|
26215
27308
|
/**
|
|
26216
27309
|
* `disabled:opacity-50 disabled:cursor-not-allowed` for the same reason as
|
|
@@ -26252,43 +27345,6 @@ var Input = forwardRef(({ className, state, leftSlot, rightSlot, ...props }, ref
|
|
|
26252
27345
|
});
|
|
26253
27346
|
Input.displayName = "Input";
|
|
26254
27347
|
//#endregion
|
|
26255
|
-
//#region src/primitives/select.tsx
|
|
26256
|
-
/**
|
|
26257
|
-
* A native `<select>` styled to match the rest of the field set.
|
|
26258
|
-
*
|
|
26259
|
-
* `appearance-none` plus an explicit `bg-surface` / `text-foreground` is what
|
|
26260
|
-
* makes it match — and it is also what USED to make a disabled one a lie. A
|
|
26261
|
-
* browser greys a disabled control by rendering its own background and text
|
|
26262
|
-
* colour; all three of those are overridden here, so a `disabled` dropdown
|
|
26263
|
-
* looked exactly like a live one and simply ignored the click. That is the
|
|
26264
|
-
* whole of the "the language selector does not open" report of 2026-08-15:
|
|
26265
|
-
* the AI section's switch was off, every control under it was correctly inert,
|
|
26266
|
-
* and nothing on screen said so.
|
|
26267
|
-
*
|
|
26268
|
-
* `disabled:` classes belong HERE and not on each call site: there are dozens
|
|
26269
|
-
* of `<Select disabled>` in this app and the one that gets forgotten is the one
|
|
26270
|
-
* that generates the ticket. Guarded by `disabled-affordance.test.tsx`.
|
|
26271
|
-
*/
|
|
26272
|
-
var Select = forwardRef(({ className, options, disabled, ...props }, ref) => {
|
|
26273
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
26274
|
-
className: "relative w-full",
|
|
26275
|
-
children: [/* @__PURE__ */ jsx("select", {
|
|
26276
|
-
ref,
|
|
26277
|
-
disabled,
|
|
26278
|
-
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),
|
|
26279
|
-
...props,
|
|
26280
|
-
children: options.map((option) => /* @__PURE__ */ jsx("option", {
|
|
26281
|
-
value: option.value,
|
|
26282
|
-
children: option.label
|
|
26283
|
-
}, option.value))
|
|
26284
|
-
}), /* @__PURE__ */ jsx("div", {
|
|
26285
|
-
className: cn("absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none text-foreground-muted", disabled === true ? "opacity-50" : ""),
|
|
26286
|
-
children: /* @__PURE__ */ jsx(ChevronDown, { className: "h-3.5 w-3.5" })
|
|
26287
|
-
})]
|
|
26288
|
-
});
|
|
26289
|
-
});
|
|
26290
|
-
Select.displayName = "Select";
|
|
26291
|
-
//#endregion
|
|
26292
27348
|
//#region src/primitives/checkbox.tsx
|
|
26293
27349
|
var Checkbox = forwardRef(({ className, ...props }, ref) => {
|
|
26294
27350
|
return /* @__PURE__ */ jsx("input", {
|
|
@@ -26577,53 +27633,6 @@ function TooltipContent({ className, children, ...props }) {
|
|
|
26577
27633
|
});
|
|
26578
27634
|
}
|
|
26579
27635
|
//#endregion
|
|
26580
|
-
//#region src/hooks/use-is-mobile.ts
|
|
26581
|
-
var MOBILE_QUERY = "(max-width: 767px)";
|
|
26582
|
-
/**
|
|
26583
|
-
* Mid breakpoint — below this width the sidebar collapses to an
|
|
26584
|
-
* icons-only rail to reclaim canvas space. Above it (and outside
|
|
26585
|
-
* mobile) the full sidebar renders. Threshold matches Tailwind `lg`
|
|
26586
|
-
* so sidebar fans out at the same point grids switch to multi-column.
|
|
26587
|
-
*/
|
|
26588
|
-
var MID_QUERY = "(min-width: 768px) and (max-width: 1023px)";
|
|
26589
|
-
/**
|
|
26590
|
-
* `matchMedia` is universal in browsers but absent in bare DOM environments.
|
|
26591
|
-
* These hooks are called from shared primitives (`DataTable` picks its narrow
|
|
26592
|
-
* layout with one), so throwing here takes down the whole page that merely
|
|
26593
|
-
* rendered a table. Where the capability is missing, report the desktop
|
|
26594
|
-
* layout — a widescreen rendering is wrong-looking; a crashed page is gone.
|
|
26595
|
-
*/
|
|
26596
|
-
function matchQuery(query) {
|
|
26597
|
-
if (typeof window.matchMedia !== "function") return null;
|
|
26598
|
-
return window.matchMedia(query);
|
|
26599
|
-
}
|
|
26600
|
-
function subscribeQuery(query) {
|
|
26601
|
-
return (callback) => {
|
|
26602
|
-
const mql = matchQuery(query);
|
|
26603
|
-
if (mql === null) return () => {};
|
|
26604
|
-
mql.addEventListener("change", callback);
|
|
26605
|
-
return () => mql.removeEventListener("change", callback);
|
|
26606
|
-
};
|
|
26607
|
-
}
|
|
26608
|
-
function getSnapshot(query) {
|
|
26609
|
-
return () => matchQuery(query)?.matches ?? false;
|
|
26610
|
-
}
|
|
26611
|
-
function getServerSnapshot() {
|
|
26612
|
-
return false;
|
|
26613
|
-
}
|
|
26614
|
-
function useIsMobile() {
|
|
26615
|
-
return useSyncExternalStore(subscribeQuery(MOBILE_QUERY), getSnapshot(MOBILE_QUERY), getServerSnapshot);
|
|
26616
|
-
}
|
|
26617
|
-
/**
|
|
26618
|
-
* True when the viewport is in the medium (`md`) Tailwind range —
|
|
26619
|
-
* 768px ≤ width < 1024px. Use to drive layouts that should pack denser
|
|
26620
|
-
* than full-desktop but more legibly than mobile (e.g. icons-only
|
|
26621
|
-
* sidebar rail, narrower stat tiles).
|
|
26622
|
-
*/
|
|
26623
|
-
function useIsMidWidth() {
|
|
26624
|
-
return useSyncExternalStore(subscribeQuery(MID_QUERY), getSnapshot(MID_QUERY), getServerSnapshot);
|
|
26625
|
-
}
|
|
26626
|
-
//#endregion
|
|
26627
27636
|
//#region src/primitives/bottom-sheet.tsx
|
|
26628
27637
|
function BottomSheet({ open, onClose, title, children, className }) {
|
|
26629
27638
|
useEffect(() => {
|
|
@@ -29831,650 +30840,230 @@ function coverageLine(monitor) {
|
|
|
29831
30840
|
const extra = [...covered].filter((c) => !SCENE_CAPTURE_VARIANTS.some((v) => v === c));
|
|
29832
30841
|
for (const c of extra) parts.push(`${variantLabel(c)} ✓`);
|
|
29833
30842
|
const missing = SCENE_CAPTURE_VARIANTS.filter((v) => !covered.has(v));
|
|
29834
|
-
if (missing.length === 0) return parts.join(" · ");
|
|
29835
|
-
if (covered.size === 0) return `${parts.join(" · ")} — nothing captured yet, so this scene is not watching.`;
|
|
29836
|
-
const names = missing.map((v) => variantLabel(v).toLowerCase()).join(" and ");
|
|
29837
|
-
return monitor.onUncoveredCondition === "judge-anyway" ? `${parts.join(" · ")} — ${names} judged against the references you do have.` : `${parts.join(" · ")} — optional; checks in ${names} are skipped.`;
|
|
29838
|
-
}
|
|
29839
|
-
/**
|
|
29840
|
-
* The calm sentence for a scene that is sitting the current light out — `null`
|
|
29841
|
-
* whenever it is checking normally.
|
|
29842
|
-
*
|
|
29843
|
-
* Separate from {@link statusLine} because it is not a verdict and must not
|
|
29844
|
-
* read like one. A paused scene has not failed, has not lost its flag, and
|
|
29845
|
-
* needs nothing from the operator; the only thing worth saying is which light
|
|
29846
|
-
* it is waiting out and that it will pick itself back up.
|
|
29847
|
-
*/
|
|
29848
|
-
function suspensionLine(monitor) {
|
|
29849
|
-
if (monitor.suspendedCondition === null) return null;
|
|
29850
|
-
const light = variantLabel(monitor.suspendedCondition).toLowerCase();
|
|
29851
|
-
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.`;
|
|
29852
|
-
}
|
|
29853
|
-
var UNAVAILABLE_SENTENCE = {
|
|
29854
|
-
"no-reference-for-condition": "can’t tell — nothing has been captured for this scene yet. Press “Capture daylight” to photograph what normal looks like.",
|
|
29855
|
-
"view-shifted": "can’t tell — the camera view has moved since the reference was captured. Re-capture it.",
|
|
29856
|
-
"no-vision-profile": "can’t tell — the vision model is unreachable, so the change is on hold.",
|
|
29857
|
-
"encoder-model-changed": "can’t tell — the embedding model changed, so the stored references are unreadable. Re-capture them.",
|
|
29858
|
-
"no-snapshot": "can’t tell — this camera did not return a frame."
|
|
29859
|
-
};
|
|
29860
|
-
/**
|
|
29861
|
-
* The one line under a scene's name. It answers, in order: can it judge, what
|
|
29862
|
-
* does it say, and how long has it said it.
|
|
29863
|
-
*/
|
|
29864
|
-
function statusLine(monitor, now) {
|
|
29865
|
-
if (monitor.states.length === 0) return "Not armed yet — capture the initial state to start watching.";
|
|
29866
|
-
if (monitor.verdict === "unknown") return monitor.unavailable !== null ? UNAVAILABLE_SENTENCE[monitor.unavailable] : "can’t tell right now.";
|
|
29867
|
-
const since = monitor.verdict === "diverged" ? monitor.divergedAt : monitor.armedAt;
|
|
29868
|
-
const held = since !== null ? ` for ${humanDuration(Math.max(0, now - since))}` : "";
|
|
29869
|
-
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.`;
|
|
29870
|
-
const paused = suspensionLine(monitor);
|
|
29871
|
-
if (paused !== null) return paused;
|
|
29872
|
-
return monitor.verdict === "diverged" ? `CHANGED${held}` : `Normal${held}`;
|
|
29873
|
-
}
|
|
29874
|
-
/** The badge word. `latched` wins over `verdict`, because the latch is the
|
|
29875
|
-
* thing the operator asked to be told and it outlives the live state.
|
|
29876
|
-
*
|
|
29877
|
-
* A scene sitting out an uncovered light reads `Paused`, in the NEUTRAL tone —
|
|
29878
|
-
* never `alarm`, and never the `danger` variant the page maps that to. A
|
|
29879
|
-
* missing IR reference must not be able to paint a red badge on a scene that
|
|
29880
|
-
* is doing exactly what it was configured to do. */
|
|
29881
|
-
function badgeFor(monitor) {
|
|
29882
|
-
if (monitor.states.length === 0) return {
|
|
29883
|
-
text: "Not armed",
|
|
29884
|
-
tone: "unknown"
|
|
29885
|
-
};
|
|
29886
|
-
if (monitor.latched) return {
|
|
29887
|
-
text: "Changed",
|
|
29888
|
-
tone: "alarm"
|
|
29889
|
-
};
|
|
29890
|
-
if (monitor.suspendedCondition !== null) return {
|
|
29891
|
-
text: "Paused",
|
|
29892
|
-
tone: "unknown"
|
|
29893
|
-
};
|
|
29894
|
-
if (monitor.verdict === "unknown") return {
|
|
29895
|
-
text: "Can’t tell",
|
|
29896
|
-
tone: "unknown"
|
|
29897
|
-
};
|
|
29898
|
-
return monitor.verdict === "diverged" ? {
|
|
29899
|
-
text: "Changed",
|
|
29900
|
-
tone: "alarm"
|
|
29901
|
-
} : {
|
|
29902
|
-
text: "Normal",
|
|
29903
|
-
tone: "ok"
|
|
29904
|
-
};
|
|
29905
|
-
}
|
|
29906
|
-
/** A rect covering most of the frame is dominated by weather, sky and parked
|
|
29907
|
-
* cars: the bin is 2 % of the pixels and CLIP will not notice it leaving. The
|
|
29908
|
-
* failure is silent INSENSITIVITY, which is the worst kind — hence a warning
|
|
29909
|
-
* rather than a refusal. */
|
|
29910
|
-
var ROI_AREA_WARN_FRACTION = .4;
|
|
29911
|
-
function roiTooLarge(roi) {
|
|
29912
|
-
return roi.width * roi.height > ROI_AREA_WARN_FRACTION;
|
|
29913
|
-
}
|
|
29914
|
-
function humanDuration(ms) {
|
|
29915
|
-
const s = Math.floor(ms / 1e3);
|
|
29916
|
-
if (s < 60) return `${s}s`;
|
|
29917
|
-
const m = Math.floor(s / 60);
|
|
29918
|
-
if (m < 60) return `${m}m`;
|
|
29919
|
-
const h = Math.floor(m / 60);
|
|
29920
|
-
if (h < 48) return `${h}h`;
|
|
29921
|
-
return `${Math.floor(h / 24)}d`;
|
|
29922
|
-
}
|
|
29923
|
-
//#endregion
|
|
29924
|
-
//#region src/composites/code-block.tsx
|
|
29925
|
-
function CodeBlock({ children, maxHeight = 300, className }) {
|
|
29926
|
-
const [copied, setCopied] = useState(false);
|
|
29927
|
-
const handleCopy = useCallback(() => {
|
|
29928
|
-
navigator.clipboard.writeText(children).then(() => {
|
|
29929
|
-
setCopied(true);
|
|
29930
|
-
setTimeout(() => setCopied(false), 2e3);
|
|
29931
|
-
});
|
|
29932
|
-
}, [children]);
|
|
29933
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
29934
|
-
className: cn("relative group", className),
|
|
29935
|
-
children: [/* @__PURE__ */ jsx(ScrollArea, {
|
|
29936
|
-
style: { maxHeight },
|
|
29937
|
-
children: /* @__PURE__ */ jsx("pre", {
|
|
29938
|
-
className: "font-mono text-xs bg-surface p-3 rounded-md border border-border-subtle",
|
|
29939
|
-
children: /* @__PURE__ */ jsx("code", { children })
|
|
29940
|
-
})
|
|
29941
|
-
}), /* @__PURE__ */ jsx("div", {
|
|
29942
|
-
className: "absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity",
|
|
29943
|
-
children: /* @__PURE__ */ jsx(IconButton, {
|
|
29944
|
-
icon: copied ? Check : Copy,
|
|
29945
|
-
"aria-label": "Copy code",
|
|
29946
|
-
variant: "ghost",
|
|
29947
|
-
size: "sm",
|
|
29948
|
-
onClick: handleCopy
|
|
29949
|
-
})
|
|
29950
|
-
})]
|
|
29951
|
-
});
|
|
29952
|
-
}
|
|
29953
|
-
//#endregion
|
|
29954
|
-
//#region src/composites/confirm-action-button.tsx
|
|
29955
|
-
/**
|
|
29956
|
-
* ConfirmActionButton — destructive-action button with a modal confirm.
|
|
29957
|
-
*
|
|
29958
|
-
* Two-step UX for any operation operators shouldn't trigger by accident:
|
|
29959
|
-
* reboot device, restart addon, regenerate token, etc. The trigger is a
|
|
29960
|
-
* normal Button that opens a Dialog; confirming runs the async action,
|
|
29961
|
-
* the trigger spinner-disables itself for the duration, and the dialog
|
|
29962
|
-
* closes on success. Errors surface inline at the dialog's bottom.
|
|
29963
|
-
*
|
|
29964
|
-
* Generic on `TResult` so callers don't have to discard the return
|
|
29965
|
-
* value. Pass an `icon` to render it inside the trigger (e.g. RotateCw
|
|
29966
|
-
* for reboot, RefreshCw for restart).
|
|
29967
|
-
*/
|
|
29968
|
-
function ConfirmActionButton({ label, icon: Icon, title = "Confirm action", description, confirmLabel, triggerVariant = "outline", confirmVariant = "danger", size = "sm", disabled, className, action, onSuccess }) {
|
|
29969
|
-
const [open, setOpen] = useState(false);
|
|
29970
|
-
const [running, setRunning] = useState(false);
|
|
29971
|
-
const [error, setError] = useState(null);
|
|
29972
|
-
const handleConfirm = async () => {
|
|
29973
|
-
setError(null);
|
|
29974
|
-
setRunning(true);
|
|
29975
|
-
try {
|
|
29976
|
-
const result = await action();
|
|
29977
|
-
onSuccess?.(result);
|
|
29978
|
-
setOpen(false);
|
|
29979
|
-
} catch (err) {
|
|
29980
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
29981
|
-
} finally {
|
|
29982
|
-
setRunning(false);
|
|
29983
|
-
}
|
|
29984
|
-
};
|
|
29985
|
-
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs(Button, {
|
|
29986
|
-
type: "button",
|
|
29987
|
-
variant: triggerVariant,
|
|
29988
|
-
size,
|
|
29989
|
-
disabled,
|
|
29990
|
-
onClick: () => {
|
|
29991
|
-
setError(null);
|
|
29992
|
-
setOpen(true);
|
|
29993
|
-
},
|
|
29994
|
-
className,
|
|
29995
|
-
children: [Icon && /* @__PURE__ */ jsx(Icon, { className: "h-3.5 w-3.5" }), label]
|
|
29996
|
-
}), /* @__PURE__ */ jsx(Dialog, {
|
|
29997
|
-
open,
|
|
29998
|
-
onOpenChange: (next) => !running && setOpen(next),
|
|
29999
|
-
children: /* @__PURE__ */ jsxs(DialogContent, { children: [
|
|
30000
|
-
/* @__PURE__ */ jsxs(DialogHeader, { children: [/* @__PURE__ */ jsxs(DialogTitle, {
|
|
30001
|
-
className: "flex items-center gap-2",
|
|
30002
|
-
children: [/* @__PURE__ */ jsx(TriangleAlert, { className: "h-4 w-4 text-amber-400" }), title]
|
|
30003
|
-
}), typeof description === "string" ? /* @__PURE__ */ jsx(DialogDescription, { children: description }) : description] }),
|
|
30004
|
-
error && /* @__PURE__ */ jsx("div", {
|
|
30005
|
-
className: cn("mt-2 px-3 py-2 rounded border border-danger/40 bg-danger/10", "text-xs text-danger"),
|
|
30006
|
-
children: error
|
|
30007
|
-
}),
|
|
30008
|
-
/* @__PURE__ */ jsxs(DialogFooter, { children: [/* @__PURE__ */ jsx(Button, {
|
|
30009
|
-
type: "button",
|
|
30010
|
-
variant: "ghost",
|
|
30011
|
-
size,
|
|
30012
|
-
disabled: running,
|
|
30013
|
-
onClick: () => setOpen(false),
|
|
30014
|
-
children: "Cancel"
|
|
30015
|
-
}), /* @__PURE__ */ jsxs(Button, {
|
|
30016
|
-
type: "button",
|
|
30017
|
-
variant: confirmVariant,
|
|
30018
|
-
size,
|
|
30019
|
-
disabled: running,
|
|
30020
|
-
onClick: () => {
|
|
30021
|
-
handleConfirm();
|
|
30022
|
-
},
|
|
30023
|
-
children: [running && /* @__PURE__ */ jsx(LoaderCircle, { className: "h-3.5 w-3.5 animate-spin" }), confirmLabel ?? label]
|
|
30024
|
-
})] })
|
|
30025
|
-
] })
|
|
30026
|
-
})] });
|
|
30027
|
-
}
|
|
30028
|
-
//#endregion
|
|
30029
|
-
//#region src/composites/copy-button.tsx
|
|
30030
|
-
/**
|
|
30031
|
-
* `CopyButton` — a compact button that copies a string `value` to the
|
|
30032
|
-
* clipboard via `navigator.clipboard.writeText`, showing a brief
|
|
30033
|
-
* "copied" check state. Addon-agnostic; used wherever a secret / URL
|
|
30034
|
-
* needs a one-click copy affordance (export setup panels, etc.).
|
|
30035
|
-
*/
|
|
30036
|
-
var COPIED_RESET_MS = 2e3;
|
|
30037
|
-
function CopyButton({ value, label, srLabel, className, disabled }) {
|
|
30038
|
-
const [copied, setCopied] = useState(false);
|
|
30039
|
-
const handleCopy = useCallback(() => {
|
|
30040
|
-
if (!value) return;
|
|
30041
|
-
navigator.clipboard.writeText(value).then(() => {
|
|
30042
|
-
setCopied(true);
|
|
30043
|
-
setTimeout(() => setCopied(false), COPIED_RESET_MS);
|
|
30044
|
-
});
|
|
30045
|
-
}, [value]);
|
|
30046
|
-
return /* @__PURE__ */ jsxs(Button, {
|
|
30047
|
-
size: "sm",
|
|
30048
|
-
variant: "ghost",
|
|
30049
|
-
type: "button",
|
|
30050
|
-
disabled: disabled || value.length === 0,
|
|
30051
|
-
onClick: handleCopy,
|
|
30052
|
-
className: cn(className),
|
|
30053
|
-
"aria-label": copied ? "Copied" : `Copy ${srLabel ?? label ?? "value"}`,
|
|
30054
|
-
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", {
|
|
30055
|
-
className: "ml-1",
|
|
30056
|
-
children: copied ? "Copied" : label
|
|
30057
|
-
}) : null]
|
|
30058
|
-
});
|
|
30059
|
-
}
|
|
30060
|
-
//#endregion
|
|
30061
|
-
//#region src/composites/data-table-layout.ts
|
|
30062
|
-
/**
|
|
30063
|
-
* At this many columns and above, `auto` switches a narrow viewport to cards.
|
|
30064
|
-
* Three columns still fit a phone at a readable size; four do not.
|
|
30065
|
-
*/
|
|
30066
|
-
var CARD_MODE_MIN_COLUMNS = 4;
|
|
30067
|
-
function resolveTableLayout({ mode, columnCount, isNarrow }) {
|
|
30068
|
-
if (!isNarrow) return "table";
|
|
30069
|
-
if (mode === "scroll") return "table";
|
|
30070
|
-
if (mode === "cards") return "cards";
|
|
30071
|
-
if (columnCount === 0) return "table";
|
|
30072
|
-
return columnCount >= 4 ? "cards" : "table";
|
|
30073
|
-
}
|
|
30074
|
-
//#endregion
|
|
30075
|
-
//#region src/lib/responsive.ts
|
|
30076
|
-
/**
|
|
30077
|
-
* Responsive design tokens — three-tier system.
|
|
30078
|
-
*
|
|
30079
|
-
* The admin-ui targets three width tiers; every component should pick
|
|
30080
|
-
* the right token from this file instead of hand-coding breakpoints.
|
|
30081
|
-
* Adopting the tokens uniformly is what makes the app feel consistent
|
|
30082
|
-
* across operator devices (mobile / tablet / desktop) and what unlocks
|
|
30083
|
-
* targeted tuning (e.g. shrink every label by 1px in one place).
|
|
30084
|
-
*
|
|
30085
|
-
* Tier definitions (Tailwind breakpoints):
|
|
30086
|
-
*
|
|
30087
|
-
* • **L1 — Compact** (`< 640px`, default = mobile portrait)
|
|
30088
|
-
* Single column, tight padding, smallest text, icon-only buttons
|
|
30089
|
-
* where labels can be inferred. The operator can still see every
|
|
30090
|
-
* stat / action — no horizontal scroll, no clipped text. The user
|
|
30091
|
-
* directive: "molta informazione in poco spazio quando la width è
|
|
30092
|
-
* bassa."
|
|
30093
|
-
*
|
|
30094
|
-
* • **L2 — Standard** (`sm` — `640–1023px`, tablet + small laptop)
|
|
30095
|
-
* 2-column grids, side panels become visible, button labels return.
|
|
30096
|
-
* Targets ~768px–1024px viewports — the most common operator sizes.
|
|
30097
|
-
*
|
|
30098
|
-
* • **L3 — Spacious** (`lg+` — `≥ 1024px`, desktop)
|
|
30099
|
-
* 4-column grids, generous padding, full-width header text,
|
|
30100
|
-
* side-by-side composite layouts. The operator has elbow room.
|
|
30101
|
-
*
|
|
30102
|
-
* Conventions:
|
|
30103
|
-
* - **Always start from L1** — the bare class is always the compact
|
|
30104
|
-
* variant, then `sm:` adds room at 640px, `lg:` at 1024px. No
|
|
30105
|
-
* desktop-first breakpoints; no max-width queries.
|
|
30106
|
-
* - **Token strings are static** — kept as `as const` so callers can
|
|
30107
|
-
* compose them via `cn()` / template literals without running a
|
|
30108
|
-
* class generator at runtime.
|
|
30109
|
-
* - **Pair the token with a comment** — the comment in the file
|
|
30110
|
-
* names every breakpoint it crosses, so a reader can predict the
|
|
30111
|
-
* layout without going to Tailwind docs.
|
|
30112
|
-
*/
|
|
30113
|
-
/** Container padding for a card-style section (header + body). */
|
|
30114
|
-
var SECTION_CARD = "rounded-lg border border-border bg-surface overflow-hidden";
|
|
30115
|
-
/** Header strip inside a card (between border-b lines). */
|
|
30116
|
-
var SECTION_HEADER = "border-b border-border px-3 sm:px-4 py-1.5 sm:py-2";
|
|
30117
|
-
/** Body padding for a card-style section. */
|
|
30118
|
-
var SECTION_BODY = "p-2 sm:p-3 lg:p-4";
|
|
30119
|
-
/** Gap between cards in a vertical stack. */
|
|
30120
|
-
var STACK_GAP = "space-y-3 sm:space-y-4";
|
|
30121
|
-
/** Gap between cards in a grid (4 cells / row at lg). */
|
|
30122
|
-
var GRID_GAP = "gap-2 sm:gap-3";
|
|
30123
|
-
/** Row padding inside a divided list (e.g. InfoRow). */
|
|
30124
|
-
var LIST_ROW = "px-3 sm:px-4 py-1.5 sm:py-2";
|
|
30125
|
-
/** 1-up at L1, 2-up at L2, 4-up at L3 — quick-stat row. */
|
|
30126
|
-
var GRID_QUICK_STATS = "grid grid-cols-2 sm:grid-cols-2 lg:grid-cols-4";
|
|
30127
|
-
/** 1-up at L1, 2-up at L2+ — paired panels (motion + detection rules). */
|
|
30128
|
-
var GRID_PAIRED = "grid grid-cols-1 lg:grid-cols-2";
|
|
30129
|
-
/** Side-by-side layout that stacks below md (canvas + side panel). */
|
|
30130
|
-
var SPLIT_PANEL_OUTER = "flex flex-col-reverse md:flex-row";
|
|
30131
|
-
/** Sidebar narrow lane in a SPLIT_PANEL_OUTER layout. */
|
|
30132
|
-
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";
|
|
30133
|
-
/**
|
|
30134
|
-
* The tier boundary at which a settings label/value row stops stacking.
|
|
30135
|
-
*
|
|
30136
|
-
* Below it (L1 — compact) the row is ONE column: label on its own line, value
|
|
30137
|
-
* on the next at full width. Above it the row is label-left / value-right as
|
|
30138
|
-
* before. The two-column form needs `SETTING_ROW_LABEL`'s lane plus a readable
|
|
30139
|
-
* value beside it; a phone has neither, and the failure mode is not "cramped"
|
|
30140
|
-
* but "the value is the part that gets cut" — the one thing the operator
|
|
30141
|
-
* opened the page to read.
|
|
30142
|
-
*/
|
|
30143
|
-
var SETTING_ROW_STACK_BREAKPOINT = "sm";
|
|
30144
|
-
/** Outer row: single column at L1, label/value columns from `sm` up. */
|
|
30145
|
-
var SETTING_ROW = "flex min-w-0 flex-col gap-0.5 py-1.5 sm:flex-row sm:items-center";
|
|
30146
|
-
/** Label lane. Full width (and free to wrap) at L1; a fixed lane from `sm` up. */
|
|
30147
|
-
var SETTING_ROW_LABEL = "min-w-0 text-[11px] leading-tight text-foreground-subtle sm:w-32 sm:shrink-0 sm:pr-2";
|
|
30148
|
-
/**
|
|
30149
|
-
* Value lane — the value text plus its trailing affordances (copy / reveal).
|
|
30150
|
-
* `w-full` at L1 so the value owns the whole row once the label is above it;
|
|
30151
|
-
* `sm:flex-1` makes it share the row again from `sm` up.
|
|
30152
|
-
*/
|
|
30153
|
-
var SETTING_ROW_VALUE = "flex w-full min-w-0 items-center gap-1 sm:flex-1";
|
|
30154
|
-
/**
|
|
30155
|
-
* The value text itself. `break-all` lets an opaque token (a key, a URL) wrap
|
|
30156
|
-
* at L1 instead of being cut mid-word; `sm:truncate` restores the single-line
|
|
30157
|
-
* desktop form, where the row is wide enough for truncation to be a choice
|
|
30158
|
-
* rather than data loss.
|
|
30159
|
-
*/
|
|
30160
|
-
var SETTING_ROW_VALUE_TEXT = "min-w-0 break-all text-xs text-foreground sm:truncate";
|
|
30161
|
-
/** Section header label (uppercase tracking-wider). */
|
|
30162
|
-
var TEXT_SECTION_LABEL = "text-[10px] sm:text-[11px] font-semibold text-foreground uppercase tracking-wider";
|
|
30163
|
-
/** Field label inside a row. */
|
|
30164
|
-
var TEXT_FIELD_LABEL = "text-[11px] sm:text-xs text-foreground";
|
|
30165
|
-
/** Sub-label / hint text. */
|
|
30166
|
-
var TEXT_HINT = "text-[10px] text-foreground-subtle";
|
|
30167
|
-
/** Primary value (a metric, badge, …). */
|
|
30168
|
-
var TEXT_VALUE = "text-[11px] sm:text-xs font-medium text-foreground";
|
|
30169
|
-
/** Big metric value (StatCard). */
|
|
30170
|
-
var TEXT_METRIC = "text-base sm:text-lg font-semibold leading-tight text-foreground";
|
|
30171
|
-
/** Default compact button. */
|
|
30172
|
-
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";
|
|
30173
|
-
/** Primary compact button. */
|
|
30174
|
-
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";
|
|
30175
|
-
/** Danger compact button. */
|
|
30176
|
-
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";
|
|
30177
|
-
/** Warning compact button (e.g. cancel-drawing). */
|
|
30178
|
-
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";
|
|
30179
|
-
/** Compact text/select/number input. */
|
|
30180
|
-
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";
|
|
30181
|
-
/** Chip-style toggle (zone / class selectors in ZoneRulesEditor). */
|
|
30182
|
-
var CHIP_BASE = "inline-flex items-center gap-1 rounded-full border px-1.5 py-0.5 text-[10px] font-medium transition-colors";
|
|
30183
|
-
var CHIP_ACTIVE = "border-primary/50 bg-primary/15 text-primary";
|
|
30184
|
-
var CHIP_INACTIVE = "border-border bg-surface text-foreground-subtle hover:bg-surface-hover";
|
|
30185
|
-
//#endregion
|
|
30186
|
-
//#region src/composites/setting-row.tsx
|
|
30187
|
-
function SettingRow({ label, children, actions, elements = "plain", className, valueClassName }) {
|
|
30188
|
-
const LabelTag = elements === "description" ? "dt" : "span";
|
|
30189
|
-
const ValueTag = elements === "description" ? "dd" : "div";
|
|
30190
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
30191
|
-
"data-setting-row": "",
|
|
30192
|
-
className: cn(SETTING_ROW, className),
|
|
30193
|
-
children: [/* @__PURE__ */ jsx(LabelTag, {
|
|
30194
|
-
"data-setting-row-label": "",
|
|
30195
|
-
className: SETTING_ROW_LABEL,
|
|
30196
|
-
children: label
|
|
30197
|
-
}), /* @__PURE__ */ jsxs(ValueTag, {
|
|
30198
|
-
"data-setting-row-value": "",
|
|
30199
|
-
className: SETTING_ROW_VALUE,
|
|
30200
|
-
children: [/* @__PURE__ */ jsx("span", {
|
|
30201
|
-
"data-setting-row-value-text": "",
|
|
30202
|
-
className: cn(SETTING_ROW_VALUE_TEXT, valueClassName),
|
|
30203
|
-
children
|
|
30204
|
-
}), actions !== void 0 && /* @__PURE__ */ jsx("span", {
|
|
30205
|
-
className: "flex shrink-0 items-center gap-0.5",
|
|
30206
|
-
children: actions
|
|
30207
|
-
})]
|
|
30208
|
-
})]
|
|
30209
|
-
});
|
|
30210
|
-
}
|
|
30211
|
-
//#endregion
|
|
30212
|
-
//#region src/composites/sort-header-button.tsx
|
|
30213
|
-
function SortHeaderButton({ label, active, dir, onSort }) {
|
|
30214
|
-
return /* @__PURE__ */ jsxs("button", {
|
|
30215
|
-
type: "button",
|
|
30216
|
-
onClick: onSort,
|
|
30217
|
-
className: "group/sort inline-flex items-center gap-0.5 hover:text-foreground transition-colors",
|
|
30218
|
-
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") })]
|
|
30219
|
-
});
|
|
30220
|
-
}
|
|
30221
|
-
//#endregion
|
|
30222
|
-
//#region src/composites/table-sort.ts
|
|
30223
|
-
/**
|
|
30224
|
-
* The toggle rule, and the only copy of it.
|
|
30225
|
-
*
|
|
30226
|
-
* none → asc → desc → none, and a click on a DIFFERENT column starts that
|
|
30227
|
-
* column fresh at `asc`. `sameColumn` says whether the clicked column is the
|
|
30228
|
-
* one currently sorted; `currentDir` is its direction (`null` when nothing is
|
|
30229
|
-
* sorted). Returns the next direction, or `null` for "unsorted".
|
|
30230
|
-
*
|
|
30231
|
-
* Split out from {@link nextTableSort} so the device list — whose sort state
|
|
30232
|
-
* keys columns by `columnId`, not `columnKey` — can reuse the rule without a
|
|
30233
|
-
* cast and without a second copy of it.
|
|
30234
|
-
*/
|
|
30235
|
-
function nextSortDirection(sameColumn, currentDir) {
|
|
30236
|
-
if (!sameColumn || currentDir === null) return "asc";
|
|
30237
|
-
return currentDir === "asc" ? "desc" : null;
|
|
30238
|
-
}
|
|
30239
|
-
/**
|
|
30240
|
-
* Toggle a column's sort on header click: none → asc → desc → none.
|
|
30241
|
-
* Clicking a DIFFERENT column starts it fresh at `asc`.
|
|
30242
|
-
*/
|
|
30243
|
-
function nextTableSort(current, columnKey) {
|
|
30244
|
-
const dir = nextSortDirection(current !== null && current.columnKey === columnKey, current === null ? null : current.dir);
|
|
30245
|
-
return dir === null ? null : {
|
|
30246
|
-
columnKey,
|
|
30247
|
-
dir
|
|
30248
|
-
};
|
|
30843
|
+
if (missing.length === 0) return parts.join(" · ");
|
|
30844
|
+
if (covered.size === 0) return `${parts.join(" · ")} — nothing captured yet, so this scene is not watching.`;
|
|
30845
|
+
const names = missing.map((v) => variantLabel(v).toLowerCase()).join(" and ");
|
|
30846
|
+
return monitor.onUncoveredCondition === "judge-anyway" ? `${parts.join(" · ")} — ${names} judged against the references you do have.` : `${parts.join(" · ")} — optional; checks in ${names} are skipped.`;
|
|
30249
30847
|
}
|
|
30250
30848
|
/**
|
|
30251
|
-
*
|
|
30252
|
-
*
|
|
30253
|
-
* Numbers compare numerically and strings compare with `localeCompare` — a
|
|
30254
|
-
* numeric column must never be ordered as text ("100" before "9"), and a text
|
|
30255
|
-
* column must respect the locale's collation. A mixed column (one row a
|
|
30256
|
-
* number, another a string) falls back to string comparison of both, which is
|
|
30257
|
-
* the only total order available; it is not a shape any caller should build on
|
|
30258
|
-
* purpose.
|
|
30849
|
+
* The calm sentence for a scene that is sitting the current light out — `null`
|
|
30850
|
+
* whenever it is checking normally.
|
|
30259
30851
|
*
|
|
30260
|
-
*
|
|
30261
|
-
*
|
|
30262
|
-
*
|
|
30852
|
+
* Separate from {@link statusLine} because it is not a verdict and must not
|
|
30853
|
+
* read like one. A paused scene has not failed, has not lost its flag, and
|
|
30854
|
+
* needs nothing from the operator; the only thing worth saying is which light
|
|
30855
|
+
* it is waiting out and that it will pick itself back up.
|
|
30263
30856
|
*/
|
|
30264
|
-
function
|
|
30265
|
-
if (
|
|
30266
|
-
|
|
30857
|
+
function suspensionLine(monitor) {
|
|
30858
|
+
if (monitor.suspendedCondition === null) return null;
|
|
30859
|
+
const light = variantLabel(monitor.suspendedCondition).toLowerCase();
|
|
30860
|
+
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.`;
|
|
30267
30861
|
}
|
|
30862
|
+
var UNAVAILABLE_SENTENCE = {
|
|
30863
|
+
"no-reference-for-condition": "can’t tell — nothing has been captured for this scene yet. Press “Capture daylight” to photograph what normal looks like.",
|
|
30864
|
+
"view-shifted": "can’t tell — the camera view has moved since the reference was captured. Re-capture it.",
|
|
30865
|
+
"no-vision-profile": "can’t tell — the vision model is unreachable, so the change is on hold.",
|
|
30866
|
+
"encoder-model-changed": "can’t tell — the embedding model changed, so the stored references are unreadable. Re-capture them.",
|
|
30867
|
+
"no-snapshot": "can’t tell — this camera did not return a frame."
|
|
30868
|
+
};
|
|
30268
30869
|
/**
|
|
30269
|
-
*
|
|
30270
|
-
*
|
|
30271
|
-
* `valueOf` is the column's `sortValue` — it is called once per row per
|
|
30272
|
-
* comparison, so it must be cheap (read a field, do not format a string).
|
|
30273
|
-
* `sort === null` ⇒ a fresh shallow copy in the incoming order.
|
|
30870
|
+
* The one line under a scene's name. It answers, in order: can it judge, what
|
|
30871
|
+
* does it say, and how long has it said it.
|
|
30274
30872
|
*/
|
|
30275
|
-
function
|
|
30276
|
-
if (
|
|
30277
|
-
|
|
30278
|
-
|
|
30279
|
-
|
|
30280
|
-
|
|
30281
|
-
|
|
30282
|
-
|
|
30283
|
-
|
|
30284
|
-
}
|
|
30285
|
-
return factor * compareValues(av, bv);
|
|
30286
|
-
});
|
|
30873
|
+
function statusLine(monitor, now) {
|
|
30874
|
+
if (monitor.states.length === 0) return "Not armed yet — capture the initial state to start watching.";
|
|
30875
|
+
if (monitor.verdict === "unknown") return monitor.unavailable !== null ? UNAVAILABLE_SENTENCE[monitor.unavailable] : "can’t tell right now.";
|
|
30876
|
+
const since = monitor.verdict === "diverged" ? monitor.divergedAt : monitor.armedAt;
|
|
30877
|
+
const held = since !== null ? ` for ${humanDuration(Math.max(0, now - since))}` : "";
|
|
30878
|
+
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.`;
|
|
30879
|
+
const paused = suspensionLine(monitor);
|
|
30880
|
+
if (paused !== null) return paused;
|
|
30881
|
+
return monitor.verdict === "diverged" ? `CHANGED${held}` : `Normal${held}`;
|
|
30287
30882
|
}
|
|
30288
|
-
/**
|
|
30289
|
-
|
|
30290
|
-
|
|
30291
|
-
|
|
30883
|
+
/** The badge word. `latched` wins over `verdict`, because the latch is the
|
|
30884
|
+
* thing the operator asked to be told and it outlives the live state.
|
|
30885
|
+
*
|
|
30886
|
+
* A scene sitting out an uncovered light reads `Paused`, in the NEUTRAL tone —
|
|
30887
|
+
* never `alarm`, and never the `danger` variant the page maps that to. A
|
|
30888
|
+
* missing IR reference must not be able to paint a red badge on a scene that
|
|
30889
|
+
* is doing exactly what it was configured to do. */
|
|
30890
|
+
function badgeFor(monitor) {
|
|
30891
|
+
if (monitor.states.length === 0) return {
|
|
30892
|
+
text: "Not armed",
|
|
30893
|
+
tone: "unknown"
|
|
30894
|
+
};
|
|
30895
|
+
if (monitor.latched) return {
|
|
30896
|
+
text: "Changed",
|
|
30897
|
+
tone: "alarm"
|
|
30898
|
+
};
|
|
30899
|
+
if (monitor.suspendedCondition !== null) return {
|
|
30900
|
+
text: "Paused",
|
|
30901
|
+
tone: "unknown"
|
|
30902
|
+
};
|
|
30903
|
+
if (monitor.verdict === "unknown") return {
|
|
30904
|
+
text: "Can’t tell",
|
|
30905
|
+
tone: "unknown"
|
|
30906
|
+
};
|
|
30907
|
+
return monitor.verdict === "diverged" ? {
|
|
30908
|
+
text: "Changed",
|
|
30909
|
+
tone: "alarm"
|
|
30910
|
+
} : {
|
|
30911
|
+
text: "Normal",
|
|
30912
|
+
tone: "ok"
|
|
30913
|
+
};
|
|
30914
|
+
}
|
|
30915
|
+
/** A rect covering most of the frame is dominated by weather, sky and parked
|
|
30916
|
+
* cars: the bin is 2 % of the pixels and CLIP will not notice it leaving. The
|
|
30917
|
+
* failure is silent INSENSITIVITY, which is the worst kind — hence a warning
|
|
30918
|
+
* rather than a refusal. */
|
|
30919
|
+
var ROI_AREA_WARN_FRACTION = .4;
|
|
30920
|
+
function roiTooLarge(roi) {
|
|
30921
|
+
return roi.width * roi.height > ROI_AREA_WARN_FRACTION;
|
|
30922
|
+
}
|
|
30923
|
+
function humanDuration(ms) {
|
|
30924
|
+
const s = Math.floor(ms / 1e3);
|
|
30925
|
+
if (s < 60) return `${s}s`;
|
|
30926
|
+
const m = Math.floor(s / 60);
|
|
30927
|
+
if (m < 60) return `${m}m`;
|
|
30928
|
+
const h = Math.floor(m / 60);
|
|
30929
|
+
if (h < 48) return `${h}h`;
|
|
30930
|
+
return `${Math.floor(h / 24)}d`;
|
|
30292
30931
|
}
|
|
30293
30932
|
//#endregion
|
|
30294
|
-
//#region src/composites/
|
|
30933
|
+
//#region src/composites/code-block.tsx
|
|
30934
|
+
function CodeBlock({ children, maxHeight = 300, className }) {
|
|
30935
|
+
const [copied, setCopied] = useState(false);
|
|
30936
|
+
const handleCopy = useCallback(() => {
|
|
30937
|
+
navigator.clipboard.writeText(children).then(() => {
|
|
30938
|
+
setCopied(true);
|
|
30939
|
+
setTimeout(() => setCopied(false), 2e3);
|
|
30940
|
+
});
|
|
30941
|
+
}, [children]);
|
|
30942
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
30943
|
+
className: cn("relative group", className),
|
|
30944
|
+
children: [/* @__PURE__ */ jsx(ScrollArea, {
|
|
30945
|
+
style: { maxHeight },
|
|
30946
|
+
children: /* @__PURE__ */ jsx("pre", {
|
|
30947
|
+
className: "font-mono text-xs bg-surface p-3 rounded-md border border-border-subtle",
|
|
30948
|
+
children: /* @__PURE__ */ jsx("code", { children })
|
|
30949
|
+
})
|
|
30950
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
30951
|
+
className: "absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity",
|
|
30952
|
+
children: /* @__PURE__ */ jsx(IconButton, {
|
|
30953
|
+
icon: copied ? Check : Copy,
|
|
30954
|
+
"aria-label": "Copy code",
|
|
30955
|
+
variant: "ghost",
|
|
30956
|
+
size: "sm",
|
|
30957
|
+
onClick: handleCopy
|
|
30958
|
+
})
|
|
30959
|
+
})]
|
|
30960
|
+
});
|
|
30961
|
+
}
|
|
30962
|
+
//#endregion
|
|
30963
|
+
//#region src/composites/confirm-action-button.tsx
|
|
30295
30964
|
/**
|
|
30296
|
-
*
|
|
30297
|
-
*
|
|
30298
|
-
* Recipes baked in:
|
|
30299
|
-
* - `overflow-x-auto` wrapper so narrow viewports don't clip the
|
|
30300
|
-
* right edge (the rounded border survives the scroll).
|
|
30301
|
-
* - `min-w-[Npx]` on the inner table so columns don't squash below
|
|
30302
|
-
* readable width before the scroll kicks in.
|
|
30303
|
-
* - `whitespace-nowrap` on headers — column titles never wrap to
|
|
30304
|
-
* two lines (`AUTO-KILL` was the regression that triggered this).
|
|
30305
|
-
* - Themed `bg-surface` + `border-border` so contrast works in both
|
|
30306
|
-
* light and dark themes.
|
|
30307
|
-
*
|
|
30308
|
-
* Column model: caller supplies `columns: DataColumn<T>[]` and
|
|
30309
|
-
* `rows: T[]`. Each column declares `header`, optional `align`,
|
|
30310
|
-
* optional `nowrap`, and a `render(row) => ReactNode` that produces
|
|
30311
|
-
* the cell content.
|
|
30312
|
-
*
|
|
30313
|
-
* <DataTable
|
|
30314
|
-
* columns={[
|
|
30315
|
-
* { key: 'username', header: 'Username', render: (u) => u.username },
|
|
30316
|
-
* { key: 'role', header: 'Role', render: (u) => <RoleBadge .../> },
|
|
30317
|
-
* { key: 'actions', header: '', align: 'right', render: (u) => <Actions .../> },
|
|
30318
|
-
* ]}
|
|
30319
|
-
* rows={users}
|
|
30320
|
-
* minWidthPx={560}
|
|
30321
|
-
* />
|
|
30322
|
-
*
|
|
30323
|
-
* If finer header markup is needed, pass `headerRender` instead of
|
|
30324
|
-
* `header` (e.g. for a sortable click affordance).
|
|
30325
|
-
*
|
|
30326
|
-
* ── Narrow viewports ──────────────────────────────────────────────────────
|
|
30327
|
-
* The table stays a table. The wrapper is `overflow-x-auto`, so a phone
|
|
30328
|
-
* scrolls the columns sideways INSIDE the table — the page body never moves
|
|
30329
|
-
* sideways, and the cells stay one line (`whitespace-nowrap`). That is the
|
|
30330
|
-
* default (`mobileMode="scroll"`). Cards are opt-in via `mobileMode="cards"`;
|
|
30331
|
-
* `auto` still applies the old column-count rule for a surface that wants it.
|
|
30332
|
-
* See `resolveTableLayout`.
|
|
30333
|
-
*
|
|
30334
|
-
* ── Scrolling the BODY, not the page ──────────────────────────────────────
|
|
30335
|
-
* `maxBodyHeight` caps the table and scrolls its rows INSIDE the container
|
|
30336
|
-
* with the column header pinned. It is ONE prop, not two, because "this list
|
|
30337
|
-
* scrolls internally" and "its header stays visible" are the same decision: a
|
|
30338
|
-
* capped table whose header scrolls away is a list of unlabelled cells.
|
|
30965
|
+
* ConfirmActionButton — destructive-action button with a modal confirm.
|
|
30339
30966
|
*
|
|
30340
|
-
*
|
|
30341
|
-
*
|
|
30342
|
-
*
|
|
30343
|
-
|
|
30344
|
-
|
|
30345
|
-
* The default cap for an internally-scrolling table.
|
|
30967
|
+
* Two-step UX for any operation operators shouldn't trigger by accident:
|
|
30968
|
+
* reboot device, restart addon, regenerate token, etc. The trigger is a
|
|
30969
|
+
* normal Button that opens a Dialog; confirming runs the async action,
|
|
30970
|
+
* the trigger spinner-disables itself for the duration, and the dialog
|
|
30971
|
+
* closes on success. Errors surface inline at the dialog's bottom.
|
|
30346
30972
|
*
|
|
30347
|
-
* `
|
|
30348
|
-
*
|
|
30349
|
-
*
|
|
30973
|
+
* Generic on `TResult` so callers don't have to discard the return
|
|
30974
|
+
* value. Pass an `icon` to render it inside the trigger (e.g. RotateCw
|
|
30975
|
+
* for reboot, RefreshCw for restart).
|
|
30350
30976
|
*/
|
|
30351
|
-
|
|
30352
|
-
|
|
30353
|
-
|
|
30354
|
-
|
|
30355
|
-
|
|
30356
|
-
|
|
30357
|
-
|
|
30358
|
-
|
|
30359
|
-
|
|
30360
|
-
|
|
30361
|
-
|
|
30362
|
-
|
|
30363
|
-
|
|
30364
|
-
|
|
30365
|
-
|
|
30366
|
-
|
|
30367
|
-
|
|
30368
|
-
|
|
30369
|
-
|
|
30370
|
-
|
|
30371
|
-
|
|
30372
|
-
|
|
30373
|
-
|
|
30374
|
-
|
|
30375
|
-
|
|
30376
|
-
|
|
30377
|
-
|
|
30378
|
-
|
|
30379
|
-
|
|
30380
|
-
|
|
30381
|
-
|
|
30382
|
-
|
|
30383
|
-
|
|
30384
|
-
|
|
30385
|
-
|
|
30386
|
-
|
|
30387
|
-
|
|
30388
|
-
|
|
30389
|
-
|
|
30390
|
-
|
|
30391
|
-
|
|
30392
|
-
|
|
30393
|
-
|
|
30394
|
-
|
|
30395
|
-
|
|
30396
|
-
|
|
30397
|
-
|
|
30398
|
-
|
|
30399
|
-
|
|
30400
|
-
|
|
30401
|
-
|
|
30402
|
-
|
|
30403
|
-
|
|
30404
|
-
|
|
30405
|
-
|
|
30406
|
-
|
|
30407
|
-
|
|
30408
|
-
|
|
30409
|
-
|
|
30410
|
-
title: sortable ? void 0 : col.notSortableReason,
|
|
30411
|
-
className: [
|
|
30412
|
-
"px-3 py-2 text-foreground-subtle font-medium bg-surface border-b border-border",
|
|
30413
|
-
scrolls ? "sticky top-0 z-10" : "",
|
|
30414
|
-
ALIGN_CLASS[align],
|
|
30415
|
-
nowrap ? "whitespace-nowrap" : "",
|
|
30416
|
-
col.width ?? ""
|
|
30417
|
-
].filter(Boolean).join(" "),
|
|
30418
|
-
children: sortable ? /* @__PURE__ */ jsx(SortHeaderButton, {
|
|
30419
|
-
label: col.headerRender ? col.headerRender() : col.header,
|
|
30420
|
-
active: isSorted,
|
|
30421
|
-
dir: isSorted && activeSort !== null ? activeSort.dir : "asc",
|
|
30422
|
-
onSort: () => setSort((prev) => nextTableSort(prev, col.key))
|
|
30423
|
-
}) : col.headerRender ? col.headerRender() : col.header
|
|
30424
|
-
}, col.key);
|
|
30425
|
-
}) })
|
|
30426
|
-
}), /* @__PURE__ */ jsx("tbody", { children: displayRows.map((row, rowIndex) => {
|
|
30427
|
-
const key = rowKey ? rowKey(row, rowIndex) : rowIndex;
|
|
30428
|
-
const extra = rowClassName?.(row, rowIndex) ?? "";
|
|
30429
|
-
const interactive = onRowClick !== void 0;
|
|
30430
|
-
return /* @__PURE__ */ jsx("tr", {
|
|
30431
|
-
onClick: interactive ? () => onRowClick(row, rowIndex) : void 0,
|
|
30432
|
-
className: [interactive ? "cursor-pointer hover:bg-primary/5" : "hover:bg-primary/5", extra].filter(Boolean).join(" "),
|
|
30433
|
-
children: columns.map((col) => {
|
|
30434
|
-
const align = col.align ?? "left";
|
|
30435
|
-
const nowrap = col.nowrap ?? true;
|
|
30436
|
-
return /* @__PURE__ */ jsx("td", {
|
|
30437
|
-
className: [
|
|
30438
|
-
"px-3 py-2 text-foreground border-b border-border",
|
|
30439
|
-
ALIGN_CLASS[align],
|
|
30440
|
-
nowrap ? "whitespace-nowrap" : ""
|
|
30441
|
-
].filter(Boolean).join(" "),
|
|
30442
|
-
children: col.render(row, rowIndex)
|
|
30443
|
-
}, col.key);
|
|
30444
|
-
})
|
|
30445
|
-
}, key);
|
|
30446
|
-
}) })]
|
|
30447
|
-
})
|
|
30448
|
-
});
|
|
30977
|
+
function ConfirmActionButton({ label, icon: Icon, title = "Confirm action", description, confirmLabel, triggerVariant = "outline", confirmVariant = "danger", size = "sm", disabled, className, action, onSuccess }) {
|
|
30978
|
+
const [open, setOpen] = useState(false);
|
|
30979
|
+
const [running, setRunning] = useState(false);
|
|
30980
|
+
const [error, setError] = useState(null);
|
|
30981
|
+
const handleConfirm = async () => {
|
|
30982
|
+
setError(null);
|
|
30983
|
+
setRunning(true);
|
|
30984
|
+
try {
|
|
30985
|
+
const result = await action();
|
|
30986
|
+
onSuccess?.(result);
|
|
30987
|
+
setOpen(false);
|
|
30988
|
+
} catch (err) {
|
|
30989
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
30990
|
+
} finally {
|
|
30991
|
+
setRunning(false);
|
|
30992
|
+
}
|
|
30993
|
+
};
|
|
30994
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs(Button, {
|
|
30995
|
+
type: "button",
|
|
30996
|
+
variant: triggerVariant,
|
|
30997
|
+
size,
|
|
30998
|
+
disabled,
|
|
30999
|
+
onClick: () => {
|
|
31000
|
+
setError(null);
|
|
31001
|
+
setOpen(true);
|
|
31002
|
+
},
|
|
31003
|
+
className,
|
|
31004
|
+
children: [Icon && /* @__PURE__ */ jsx(Icon, { className: "h-3.5 w-3.5" }), label]
|
|
31005
|
+
}), /* @__PURE__ */ jsx(Dialog, {
|
|
31006
|
+
open,
|
|
31007
|
+
onOpenChange: (next) => !running && setOpen(next),
|
|
31008
|
+
children: /* @__PURE__ */ jsxs(DialogContent, { children: [
|
|
31009
|
+
/* @__PURE__ */ jsxs(DialogHeader, { children: [/* @__PURE__ */ jsxs(DialogTitle, {
|
|
31010
|
+
className: "flex items-center gap-2",
|
|
31011
|
+
children: [/* @__PURE__ */ jsx(TriangleAlert, { className: "h-4 w-4 text-amber-400" }), title]
|
|
31012
|
+
}), typeof description === "string" ? /* @__PURE__ */ jsx(DialogDescription, { children: description }) : description] }),
|
|
31013
|
+
error && /* @__PURE__ */ jsx("div", {
|
|
31014
|
+
className: cn("mt-2 px-3 py-2 rounded border border-danger/40 bg-danger/10", "text-xs text-danger"),
|
|
31015
|
+
children: error
|
|
31016
|
+
}),
|
|
31017
|
+
/* @__PURE__ */ jsxs(DialogFooter, { children: [/* @__PURE__ */ jsx(Button, {
|
|
31018
|
+
type: "button",
|
|
31019
|
+
variant: "ghost",
|
|
31020
|
+
size,
|
|
31021
|
+
disabled: running,
|
|
31022
|
+
onClick: () => setOpen(false),
|
|
31023
|
+
children: "Cancel"
|
|
31024
|
+
}), /* @__PURE__ */ jsxs(Button, {
|
|
31025
|
+
type: "button",
|
|
31026
|
+
variant: confirmVariant,
|
|
31027
|
+
size,
|
|
31028
|
+
disabled: running,
|
|
31029
|
+
onClick: () => {
|
|
31030
|
+
handleConfirm();
|
|
31031
|
+
},
|
|
31032
|
+
children: [running && /* @__PURE__ */ jsx(LoaderCircle, { className: "h-3.5 w-3.5 animate-spin" }), confirmLabel ?? label]
|
|
31033
|
+
})] })
|
|
31034
|
+
] })
|
|
31035
|
+
})] });
|
|
30449
31036
|
}
|
|
31037
|
+
//#endregion
|
|
31038
|
+
//#region src/composites/copy-button.tsx
|
|
30450
31039
|
/**
|
|
30451
|
-
*
|
|
30452
|
-
*
|
|
30453
|
-
*
|
|
30454
|
-
*
|
|
30455
|
-
* full-width strip at the foot of the card.
|
|
31040
|
+
* `CopyButton` — a compact button that copies a string `value` to the
|
|
31041
|
+
* clipboard via `navigator.clipboard.writeText`, showing a brief
|
|
31042
|
+
* "copied" check state. Addon-agnostic; used wherever a secret / URL
|
|
31043
|
+
* needs a one-click copy affordance (export setup panels, etc.).
|
|
30456
31044
|
*/
|
|
30457
|
-
|
|
30458
|
-
|
|
30459
|
-
const
|
|
30460
|
-
const
|
|
30461
|
-
|
|
30462
|
-
|
|
30463
|
-
|
|
30464
|
-
|
|
30465
|
-
|
|
30466
|
-
|
|
30467
|
-
|
|
30468
|
-
|
|
30469
|
-
|
|
30470
|
-
|
|
30471
|
-
|
|
30472
|
-
|
|
30473
|
-
|
|
30474
|
-
|
|
30475
|
-
|
|
30476
|
-
|
|
30477
|
-
|
|
31045
|
+
var COPIED_RESET_MS = 2e3;
|
|
31046
|
+
function CopyButton({ value, label, srLabel, className, disabled }) {
|
|
31047
|
+
const [copied, setCopied] = useState(false);
|
|
31048
|
+
const handleCopy = useCallback(() => {
|
|
31049
|
+
if (!value) return;
|
|
31050
|
+
navigator.clipboard.writeText(value).then(() => {
|
|
31051
|
+
setCopied(true);
|
|
31052
|
+
setTimeout(() => setCopied(false), COPIED_RESET_MS);
|
|
31053
|
+
});
|
|
31054
|
+
}, [value]);
|
|
31055
|
+
return /* @__PURE__ */ jsxs(Button, {
|
|
31056
|
+
size: "sm",
|
|
31057
|
+
variant: "ghost",
|
|
31058
|
+
type: "button",
|
|
31059
|
+
disabled: disabled || value.length === 0,
|
|
31060
|
+
onClick: handleCopy,
|
|
31061
|
+
className: cn(className),
|
|
31062
|
+
"aria-label": copied ? "Copied" : `Copy ${srLabel ?? label ?? "value"}`,
|
|
31063
|
+
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", {
|
|
31064
|
+
className: "ml-1",
|
|
31065
|
+
children: copied ? "Copied" : label
|
|
31066
|
+
}) : null]
|
|
30478
31067
|
});
|
|
30479
31068
|
}
|
|
30480
31069
|
//#endregion
|
|
@@ -46532,99 +47121,6 @@ function fuzzyMatch(query, target) {
|
|
|
46532
47121
|
return normalizeForSearch(target).includes(nq);
|
|
46533
47122
|
}
|
|
46534
47123
|
//#endregion
|
|
46535
|
-
//#region src/composites/device-list/pagination.tsx
|
|
46536
|
-
var NEIGHBORS = 2;
|
|
46537
|
-
function computePageButtons(currentPage, totalPages) {
|
|
46538
|
-
if (totalPages <= 1) return [{
|
|
46539
|
-
kind: "page",
|
|
46540
|
-
n: 1
|
|
46541
|
-
}];
|
|
46542
|
-
if (totalPages <= 7) {
|
|
46543
|
-
const all = [];
|
|
46544
|
-
for (let i = 1; i <= totalPages; i++) all.push({
|
|
46545
|
-
kind: "page",
|
|
46546
|
-
n: i
|
|
46547
|
-
});
|
|
46548
|
-
return all;
|
|
46549
|
-
}
|
|
46550
|
-
const result = [];
|
|
46551
|
-
const leftBound = Math.max(2, currentPage - NEIGHBORS);
|
|
46552
|
-
const rightBound = Math.min(totalPages - 1, currentPage + NEIGHBORS);
|
|
46553
|
-
result.push({
|
|
46554
|
-
kind: "page",
|
|
46555
|
-
n: 1
|
|
46556
|
-
});
|
|
46557
|
-
if (leftBound > 2) result.push({
|
|
46558
|
-
kind: "ellipsis",
|
|
46559
|
-
side: "left"
|
|
46560
|
-
});
|
|
46561
|
-
for (let i = leftBound; i <= rightBound; i++) result.push({
|
|
46562
|
-
kind: "page",
|
|
46563
|
-
n: i
|
|
46564
|
-
});
|
|
46565
|
-
if (rightBound < totalPages - 1) result.push({
|
|
46566
|
-
kind: "ellipsis",
|
|
46567
|
-
side: "right"
|
|
46568
|
-
});
|
|
46569
|
-
result.push({
|
|
46570
|
-
kind: "page",
|
|
46571
|
-
n: totalPages
|
|
46572
|
-
});
|
|
46573
|
-
return result;
|
|
46574
|
-
}
|
|
46575
|
-
function Pagination({ currentPage, totalItems, pageSize, onPageChange, itemNoun = "devices" }) {
|
|
46576
|
-
if (totalItems <= pageSize) return null;
|
|
46577
|
-
const totalPages = Math.ceil(totalItems / pageSize);
|
|
46578
|
-
const startIdx = (currentPage - 1) * pageSize + 1;
|
|
46579
|
-
const endIdx = Math.min(currentPage * pageSize, totalItems);
|
|
46580
|
-
const buttons = computePageButtons(currentPage, totalPages);
|
|
46581
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
46582
|
-
className: "flex items-center justify-between gap-4 flex-wrap pt-2 text-[11px] text-foreground-subtle",
|
|
46583
|
-
children: [/* @__PURE__ */ jsxs("span", { children: [
|
|
46584
|
-
"Showing ",
|
|
46585
|
-
startIdx,
|
|
46586
|
-
"–",
|
|
46587
|
-
endIdx,
|
|
46588
|
-
" of ",
|
|
46589
|
-
totalItems,
|
|
46590
|
-
" ",
|
|
46591
|
-
itemNoun
|
|
46592
|
-
] }), /* @__PURE__ */ jsxs("span", {
|
|
46593
|
-
className: "inline-flex items-center gap-1",
|
|
46594
|
-
children: [
|
|
46595
|
-
/* @__PURE__ */ jsx("button", {
|
|
46596
|
-
type: "button",
|
|
46597
|
-
"aria-label": "Previous page",
|
|
46598
|
-
disabled: currentPage === 1,
|
|
46599
|
-
onClick: () => onPageChange(currentPage - 1),
|
|
46600
|
-
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"),
|
|
46601
|
-
children: /* @__PURE__ */ jsx(ChevronLeft, { className: "h-3 w-3" })
|
|
46602
|
-
}),
|
|
46603
|
-
buttons.map((b, i) => {
|
|
46604
|
-
if (b.kind === "ellipsis") return /* @__PURE__ */ jsx("span", {
|
|
46605
|
-
className: "px-1 text-foreground-subtle/60 select-none",
|
|
46606
|
-
children: "…"
|
|
46607
|
-
}, `e-${i}`);
|
|
46608
|
-
return /* @__PURE__ */ jsx("button", {
|
|
46609
|
-
type: "button",
|
|
46610
|
-
onClick: () => onPageChange(b.n),
|
|
46611
|
-
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"),
|
|
46612
|
-
children: b.n
|
|
46613
|
-
}, `p-${b.n}`);
|
|
46614
|
-
}),
|
|
46615
|
-
/* @__PURE__ */ jsx("button", {
|
|
46616
|
-
type: "button",
|
|
46617
|
-
"aria-label": "Next page",
|
|
46618
|
-
disabled: currentPage === totalPages,
|
|
46619
|
-
onClick: () => onPageChange(currentPage + 1),
|
|
46620
|
-
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"),
|
|
46621
|
-
children: /* @__PURE__ */ jsx(ChevronRight, { className: "h-3 w-3" })
|
|
46622
|
-
})
|
|
46623
|
-
]
|
|
46624
|
-
})]
|
|
46625
|
-
});
|
|
46626
|
-
}
|
|
46627
|
-
//#endregion
|
|
46628
47124
|
//#region src/composites/device-list/multi-select.tsx
|
|
46629
47125
|
/**
|
|
46630
47126
|
* Compact multi-select dropdown — a single chip-sized trigger ("Label · N")
|
|
@@ -53230,4 +53726,4 @@ var SceneMonitorEditor = lazy(() => import("./SceneMonitorEditor-BvDy3x3k.js").t
|
|
|
53230
53726
|
/** Lazy-wrapped `PrivacyMaskSettings` — code-split off the main bundle. */
|
|
53231
53727
|
var PrivacyMaskSettings = lazy(() => import("./PrivacyMaskSettings-PkYs077O.js").then((m) => ({ default: m.PrivacyMaskSettings })));
|
|
53232
53728
|
//#endregion
|
|
53233
|
-
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 };
|
|
53729
|
+
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, usePipelineAnalyticsOwnersWithMedia, 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 };
|