@camstack/ui-library 0.1.52 → 0.1.54

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/index.js CHANGED
@@ -13,6 +13,8 @@ import * as __mfHostTrpcClient from "@trpc/client";
13
13
  import { createTRPCClient, createWSClient, httpLink, splitLink, wsLink } from "@trpc/client";
14
14
  import * as __mfHostTrpcReactQuery from "@trpc/react-query";
15
15
  import { createTRPCReact } from "@trpc/react-query";
16
+ import * as __mfHostReactKonva from "react-konva";
17
+ import * as __mfHostKonva from "konva";
16
18
  import { ALL_CAPABILITY_DEFINITIONS, DeviceExportStatusSchema, EventCategory, ExposedDeviceSchema, KNOWN_CAP_NAMES, createDeviceProxy } from "@camstack/types";
17
19
  import { createSystem } from "@camstack/sdk";
18
20
  import { z } from "zod";
@@ -6851,6 +6853,8 @@ function populateShareCache() {
6851
6853
  cache.share["@tanstack/react-query"] ??= __mfHostTanstackQuery;
6852
6854
  cache.share["@trpc/client"] ??= __mfHostTrpcClient;
6853
6855
  cache.share["@trpc/react-query"] ??= __mfHostTrpcReactQuery;
6856
+ cache.share["react-konva"] ??= __mfHostReactKonva;
6857
+ cache.share["konva"] ??= __mfHostKonva;
6854
6858
  cache.share["@camstack/ui-library"] ??= readHostGlobal("__camstackUiLibrary");
6855
6859
  cache.share["@camstack/sdk"] ??= readHostGlobal("__camstackSdk");
6856
6860
  cache.share["@camstack/types"] ??= readHostGlobal("__camstackTypes");
@@ -6869,6 +6873,8 @@ function ensureMfHostInit() {
6869
6873
  "@tanstack/react-query": npmShared(() => __mfHostTanstackQuery),
6870
6874
  "@trpc/client": npmShared(() => __mfHostTrpcClient),
6871
6875
  "@trpc/react-query": npmShared(() => __mfHostTrpcReactQuery),
6876
+ "react-konva": npmShared(() => __mfHostReactKonva),
6877
+ "konva": npmShared(() => __mfHostKonva),
6872
6878
  "@camstack/ui-library": npmShared(() => readHostGlobal("__camstackUiLibrary")),
6873
6879
  "@camstack/sdk": npmShared(() => readHostGlobal("__camstackSdk")),
6874
6880
  "@camstack/types": npmShared(() => readHostGlobal("__camstackTypes"))
@@ -15924,6 +15930,12 @@ var useDecoderPushPacket = trpc.decoder.pushPacket.useQuery;
15924
15930
  var useDecoderOpenStream = trpc.decoder.openStream.useQuery;
15925
15931
  /** Generated alias around `trpc.decoder.pullFrames.useQuery`. */
15926
15932
  var useDecoderPullFrames = trpc.decoder.pullFrames.useQuery;
15933
+ /** Generated alias around `trpc.decoder.pullHandles.useQuery`. */
15934
+ var useDecoderPullHandles = trpc.decoder.pullHandles.useQuery;
15935
+ /** Generated alias around `trpc.decoder.getFrame.useQuery`. */
15936
+ var useDecoderGetFrame = trpc.decoder.getFrame.useQuery;
15937
+ /** Generated alias around `trpc.decoder.getShmStats.useQuery`. */
15938
+ var useDecoderGetShmStats = trpc.decoder.getShmStats.useQuery;
15927
15939
  /** Generated alias around `trpc.decoder.updateConfig.useQuery`. */
15928
15940
  var useDecoderUpdateConfig = trpc.decoder.updateConfig.useQuery;
15929
15941
  /** Generated alias around `trpc.decoder.getStats.useQuery`. */
@@ -16208,6 +16220,12 @@ var useMotionDetectionApplyDeviceSettingsPatch = trpc.motionDetection.applyDevic
16208
16220
  var useMotionTriggerSetMotionTrigger = trpc.motionTrigger.setMotionTrigger.useMutation;
16209
16221
  /** Generated alias around `trpc.motionTrigger.getStatus.useQuery`. */
16210
16222
  var useMotionTriggerGetStatus = trpc.motionTrigger.getStatus.useQuery;
16223
+ /** Generated alias around `trpc.motionZones.getOptions.useQuery`. */
16224
+ var useMotionZonesGetOptions = trpc.motionZones.getOptions.useQuery;
16225
+ /** Generated alias around `trpc.motionZones.setZone.useMutation`. */
16226
+ var useMotionZonesSetZone = trpc.motionZones.setZone.useMutation;
16227
+ /** Generated alias around `trpc.motionZones.getStatus.useQuery`. */
16228
+ var useMotionZonesGetStatus = trpc.motionZones.getStatus.useQuery;
16211
16229
  /** Generated alias around `trpc.mqttBroker.listBrokers.useQuery`. */
16212
16230
  var useMqttBrokerListBrokers = trpc.mqttBroker.listBrokers.useQuery;
16213
16231
  /** Generated alias around `trpc.mqttBroker.getBrokerConfig.useQuery`. */
@@ -16260,6 +16278,8 @@ var useNodesShutdownNode = trpc.nodes.shutdownNode.useMutation;
16260
16278
  var useNodesRenameNode = trpc.nodes.renameNode.useMutation;
16261
16279
  /** Generated alias around `trpc.nodes.clusterAddonStatus.useQuery`. */
16262
16280
  var useNodesClusterAddonStatus = trpc.nodes.clusterAddonStatus.useQuery;
16281
+ /** Generated alias around `trpc.nodes.getCapUsageGraph.useQuery`. */
16282
+ var useNodesGetCapUsageGraph = trpc.nodes.getCapUsageGraph.useQuery;
16263
16283
  /** Generated alias around `trpc.nodes.getNodeAddons.useQuery`. */
16264
16284
  var useNodesGetNodeAddons = trpc.nodes.getNodeAddons.useQuery;
16265
16285
  /** Generated alias around `trpc.nodes.setProcessLogLevel.useMutation`. */
@@ -16478,10 +16498,18 @@ var usePtzStop = trpc.ptz.stop.useMutation;
16478
16498
  var usePtzGetPresets = trpc.ptz.getPresets.useQuery;
16479
16499
  /** Generated alias around `trpc.ptz.goToPreset.useMutation`. */
16480
16500
  var usePtzGoToPreset = trpc.ptz.goToPreset.useMutation;
16501
+ /** Generated alias around `trpc.ptz.savePreset.useMutation`. */
16502
+ var usePtzSavePreset = trpc.ptz.savePreset.useMutation;
16503
+ /** Generated alias around `trpc.ptz.deletePreset.useMutation`. */
16504
+ var usePtzDeletePreset = trpc.ptz.deletePreset.useMutation;
16505
+ /** Generated alias around `trpc.ptz.getOptions.useQuery`. */
16506
+ var usePtzGetOptions = trpc.ptz.getOptions.useQuery;
16481
16507
  /** Generated alias around `trpc.ptz.goHome.useMutation`. */
16482
16508
  var usePtzGoHome = trpc.ptz.goHome.useMutation;
16483
16509
  /** Generated alias around `trpc.ptz.getPosition.useQuery`. */
16484
16510
  var usePtzGetPosition = trpc.ptz.getPosition.useQuery;
16511
+ /** Generated alias around `trpc.ptz.setAutofocus.useMutation`. */
16512
+ var usePtzSetAutofocus = trpc.ptz.setAutofocus.useMutation;
16485
16513
  /** Generated alias around `trpc.ptz.getStatus.useQuery`. */
16486
16514
  var usePtzGetStatus = trpc.ptz.getStatus.useQuery;
16487
16515
  /** Generated alias around `trpc.ptzAutotrack.getStatus.useQuery`. */
@@ -16638,8 +16666,18 @@ var useStreamBrokerGetStreamUrl = trpc.streamBroker.getStreamUrl.useQuery;
16638
16666
  var useStreamBrokerGetStreamWithCodec = trpc.streamBroker.getStreamWithCodec.useMutation;
16639
16667
  /** Generated alias around `trpc.streamBroker.releaseStreamWithCodec.useMutation`. */
16640
16668
  var useStreamBrokerReleaseStreamWithCodec = trpc.streamBroker.releaseStreamWithCodec.useMutation;
16641
- /** Generated alias around `trpc.streamBroker.getBroker.useQuery`. */
16642
- var useStreamBrokerGetBroker = trpc.streamBroker.getBroker.useQuery;
16669
+ /** Generated alias around `trpc.streamBroker.subscribeAudioChunks.useMutation`. */
16670
+ var useStreamBrokerSubscribeAudioChunks = trpc.streamBroker.subscribeAudioChunks.useMutation;
16671
+ /** Generated alias around `trpc.streamBroker.pullAudioChunks.useQuery`. */
16672
+ var useStreamBrokerPullAudioChunks = trpc.streamBroker.pullAudioChunks.useQuery;
16673
+ /** Generated alias around `trpc.streamBroker.unsubscribeAudioChunks.useMutation`. */
16674
+ var useStreamBrokerUnsubscribeAudioChunks = trpc.streamBroker.unsubscribeAudioChunks.useMutation;
16675
+ /** Generated alias around `trpc.streamBroker.subscribeFrames.useMutation`. */
16676
+ var useStreamBrokerSubscribeFrames = trpc.streamBroker.subscribeFrames.useMutation;
16677
+ /** Generated alias around `trpc.streamBroker.pullFrameHandles.useQuery`. */
16678
+ var useStreamBrokerPullFrameHandles = trpc.streamBroker.pullFrameHandles.useQuery;
16679
+ /** Generated alias around `trpc.streamBroker.unsubscribeFrames.useMutation`. */
16680
+ var useStreamBrokerUnsubscribeFrames = trpc.streamBroker.unsubscribeFrames.useMutation;
16643
16681
  /** Generated alias around `trpc.streamBroker.setPreBufferDuration.useMutation`. */
16644
16682
  var useStreamBrokerSetPreBufferDuration = trpc.streamBroker.setPreBufferDuration.useMutation;
16645
16683
  /** Generated alias around `trpc.streamBroker.getPreBufferInfo.useQuery`. */
@@ -16662,6 +16700,14 @@ var useStreamBrokerGetDeviceSettingsContribution = trpc.streamBroker.getDeviceSe
16662
16700
  var useStreamBrokerGetDeviceLiveContribution = trpc.streamBroker.getDeviceLiveContribution.useQuery;
16663
16701
  /** Generated alias around `trpc.streamBroker.applyDeviceSettingsPatch.useMutation`. */
16664
16702
  var useStreamBrokerApplyDeviceSettingsPatch = trpc.streamBroker.applyDeviceSettingsPatch.useMutation;
16703
+ /** Generated alias around `trpc.streamParams.getOptions.useQuery`. */
16704
+ var useStreamParamsGetOptions = trpc.streamParams.getOptions.useQuery;
16705
+ /** Generated alias around `trpc.streamParams.setProfile.useMutation`. */
16706
+ var useStreamParamsSetProfile = trpc.streamParams.setProfile.useMutation;
16707
+ /** Generated alias around `trpc.streamParams.getConfigSchema.useQuery`. */
16708
+ var useStreamParamsGetConfigSchema = trpc.streamParams.getConfigSchema.useQuery;
16709
+ /** Generated alias around `trpc.streamParams.getStatus.useQuery`. */
16710
+ var useStreamParamsGetStatus = trpc.streamParams.getStatus.useQuery;
16665
16711
  /** Generated alias around `trpc.switch.setState.useMutation`. */
16666
16712
  var useSwitchSetState = trpc.switch.setState.useMutation;
16667
16713
  /** Generated alias around `trpc.switch.getStatus.useQuery`. */
@@ -16724,6 +16770,18 @@ var useUserManagementDisableTotp = trpc.userManagement.disableTotp.useMutation;
16724
16770
  var useUserManagementGetTotpStatus = trpc.userManagement.getTotpStatus.useQuery;
16725
16771
  /** Generated alias around `trpc.userManagement.verifyTotp.useMutation`. */
16726
16772
  var useUserManagementVerifyTotp = trpc.userManagement.verifyTotp.useMutation;
16773
+ /** Generated alias around `trpc.userManagement.oauthIssueCode.useMutation`. */
16774
+ var useUserManagementOauthIssueCode = trpc.userManagement.oauthIssueCode.useMutation;
16775
+ /** Generated alias around `trpc.userManagement.oauthExchangeCode.useMutation`. */
16776
+ var useUserManagementOauthExchangeCode = trpc.userManagement.oauthExchangeCode.useMutation;
16777
+ /** Generated alias around `trpc.userManagement.oauthRefresh.useMutation`. */
16778
+ var useUserManagementOauthRefresh = trpc.userManagement.oauthRefresh.useMutation;
16779
+ /** Generated alias around `trpc.userManagement.oauthVerifyAccessToken.useQuery`. */
16780
+ var useUserManagementOauthVerifyAccessToken = trpc.userManagement.oauthVerifyAccessToken.useQuery;
16781
+ /** Generated alias around `trpc.userManagement.listOauthSessions.useQuery`. */
16782
+ var useUserManagementListOauthSessions = trpc.userManagement.listOauthSessions.useQuery;
16783
+ /** Generated alias around `trpc.userManagement.revokeOauthSession.useMutation`. */
16784
+ var useUserManagementRevokeOauthSession = trpc.userManagement.revokeOauthSession.useMutation;
16727
16785
  /** Generated alias around `trpc.webrtcSession.listStreams.useQuery`. */
16728
16786
  var useWebrtcSessionListStreams = trpc.webrtcSession.listStreams.useQuery;
16729
16787
  /** Generated alias around `trpc.webrtcSession.createSession.useMutation`. */
@@ -16767,8 +16825,16 @@ var useZonesUpdateZone = trpc.zones.updateZone.useMutation;
16767
16825
  * `vite.widgets.config.ts`). At runtime we fetch the public list of
16768
16826
  * addon-contributed widgets via `useAddonWidgetsListWidgets()`, register
16769
16827
  * each remote (`registerRemotes`) once, then resolve a widget by
16770
- * loading the exposed `./widgets` module (`loadRemote`) whose default
16771
- * export is a `Record<stableId, ComponentType<WidgetProps>>` map.
16828
+ * loading the exposed module (`loadRemote`) whose default export is a
16829
+ * `Record<componentKey, ComponentType>` map.
16830
+ *
16831
+ * Unified UI-contribution model (Task 10) — a widget declaration IS a
16832
+ * `UiContribution` with `kind:'remote'`. Both the device-detail path
16833
+ * (`ContributionRenderer`) and the dashboard render widgets through the
16834
+ * same `loadRemoteBundle` MF path. `loadRemoteBundle` is exported and
16835
+ * generic over `(remoteName, exposedModule, entryUrl)` so any
16836
+ * `UiContributionRemote` descriptor resolves without an aggregator-keyed
16837
+ * lookup.
16772
16838
  *
16773
16839
  * Why MF instead of raw ESM:
16774
16840
  * - automatic dedup of shared deps (react/react-dom/@tanstack/etc.)
@@ -16778,9 +16844,7 @@ var useZonesUpdateZone = trpc.zones.updateZone.useMutation;
16778
16844
  * wins by default).
16779
16845
  * - cross-bundle context invariant preserved automatically: every
16780
16846
  * remote's `@camstack/ui-library` import resolves to the host's
16781
- * instance, so `createContext()` references match across bundles
16782
- * (the duplicate-Context bug that motivated `createSharedContext`
16783
- * falls out for free).
16847
+ * instance, so `createContext()` references match across bundles.
16784
16848
  *
16785
16849
  * Live-update — listens to `addon.widget-ready` so newly-loaded addons
16786
16850
  * surface their widgets without a manual page reload (the aggregator
@@ -16791,11 +16855,11 @@ var useZonesUpdateZone = trpc.zones.updateZone.useMutation;
16791
16855
  */
16792
16856
  var WidgetRegistryContext = createSharedContext("camstack:widget-registry", null);
16793
16857
  /**
16794
- * Process-global cache for resolved widget bundles, keyed by
16795
- * `remoteName` (every remote exposes a single `./widgets` module whose
16796
- * default export is the `Record<stableId, Component>` map). Survives
16797
- * provider remount — avoids a full re-fetch + re-init of the MF remote
16798
- * when admin-ui's tree cycles.
16858
+ * Process-global cache for resolved remote bundles, keyed by
16859
+ * `<remoteName>::<exposedModule>`. Each cached value is the exposed
16860
+ * module's default-record map (`Record<componentKey, Component>`).
16861
+ * Survives provider remount — avoids a full re-fetch + re-init of the
16862
+ * MF remote when admin-ui's tree cycles.
16799
16863
  */
16800
16864
  var bundleModuleCache = /* @__PURE__ */ new Map();
16801
16865
  var bundleInflight = /* @__PURE__ */ new Map();
@@ -16807,6 +16871,10 @@ var bundleInflight = /* @__PURE__ */ new Map();
16807
16871
  * update.
16808
16872
  */
16809
16873
  var registeredRemotes = /* @__PURE__ */ new Set();
16874
+ /** Cache key for `bundleModuleCache` — one entry per `(remote, module)`. */
16875
+ function bundleCacheKey(remoteName, exposedModule) {
16876
+ return `${remoteName}::${exposedModule}`;
16877
+ }
16810
16878
  function isRemoteWidgetsModule(value) {
16811
16879
  if (!value || typeof value !== "object") return false;
16812
16880
  const def = value.default;
@@ -16815,8 +16883,7 @@ function isRemoteWidgetsModule(value) {
16815
16883
  /**
16816
16884
  * Diagnostic-only string for the "Got: …" tail in the not-a-record
16817
16885
  * error. Returns a JSON-friendly snapshot of the module's keys + proto
16818
- * name without exposing the value itself (the value is the unknown
16819
- * thing the module factory produced, may be huge or contain PII).
16886
+ * name without exposing the value itself.
16820
16887
  */
16821
16888
  function describeRemoteShape(mod) {
16822
16889
  if (!mod || typeof mod !== "object") return typeof mod;
@@ -16830,13 +16897,19 @@ function describeRemoteShape(mod) {
16830
16897
  };
16831
16898
  }
16832
16899
  /**
16833
- * Register the MF remote (idempotent) and load its `./widgets` module.
16834
- * Returns the resolved `Record<stableId, Component>` map.
16900
+ * Register the MF remote (idempotent) and load one of its exposed
16901
+ * modules. Returns the resolved `Record<componentKey, Component>` map
16902
+ * (the exposed module's `default` export).
16903
+ *
16904
+ * Exported so the unified `ContributionRenderer` `kind:'remote'` path
16905
+ * can load an arbitrary `{ remoteName, exposedModule }` descriptor —
16906
+ * the same path the `WidgetRegistry` uses internally for widgets.
16835
16907
  */
16836
- async function loadRemoteBundle(remoteName, entryUrl) {
16837
- const cached = bundleModuleCache.get(remoteName);
16908
+ async function loadRemoteBundle(remoteName, exposedModule, entryUrl) {
16909
+ const cacheKey = bundleCacheKey(remoteName, exposedModule);
16910
+ const cached = bundleModuleCache.get(cacheKey);
16838
16911
  if (cached) return cached;
16839
- const inflight = bundleInflight.get(remoteName);
16912
+ const inflight = bundleInflight.get(cacheKey);
16840
16913
  if (inflight) return inflight;
16841
16914
  if (!registeredRemotes.has(remoteName)) {
16842
16915
  ensureMfHostInit();
@@ -16847,22 +16920,26 @@ async function loadRemoteBundle(remoteName, entryUrl) {
16847
16920
  }], { force: false });
16848
16921
  registeredRemotes.add(remoteName);
16849
16922
  }
16850
- const promise = loadRemote(`${remoteName}/widgets`).then((mod) => {
16923
+ const promise = loadRemote(`${remoteName}/${exposedModule.startsWith("./") ? exposedModule.slice(2) : exposedModule}`).then((mod) => {
16851
16924
  if (!isRemoteWidgetsModule(mod)) {
16852
16925
  const shape = describeRemoteShape(mod);
16853
- throw new Error(`Widget remote ${remoteName} (${entryUrl}) does not expose a default record on './widgets'. Got: ${JSON.stringify(shape)}`);
16926
+ throw new Error(`Remote ${remoteName} (${entryUrl}) does not expose a default record on '${exposedModule}'. Got: ${JSON.stringify(shape)}`);
16854
16927
  }
16855
16928
  const map = mod.default;
16856
- bundleModuleCache.set(remoteName, map);
16857
- bundleInflight.delete(remoteName);
16929
+ bundleModuleCache.set(cacheKey, map);
16930
+ bundleInflight.delete(cacheKey);
16858
16931
  return map;
16859
16932
  }).catch((err) => {
16860
- bundleInflight.delete(remoteName);
16933
+ bundleInflight.delete(cacheKey);
16861
16934
  throw err;
16862
16935
  });
16863
- bundleInflight.set(remoteName, promise);
16936
+ bundleInflight.set(cacheKey, promise);
16864
16937
  return promise;
16865
16938
  }
16939
+ /** Synchronous peek at the resolved-bundle cache — `undefined` if not yet loaded. */
16940
+ function peekBundle(remoteName, exposedModule) {
16941
+ return bundleModuleCache.get(bundleCacheKey(remoteName, exposedModule));
16942
+ }
16866
16943
  var BOOT_WINDOW_MS = 3e4;
16867
16944
  var POLL_INTERVAL_MS = 2e3;
16868
16945
  function WidgetRegistryProvider({ children }) {
@@ -16882,21 +16959,24 @@ function WidgetRegistryProvider({ children }) {
16882
16959
  queryClient.invalidateQueries({ queryKey: [["addonWidgets", "listWidgets"]] });
16883
16960
  });
16884
16961
  const [resolvedTick, setResolvedTick] = useState(0);
16962
+ const widgets = useMemo(() => rawWidgets ?? [], [rawWidgets]);
16885
16963
  useEffect(() => {
16886
- if (!rawWidgets) return;
16964
+ if (widgets.length === 0) return;
16887
16965
  let cancelled = false;
16888
- const seenRemotes = /* @__PURE__ */ new Set();
16889
- for (const w of rawWidgets) {
16890
- if (seenRemotes.has(w.remoteName)) continue;
16891
- seenRemotes.add(w.remoteName);
16892
- if (bundleModuleCache.has(w.remoteName)) continue;
16966
+ const seen = /* @__PURE__ */ new Set();
16967
+ for (const w of widgets) {
16968
+ const key = bundleCacheKey(w.remote.remoteName, w.remote.exposedModule);
16969
+ if (seen.has(key)) continue;
16970
+ seen.add(key);
16971
+ if (bundleModuleCache.has(key)) continue;
16893
16972
  const entryUrl = w.bundleUrl;
16894
- loadRemoteBundle(w.remoteName, entryUrl).then(() => {
16973
+ loadRemoteBundle(w.remote.remoteName, w.remote.exposedModule, entryUrl).then(() => {
16895
16974
  if (!cancelled) setResolvedTick((t) => t + 1);
16896
16975
  }).catch((err) => {
16897
16976
  const reason = err instanceof Error ? err.message : String(err);
16898
16977
  (typeof globalThis !== "undefined" ? globalThis.console : void 0)?.error?.("[WidgetRegistry] Failed to load widget remote", {
16899
- remoteName: w.remoteName,
16978
+ remoteName: w.remote.remoteName,
16979
+ exposedModule: w.remote.exposedModule,
16900
16980
  entryUrl,
16901
16981
  reason
16902
16982
  });
@@ -16905,19 +16985,26 @@ function WidgetRegistryProvider({ children }) {
16905
16985
  return () => {
16906
16986
  cancelled = true;
16907
16987
  };
16908
- }, [rawWidgets]);
16988
+ }, [widgets]);
16909
16989
  const registry = useMemo(() => {
16910
- const widgets = rawWidgets ?? [];
16911
16990
  const byId = /* @__PURE__ */ new Map();
16912
- for (const w of widgets) byId.set(`${w.addonId}/${w.stableId}`, w);
16991
+ const entryUrlByRemote = /* @__PURE__ */ new Map();
16992
+ for (const w of widgets) {
16993
+ byId.set(`${w.addonId}/${w.stableId}`, w);
16994
+ entryUrlByRemote.set(w.remote.remoteName, w.bundleUrl);
16995
+ }
16913
16996
  const toMetadata = (widgetId, entry) => ({
16914
16997
  widgetId,
16915
16998
  addonId: entry.addonId,
16916
16999
  stableId: entry.stableId,
17000
+ tab: entry.tab,
17001
+ subTab: entry.subTab,
16917
17002
  label: entry.label,
17003
+ order: entry.order,
17004
+ kind: entry.kind,
17005
+ remote: entry.remote,
16918
17006
  description: entry.description,
16919
17007
  icon: entry.icon,
16920
- remoteName: entry.remoteName,
16921
17008
  bundleUrl: entry.bundleUrl,
16922
17009
  hosts: entry.hosts,
16923
17010
  requires: entry.requires,
@@ -16930,9 +17017,9 @@ function WidgetRegistryProvider({ children }) {
16930
17017
  resolve: (widgetId) => {
16931
17018
  const entry = byId.get(widgetId);
16932
17019
  if (!entry) return void 0;
16933
- const bundle = bundleModuleCache.get(entry.remoteName);
17020
+ const bundle = peekBundle(entry.remote.remoteName, entry.remote.exposedModule);
16934
17021
  if (!bundle) return null;
16935
- const Component = bundle[entry.stableId];
17022
+ const Component = bundle[entry.remote.componentKey ?? entry.stableId];
16936
17023
  if (!Component) return void 0;
16937
17024
  return Component;
16938
17025
  },
@@ -16945,15 +17032,23 @@ function WidgetRegistryProvider({ children }) {
16945
17032
  const out = [];
16946
17033
  for (const [widgetId, entry] of byId) out.push(toMetadata(widgetId, entry));
16947
17034
  return out;
16948
- }
17035
+ },
17036
+ entryUrlFor: (remoteName) => entryUrlByRemote.get(remoteName)
16949
17037
  };
16950
- }, [rawWidgets, resolvedTick]);
17038
+ }, [widgets, resolvedTick]);
16951
17039
  return /* @__PURE__ */ jsx(WidgetRegistryContext.Provider, {
16952
17040
  value: registry,
16953
17041
  children
16954
17042
  });
16955
17043
  }
16956
- /** Returns the registered widget component, `null` while loading, or `undefined` if unknown. */
17044
+ /**
17045
+ * Returns the registered widget component, `null` while loading, or
17046
+ * `undefined` if unknown.
17047
+ *
17048
+ * @deprecated Unused by all render paths since the unified
17049
+ * UI-contribution model (Task 10). Use `useRemoteComponent` /
17050
+ * `<WidgetSlot>` instead. Kept for external addons that may import it.
17051
+ */
16957
17052
  function useWidget(widgetId) {
16958
17053
  return useWidgetRegistry().resolve(widgetId);
16959
17054
  }
@@ -16967,6 +17062,68 @@ function useWidgetMetadata(widgetId) {
16967
17062
  function useAllWidgets() {
16968
17063
  return useWidgetRegistry().listAll();
16969
17064
  }
17065
+ /**
17066
+ * Resolve a `kind:'remote'` `UiContributionRemote` descriptor to a
17067
+ * React component. Drives the unified `ContributionRenderer`
17068
+ * `kind:'remote'` path — both device-detail contributions and dashboard
17069
+ * widgets resolve through the same MF `loadRemoteBundle` loader.
17070
+ *
17071
+ * Returns:
17072
+ * - `undefined` — the remote couldn't be resolved (no entry URL known,
17073
+ * or the remote exposed no component for `componentKey`),
17074
+ * - `null` — the bundle is still loading,
17075
+ * - the component otherwise.
17076
+ *
17077
+ * `entryUrl` may be passed explicitly; when omitted it's resolved from
17078
+ * the registry's aggregator-stamped `bundleUrl` for `remote.remoteName`.
17079
+ */
17080
+ function useRemoteComponent(remote, entryUrl) {
17081
+ const reg = useOptionalWidgetRegistry();
17082
+ const resolvedEntryUrl = entryUrl ?? reg?.entryUrlFor(remote.remoteName);
17083
+ const [tick, setTick] = useState(0);
17084
+ const [loadFailed, setLoadFailed] = useState(false);
17085
+ useEffect(() => {
17086
+ if (!resolvedEntryUrl) return;
17087
+ setLoadFailed(false);
17088
+ if (peekBundle(remote.remoteName, remote.exposedModule)) return;
17089
+ let cancelled = false;
17090
+ loadRemoteBundle(remote.remoteName, remote.exposedModule, resolvedEntryUrl).then(() => {
17091
+ if (!cancelled) setTick((t) => t + 1);
17092
+ }).catch((err) => {
17093
+ const reason = err instanceof Error ? err.message : String(err);
17094
+ (typeof globalThis !== "undefined" ? globalThis.console : void 0)?.error?.("[WidgetRegistry] Failed to load remote component", {
17095
+ remoteName: remote.remoteName,
17096
+ exposedModule: remote.exposedModule,
17097
+ entryUrl: resolvedEntryUrl,
17098
+ reason
17099
+ });
17100
+ if (!cancelled) setLoadFailed(true);
17101
+ });
17102
+ return () => {
17103
+ cancelled = true;
17104
+ };
17105
+ }, [
17106
+ remote.remoteName,
17107
+ remote.exposedModule,
17108
+ resolvedEntryUrl
17109
+ ]);
17110
+ return useMemo(() => {
17111
+ if (!resolvedEntryUrl) return void 0;
17112
+ if (loadFailed) return void 0;
17113
+ const bundle = peekBundle(remote.remoteName, remote.exposedModule);
17114
+ if (!bundle) return null;
17115
+ const componentKey = remote.componentKey;
17116
+ if (componentKey === void 0) return Object.values(bundle)[0] ?? void 0;
17117
+ return bundle[componentKey] ?? void 0;
17118
+ }, [
17119
+ remote.remoteName,
17120
+ remote.exposedModule,
17121
+ remote.componentKey,
17122
+ resolvedEntryUrl,
17123
+ tick,
17124
+ loadFailed
17125
+ ]);
17126
+ }
16970
17127
  /** Read the registry instance — throws when no provider is mounted. */
16971
17128
  function useWidgetRegistry() {
16972
17129
  const ctx = useOptionalWidgetRegistry();
@@ -16981,163 +17138,1429 @@ function useContextSafe(ctx) {
16981
17138
  return useContext(ctx);
16982
17139
  }
16983
17140
  //#endregion
16984
- //#region src/composites/widget-slot.tsx
17141
+ //#region src/hooks/use-ptz.ts
16985
17142
  /**
16986
- * <WidgetSlot>single host-side mount point for any addon-contributed
16987
- * widget. Consumers reference a widget by its public id
16988
- * (`<addonId>/<stableId>`), the slot looks the component up in the
16989
- * shared `WidgetRegistry`, validates host context against the widget's
16990
- * `requires` metadata, and renders one of:
16991
- * - skeleton placeholder while the bundle is still loading,
16992
- * - inline error fallback when the widget id is unknown OR the host
16993
- * didn't supply a required context (`deviceContext` /
16994
- * `integrationContext`),
16995
- * - the resolved component otherwise.
17143
+ * usePTZPTZ control hook for device-scoped pan / tilt / zoom.
16996
17144
  *
16997
- * The slot is intentionally STUPID no styling beyond the skeleton/
16998
- * error fallback. Layout (card vs inline, sizing) is the host's
16999
- * responsibility.
17145
+ * Wraps the `ptz` capability methods through the canonical
17146
+ * `useDeviceProxy` surface every call goes through
17147
+ * `dev.ptz?.<method>(...)` with deviceId/nodeId auto-injected. The
17148
+ * hook stays bare-bones around state (`busy`, `error`, `presets`)
17149
+ * so the operator-facing component (PTZOverlay) stays trivial.
17150
+ *
17151
+ * Returns:
17152
+ * - `move(direction)`: discrete one-shot move (cap.move + cap.stop).
17153
+ * Use for short pulse moves triggered by tapping a d-pad button.
17154
+ * - `startContinuous(direction)` / `stopContinuous()`: gesture-driven
17155
+ * continuous motion (`cap.continuousMove` + `cap.stop`). Use for
17156
+ * long-press handlers on touch / mouse-down handlers on desktop.
17157
+ * - `zoom('in' | 'out')`: discrete zoom step.
17158
+ * - `goHome()`: jump to preset 0.
17159
+ * - `presets` + `goToPreset(presetId)`: list and jump to named presets.
17160
+ *
17161
+ * Parametrised by the same `UseDeviceProxyTrpc` shape every other
17162
+ * device hook uses — works under admin-ui (BackendClient.trpc) and
17163
+ * addon pages (AddonPageProps.trpc) alike.
17000
17164
  */
17001
- function WidgetSlot(props) {
17002
- const { widgetId, host = "device-tab", config, deviceId, integrationId, instanceId, size, columns, rows } = props;
17003
- const Component = useWidget(widgetId);
17004
- const metadata = useWidgetMetadata(widgetId);
17005
- const resolvedInstanceId = useMemo(() => instanceId ?? widgetId, [instanceId, widgetId]);
17006
- if (Component === void 0 && metadata === void 0) return /* @__PURE__ */ jsx(WidgetMissingError, {
17007
- widgetId,
17008
- reason: "unknown"
17009
- });
17010
- if (Component === void 0) return /* @__PURE__ */ jsx(WidgetMissingError, {
17011
- widgetId,
17012
- reason: "missing-export"
17013
- });
17014
- if (Component === null) return /* @__PURE__ */ jsx(WidgetSkeleton, {});
17015
- if (metadata) {
17016
- if (metadata.requires.deviceContext && deviceId === void 0) return /* @__PURE__ */ jsx(WidgetMissingError, {
17017
- widgetId,
17018
- reason: "missing-device-context"
17019
- });
17020
- if (metadata.requires.integrationContext && integrationId === void 0) return /* @__PURE__ */ jsx(WidgetMissingError, {
17021
- widgetId,
17022
- reason: "missing-integration-context"
17023
- });
17165
+ var DIRECTION_VECTORS = {
17166
+ "up": {
17167
+ pan: 0,
17168
+ tilt: 1
17169
+ },
17170
+ "down": {
17171
+ pan: 0,
17172
+ tilt: -1
17173
+ },
17174
+ "left": {
17175
+ pan: -1,
17176
+ tilt: 0
17177
+ },
17178
+ "right": {
17179
+ pan: 1,
17180
+ tilt: 0
17181
+ },
17182
+ "up-left": {
17183
+ pan: -1,
17184
+ tilt: 1
17185
+ },
17186
+ "up-right": {
17187
+ pan: 1,
17188
+ tilt: 1
17189
+ },
17190
+ "down-left": {
17191
+ pan: -1,
17192
+ tilt: -1
17193
+ },
17194
+ "down-right": {
17195
+ pan: 1,
17196
+ tilt: -1
17024
17197
  }
17025
- return /* @__PURE__ */ jsx(Component, {
17026
- instanceId: resolvedInstanceId,
17027
- host,
17028
- config,
17029
- deviceId,
17030
- integrationId,
17031
- size,
17032
- columns,
17033
- rows
17034
- });
17035
- }
17036
- function WidgetSkeleton() {
17037
- return /* @__PURE__ */ jsxs("div", {
17038
- className: "rounded-lg border border-border bg-surface/40 p-4 animate-pulse",
17039
- children: [
17040
- /* @__PURE__ */ jsx("div", { className: "h-3 w-24 bg-foreground-subtle/20 rounded mb-2" }),
17041
- /* @__PURE__ */ jsx("div", { className: "h-2 w-full bg-foreground-subtle/10 rounded mb-1" }),
17042
- /* @__PURE__ */ jsx("div", { className: "h-2 w-3/4 bg-foreground-subtle/10 rounded" })
17043
- ]
17044
- });
17045
- }
17046
- function WidgetMissingError({ widgetId, reason }) {
17047
- return /* @__PURE__ */ jsx("div", {
17048
- className: "rounded-lg border border-warning/30 bg-warning/10 px-3 py-2 text-xs text-warning",
17049
- children: reason === "unknown" ? `Widget "${widgetId}" not registered` : reason === "missing-export" ? `Widget "${widgetId}" bundle does not export this stableId` : reason === "missing-device-context" ? `Widget "${widgetId}" requires a deviceId` : `Widget "${widgetId}" requires an integrationId`
17050
- });
17051
- }
17052
- //#endregion
17053
- //#region src/composites/config-form-field.tsx
17054
- var INPUT_CLASS = "w-full rounded-md border border-border bg-background px-2.5 py-1.5 text-xs text-foreground focus:border-primary focus:ring-1 focus:ring-primary/30 outline-none disabled:opacity-50 disabled:cursor-not-allowed";
17055
- var LABEL_CLASS = "block text-[11px] font-medium text-foreground mb-1";
17056
- var DESC_CLASS = "text-[10px] text-foreground-subtle mt-0.5";
17057
- function FieldWrapper({ label, description, required, span, children, translationFn }) {
17058
- const colSpanClass = span === 2 ? "col-span-2" : span === 3 ? "col-span-3" : span === 4 ? "col-span-4" : "col-span-1";
17059
- const resolvedLabel = resolveLabel(label, translationFn);
17060
- const resolvedDescription = resolveLabel(description, translationFn);
17061
- return /* @__PURE__ */ jsxs("div", {
17062
- className: colSpanClass,
17063
- children: [
17064
- resolvedLabel !== void 0 && resolvedLabel !== "" && /* @__PURE__ */ jsxs("label", {
17065
- className: LABEL_CLASS,
17066
- children: [resolvedLabel, required && /* @__PURE__ */ jsx("span", {
17067
- className: "text-danger ml-0.5",
17068
- children: "*"
17069
- })]
17070
- }),
17071
- children,
17072
- resolvedDescription && /* @__PURE__ */ jsx("p", {
17073
- className: DESC_CLASS,
17074
- children: resolvedDescription
17075
- })
17076
- ]
17077
- });
17078
- }
17079
- function TextField({ field, value, onChange, disabled, translationFn }) {
17080
- return /* @__PURE__ */ jsx(FieldWrapper, {
17081
- label: field.label,
17082
- description: field.description,
17083
- required: field.required,
17084
- span: field.span,
17085
- translationFn,
17086
- children: /* @__PURE__ */ jsx("input", {
17087
- type: field.inputType ?? "text",
17088
- className: INPUT_CLASS,
17089
- value: value === void 0 || value === null ? "" : String(value),
17090
- placeholder: field.placeholder,
17091
- maxLength: field.maxLength,
17092
- pattern: field.pattern,
17093
- disabled: disabled || field.disabled,
17094
- onChange: (e) => onChange(e.target.value)
17095
- })
17096
- });
17097
- }
17098
- function NumberField({ field, value, onChange, disabled, translationFn }) {
17099
- const [local, setLocal] = useState(value === void 0 || value === null ? "" : String(value));
17100
- const focusedRef = useRef(false);
17198
+ };
17199
+ function usePTZ(trpc, deviceId, hookOptions) {
17200
+ const defaultSpeed = hookOptions?.defaultSpeed ?? .5;
17201
+ const pulseMs = hookOptions?.pulseMs ?? 250;
17202
+ const enabled = hookOptions?.enabled ?? true;
17203
+ const ptz = useDeviceProxy(trpc, enabled ? deviceId : null)?.ptz;
17204
+ const [presets, setPresets] = useState([]);
17205
+ const [options, setOptions] = useState(null);
17206
+ const [busy, setBusy] = useState(false);
17207
+ const [error, setError] = useState(null);
17208
+ const isAbsentProvider = (err) => {
17209
+ const msg = err instanceof Error ? err.message : String(err);
17210
+ return msg.includes("provider not available") || msg.includes("no 'ptz' binding");
17211
+ };
17212
+ const refreshPresets = useCallback(async () => {
17213
+ if (!enabled || !ptz) return;
17214
+ try {
17215
+ setPresets(await ptz.getPresets({}));
17216
+ } catch (err) {
17217
+ if (isAbsentProvider(err)) return;
17218
+ setError(err instanceof Error ? err.message : String(err));
17219
+ }
17220
+ }, [ptz, enabled]);
17221
+ const refreshOptions = useCallback(async () => {
17222
+ if (!enabled || !ptz) return;
17223
+ try {
17224
+ setOptions(await ptz.getOptions({}));
17225
+ } catch (err) {
17226
+ if (isAbsentProvider(err)) return;
17227
+ setError(err instanceof Error ? err.message : String(err));
17228
+ }
17229
+ }, [ptz, enabled]);
17101
17230
  useEffect(() => {
17102
- if (focusedRef.current) return;
17103
- setLocal(value === void 0 || value === null ? "" : String(value));
17104
- }, [value]);
17105
- const handleChange = (raw) => {
17106
- setLocal(raw);
17107
- if (raw === "" || raw === "-") {
17108
- onChange(void 0);
17231
+ refreshPresets();
17232
+ refreshOptions();
17233
+ }, [refreshPresets, refreshOptions]);
17234
+ const wrap = useCallback(async (fn) => {
17235
+ if (!enabled || !ptz) return void 0;
17236
+ setError(null);
17237
+ setBusy(true);
17238
+ try {
17239
+ return await fn();
17240
+ } catch (err) {
17241
+ setError(err instanceof Error ? err.message : String(err));
17109
17242
  return;
17243
+ } finally {
17244
+ setBusy(false);
17110
17245
  }
17111
- const parsed = Number(raw);
17112
- if (Number.isNaN(parsed)) return;
17113
- onChange(parsed);
17114
- };
17115
- return /* @__PURE__ */ jsx(FieldWrapper, {
17116
- label: field.label,
17117
- description: field.description,
17118
- required: field.required,
17119
- span: field.span,
17120
- translationFn,
17121
- children: /* @__PURE__ */ jsxs("div", {
17122
- className: "flex items-center gap-1",
17123
- children: [/* @__PURE__ */ jsx("input", {
17124
- type: "number",
17125
- className: INPUT_CLASS,
17126
- value: local,
17127
- placeholder: field.placeholder,
17128
- min: field.min,
17129
- max: field.max,
17130
- step: field.step,
17131
- disabled: disabled || field.disabled,
17132
- onFocus: () => {
17133
- focusedRef.current = true;
17134
- },
17135
- onBlur: () => {
17136
- focusedRef.current = false;
17137
- },
17138
- onChange: (e) => handleChange(e.target.value)
17139
- }), field.unit && /* @__PURE__ */ jsx("span", {
17140
- className: "text-xs text-foreground-subtle whitespace-nowrap",
17246
+ }, [enabled, ptz]);
17247
+ return {
17248
+ move: useCallback(async (direction, speed) => {
17249
+ if (!ptz) return;
17250
+ const v = DIRECTION_VECTORS[direction];
17251
+ const s = speed ?? defaultSpeed;
17252
+ await wrap(async () => {
17253
+ await ptz.move({
17254
+ pan: v.pan,
17255
+ tilt: v.tilt,
17256
+ speed: s
17257
+ });
17258
+ await new Promise((r) => setTimeout(r, pulseMs));
17259
+ await ptz.stop({});
17260
+ });
17261
+ }, [
17262
+ ptz,
17263
+ defaultSpeed,
17264
+ pulseMs,
17265
+ wrap
17266
+ ]),
17267
+ startContinuous: useCallback(async (direction, speed) => {
17268
+ if (!ptz) return;
17269
+ const v = DIRECTION_VECTORS[direction];
17270
+ const s = speed ?? defaultSpeed;
17271
+ await wrap(() => ptz.continuousMove({
17272
+ pan: v.pan,
17273
+ tilt: v.tilt,
17274
+ speed: s
17275
+ }));
17276
+ }, [
17277
+ ptz,
17278
+ defaultSpeed,
17279
+ wrap
17280
+ ]),
17281
+ stopContinuous: useCallback(async () => {
17282
+ if (!ptz) return;
17283
+ await wrap(() => ptz.stop({}));
17284
+ }, [ptz, wrap]),
17285
+ zoom: useCallback(async (direction, speed) => {
17286
+ if (!ptz) return;
17287
+ const z = direction === "in" ? 1 : -1;
17288
+ const s = speed ?? defaultSpeed;
17289
+ await wrap(async () => {
17290
+ await ptz.move({
17291
+ zoom: z,
17292
+ speed: s
17293
+ });
17294
+ await new Promise((r) => setTimeout(r, pulseMs));
17295
+ await ptz.stop({});
17296
+ });
17297
+ }, [
17298
+ ptz,
17299
+ defaultSpeed,
17300
+ pulseMs,
17301
+ wrap
17302
+ ]),
17303
+ goHome: useCallback(async () => {
17304
+ if (!ptz) return;
17305
+ await wrap(() => ptz.goHome({}));
17306
+ }, [ptz, wrap]),
17307
+ goToPreset: useCallback(async (presetId) => {
17308
+ if (!ptz) return;
17309
+ await wrap(() => ptz.goToPreset({ presetId }));
17310
+ }, [ptz, wrap]),
17311
+ savePreset: useCallback(async (name) => {
17312
+ if (!ptz) return void 0;
17313
+ return wrap(async () => {
17314
+ const usedIds = new Set(presets.map((p) => Number(p.id)).filter((n) => Number.isInteger(n) && n >= 0));
17315
+ let nextId = 0;
17316
+ while (usedIds.has(nextId)) nextId += 1;
17317
+ const presetId = String(nextId);
17318
+ await ptz.savePreset({
17319
+ presetId,
17320
+ name
17321
+ });
17322
+ await refreshPresets();
17323
+ return presetId;
17324
+ });
17325
+ }, [
17326
+ ptz,
17327
+ presets,
17328
+ refreshPresets,
17329
+ wrap
17330
+ ]),
17331
+ deletePreset: useCallback(async (presetId) => {
17332
+ if (!ptz) return;
17333
+ await wrap(async () => {
17334
+ await ptz.deletePreset({ presetId });
17335
+ await refreshPresets();
17336
+ });
17337
+ }, [
17338
+ ptz,
17339
+ refreshPresets,
17340
+ wrap
17341
+ ]),
17342
+ presets,
17343
+ refreshPresets,
17344
+ options,
17345
+ busy,
17346
+ error
17347
+ };
17348
+ }
17349
+ //#endregion
17350
+ //#region src/composites/ptz-overlay.tsx
17351
+ /**
17352
+ * PTZOverlay — pan / tilt / zoom controls.
17353
+ *
17354
+ * Two visual variants driven by `mode`:
17355
+ * - `'overlay'` (default): translucent dark pill positioned bottom-
17356
+ * right of the camera viewport. Used as `extraOverlay` on the
17357
+ * `CameraStreamPlayer` so operators can drive PTZ without leaving
17358
+ * the live view. Opaque background + subtle ring keeps the d-pad
17359
+ * legible against any frame.
17360
+ * - `'panel'`: full-bleed inside a host container (e.g. the floating
17361
+ * PTZ panel in DeviceDetail). No absolute positioning, no inner
17362
+ * wrapper card — the host's chrome is the only frame. Inherits the
17363
+ * surrounding `bg-surface` so the dark theme reads consistently
17364
+ * instead of the previous always-dark-bubble look.
17365
+ *
17366
+ * Interaction model is identical across modes: short tap fires a
17367
+ * discrete pulse (`move`); long press starts continuous motion until
17368
+ * release (`startContinuous` + `stopContinuous` on pointer up).
17369
+ */
17370
+ function DPadButton({ direction, icon: Icon, disabled, className, variant, onMove, onStart, onStop }) {
17371
+ const [pressedAt, setPressedAt] = useState(null);
17372
+ const [continuous, setContinuous] = useState(false);
17373
+ const handlePointerDown = useCallback((e) => {
17374
+ if (disabled) return;
17375
+ e.currentTarget.setPointerCapture(e.pointerId);
17376
+ setPressedAt(Date.now());
17377
+ setContinuous(false);
17378
+ const timer = setTimeout(() => {
17379
+ setContinuous(true);
17380
+ onStart(direction);
17381
+ }, 250);
17382
+ e.currentTarget.dataset.timer = String(timer);
17383
+ }, [
17384
+ direction,
17385
+ disabled,
17386
+ onStart
17387
+ ]);
17388
+ const handlePointerUp = useCallback((e) => {
17389
+ const timerId = Number(e.currentTarget.dataset.timer);
17390
+ if (timerId) clearTimeout(timerId);
17391
+ e.currentTarget.dataset.timer = "";
17392
+ if (continuous) onStop();
17393
+ else if (pressedAt !== null) onMove(direction);
17394
+ setPressedAt(null);
17395
+ setContinuous(false);
17396
+ }, [
17397
+ continuous,
17398
+ direction,
17399
+ onMove,
17400
+ onStop,
17401
+ pressedAt
17402
+ ]);
17403
+ const handlePointerCancel = useCallback((e) => {
17404
+ const timerId = Number(e.currentTarget.dataset.timer);
17405
+ if (timerId) clearTimeout(timerId);
17406
+ e.currentTarget.dataset.timer = "";
17407
+ if (continuous) onStop();
17408
+ setPressedAt(null);
17409
+ setContinuous(false);
17410
+ }, [continuous, onStop]);
17411
+ const sizeClass = variant === "panel" ? "h-9 w-9" : "h-7 w-7";
17412
+ const iconSizeClass = variant === "panel" ? "h-4 w-4" : "h-3.5 w-3.5";
17413
+ return /* @__PURE__ */ jsx("button", {
17414
+ type: "button",
17415
+ disabled,
17416
+ onPointerDown: handlePointerDown,
17417
+ onPointerUp: handlePointerUp,
17418
+ onPointerCancel: handlePointerCancel,
17419
+ className: cn("flex items-center justify-center rounded transition-colors disabled:opacity-40", variant === "panel" ? "text-foreground hover:bg-surface-hover active:bg-surface-hover/80" : "text-white hover:bg-white/15 active:bg-white/25", sizeClass, className),
17420
+ title: direction,
17421
+ children: /* @__PURE__ */ jsx(Icon, { className: iconSizeClass })
17422
+ });
17423
+ }
17424
+ function PTZOverlay({ controls, mode = "overlay", showPresets, showZoom = true, showHome = true, className }) {
17425
+ const { move, startContinuous, stopContinuous, zoom, goHome, goToPreset, savePreset, deletePreset, presets, options, busy, error } = controls;
17426
+ const confirm = useConfirm();
17427
+ const [presetsOpen, setPresetsOpen] = useState(false);
17428
+ const [presetName, setPresetName] = useState("");
17429
+ const presetsVisible = (showPresets ?? presets.length > 0) && presets.length > 0;
17430
+ const isPanel = mode === "panel";
17431
+ const presetManagementVisible = isPanel && (options?.supportsPresets ?? false);
17432
+ const maxPresetsReached = options?.maxPresets !== void 0 && presets.length >= options.maxPresets;
17433
+ const handleSavePreset = useCallback(async () => {
17434
+ const name = presetName.trim();
17435
+ if (!name || busy || maxPresetsReached) return;
17436
+ if (await savePreset(name) !== void 0) setPresetName("");
17437
+ }, [
17438
+ presetName,
17439
+ busy,
17440
+ maxPresetsReached,
17441
+ savePreset
17442
+ ]);
17443
+ const handleDeletePreset = useCallback(async (presetId, label) => {
17444
+ if (!await confirm({
17445
+ title: "Delete preset",
17446
+ message: `Delete PTZ preset "${label}"? This removes it from the camera.`,
17447
+ confirmLabel: "Delete",
17448
+ variant: "danger"
17449
+ })) return;
17450
+ await deletePreset(presetId);
17451
+ }, [confirm, deletePreset]);
17452
+ const containerClass = isPanel ? "flex flex-col items-stretch gap-2 w-full h-full p-3" : "pointer-events-auto absolute top-3 right-3 z-10 flex flex-col items-end gap-2";
17453
+ const rowClass = isPanel ? cn("flex items-center gap-3 rounded-lg p-2", busy && "ring-1 ring-primary/40") : cn("flex items-center gap-2 rounded-lg border border-white/30 bg-zinc-900/90 backdrop-blur-md p-2 shadow-lg shadow-black/40", busy && "ring-1 ring-primary/40");
17454
+ const sideButtonSize = isPanel ? "h-9 w-9" : "h-7 w-7";
17455
+ const sideIconSize = isPanel ? "h-4 w-4" : "h-3.5 w-3.5";
17456
+ const sideButtonHover = isPanel ? "text-foreground hover:bg-surface-hover" : "text-white hover:bg-white/15";
17457
+ const sepClass = isPanel ? "h-12 w-px bg-border mx-1" : "h-12 w-px bg-white/15 mx-1";
17458
+ return /* @__PURE__ */ jsxs("div", {
17459
+ className: cn(containerClass, className),
17460
+ children: [
17461
+ error && /* @__PURE__ */ jsxs("div", {
17462
+ className: "rounded bg-danger/90 px-2 py-1 text-[10px] font-medium text-white shadow-lg max-w-[200px] self-center",
17463
+ children: ["PTZ: ", error]
17464
+ }),
17465
+ /* @__PURE__ */ jsxs("div", {
17466
+ className: cn(rowClass, isPanel && "justify-center"),
17467
+ children: [
17468
+ /* @__PURE__ */ jsxs("div", {
17469
+ className: "grid grid-cols-3 gap-0.5",
17470
+ children: [
17471
+ /* @__PURE__ */ jsx("span", {
17472
+ "aria-hidden": true,
17473
+ className: isPanel ? "h-9 w-9" : "h-7 w-7"
17474
+ }),
17475
+ /* @__PURE__ */ jsx(DPadButton, {
17476
+ direction: "up",
17477
+ icon: ArrowUp,
17478
+ variant: mode,
17479
+ onMove: move,
17480
+ onStart: startContinuous,
17481
+ onStop: stopContinuous
17482
+ }),
17483
+ /* @__PURE__ */ jsx("span", {
17484
+ "aria-hidden": true,
17485
+ className: isPanel ? "h-9 w-9" : "h-7 w-7"
17486
+ }),
17487
+ /* @__PURE__ */ jsx(DPadButton, {
17488
+ direction: "left",
17489
+ icon: ArrowLeft,
17490
+ variant: mode,
17491
+ onMove: move,
17492
+ onStart: startContinuous,
17493
+ onStop: stopContinuous
17494
+ }),
17495
+ /* @__PURE__ */ jsx("span", {
17496
+ "aria-hidden": true,
17497
+ className: isPanel ? "h-9 w-9" : "h-7 w-7"
17498
+ }),
17499
+ /* @__PURE__ */ jsx(DPadButton, {
17500
+ direction: "right",
17501
+ icon: ArrowRight,
17502
+ variant: mode,
17503
+ onMove: move,
17504
+ onStart: startContinuous,
17505
+ onStop: stopContinuous
17506
+ }),
17507
+ /* @__PURE__ */ jsx("span", {
17508
+ "aria-hidden": true,
17509
+ className: isPanel ? "h-9 w-9" : "h-7 w-7"
17510
+ }),
17511
+ /* @__PURE__ */ jsx(DPadButton, {
17512
+ direction: "down",
17513
+ icon: ArrowDown,
17514
+ variant: mode,
17515
+ onMove: move,
17516
+ onStart: startContinuous,
17517
+ onStop: stopContinuous
17518
+ }),
17519
+ /* @__PURE__ */ jsx("span", {
17520
+ "aria-hidden": true,
17521
+ className: isPanel ? "h-9 w-9" : "h-7 w-7"
17522
+ })
17523
+ ]
17524
+ }),
17525
+ (showZoom || showHome) && /* @__PURE__ */ jsx("div", { className: sepClass }),
17526
+ /* @__PURE__ */ jsxs("div", {
17527
+ className: "flex flex-col gap-0.5",
17528
+ children: [showZoom && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("button", {
17529
+ type: "button",
17530
+ onClick: () => zoom("in"),
17531
+ disabled: busy,
17532
+ title: "Zoom in",
17533
+ className: cn("flex items-center justify-center rounded disabled:opacity-40", sideButtonSize, sideButtonHover),
17534
+ children: /* @__PURE__ */ jsx(ZoomIn, { className: sideIconSize })
17535
+ }), /* @__PURE__ */ jsx("button", {
17536
+ type: "button",
17537
+ onClick: () => zoom("out"),
17538
+ disabled: busy,
17539
+ title: "Zoom out",
17540
+ className: cn("flex items-center justify-center rounded disabled:opacity-40", sideButtonSize, sideButtonHover),
17541
+ children: /* @__PURE__ */ jsx(ZoomOut, { className: sideIconSize })
17542
+ })] }), showHome && /* @__PURE__ */ jsx("button", {
17543
+ type: "button",
17544
+ onClick: () => goHome(),
17545
+ disabled: busy,
17546
+ title: "Go home",
17547
+ className: cn("flex items-center justify-center rounded disabled:opacity-40", sideButtonSize, sideButtonHover),
17548
+ children: /* @__PURE__ */ jsx(House, { className: sideIconSize })
17549
+ })]
17550
+ }),
17551
+ presetsVisible && /* @__PURE__ */ jsxs("div", {
17552
+ className: "relative",
17553
+ children: [/* @__PURE__ */ jsxs("button", {
17554
+ type: "button",
17555
+ onClick: () => setPresetsOpen((v) => !v),
17556
+ disabled: busy,
17557
+ className: cn("flex items-center gap-1 rounded px-2 text-[10px] disabled:opacity-40", isPanel ? "h-9 text-foreground hover:bg-surface-hover" : "h-7 text-white hover:bg-white/15"),
17558
+ title: "Presets",
17559
+ children: ["Presets", /* @__PURE__ */ jsx(ChevronDown, { className: cn("h-3 w-3 transition-transform", presetsOpen && "rotate-180") })]
17560
+ }), presetsOpen && /* @__PURE__ */ jsx("div", {
17561
+ className: cn("absolute right-0 top-full mt-1 min-w-[140px] rounded-lg shadow-xl py-1 z-20", isPanel ? "bg-surface border border-border" : "border border-white/30 bg-zinc-900/95 backdrop-blur-md"),
17562
+ children: presets.map((p) => /* @__PURE__ */ jsxs("div", {
17563
+ className: cn("group flex items-center", isPanel ? "hover:bg-surface-hover" : "hover:bg-white/15"),
17564
+ children: [/* @__PURE__ */ jsx("button", {
17565
+ type: "button",
17566
+ onClick: () => {
17567
+ goToPreset(p.id);
17568
+ setPresetsOpen(false);
17569
+ },
17570
+ disabled: busy,
17571
+ className: cn("flex-1 px-3 py-1.5 text-left text-[10px] disabled:opacity-40", isPanel ? "text-foreground" : "text-white"),
17572
+ children: p.name || p.id
17573
+ }), presetManagementVisible && /* @__PURE__ */ jsx("button", {
17574
+ type: "button",
17575
+ onClick: () => void handleDeletePreset(p.id, p.name || p.id),
17576
+ disabled: busy,
17577
+ title: `Delete preset ${p.name || p.id}`,
17578
+ className: cn("mr-1 flex h-5 w-5 shrink-0 items-center justify-center rounded", "text-foreground-subtle hover:bg-danger/15 hover:text-danger disabled:opacity-40"),
17579
+ children: /* @__PURE__ */ jsx(X, { className: "h-3 w-3" })
17580
+ })]
17581
+ }, p.id))
17582
+ })]
17583
+ })
17584
+ ]
17585
+ }),
17586
+ presetManagementVisible && /* @__PURE__ */ jsxs("div", {
17587
+ className: "flex flex-col gap-1 rounded-lg border border-border p-2",
17588
+ children: [/* @__PURE__ */ jsxs("div", {
17589
+ className: "flex items-center gap-2",
17590
+ children: [/* @__PURE__ */ jsx("input", {
17591
+ type: "text",
17592
+ value: presetName,
17593
+ onChange: (e) => setPresetName(e.target.value),
17594
+ onKeyDown: (e) => {
17595
+ if (e.key === "Enter") handleSavePreset();
17596
+ },
17597
+ disabled: busy || maxPresetsReached,
17598
+ placeholder: "New preset name",
17599
+ maxLength: 64,
17600
+ className: cn("min-w-0 flex-1 rounded border border-border bg-surface px-2 py-1 text-[11px] text-foreground", "placeholder:text-foreground-subtle focus:outline-none focus:ring-1 focus:ring-primary/50", "disabled:opacity-40")
17601
+ }), /* @__PURE__ */ jsxs("button", {
17602
+ type: "button",
17603
+ onClick: () => void handleSavePreset(),
17604
+ disabled: busy || maxPresetsReached || presetName.trim().length === 0,
17605
+ title: maxPresetsReached ? `Camera preset limit reached (${options?.maxPresets})` : "Save current position as a new preset",
17606
+ className: cn("flex h-7 shrink-0 items-center gap-1 rounded px-2 text-[10px] font-medium", "bg-primary/15 text-primary hover:bg-primary/25 disabled:opacity-40"),
17607
+ children: [/* @__PURE__ */ jsx(Plus, { className: "h-3 w-3" }), "Save current position"]
17608
+ })]
17609
+ }), maxPresetsReached && /* @__PURE__ */ jsxs("span", {
17610
+ className: "text-[10px] text-foreground-subtle",
17611
+ children: [
17612
+ "Camera preset limit reached (",
17613
+ options?.maxPresets,
17614
+ "). Delete a preset to add a new one."
17615
+ ]
17616
+ })]
17617
+ })
17618
+ ]
17619
+ });
17620
+ }
17621
+ //#endregion
17622
+ //#region src/composites/cap-settings/PtzPanel.tsx
17623
+ /**
17624
+ * PtzPanel — live pan/tilt/zoom controls for the `ptz` capability. A
17625
+ * cap-settings component (ui-library), mounted as a cap-contributed
17626
+ * top-level device-detail tab via the cap-UI contribution mechanism
17627
+ * (the `ptz` cap declares `ui: { tab: 'ptz', kind: 'static', ... }`).
17628
+ *
17629
+ * Consolidates the former admin-ui `PTZPanelContent`: `usePTZ` drives
17630
+ * the controls, `PTZOverlay` renders them (`mode='panel'`), and an
17631
+ * Autofocus toggle is shown only when `getOptions().hasAutofocus`.
17632
+ * Autotrack is its own cap-UI contribution (`ptz-autotrack`), mounted
17633
+ * independently by the contribution mechanism — not nested here.
17634
+ */
17635
+ /** Autofocus toggle — only mounted when `hasAutofocus`. Reads the cap's
17636
+ * `getStatus().autofocus` and drives `setAutofocus`. */
17637
+ function AutofocusToggle({ deviceId }) {
17638
+ const dev = useDeviceProxy(useSystem().trpcClient, deviceId);
17639
+ const [enabled, setEnabled] = useState(null);
17640
+ const [busy, setBusy] = useState(false);
17641
+ useEffect(() => {
17642
+ if (!dev) return void 0;
17643
+ let cancelled = false;
17644
+ (async () => {
17645
+ try {
17646
+ const status = await dev.ptz?.getStatus({});
17647
+ if (cancelled || !status) return;
17648
+ setEnabled(status.autofocus);
17649
+ } catch {}
17650
+ })();
17651
+ return () => {
17652
+ cancelled = true;
17653
+ };
17654
+ }, [dev]);
17655
+ const toggle = useCallback(async () => {
17656
+ if (!dev?.ptz || enabled === null) return;
17657
+ setBusy(true);
17658
+ try {
17659
+ const next = !enabled;
17660
+ await dev.ptz.setAutofocus({ enabled: next });
17661
+ setEnabled(next);
17662
+ } catch (err) {
17663
+ console.error("ptz.setAutofocus failed", err);
17664
+ } finally {
17665
+ setBusy(false);
17666
+ }
17667
+ }, [dev, enabled]);
17668
+ if (enabled === null) return null;
17669
+ return /* @__PURE__ */ jsxs("div", {
17670
+ className: "flex items-center justify-between border-t border-border/40 mt-2 pt-2",
17671
+ children: [/* @__PURE__ */ jsx("span", {
17672
+ className: "text-[10.5px] font-semibold text-foreground",
17673
+ children: "Autofocus"
17674
+ }), /* @__PURE__ */ jsxs("button", {
17675
+ type: "button",
17676
+ onClick: () => void toggle(),
17677
+ disabled: busy,
17678
+ "aria-pressed": enabled,
17679
+ className: `flex items-center gap-1.5 rounded-md px-2 py-1 text-[10px] font-medium transition-colors ${enabled ? "bg-success/15 text-success hover:bg-success/25" : "bg-surface-elevated/40 text-foreground-subtle hover:bg-surface-hover hover:text-foreground"} disabled:opacity-50`,
17680
+ children: [/* @__PURE__ */ jsx("span", { className: `h-1.5 w-1.5 rounded-full ${enabled ? "bg-success" : "bg-foreground-subtle/40"}` }), enabled ? "On" : "Off"]
17681
+ })]
17682
+ });
17683
+ }
17684
+ function PtzPanel({ deviceId }) {
17685
+ const system = useSystem();
17686
+ const ready = Number.isFinite(deviceId);
17687
+ const controls = usePTZ(system.trpcClient, ready ? deviceId : 0, { enabled: ready });
17688
+ if (!ready) return /* @__PURE__ */ jsxs("div", {
17689
+ className: "flex items-center justify-center h-full text-[11px] text-foreground-subtle italic",
17690
+ children: [
17691
+ "Device #",
17692
+ deviceId,
17693
+ " not loaded."
17694
+ ]
17695
+ });
17696
+ const hasAutofocus = controls.options?.hasAutofocus === true;
17697
+ return /* @__PURE__ */ jsxs("div", {
17698
+ className: "flex flex-col h-full overflow-y-auto",
17699
+ children: [
17700
+ /* @__PURE__ */ jsx("h3", {
17701
+ className: "text-[11px] font-semibold text-foreground-subtle uppercase tracking-wider mb-2",
17702
+ children: "PTZ"
17703
+ }),
17704
+ /* @__PURE__ */ jsx(PTZOverlay, {
17705
+ controls,
17706
+ mode: "panel"
17707
+ }),
17708
+ hasAutofocus && /* @__PURE__ */ jsx(AutofocusToggle, { deviceId })
17709
+ ]
17710
+ });
17711
+ }
17712
+ //#endregion
17713
+ //#region src/hooks/use-device-autotrack.ts
17714
+ /**
17715
+ * `useDeviceAutotrack` — typed wrapper around the `ptz-autotrack` cap.
17716
+ *
17717
+ * Surface:
17718
+ * - `status` — current `{enabled, lastChangedAt, currentSettings}`,
17719
+ * polled at 5s intervals while the hook is mounted.
17720
+ * - `setEnabled(on)` — flip the on/off state.
17721
+ * - `setSettings(partial)` — patch one or more settings keys.
17722
+ * - `isPending` — true while a mutation is in flight.
17723
+ *
17724
+ * Returns `null` for `status` when the cap isn't available (device
17725
+ * doesn't support autotrack) — callers gate render on this OR on the
17726
+ * device's `PtzAutotrack` feature flag.
17727
+ */
17728
+ function useDeviceAutotrack(deviceId) {
17729
+ const queryClient = useQueryClient();
17730
+ const statusQuery = usePtzAutotrackGetStatus({ deviceId: deviceId ?? 0 }, {
17731
+ enabled: deviceId !== null && Number.isFinite(deviceId),
17732
+ refetchInterval: 5e3,
17733
+ retry: 1
17734
+ });
17735
+ const setEnabledMutation = usePtzAutotrackSetEnabled({ onSuccess: () => {
17736
+ queryClient.invalidateQueries({ queryKey: [["ptzAutotrack"]] });
17737
+ } });
17738
+ const setSettingsMutation = usePtzAutotrackSetSettings({ onSuccess: () => {
17739
+ queryClient.invalidateQueries({ queryKey: [["ptzAutotrack"]] });
17740
+ } });
17741
+ const setEnabled = useCallback(async (on) => {
17742
+ if (deviceId === null) return;
17743
+ await setEnabledMutation.mutateAsync({
17744
+ deviceId,
17745
+ enabled: on
17746
+ });
17747
+ }, [deviceId, setEnabledMutation]);
17748
+ const setSettings = useCallback(async (patch) => {
17749
+ if (deviceId === null) return;
17750
+ await setSettingsMutation.mutateAsync({
17751
+ deviceId,
17752
+ settings: patch
17753
+ });
17754
+ }, [deviceId, setSettingsMutation]);
17755
+ const errorMsg = (() => {
17756
+ if (statusQuery.error) return statusQuery.error.message;
17757
+ if (setEnabledMutation.error) return setEnabledMutation.error.message;
17758
+ if (setSettingsMutation.error) return setSettingsMutation.error.message;
17759
+ return null;
17760
+ })();
17761
+ return {
17762
+ status: statusQuery.data ?? null,
17763
+ isLoading: statusQuery.isLoading,
17764
+ isPending: setEnabledMutation.isPending || setSettingsMutation.isPending,
17765
+ error: errorMsg,
17766
+ setEnabled,
17767
+ setSettings
17768
+ };
17769
+ }
17770
+ //#endregion
17771
+ //#region src/composites/cap-settings/AutotrackSection.tsx
17772
+ /**
17773
+ * Autotrack settings card — rendered inside the PTZ panel when the
17774
+ * device has `DeviceFeature.PtzAutotrack`. Three knobs (cross-vendor
17775
+ * schema): target type, stop delay, disappear delay, plus an enable /
17776
+ * disable toggle that drives the `ptz-autotrack` cap directly.
17777
+ *
17778
+ * Save semantics:
17779
+ * - Toggle (enable/disable) calls `setEnabled` immediately.
17780
+ * - Form fields debounce-save on blur — no explicit Save button.
17781
+ *
17782
+ * Vendor capability hints:
17783
+ * - The cap's status carries `currentSettings` from the camera's
17784
+ * own GET; rendered on every poll so the operator sees what's
17785
+ * really applied vs what they typed.
17786
+ * - When the firmware ignored a setting, the field stays editable.
17787
+ */
17788
+ function AutotrackSection({ deviceId }) {
17789
+ const { status, isLoading, isPending, error, setEnabled, setSettings } = useDeviceAutotrack(Number.isFinite(deviceId) ? deviceId : null);
17790
+ const [draft, setDraft] = useState({
17791
+ targetType: "",
17792
+ stopDelaySeconds: 30,
17793
+ disappearDelaySeconds: 15
17794
+ });
17795
+ const [editing, setEditing] = useState(false);
17796
+ useEffect(() => {
17797
+ if (editing) return;
17798
+ const s = status?.currentSettings;
17799
+ if (!s) return;
17800
+ setDraft({
17801
+ targetType: s.targetType,
17802
+ stopDelaySeconds: s.stopDelaySeconds,
17803
+ disappearDelaySeconds: s.disappearDelaySeconds
17804
+ });
17805
+ }, [status, editing]);
17806
+ if (isLoading && !status) return /* @__PURE__ */ jsxs("div", {
17807
+ className: "flex items-center gap-2 px-3 py-2 text-[10.5px] text-foreground-subtle",
17808
+ children: [/* @__PURE__ */ jsx(LoaderCircle, { className: "h-3 w-3 animate-spin" }), "Loading autotrack…"]
17809
+ });
17810
+ return /* @__PURE__ */ jsxs("div", {
17811
+ className: "border-t border-border/40 mt-2 pt-2 px-1 space-y-2",
17812
+ children: [
17813
+ /* @__PURE__ */ jsxs("div", {
17814
+ className: "flex items-center justify-between px-2",
17815
+ children: [/* @__PURE__ */ jsxs("h3", {
17816
+ className: "flex items-center gap-1.5 text-[11px] font-semibold text-foreground-subtle uppercase tracking-wider",
17817
+ children: [/* @__PURE__ */ jsx(Crosshair, { className: "h-3 w-3 text-primary" }), "Autotrack"]
17818
+ }), /* @__PURE__ */ jsxs("button", {
17819
+ type: "button",
17820
+ onClick: () => {
17821
+ setEnabled(!(status?.enabled ?? false));
17822
+ },
17823
+ disabled: isPending,
17824
+ className: `flex items-center gap-1.5 rounded-md px-2 py-1 text-[10px] font-medium transition-colors ${status?.enabled ? "bg-success/15 text-success hover:bg-success/25" : "bg-surface-elevated/40 text-foreground-subtle hover:bg-surface-hover hover:text-foreground"} disabled:opacity-50`,
17825
+ "aria-pressed": status?.enabled ?? false,
17826
+ children: [/* @__PURE__ */ jsx("span", { className: `h-1.5 w-1.5 rounded-full ${status?.enabled ? "bg-success" : "bg-foreground-subtle/40"}` }), status?.enabled ? "Enabled" : "Disabled"]
17827
+ })]
17828
+ }),
17829
+ error && /* @__PURE__ */ jsxs("div", {
17830
+ className: "flex items-start gap-1.5 rounded-md border border-danger/30 bg-danger/5 px-2 py-1.5 text-[10px] text-danger",
17831
+ children: [/* @__PURE__ */ jsx(CircleAlert, { className: "h-3 w-3 flex-shrink-0 mt-0.5" }), /* @__PURE__ */ jsx("span", {
17832
+ className: "leading-snug break-words",
17833
+ children: error
17834
+ })]
17835
+ }),
17836
+ /* @__PURE__ */ jsxs("div", {
17837
+ className: "grid grid-cols-2 gap-2 px-2",
17838
+ children: [
17839
+ (status?.supportedTargetTypes?.length ?? 0) > 0 && /* @__PURE__ */ jsxs("label", {
17840
+ className: "col-span-2 flex flex-col gap-1",
17841
+ children: [/* @__PURE__ */ jsx("span", {
17842
+ className: "text-[10px] font-medium text-foreground-subtle",
17843
+ children: "Target type"
17844
+ }), /* @__PURE__ */ jsx("select", {
17845
+ value: draft.targetType,
17846
+ onFocus: () => setEditing(true),
17847
+ onChange: (e) => setDraft((d) => ({
17848
+ ...d,
17849
+ targetType: e.target.value
17850
+ })),
17851
+ onBlur: () => {
17852
+ setEditing(false);
17853
+ const current = status?.currentSettings?.targetType ?? "";
17854
+ if (draft.targetType !== current) setSettings({ targetType: draft.targetType });
17855
+ },
17856
+ disabled: isPending,
17857
+ className: "rounded-md border border-border bg-surface px-2 py-1 text-[11px] text-foreground focus:border-primary focus:outline-none disabled:opacity-50",
17858
+ children: (status?.supportedTargetTypes ?? []).map((opt) => /* @__PURE__ */ jsx("option", {
17859
+ value: opt.value,
17860
+ children: opt.label
17861
+ }, opt.value))
17862
+ })]
17863
+ }),
17864
+ /* @__PURE__ */ jsxs("label", {
17865
+ className: "flex flex-col gap-1",
17866
+ children: [/* @__PURE__ */ jsx("span", {
17867
+ className: "text-[10px] font-medium text-foreground-subtle",
17868
+ children: "Stop delay (s)"
17869
+ }), /* @__PURE__ */ jsx("input", {
17870
+ type: "number",
17871
+ min: 0,
17872
+ max: 300,
17873
+ value: draft.stopDelaySeconds,
17874
+ onFocus: () => setEditing(true),
17875
+ onChange: (e) => setDraft((d) => ({
17876
+ ...d,
17877
+ stopDelaySeconds: Number(e.target.value) || 0
17878
+ })),
17879
+ onBlur: () => {
17880
+ setEditing(false);
17881
+ const current = status?.currentSettings?.stopDelaySeconds ?? 30;
17882
+ if (draft.stopDelaySeconds !== current) setSettings({ stopDelaySeconds: draft.stopDelaySeconds });
17883
+ },
17884
+ disabled: isPending,
17885
+ className: "rounded-md border border-border bg-surface px-2 py-1 text-[11px] text-foreground focus:border-primary focus:outline-none disabled:opacity-50"
17886
+ })]
17887
+ }),
17888
+ /* @__PURE__ */ jsxs("label", {
17889
+ className: "flex flex-col gap-1",
17890
+ children: [/* @__PURE__ */ jsx("span", {
17891
+ className: "text-[10px] font-medium text-foreground-subtle",
17892
+ children: "Disappear delay (s)"
17893
+ }), /* @__PURE__ */ jsx("input", {
17894
+ type: "number",
17895
+ min: 0,
17896
+ max: 300,
17897
+ value: draft.disappearDelaySeconds,
17898
+ onFocus: () => setEditing(true),
17899
+ onChange: (e) => setDraft((d) => ({
17900
+ ...d,
17901
+ disappearDelaySeconds: Number(e.target.value) || 0
17902
+ })),
17903
+ onBlur: () => {
17904
+ setEditing(false);
17905
+ const current = status?.currentSettings?.disappearDelaySeconds ?? 15;
17906
+ if (draft.disappearDelaySeconds !== current) setSettings({ disappearDelaySeconds: draft.disappearDelaySeconds });
17907
+ },
17908
+ disabled: isPending,
17909
+ className: "rounded-md border border-border bg-surface px-2 py-1 text-[11px] text-foreground focus:border-primary focus:outline-none disabled:opacity-50"
17910
+ })]
17911
+ })
17912
+ ]
17913
+ }),
17914
+ /* @__PURE__ */ jsx("p", {
17915
+ className: "px-2 text-[9.5px] text-foreground-subtle leading-snug italic",
17916
+ children: "Settings save on blur. Vendor support varies — Reolink honours all three; Hikvision honours target type + stop delay only (disappear delay is accepted but ignored at firmware push)."
17917
+ })
17918
+ ]
17919
+ });
17920
+ }
17921
+ //#endregion
17922
+ //#region src/contexts/player-overlays.tsx
17923
+ /**
17924
+ * Player overlay registry — pluggable layers + toolbar buttons for
17925
+ * the device-detail live-frame `StreamPanel`.
17926
+ *
17927
+ * Why a registry: previously `StreamPanelContent` hardcoded each
17928
+ * overlay (PTZ, intercom toggle, zone editor) and threaded the
17929
+ * editing state by hand. Adding a new feature (audio waveform,
17930
+ * detection bbox tracker, recording controls, …) meant another
17931
+ * round of plumbing through the host file and StreamPanel's prop
17932
+ * surface. The registry lets a sibling component (Detection tab,
17933
+ * Live Stats tab, an addon-page extension) declare its own layer
17934
+ * imperatively via a hook, and the host renders whatever's
17935
+ * registered.
17936
+ *
17937
+ * Two registries live side-by-side:
17938
+ *
17939
+ * 1. **layers** — absolute-positioned React nodes drawn over the
17940
+ * video frame (zones polygon canvas, motion bbox overlay,
17941
+ * audio waveform). Rendered ordered by `order` (lower first
17942
+ * → bottom; higher → top); the last-registered layer wins
17943
+ * ties.
17944
+ *
17945
+ * 2. **toolbar buttons** — controls surfaced in the player's
17946
+ * always-visible toolbar cluster (next to Intercom + Play/Stop).
17947
+ * Each carries an icon, label, controlled `active` flag, and
17948
+ * a click handler. The host (StreamPanel) renders them
17949
+ * uniformly; tone variants stay simple (`'default' | 'primary'`).
17950
+ *
17951
+ * Lifecycle: hooks register their layer / button on mount and
17952
+ * unregister on unmount, so swapping tabs (e.g. leaving Detection)
17953
+ * automatically tears down the registration. Re-registering with
17954
+ * the same `id` replaces the prior entry — meaning a single hook
17955
+ * call can update its overlay/button props on every render without
17956
+ * leaking entries.
17957
+ *
17958
+ * Provider scope: typically wraps the whole device-detail subtree
17959
+ * so the StreamPanel + every tab share the same registry. One
17960
+ * provider per `deviceId`; switching device IDs unmounts the
17961
+ * provider naturally.
17962
+ */
17963
+ var PlayerOverlaysStateContext = createSharedContext("camstack:player-overlays-state", null);
17964
+ var PlayerOverlaysActionsContext = createSharedContext("camstack:player-overlays-actions", null);
17965
+ function PlayerOverlaysProvider({ children }) {
17966
+ const [layers, setLayers] = useState(() => /* @__PURE__ */ new Map());
17967
+ const [buttons, setButtons] = useState(() => /* @__PURE__ */ new Map());
17968
+ const setLayer = useCallback((layer) => {
17969
+ setLayers((prev) => {
17970
+ const next = new Map(prev);
17971
+ next.set(layer.id, layer);
17972
+ return next;
17973
+ });
17974
+ }, []);
17975
+ const removeLayer = useCallback((id) => {
17976
+ setLayers((prev) => {
17977
+ if (!prev.has(id)) return prev;
17978
+ const next = new Map(prev);
17979
+ next.delete(id);
17980
+ return next;
17981
+ });
17982
+ }, []);
17983
+ const setButton = useCallback((button) => {
17984
+ setButtons((prev) => {
17985
+ const next = new Map(prev);
17986
+ next.set(button.id, button);
17987
+ return next;
17988
+ });
17989
+ }, []);
17990
+ const removeButton = useCallback((id) => {
17991
+ setButtons((prev) => {
17992
+ if (!prev.has(id)) return prev;
17993
+ const next = new Map(prev);
17994
+ next.delete(id);
17995
+ return next;
17996
+ });
17997
+ }, []);
17998
+ const stateValue = useMemo(() => ({
17999
+ layers,
18000
+ buttons
18001
+ }), [layers, buttons]);
18002
+ const actionsValue = useMemo(() => ({
18003
+ setLayer,
18004
+ removeLayer,
18005
+ setButton,
18006
+ removeButton
18007
+ }), [
18008
+ setLayer,
18009
+ removeLayer,
18010
+ setButton,
18011
+ removeButton
18012
+ ]);
18013
+ return /* @__PURE__ */ jsx(PlayerOverlaysStateContext.Provider, {
18014
+ value: stateValue,
18015
+ children: /* @__PURE__ */ jsx(PlayerOverlaysActionsContext.Provider, {
18016
+ value: actionsValue,
18017
+ children
18018
+ })
18019
+ });
18020
+ }
18021
+ /** Snapshot of registered layers ordered by `order` (asc, ties resolved
18022
+ * by insertion order of the underlying Map). Returns `[]` outside a
18023
+ * provider. */
18024
+ function usePlayerOverlayLayers() {
18025
+ const state = useContext(PlayerOverlaysStateContext);
18026
+ return useMemo(() => {
18027
+ if (!state) return [];
18028
+ return [...state.layers.values()].sort((a, b) => a.order - b.order);
18029
+ }, [state]);
18030
+ }
18031
+ /** Snapshot of registered toolbar buttons, ordered by `order` (asc). */
18032
+ function usePlayerToolbarButtons() {
18033
+ const state = useContext(PlayerOverlaysStateContext);
18034
+ return useMemo(() => {
18035
+ if (!state) return [];
18036
+ return [...state.buttons.values()].sort((a, b) => a.order - b.order);
18037
+ }, [state]);
18038
+ }
18039
+ /**
18040
+ * Register an overlay layer for the lifetime of the calling component.
18041
+ * Re-registers on every render with the latest spec; auto-unregisters
18042
+ * on unmount. Pass `null` to skip registration when the layer is
18043
+ * conditionally enabled (the hook still runs every render — keeps
18044
+ * react-hook order stable across spec === null toggles).
18045
+ *
18046
+ * Callers that build the spec inline should memoise it (`useMemo`)
18047
+ * to avoid re-registering on every parent render — context-write
18048
+ * effects depend on referential equality of the spec object.
18049
+ */
18050
+ function usePlayerOverlayLayer(spec) {
18051
+ const actions = useContext(PlayerOverlaysActionsContext);
18052
+ useEffect(() => {
18053
+ if (!actions || !spec) return void 0;
18054
+ actions.setLayer(spec);
18055
+ return () => actions.removeLayer(spec.id);
18056
+ }, [actions, spec]);
18057
+ }
18058
+ /** Same shape as `usePlayerOverlayLayer`, scoped to toolbar buttons. */
18059
+ function usePlayerToolbarButton(spec) {
18060
+ const actions = useContext(PlayerOverlaysActionsContext);
18061
+ useEffect(() => {
18062
+ if (!actions || !spec) return void 0;
18063
+ actions.setButton(spec);
18064
+ return () => actions.removeButton(spec.id);
18065
+ }, [actions, spec]);
18066
+ }
18067
+ //#endregion
18068
+ //#region src/composites/cap-settings/MotionGridCanvas.tsx
18069
+ /**
18070
+ * MotionGridCanvas — the on-frame motion-zone grid editor.
18071
+ *
18072
+ * This component is *only* the editable lattice painted over the live
18073
+ * frame: a plain CSS-grid of `gridWidth × gridHeight` cells. It carries
18074
+ * NO controls — enabled / sensitivity / save / quick-actions all live
18075
+ * in the management bar inside the settings section (`MotionZonesTab`),
18076
+ * so the live frame stays unobstructed.
18077
+ *
18078
+ * Interaction:
18079
+ * - Click a cell → toggle it.
18080
+ * - Press + drag → paint every touched cell to whatever the
18081
+ * first-touched cell flipped TO (the standard grid-mask gesture).
18082
+ *
18083
+ * Purely controlled — every change is forwarded through `onCellsChange`;
18084
+ * the owning `MotionZonesTab` keeps the single source of truth.
18085
+ */
18086
+ function MotionGridCanvas({ options, cells, onCellsChange }) {
18087
+ const { gridWidth, gridHeight } = options;
18088
+ const total = gridWidth * gridHeight;
18089
+ const paintingRef = useRef(false);
18090
+ const paintValueRef = useRef(true);
18091
+ const paintedRef = useRef(/* @__PURE__ */ new Set());
18092
+ const [, forceTick] = useState(0);
18093
+ const applyCell = useCallback((index, value) => {
18094
+ if (index < 0 || index >= total) return;
18095
+ if (cells[index] === value) return;
18096
+ const next = [...cells];
18097
+ while (next.length < total) next.push(false);
18098
+ next.length = total;
18099
+ next[index] = value;
18100
+ onCellsChange(next);
18101
+ }, [
18102
+ cells,
18103
+ total,
18104
+ onCellsChange
18105
+ ]);
18106
+ const handlePointerDown = useCallback((index) => (e) => {
18107
+ e.preventDefault();
18108
+ const nextValue = !cells[index];
18109
+ paintingRef.current = true;
18110
+ paintValueRef.current = nextValue;
18111
+ paintedRef.current = new Set([index]);
18112
+ applyCell(index, nextValue);
18113
+ forceTick((t) => t + 1);
18114
+ }, [cells, applyCell]);
18115
+ const handlePointerEnter = useCallback((index) => () => {
18116
+ if (!paintingRef.current) return;
18117
+ if (paintedRef.current.has(index)) return;
18118
+ paintedRef.current.add(index);
18119
+ applyCell(index, paintValueRef.current);
18120
+ }, [applyCell]);
18121
+ const endPaint = useCallback(() => {
18122
+ if (!paintingRef.current) return;
18123
+ paintingRef.current = false;
18124
+ paintedRef.current = /* @__PURE__ */ new Set();
18125
+ }, []);
18126
+ return /* @__PURE__ */ jsx("div", {
18127
+ className: "absolute inset-0 select-none grid",
18128
+ style: {
18129
+ gridTemplateColumns: `repeat(${String(gridWidth)}, 1fr)`,
18130
+ gridTemplateRows: `repeat(${String(gridHeight)}, 1fr)`
18131
+ },
18132
+ onPointerUp: endPaint,
18133
+ onPointerLeave: endPaint,
18134
+ children: Array.from({ length: total }, (_, i) => {
18135
+ const active = cells[i] === true;
18136
+ return /* @__PURE__ */ jsx("button", {
18137
+ type: "button",
18138
+ "aria-label": `motion cell ${String(i)}`,
18139
+ "aria-pressed": active,
18140
+ onPointerDown: handlePointerDown(i),
18141
+ onPointerEnter: handlePointerEnter(i),
18142
+ className: active ? "border border-primary/70 bg-primary/55 hover:bg-primary/65 transition-colors" : "border border-white/25 bg-white/10 hover:bg-white/20 transition-colors"
18143
+ }, i);
18144
+ })
18145
+ });
18146
+ }
18147
+ //#endregion
18148
+ //#region src/composites/cap-settings/MotionZonesSettings.tsx
18149
+ /**
18150
+ * MotionZonesSettings — management surface for the on-camera
18151
+ * `motion-zones` capability. A cap-settings component (ui-library),
18152
+ * mounted into the device-detail Config tab via the cap-UI
18153
+ * contribution mechanism (the `motion-zones` cap declares a `ui` block).
18154
+ *
18155
+ * Split of concerns (mirrors the analytics zones drawer):
18156
+ * - The editable GRID is painted over the live frame by
18157
+ * `MotionGridCanvas`, registered as a player-overlay layer only
18158
+ * while the operator has toggled "Edit grid" on (OFF by default).
18159
+ * - Every CONTROL lives here, in the settings section, NOT on the
18160
+ * frame: a quick-action bar (All on / All off / Invert), Save /
18161
+ * Revert, and the cell-count status.
18162
+ * - Detection enable + sensitivity are NOT edited here — they are
18163
+ * the camera's on-board motion master switch, owned by the
18164
+ * driver's "On-camera motion" settings section. The grid editor
18165
+ * only paints the mask (`setZone` with a `cells`-only patch;
18166
+ * enable / sensitivity are left untouched camera-side).
18167
+ *
18168
+ * The grid editor draws nothing on cameras without the `motion-zones`
18169
+ * cap — the fetch self-gates on a failed `getOptions`.
18170
+ */
18171
+ /** "Camera doesn't expose the cap" — swallow so the editor stays
18172
+ * hidden on unsupported devices (mirrors `usePTZ`'s gate). */
18173
+ function isAbsentProvider(err) {
18174
+ const msg = err instanceof Error ? err.message : String(err);
18175
+ return msg.includes("provider not available") || msg.includes("motion-zones");
18176
+ }
18177
+ /** Coerce an arbitrary `cells` array to exactly `gridWidth*gridHeight`. */
18178
+ function normaliseCells(cells, options) {
18179
+ const total = options.gridWidth * options.gridHeight;
18180
+ const next = new Array(total);
18181
+ for (let i = 0; i < total; i += 1) next[i] = cells[i] === true;
18182
+ return next;
18183
+ }
18184
+ function cellsEqual(a, b) {
18185
+ if (a.length !== b.length) return false;
18186
+ for (let i = 0; i < a.length; i += 1) if (a[i] !== b[i]) return false;
18187
+ return true;
18188
+ }
18189
+ function MotionZonesSettings({ deviceId }) {
18190
+ const dev = useDeviceProxy(useSystem().trpcClient, deviceId);
18191
+ const [options, setOptions] = useState(null);
18192
+ const [unsupported, setUnsupported] = useState(false);
18193
+ const [cells, setCells] = useState(null);
18194
+ const [committed, setCommitted] = useState(null);
18195
+ const [saving, setSaving] = useState(false);
18196
+ const [editingGrid, setEditingGrid] = useState(false);
18197
+ const seededRef = useRef(false);
18198
+ useEffect(() => {
18199
+ if (!dev) return void 0;
18200
+ let cancelled = false;
18201
+ (async () => {
18202
+ try {
18203
+ const opts = await dev.motionZones?.getOptions({});
18204
+ if (cancelled || !opts) return;
18205
+ setOptions(opts);
18206
+ if (seededRef.current) return;
18207
+ const status = await dev.motionZones?.getStatus({});
18208
+ if (cancelled || !status) return;
18209
+ seededRef.current = true;
18210
+ const norm = normaliseCells(status.cells, opts);
18211
+ setCommitted(norm);
18212
+ setCells(norm);
18213
+ } catch (err) {
18214
+ if (cancelled) return;
18215
+ if (isAbsentProvider(err)) setUnsupported(true);
18216
+ else console.error("motion-zones load failed", err);
18217
+ }
18218
+ })();
18219
+ return () => {
18220
+ cancelled = true;
18221
+ };
18222
+ }, [dev]);
18223
+ const dirty = useMemo(() => cells !== null && committed !== null && !cellsEqual(cells, committed), [cells, committed]);
18224
+ const activeCount = useMemo(() => cells ? cells.reduce((n, c) => c ? n + 1 : n, 0) : 0, [cells]);
18225
+ const total = options ? options.gridWidth * options.gridHeight : 0;
18226
+ const setAll = useCallback((value) => {
18227
+ if (total > 0) setCells(new Array(total).fill(value));
18228
+ }, [total]);
18229
+ const invert = useCallback(() => {
18230
+ setCells((prev) => prev ? prev.map((c) => !c) : prev);
18231
+ }, []);
18232
+ const revert = useCallback(() => {
18233
+ if (committed) setCells([...committed]);
18234
+ }, [committed]);
18235
+ const save = useCallback(async () => {
18236
+ if (!cells || !dev?.motionZones || !options) return;
18237
+ setSaving(true);
18238
+ try {
18239
+ await dev.motionZones.setZone({ patch: { cells: [...cells] } });
18240
+ const fresh = await dev.motionZones.getStatus({});
18241
+ if (fresh) {
18242
+ const norm = normaliseCells(fresh.cells, options);
18243
+ setCommitted(norm);
18244
+ setCells(norm);
18245
+ }
18246
+ } catch (err) {
18247
+ console.error("motion-zones.setZone failed", err);
18248
+ } finally {
18249
+ setSaving(false);
18250
+ }
18251
+ }, [
18252
+ cells,
18253
+ dev,
18254
+ options
18255
+ ]);
18256
+ usePlayerOverlayLayer(useMemo(() => editingGrid && !unsupported && options && cells ? {
18257
+ id: "motion-zones",
18258
+ order: 110,
18259
+ node: /* @__PURE__ */ jsx(MotionGridCanvas, {
18260
+ options,
18261
+ cells,
18262
+ onCellsChange: setCells
18263
+ })
18264
+ } : null, [
18265
+ editingGrid,
18266
+ unsupported,
18267
+ options,
18268
+ cells
18269
+ ]));
18270
+ return /* @__PURE__ */ jsxs("div", {
18271
+ className: "flex flex-col gap-3",
18272
+ children: [/* @__PURE__ */ jsx("h3", {
18273
+ className: "text-[11px] font-semibold text-foreground-subtle uppercase tracking-wider",
18274
+ children: "Motion Zones"
18275
+ }), unsupported ? /* @__PURE__ */ jsx("p", {
18276
+ className: `${TEXT_HINT} leading-relaxed`,
18277
+ children: "This camera doesn't expose an on-board motion-detection grid."
18278
+ }) : !(!unsupported && options !== null && cells !== null) ? /* @__PURE__ */ jsx("p", {
18279
+ className: `${TEXT_HINT} leading-relaxed`,
18280
+ children: "Loading the camera's motion grid…"
18281
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("p", {
18282
+ className: `${TEXT_HINT} leading-relaxed`,
18283
+ children: [
18284
+ "Toggle",
18285
+ " ",
18286
+ /* @__PURE__ */ jsx("strong", {
18287
+ className: "text-foreground",
18288
+ children: "Edit grid"
18289
+ }),
18290
+ " to paint the region the camera watches for motion directly on the live frame, then ",
18291
+ /* @__PURE__ */ jsx("strong", {
18292
+ className: "text-foreground",
18293
+ children: "Save"
18294
+ }),
18295
+ " to push the mask to the camera. Detection on/off and sensitivity are set in the",
18296
+ " ",
18297
+ /* @__PURE__ */ jsx("strong", {
18298
+ className: "text-foreground",
18299
+ children: "On-camera motion"
18300
+ }),
18301
+ " section."
18302
+ ]
18303
+ }), /* @__PURE__ */ jsxs("div", {
18304
+ className: "flex items-center gap-2 flex-wrap",
18305
+ children: [
18306
+ /* @__PURE__ */ jsx("button", {
18307
+ type: "button",
18308
+ onClick: () => setEditingGrid((v) => !v),
18309
+ disabled: saving,
18310
+ "aria-pressed": editingGrid,
18311
+ className: editingGrid ? "rounded-md border border-primary/50 bg-primary/15 px-2.5 py-1 text-[11px] font-medium text-primary hover:bg-primary/25 disabled:opacity-40 transition-colors" : "rounded-md border border-border bg-surface px-2.5 py-1 text-[11px] font-medium text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors",
18312
+ children: editingGrid ? "Done editing" : "Edit grid"
18313
+ }),
18314
+ /* @__PURE__ */ jsx("button", {
18315
+ type: "button",
18316
+ onClick: () => setAll(true),
18317
+ disabled: saving,
18318
+ className: "rounded-md border border-border bg-surface px-2 py-1 text-[11px] font-medium text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors",
18319
+ children: "All on"
18320
+ }),
18321
+ /* @__PURE__ */ jsx("button", {
18322
+ type: "button",
18323
+ onClick: () => setAll(false),
18324
+ disabled: saving,
18325
+ className: "rounded-md border border-border bg-surface px-2 py-1 text-[11px] font-medium text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors",
18326
+ children: "All off"
18327
+ }),
18328
+ /* @__PURE__ */ jsx("button", {
18329
+ type: "button",
18330
+ onClick: invert,
18331
+ disabled: saving,
18332
+ className: "rounded-md border border-border bg-surface px-2 py-1 text-[11px] font-medium text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors",
18333
+ children: "Invert"
18334
+ }),
18335
+ /* @__PURE__ */ jsxs("span", {
18336
+ className: `${TEXT_HINT} ml-1 tabular-nums`,
18337
+ children: [
18338
+ activeCount,
18339
+ " / ",
18340
+ total,
18341
+ " cells · ",
18342
+ options.gridWidth,
18343
+ "×",
18344
+ options.gridHeight
18345
+ ]
18346
+ }),
18347
+ /* @__PURE__ */ jsx("span", { className: "flex-1" }),
18348
+ /* @__PURE__ */ jsx("button", {
18349
+ type: "button",
18350
+ onClick: revert,
18351
+ disabled: saving || !dirty,
18352
+ className: "rounded-md border border-border bg-surface px-2 py-1 text-[11px] text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors",
18353
+ children: "Revert"
18354
+ }),
18355
+ /* @__PURE__ */ jsx("button", {
18356
+ type: "button",
18357
+ onClick: () => void save(),
18358
+ disabled: saving || !dirty,
18359
+ className: "rounded-md border border-primary/50 bg-primary/15 px-2.5 py-1 text-[11px] font-medium text-primary hover:bg-primary/25 disabled:opacity-40 transition-colors",
18360
+ children: saving ? "Saving…" : "Save"
18361
+ })
18362
+ ]
18363
+ })] })]
18364
+ });
18365
+ }
18366
+ //#endregion
18367
+ //#region src/widgets/host-widgets.ts
18368
+ /** Host widgets — ui-library React components embeddable as a
18369
+ * `type:'widget'` ConfigField. `WidgetSlot` resolves these BEFORE the
18370
+ * Module-Federation `WidgetRegistry`. ids are namespaced `host/<name>`. */
18371
+ var HOST_WIDGETS = {
18372
+ "host/motion-zones-grid": MotionZonesSettings,
18373
+ "host/ptz-panel": PtzPanel,
18374
+ "host/ptz-autotrack": AutotrackSection
18375
+ };
18376
+ //#endregion
18377
+ //#region src/composites/widget-slot.tsx
18378
+ /**
18379
+ * <WidgetSlot> — single host-side mount point for any addon-contributed
18380
+ * widget. Consumers reference a widget by its public id
18381
+ * (`<addonId>/<stableId>`), the slot looks the widget's metadata up in
18382
+ * the shared `WidgetRegistry`, resolves the component through the
18383
+ * unified `useRemoteComponent` Module-Federation loader (the SAME path
18384
+ * `ContributionRenderer`'s `kind:'remote'` branch uses), validates host
18385
+ * context against the widget's `requires` metadata, and renders one of:
18386
+ * - skeleton placeholder while the bundle is still loading,
18387
+ * - inline error fallback when the widget id is unknown OR the host
18388
+ * didn't supply a required context (`deviceContext` /
18389
+ * `integrationContext`),
18390
+ * - the resolved component otherwise.
18391
+ *
18392
+ * The slot is intentionally STUPID — no styling beyond the skeleton/
18393
+ * error fallback. Layout (card vs inline, sizing) is the host's
18394
+ * responsibility.
18395
+ */
18396
+ function WidgetSlot(props) {
18397
+ const { widgetId, deviceId } = props;
18398
+ const HostComponent = HOST_WIDGETS[widgetId];
18399
+ if (HostComponent !== void 0) {
18400
+ if (deviceId === void 0) return /* @__PURE__ */ jsx(WidgetMissingError, {
18401
+ widgetId,
18402
+ reason: "missing-device-context"
18403
+ });
18404
+ return /* @__PURE__ */ jsx(HostComponent, { deviceId });
18405
+ }
18406
+ return /* @__PURE__ */ jsx(RemoteWidgetSlot, { ...props });
18407
+ }
18408
+ /**
18409
+ * Module-Federation remote-widget path — looks the widget's metadata up
18410
+ * in the shared `WidgetRegistry` and resolves it through `useRemoteComponent`.
18411
+ */
18412
+ function RemoteWidgetSlot(props) {
18413
+ const { widgetId } = props;
18414
+ const metadata = useWidgetMetadata(widgetId);
18415
+ if (metadata === void 0) return /* @__PURE__ */ jsx(WidgetMissingError, {
18416
+ widgetId,
18417
+ reason: "unknown"
18418
+ });
18419
+ return /* @__PURE__ */ jsx(ResolvedWidgetSlot, {
18420
+ ...props,
18421
+ metadata
18422
+ });
18423
+ }
18424
+ /**
18425
+ * Inner host — resolves the widget component through the unified
18426
+ * `useRemoteComponent` MF loader (shared with `ContributionRenderer`'s
18427
+ * `kind:'remote'` branch). Mounted only once a `metadata` descriptor is
18428
+ * known, so the hook always receives a valid `remote` descriptor.
18429
+ */
18430
+ function ResolvedWidgetSlot(props) {
18431
+ const { widgetId, host = "device-tab", config, deviceId, integrationId, instanceId, size, columns, rows, metadata } = props;
18432
+ const Component = useRemoteComponent(metadata.remote, metadata.bundleUrl);
18433
+ const resolvedInstanceId = useMemo(() => instanceId ?? widgetId, [instanceId, widgetId]);
18434
+ if (Component === null) return /* @__PURE__ */ jsx(WidgetSkeleton, {});
18435
+ if (Component === void 0) return /* @__PURE__ */ jsx(WidgetMissingError, {
18436
+ widgetId,
18437
+ reason: "missing-export"
18438
+ });
18439
+ if (metadata.requires.deviceContext && deviceId === void 0) return /* @__PURE__ */ jsx(WidgetMissingError, {
18440
+ widgetId,
18441
+ reason: "missing-device-context"
18442
+ });
18443
+ if (metadata.requires.integrationContext && integrationId === void 0) return /* @__PURE__ */ jsx(WidgetMissingError, {
18444
+ widgetId,
18445
+ reason: "missing-integration-context"
18446
+ });
18447
+ return /* @__PURE__ */ jsx(Component, {
18448
+ instanceId: resolvedInstanceId,
18449
+ host,
18450
+ config,
18451
+ deviceId,
18452
+ integrationId,
18453
+ size,
18454
+ columns,
18455
+ rows
18456
+ });
18457
+ }
18458
+ function WidgetSkeleton() {
18459
+ return /* @__PURE__ */ jsxs("div", {
18460
+ className: "rounded-lg border border-border bg-surface/40 p-4 animate-pulse",
18461
+ children: [
18462
+ /* @__PURE__ */ jsx("div", { className: "h-3 w-24 bg-foreground-subtle/20 rounded mb-2" }),
18463
+ /* @__PURE__ */ jsx("div", { className: "h-2 w-full bg-foreground-subtle/10 rounded mb-1" }),
18464
+ /* @__PURE__ */ jsx("div", { className: "h-2 w-3/4 bg-foreground-subtle/10 rounded" })
18465
+ ]
18466
+ });
18467
+ }
18468
+ function WidgetMissingError({ widgetId, reason }) {
18469
+ return /* @__PURE__ */ jsx("div", {
18470
+ className: "rounded-lg border border-warning/30 bg-warning/10 px-3 py-2 text-xs text-warning",
18471
+ children: reason === "unknown" ? `Widget "${widgetId}" not registered` : reason === "missing-export" ? `Widget "${widgetId}" bundle does not export this stableId` : reason === "missing-device-context" ? `Widget "${widgetId}" requires a deviceId` : `Widget "${widgetId}" requires an integrationId`
18472
+ });
18473
+ }
18474
+ //#endregion
18475
+ //#region src/composites/config-form-field.tsx
18476
+ var INPUT_CLASS = "w-full rounded-md border border-border bg-background px-2.5 py-1.5 text-xs text-foreground focus:border-primary focus:ring-1 focus:ring-primary/30 outline-none disabled:opacity-50 disabled:cursor-not-allowed";
18477
+ var LABEL_CLASS = "block text-[11px] font-medium text-foreground mb-1";
18478
+ var DESC_CLASS = "text-[10px] text-foreground-subtle mt-0.5";
18479
+ function FieldWrapper({ label, description, required, span, children, translationFn }) {
18480
+ const colSpanClass = span === 2 ? "col-span-2" : span === 3 ? "col-span-3" : span === 4 ? "col-span-4" : "col-span-1";
18481
+ const resolvedLabel = resolveLabel(label, translationFn);
18482
+ const resolvedDescription = resolveLabel(description, translationFn);
18483
+ const hasLabel = resolvedLabel !== void 0 && resolvedLabel !== "";
18484
+ return /* @__PURE__ */ jsxs("div", {
18485
+ className: `${colSpanClass} min-w-0`,
18486
+ children: [
18487
+ hasLabel && /* @__PURE__ */ jsxs("label", {
18488
+ className: LABEL_CLASS,
18489
+ children: [resolvedLabel, required && /* @__PURE__ */ jsx("span", {
18490
+ className: "text-danger ml-0.5",
18491
+ children: "*"
18492
+ })]
18493
+ }),
18494
+ children,
18495
+ resolvedDescription && /* @__PURE__ */ jsx("p", {
18496
+ className: DESC_CLASS,
18497
+ children: resolvedDescription
18498
+ })
18499
+ ]
18500
+ });
18501
+ }
18502
+ function TextField({ field, value, onChange, disabled, translationFn }) {
18503
+ return /* @__PURE__ */ jsx(FieldWrapper, {
18504
+ label: field.label,
18505
+ description: field.description,
18506
+ required: field.required,
18507
+ span: field.span,
18508
+ translationFn,
18509
+ children: /* @__PURE__ */ jsx("input", {
18510
+ type: field.inputType ?? "text",
18511
+ className: INPUT_CLASS,
18512
+ value: value === void 0 || value === null ? "" : String(value),
18513
+ placeholder: field.placeholder,
18514
+ maxLength: field.maxLength,
18515
+ pattern: field.pattern,
18516
+ disabled: disabled || field.disabled,
18517
+ onChange: (e) => onChange(e.target.value)
18518
+ })
18519
+ });
18520
+ }
18521
+ function NumberField({ field, value, onChange, disabled, translationFn }) {
18522
+ const [local, setLocal] = useState(value === void 0 || value === null ? "" : String(value));
18523
+ const focusedRef = useRef(false);
18524
+ useEffect(() => {
18525
+ if (focusedRef.current) return;
18526
+ setLocal(value === void 0 || value === null ? "" : String(value));
18527
+ }, [value]);
18528
+ const handleChange = (raw) => {
18529
+ setLocal(raw);
18530
+ if (raw === "" || raw === "-") {
18531
+ onChange(void 0);
18532
+ return;
18533
+ }
18534
+ const parsed = Number(raw);
18535
+ if (Number.isNaN(parsed)) return;
18536
+ onChange(parsed);
18537
+ };
18538
+ return /* @__PURE__ */ jsx(FieldWrapper, {
18539
+ label: field.label,
18540
+ description: field.description,
18541
+ required: field.required,
18542
+ span: field.span,
18543
+ translationFn,
18544
+ children: /* @__PURE__ */ jsxs("div", {
18545
+ className: "flex items-center gap-1",
18546
+ children: [/* @__PURE__ */ jsx("input", {
18547
+ type: "number",
18548
+ className: INPUT_CLASS,
18549
+ value: local,
18550
+ placeholder: field.placeholder,
18551
+ min: field.min,
18552
+ max: field.max,
18553
+ step: field.step,
18554
+ disabled: disabled || field.disabled,
18555
+ onFocus: () => {
18556
+ focusedRef.current = true;
18557
+ },
18558
+ onBlur: () => {
18559
+ focusedRef.current = false;
18560
+ },
18561
+ onChange: (e) => handleChange(e.target.value)
18562
+ }), field.unit && /* @__PURE__ */ jsx("span", {
18563
+ className: "text-xs text-foreground-subtle whitespace-nowrap",
17141
18564
  children: field.unit
17142
18565
  })]
17143
18566
  })
@@ -18246,18 +19669,12 @@ function AddonActionButtonField({ field, values, disabled, onAction }) {
18246
19669
  }
18247
19670
  function WidgetField({ field }) {
18248
19671
  const deviceId = useDeviceId();
18249
- return /* @__PURE__ */ jsx(FieldWrapper, {
18250
- label: field.label,
18251
- description: field.description,
18252
- required: field.required,
18253
- span: field.span,
18254
- children: /* @__PURE__ */ jsx(WidgetSlot, {
18255
- widgetId: field.widgetId,
18256
- host: "device-tab",
18257
- config: field.widgetConfig,
18258
- deviceId: deviceId ?? void 0,
18259
- instanceId: field.key
18260
- })
19672
+ return /* @__PURE__ */ jsx(WidgetSlot, {
19673
+ widgetId: field.widgetId,
19674
+ host: "device-tab",
19675
+ config: field.widgetConfig,
19676
+ deviceId: deviceId ?? void 0,
19677
+ instanceId: field.key
18261
19678
  });
18262
19679
  }
18263
19680
  function formatReadonlyValue(value, unit) {
@@ -18748,7 +20165,7 @@ function ConfigFormBuilder({ schema, values, onChange, disabled, translationFn,
18748
20165
  * + patch loop. `onAfterChange` fires once the patch is applied so
18749
20166
  * consumers can re-read any derived state.
18750
20167
  */
18751
- var Chevron = ({ open }) => /* @__PURE__ */ jsx("svg", {
20168
+ var Chevron$1 = ({ open }) => /* @__PURE__ */ jsx("svg", {
18752
20169
  className: `h-3 w-3 transition-transform ${open ? "rotate-90" : ""}`,
18753
20170
  viewBox: "0 0 24 24",
18754
20171
  fill: "none",
@@ -18913,7 +20330,7 @@ function AddonGlobalSettingsForm({ trpc, addonId, nodeId, title, disabled, onAft
18913
20330
  className: "w-full px-4 py-2 flex items-center gap-2 hover:bg-surface-hover text-left",
18914
20331
  "aria-expanded": open,
18915
20332
  children: [
18916
- /* @__PURE__ */ jsx(Chevron, { open }),
20333
+ /* @__PURE__ */ jsx(Chevron$1, { open }),
18917
20334
  /* @__PURE__ */ jsx("h3", {
18918
20335
  className: "text-xs font-semibold text-foreground uppercase tracking-wide flex-1",
18919
20336
  children: title ?? addonId
@@ -19483,184 +20900,38 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
19483
20900
  }) : /* @__PURE__ */ jsx("svg", {
19484
20901
  className: "h-3.5 w-3.5",
19485
20902
  viewBox: "0 0 24 24",
19486
- fill: "none",
19487
- stroke: "currentColor",
19488
- strokeWidth: "2",
19489
- strokeLinecap: "round",
19490
- children: /* @__PURE__ */ jsx("path", { d: "M8 3H5a2 2 0 00-2 2v3m18 0V5a2 2 0 00-2-2h-3m0 18h3a2 2 0 002-2v-3M3 16v3a2 2 0 002 2h3" })
19491
- })
19492
- })]
19493
- })]
19494
- })
19495
- ]
19496
- });
19497
- }
19498
- function ToolbarButton$1({ onClick, title, children }) {
19499
- return /* @__PURE__ */ jsx("button", {
19500
- onClick,
19501
- title,
19502
- className: "rounded-full p-1.5 text-white/80 hover:text-white hover:bg-white/20 transition-colors",
19503
- children
19504
- });
19505
- }
19506
- async function waitForIceGathering(pc) {
19507
- if (pc.iceGatheringState === "complete") return;
19508
- return new Promise((resolve) => {
19509
- const handler = () => {
19510
- if (pc.iceGatheringState === "complete") {
19511
- pc.removeEventListener("icegatheringstatechange", handler);
19512
- resolve();
19513
- }
19514
- };
19515
- pc.addEventListener("icegatheringstatechange", handler);
19516
- setTimeout(resolve, 5e3);
19517
- });
19518
- }
19519
- //#endregion
19520
- //#region src/contexts/player-overlays.tsx
19521
- /**
19522
- * Player overlay registry — pluggable layers + toolbar buttons for
19523
- * the device-detail live-frame `StreamPanel`.
19524
- *
19525
- * Why a registry: previously `StreamPanelContent` hardcoded each
19526
- * overlay (PTZ, intercom toggle, zone editor) and threaded the
19527
- * editing state by hand. Adding a new feature (audio waveform,
19528
- * detection bbox tracker, recording controls, …) meant another
19529
- * round of plumbing through the host file and StreamPanel's prop
19530
- * surface. The registry lets a sibling component (Detection tab,
19531
- * Live Stats tab, an addon-page extension) declare its own layer
19532
- * imperatively via a hook, and the host renders whatever's
19533
- * registered.
19534
- *
19535
- * Two registries live side-by-side:
19536
- *
19537
- * 1. **layers** — absolute-positioned React nodes drawn over the
19538
- * video frame (zones polygon canvas, motion bbox overlay,
19539
- * audio waveform). Rendered ordered by `order` (lower first
19540
- * → bottom; higher → top); the last-registered layer wins
19541
- * ties.
19542
- *
19543
- * 2. **toolbar buttons** — controls surfaced in the player's
19544
- * always-visible toolbar cluster (next to Intercom + Play/Stop).
19545
- * Each carries an icon, label, controlled `active` flag, and
19546
- * a click handler. The host (StreamPanel) renders them
19547
- * uniformly; tone variants stay simple (`'default' | 'primary'`).
19548
- *
19549
- * Lifecycle: hooks register their layer / button on mount and
19550
- * unregister on unmount, so swapping tabs (e.g. leaving Detection)
19551
- * automatically tears down the registration. Re-registering with
19552
- * the same `id` replaces the prior entry — meaning a single hook
19553
- * call can update its overlay/button props on every render without
19554
- * leaking entries.
19555
- *
19556
- * Provider scope: typically wraps the whole device-detail subtree
19557
- * so the StreamPanel + every tab share the same registry. One
19558
- * provider per `deviceId`; switching device IDs unmounts the
19559
- * provider naturally.
19560
- */
19561
- var PlayerOverlaysStateContext = createSharedContext("camstack:player-overlays-state", null);
19562
- var PlayerOverlaysActionsContext = createSharedContext("camstack:player-overlays-actions", null);
19563
- function PlayerOverlaysProvider({ children }) {
19564
- const [layers, setLayers] = useState(() => /* @__PURE__ */ new Map());
19565
- const [buttons, setButtons] = useState(() => /* @__PURE__ */ new Map());
19566
- const setLayer = useCallback((layer) => {
19567
- setLayers((prev) => {
19568
- const next = new Map(prev);
19569
- next.set(layer.id, layer);
19570
- return next;
19571
- });
19572
- }, []);
19573
- const removeLayer = useCallback((id) => {
19574
- setLayers((prev) => {
19575
- if (!prev.has(id)) return prev;
19576
- const next = new Map(prev);
19577
- next.delete(id);
19578
- return next;
19579
- });
19580
- }, []);
19581
- const setButton = useCallback((button) => {
19582
- setButtons((prev) => {
19583
- const next = new Map(prev);
19584
- next.set(button.id, button);
19585
- return next;
19586
- });
19587
- }, []);
19588
- const removeButton = useCallback((id) => {
19589
- setButtons((prev) => {
19590
- if (!prev.has(id)) return prev;
19591
- const next = new Map(prev);
19592
- next.delete(id);
19593
- return next;
19594
- });
19595
- }, []);
19596
- const stateValue = useMemo(() => ({
19597
- layers,
19598
- buttons
19599
- }), [layers, buttons]);
19600
- const actionsValue = useMemo(() => ({
19601
- setLayer,
19602
- removeLayer,
19603
- setButton,
19604
- removeButton
19605
- }), [
19606
- setLayer,
19607
- removeLayer,
19608
- setButton,
19609
- removeButton
19610
- ]);
19611
- return /* @__PURE__ */ jsx(PlayerOverlaysStateContext.Provider, {
19612
- value: stateValue,
19613
- children: /* @__PURE__ */ jsx(PlayerOverlaysActionsContext.Provider, {
19614
- value: actionsValue,
19615
- children
19616
- })
20903
+ fill: "none",
20904
+ stroke: "currentColor",
20905
+ strokeWidth: "2",
20906
+ strokeLinecap: "round",
20907
+ children: /* @__PURE__ */ jsx("path", { d: "M8 3H5a2 2 0 00-2 2v3m18 0V5a2 2 0 00-2-2h-3m0 18h3a2 2 0 002-2v-3M3 16v3a2 2 0 002 2h3" })
20908
+ })
20909
+ })]
20910
+ })]
20911
+ })
20912
+ ]
19617
20913
  });
19618
20914
  }
19619
- /** Snapshot of registered layers ordered by `order` (asc, ties resolved
19620
- * by insertion order of the underlying Map). Returns `[]` outside a
19621
- * provider. */
19622
- function usePlayerOverlayLayers() {
19623
- const state = useContext(PlayerOverlaysStateContext);
19624
- return useMemo(() => {
19625
- if (!state) return [];
19626
- return [...state.layers.values()].sort((a, b) => a.order - b.order);
19627
- }, [state]);
19628
- }
19629
- /** Snapshot of registered toolbar buttons, ordered by `order` (asc). */
19630
- function usePlayerToolbarButtons() {
19631
- const state = useContext(PlayerOverlaysStateContext);
19632
- return useMemo(() => {
19633
- if (!state) return [];
19634
- return [...state.buttons.values()].sort((a, b) => a.order - b.order);
19635
- }, [state]);
19636
- }
19637
- /**
19638
- * Register an overlay layer for the lifetime of the calling component.
19639
- * Re-registers on every render with the latest spec; auto-unregisters
19640
- * on unmount. Pass `null` to skip registration when the layer is
19641
- * conditionally enabled (the hook still runs every render — keeps
19642
- * react-hook order stable across spec === null toggles).
19643
- *
19644
- * Callers that build the spec inline should memoise it (`useMemo`)
19645
- * to avoid re-registering on every parent render — context-write
19646
- * effects depend on referential equality of the spec object.
19647
- */
19648
- function usePlayerOverlayLayer(spec) {
19649
- const actions = useContext(PlayerOverlaysActionsContext);
19650
- useEffect(() => {
19651
- if (!actions || !spec) return void 0;
19652
- actions.setLayer(spec);
19653
- return () => actions.removeLayer(spec.id);
19654
- }, [actions, spec]);
20915
+ function ToolbarButton$1({ onClick, title, children }) {
20916
+ return /* @__PURE__ */ jsx("button", {
20917
+ onClick,
20918
+ title,
20919
+ className: "rounded-full p-1.5 text-white/80 hover:text-white hover:bg-white/20 transition-colors",
20920
+ children
20921
+ });
19655
20922
  }
19656
- /** Same shape as `usePlayerOverlayLayer`, scoped to toolbar buttons. */
19657
- function usePlayerToolbarButton(spec) {
19658
- const actions = useContext(PlayerOverlaysActionsContext);
19659
- useEffect(() => {
19660
- if (!actions || !spec) return void 0;
19661
- actions.setButton(spec);
19662
- return () => actions.removeButton(spec.id);
19663
- }, [actions, spec]);
20923
+ async function waitForIceGathering(pc) {
20924
+ if (pc.iceGatheringState === "complete") return;
20925
+ return new Promise((resolve) => {
20926
+ const handler = () => {
20927
+ if (pc.iceGatheringState === "complete") {
20928
+ pc.removeEventListener("icegatheringstatechange", handler);
20929
+ resolve();
20930
+ }
20931
+ };
20932
+ pc.addEventListener("icegatheringstatechange", handler);
20933
+ setTimeout(resolve, 5e3);
20934
+ });
19664
20935
  }
19665
20936
  //#endregion
19666
20937
  //#region src/composites/stream-panel.tsx
@@ -22146,7 +23417,7 @@ var LEVEL_OPTIONS = [
22146
23417
  "error"
22147
23418
  ];
22148
23419
  /** Max live entries kept in the ring buffer. */
22149
- var MAX_LIVE_ENTRIES$2 = 500;
23420
+ var MAX_LIVE_ENTRIES$1 = 500;
22150
23421
  function passesLevel(logLevel, filterLevel) {
22151
23422
  if (!filterLevel) return true;
22152
23423
  return (LEVEL_SEVERITY[logLevel] ?? 0) >= (LEVEL_SEVERITY[filterLevel] ?? 0);
@@ -22175,7 +23446,7 @@ function LogStream({ agentId: propsAgentId, addonId: propsAddonId, deviceId: pro
22175
23446
  integrationId,
22176
23447
  requestId
22177
23448
  ]);
22178
- const fallbackBuffer = useLiveBuffer(MAX_LIVE_ENTRIES$2);
23449
+ const fallbackBuffer = useLiveBuffer(MAX_LIVE_ENTRIES$1);
22179
23450
  const buffer = externalBuffer ?? fallbackBuffer;
22180
23451
  const liveLogs = buffer.entries;
22181
23452
  const [clearedAt, setClearedAt] = useState(0);
@@ -22502,7 +23773,7 @@ function LogStream({ agentId: propsAgentId, addonId: propsAddonId, deviceId: pro
22502
23773
  }),
22503
23774
  /* @__PURE__ */ jsx("td", {
22504
23775
  className: "px-1 py-1 align-top w-5",
22505
- children: /* @__PURE__ */ jsx(RowCopyButton$2, {
23776
+ children: /* @__PURE__ */ jsx(RowCopyButton$1, {
22506
23777
  copied: copiedRowKey === key,
22507
23778
  onCopy: () => copyOne(log, key)
22508
23779
  })
@@ -22541,7 +23812,7 @@ function ScopeBadge$2({ label, value }) {
22541
23812
  * "did it work" affordance — same UX language the toolbar Copy
22542
23813
  * button already uses for its `Copied!` label.
22543
23814
  */
22544
- function RowCopyButton$2({ copied, onCopy }) {
23815
+ function RowCopyButton$1({ copied, onCopy }) {
22545
23816
  return /* @__PURE__ */ jsx("button", {
22546
23817
  type: "button",
22547
23818
  title: "Copy this row",
@@ -22866,7 +24137,7 @@ function hasContent(entry) {
22866
24137
  if (cat === EventCategory.DetectionResult || cat === EventCategory.PipelineAudioInferenceResult) return summarizeEvent(cat, entry.data) !== null;
22867
24138
  return true;
22868
24139
  }
22869
- var MAX_LIVE_ENTRIES$1 = 300;
24140
+ var MAX_LIVE_ENTRIES = 300;
22870
24141
  function EventStream({ agentId, addonId, deviceId, defaultCategories, categories: legacyCategories, category: propsCategory, maxHeight = "max-h-96", limit = 50, showCategoryFilter = true, showFilters: _showFilters = false, liveBuffer: externalBuffer, onClose, className }) {
22871
24142
  const defaultsArray = useMemo(() => defaultCategories ?? legacyCategories ?? DEFAULT_EVENT_CATEGORIES, [defaultCategories, legacyCategories]);
22872
24143
  const defaultsKey = useMemo(() => [...defaultsArray].sort().join(","), [defaultsArray]);
@@ -22878,7 +24149,7 @@ function EventStream({ agentId, addonId, deviceId, defaultCategories, categories
22878
24149
  setActiveCategories(new Set(defaultsArray));
22879
24150
  }
22880
24151
  }, [defaultsKey, defaultsArray]);
22881
- const fallbackBuffer = useLiveBuffer(MAX_LIVE_ENTRIES$1);
24152
+ const fallbackBuffer = useLiveBuffer(MAX_LIVE_ENTRIES);
22882
24153
  const buffer = externalBuffer ?? fallbackBuffer;
22883
24154
  const liveEvents = buffer.entries;
22884
24155
  const [autoScroll, setAutoScroll] = useState(true);
@@ -23202,7 +24473,7 @@ function EventStream({ agentId, addonId, deviceId, defaultCategories, categories
23202
24473
  className: "text-foreground-subtle whitespace-nowrap w-[70px] shrink-0 pt-0.5",
23203
24474
  children: new Date(event.timestamp).toLocaleTimeString()
23204
24475
  }),
23205
- /* @__PURE__ */ jsx(RowCopyButton$1, {
24476
+ /* @__PURE__ */ jsx(RowCopyButton, {
23206
24477
  copied: copiedRowId === event.id,
23207
24478
  onCopy: () => copyOne(event)
23208
24479
  }),
@@ -23392,7 +24663,7 @@ function ScopeBadge$1({ label, value }) {
23392
24663
  * click doesn't toggle the row's expand state. Flashes a Check
23393
24664
  * for ~1s after copy.
23394
24665
  */
23395
- function RowCopyButton$1({ copied, onCopy }) {
24666
+ function RowCopyButton({ copied, onCopy }) {
23396
24667
  return /* @__PURE__ */ jsx("button", {
23397
24668
  type: "button",
23398
24669
  title: "Copy this event",
@@ -23659,162 +24930,137 @@ function StatusBadge$1({ status }) {
23659
24930
  //#endregion
23660
24931
  //#region src/composites/state-values-stream.tsx
23661
24932
  /**
23662
- * StateValuesStream — runtime-state change viewer for ui-library.
24933
+ * StateValuesStream — live current-state tree for ui-library.
24934
+ *
24935
+ * Renders a single device's CURRENT runtime-state as a scrollable,
24936
+ * collapsible hierarchical tree (NOT a chronological change log).
23663
24937
  *
23664
- * Subscribes to `EventCategory.DeviceStateChanged` events scoped to a
23665
- * single device. Each row shows: timestamp, capName, slice JSON.
23666
- * Supports cap-name multiselect filter + free-text search.
24938
+ * - Seed: on mount, `trpc.deviceState.getAllSnapshots` returns the
24939
+ * whole-system snapshot; we pick out this device's entry and build
24940
+ * a `Map<capName, slice>` of its current state.
24941
+ * - Realtime: an `EventCategory.DeviceStateChanged` tRPC subscription
24942
+ * scoped to the device patches each cap's slice into the map in
24943
+ * place — the matching tree node re-renders, no rows are appended.
24944
+ * - Tree: top level = cap names (sorted, expanded by default); each
24945
+ * cap node expands to its slice keys; nested objects/arrays expand
24946
+ * recursively, deep nesting collapsed by default. A per-cap
24947
+ * "updated Ns ago" indicator surfaces realtime activity.
24948
+ * - Filter/search: cap-name multiselect popover + free-text search,
24949
+ * both applied to the tree (filter which caps show; search matches
24950
+ * keys/values anywhere in the slice).
23667
24951
  *
23668
- * Live subscription is mounted only while this component is rendered,
24952
+ * The live subscription is mounted only while this component renders,
23669
24953
  * so the parent can mount/unmount it on tab switches and the tRPC
23670
24954
  * subscription is torn down automatically when hidden.
23671
24955
  */
23672
- function isStateChangeForDevice(raw, deviceId) {
23673
- if (!raw || typeof raw !== "object") return false;
23674
- const e = raw;
23675
- if (e.category !== EventCategory.DeviceStateChanged) return false;
23676
- const data = e.data;
23677
- if (!data || typeof data !== "object") return false;
23678
- if (data.deviceId !== deviceId) return false;
23679
- if (typeof data.capName !== "string") return false;
23680
- if (!data.slice || typeof data.slice !== "object") return false;
23681
- return true;
23682
- }
23683
- function toEntry(raw) {
24956
+ /** Narrow an untrusted live-stream event to a `DeviceStateChanged`
24957
+ * for THIS device, returning the cap name + slice it carries. */
24958
+ function readStateChange(raw, deviceId) {
23684
24959
  if (!raw || typeof raw !== "object") return null;
23685
24960
  const e = raw;
23686
- const id = typeof e.id === "string" ? e.id : null;
23687
- const timestamp = typeof e.timestamp === "string" ? e.timestamp : null;
24961
+ if (e.category !== EventCategory.DeviceStateChanged) return null;
23688
24962
  const data = e.data;
23689
- if (!id || !timestamp || !data || typeof data !== "object") return null;
23690
- if (typeof data.capName !== "string" || !data.slice || typeof data.slice !== "object") return null;
24963
+ if (!data || typeof data !== "object") return null;
24964
+ if (data.deviceId !== deviceId) return null;
24965
+ if (typeof data.capName !== "string") return null;
24966
+ if (!data.slice || typeof data.slice !== "object") return null;
23691
24967
  return {
23692
- id,
23693
- timestamp,
23694
24968
  capName: data.capName,
23695
24969
  slice: data.slice
23696
24970
  };
23697
24971
  }
23698
- var MAX_LIVE_ENTRIES = 300;
24972
+ function isSystemSnapshot(raw) {
24973
+ return !!raw && typeof raw === "object" && !Array.isArray(raw);
24974
+ }
23699
24975
  /**
23700
24976
  * Every device-scoped cap name, sorted alphabetically — same shape
23701
24977
  * as `ALL_EVENT_CATEGORIES` in event-stream.tsx so the filter
23702
24978
  * surface is symmetric across the three live-streams (Logs, Events,
23703
- * State). Computed once at module load; the user can pick from this
23704
- * full list rather than only the caps that have already emitted at
23705
- * least one slice change in the current session.
24979
+ * State). Computed once at module load.
23706
24980
  */
23707
24981
  var ALL_DEVICE_CAP_NAMES = Object.freeze([...new Set(ALL_CAPABILITY_DEFINITIONS.filter((c) => c.scope === "device").map((c) => c.name))].sort());
23708
- function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", limit = 50, liveBuffer: externalBuffer, onClose, className }) {
24982
+ /** Depth at which tree nodes start collapsed. Cap nodes (depth 0) and
24983
+ * their direct slice keys (depth 1) render expanded; anything nested
24984
+ * deeper opens on demand. */
24985
+ var DEFAULT_EXPAND_DEPTH = 1;
24986
+ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", onClose, className }) {
23709
24987
  const [activeCaps, setActiveCaps] = useState(useMemo(() => new Set(defaultCaps ?? []), [defaultCaps]));
23710
24988
  const [searchText, setSearchText] = useState("");
23711
- const [autoScroll, setAutoScroll] = useState(true);
23712
- const [expandedRows, setExpandedRows] = useState(/* @__PURE__ */ new Set());
23713
- const [clearedAt, setClearedAt] = useState(0);
23714
24989
  const [filterOpen, setFilterOpen] = useState(false);
23715
24990
  const [filterSearch, setFilterSearch] = useState("");
23716
24991
  const filterRootRef = useRef(null);
23717
- const fallbackBuffer = useLiveBuffer(MAX_LIVE_ENTRIES);
23718
- const buffer = externalBuffer ?? fallbackBuffer;
23719
- const liveEvents = buffer.entries;
23720
- const scrollRef = useRef(null);
24992
+ const [capStates, setCapStates] = useState(/* @__PURE__ */ new Map());
24993
+ const [collapseOverrides, setCollapseOverrides] = useState(/* @__PURE__ */ new Map());
24994
+ const [, setNowTick] = useState(0);
23721
24995
  const prevDeviceRef = useRef(deviceId);
23722
24996
  useEffect(() => {
23723
24997
  if (prevDeviceRef.current !== deviceId) {
23724
24998
  prevDeviceRef.current = deviceId;
23725
- buffer.reset();
23726
- setExpandedRows(/* @__PURE__ */ new Set());
23727
- setClearedAt(0);
24999
+ setCapStates(/* @__PURE__ */ new Map());
25000
+ setCollapseOverrides(/* @__PURE__ */ new Map());
23728
25001
  }
23729
- }, [deviceId, buffer]);
23730
- const recentInput = useMemo(() => ({
23731
- deviceId,
23732
- category: EventCategory.DeviceStateChanged,
23733
- limit
23734
- }), [deviceId, limit]);
23735
- const { data: initialEvents, isLoading } = trpc.systemEvents.getRecent.useQuery(recentInput, { staleTime: 3e4 });
23736
- const activeCapsRef = useRef(activeCaps);
25002
+ }, [deviceId]);
25003
+ const { data: snapshot, isLoading } = trpc.deviceState.getAllSnapshots.useQuery({}, { staleTime: 3e4 });
25004
+ const seededRef = useRef(false);
25005
+ useEffect(() => {
25006
+ if (seededRef.current) return;
25007
+ if (!isSystemSnapshot(snapshot)) return;
25008
+ const deviceSlices = snapshot[String(deviceId)];
25009
+ seededRef.current = true;
25010
+ if (!deviceSlices) return;
25011
+ const now = Date.now();
25012
+ setCapStates((prev) => {
25013
+ const next = new Map(prev);
25014
+ for (const [capName, slice] of Object.entries(deviceSlices)) {
25015
+ if (next.has(capName)) continue;
25016
+ next.set(capName, {
25017
+ slice,
25018
+ updatedAt: now
25019
+ });
25020
+ }
25021
+ return next;
25022
+ });
25023
+ }, [snapshot, deviceId]);
23737
25024
  useEffect(() => {
23738
- activeCapsRef.current = activeCaps;
23739
- }, [activeCaps]);
25025
+ seededRef.current = false;
25026
+ }, [deviceId]);
23740
25027
  trpc.systemEvents.subscribe.useSubscription({
23741
25028
  deviceId,
23742
25029
  category: EventCategory.DeviceStateChanged
23743
25030
  }, { onData: (raw) => {
23744
- if (!isStateChangeForDevice(raw, deviceId)) return;
23745
- const entry = toEntry(raw);
23746
- if (!entry) return;
23747
- const active = activeCapsRef.current;
23748
- if (active.size > 0 && !active.has(entry.capName)) return;
23749
- buffer.append(entry);
25031
+ const change = readStateChange(raw, deviceId);
25032
+ if (!change) return;
25033
+ setCapStates((prev) => {
25034
+ const next = new Map(prev);
25035
+ next.set(change.capName, {
25036
+ slice: change.slice,
25037
+ updatedAt: Date.now()
25038
+ });
25039
+ return next;
25040
+ });
23750
25041
  } });
23751
- const prevLiveCountRef = useRef(liveEvents.length);
23752
25042
  useEffect(() => {
23753
- const el = scrollRef.current;
23754
- if (!el) {
23755
- prevLiveCountRef.current = liveEvents.length;
23756
- return;
23757
- }
23758
- const prevCount = prevLiveCountRef.current;
23759
- prevLiveCountRef.current = liveEvents.length;
23760
- if (liveEvents.length <= prevCount) return;
23761
- if (autoScroll && el.scrollTop <= 8) el.scrollTo({ top: 0 });
23762
- else if (!autoScroll && el.scrollTop > 0) {
23763
- const firstRow = el.firstElementChild?.firstElementChild;
23764
- const rowHeight = firstRow instanceof HTMLElement ? firstRow.offsetHeight : 28;
23765
- const newCount = liveEvents.length - prevCount;
23766
- el.scrollTop += rowHeight * newCount;
23767
- }
23768
- }, [liveEvents.length, autoScroll]);
23769
- const allEntries = useMemo(() => {
23770
- const byId = /* @__PURE__ */ new Map();
23771
- for (const e of initialEvents ?? []) {
23772
- const entry = toEntry(e);
23773
- if (entry) byId.set(entry.id, entry);
23774
- }
23775
- for (const e of liveEvents) byId.set(e.id, e);
23776
- let list = [...byId.values()];
23777
- if (clearedAt > 0) list = list.filter((e) => new Date(e.timestamp).getTime() > clearedAt);
23778
- if (activeCaps.size > 0) list = list.filter((e) => activeCaps.has(e.capName));
25043
+ const id = setInterval(() => setNowTick((t) => t + 1), 5e3);
25044
+ return () => clearInterval(id);
25045
+ }, []);
25046
+ const visibleCaps = useMemo(() => {
25047
+ const caps = new Set(ALL_DEVICE_CAP_NAMES);
25048
+ for (const capName of capStates.keys()) caps.add(capName);
25049
+ return [...caps].sort();
25050
+ }, [capStates]);
25051
+ const filteredCapNames = useMemo(() => {
23779
25052
  const needle = searchText.trim().toLowerCase();
23780
- if (needle) list = list.filter((e) => {
23781
- if (e.capName.toLowerCase().includes(needle)) return true;
23782
- try {
23783
- if (new Date(e.timestamp).toLocaleTimeString().toLowerCase().includes(needle)) return true;
23784
- } catch {}
23785
- try {
23786
- if (Object.entries(e.slice).map(([k, v]) => `${k}=${formatValue(v)}`).join(" · ").toLowerCase().includes(needle)) return true;
23787
- } catch {}
23788
- try {
23789
- if (JSON.stringify(e.slice).toLowerCase().includes(needle)) return true;
23790
- } catch {}
23791
- return false;
25053
+ return [...capStates.keys()].sort().filter((capName) => {
25054
+ if (activeCaps.size > 0 && !activeCaps.has(capName)) return false;
25055
+ if (!needle) return true;
25056
+ if (capName.toLowerCase().includes(needle)) return true;
25057
+ return sliceMatchesSearch(capStates.get(capName)?.slice ?? {}, needle);
23792
25058
  });
23793
- return list.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
23794
25059
  }, [
23795
- initialEvents,
23796
- liveEvents,
25060
+ capStates,
23797
25061
  activeCaps,
23798
- searchText,
23799
- clearedAt
25062
+ searchText
23800
25063
  ]);
23801
- const visibleCaps = useMemo(() => {
23802
- const caps = new Set(ALL_DEVICE_CAP_NAMES);
23803
- for (const e of initialEvents ?? []) {
23804
- const entry = toEntry(e);
23805
- if (entry) caps.add(entry.capName);
23806
- }
23807
- for (const e of liveEvents) caps.add(e.capName);
23808
- return [...caps].sort();
23809
- }, [initialEvents, liveEvents]);
23810
- const toggleRow = useCallback((id) => {
23811
- setExpandedRows((prev) => {
23812
- const next = new Set(prev);
23813
- if (next.has(id)) next.delete(id);
23814
- else next.add(id);
23815
- return next;
23816
- });
23817
- }, []);
23818
25064
  const toggleCap = useCallback((cap) => {
23819
25065
  setActiveCaps((prev) => {
23820
25066
  const next = new Set(prev);
@@ -23827,34 +25073,36 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", limi
23827
25073
  setActiveCaps(new Set(defaultCaps ?? []));
23828
25074
  setSearchText("");
23829
25075
  }, [defaultCaps]);
23830
- const handleClear = useCallback(() => {
23831
- setClearedAt(Date.now());
23832
- buffer.reset();
23833
- }, [buffer]);
23834
- const [copiedAll, setCopiedAll] = useState(false);
23835
- const [copiedRowId, setCopiedRowId] = useState(null);
23836
- const formatEntryForCopy = useCallback((entry) => {
23837
- return `${new Date(entry.timestamp).toISOString()} [${entry.capName}] slice=${JSON.stringify(entry.slice)}`;
23838
- }, []);
23839
- const handleCopyAll = useCallback(() => {
23840
- const lines = allEntries.map(formatEntryForCopy);
23841
- navigator.clipboard.writeText(lines.join("\n")).then(() => {
23842
- setCopiedAll(true);
23843
- setTimeout(() => setCopiedAll(false), 1500);
23844
- });
23845
- }, [allEntries, formatEntryForCopy]);
23846
- const copyOne = useCallback((entry) => {
23847
- navigator.clipboard.writeText(formatEntryForCopy(entry)).then(() => {
23848
- setCopiedRowId(entry.id);
23849
- setTimeout(() => setCopiedRowId((prev) => prev === entry.id ? null : prev), 1200);
23850
- });
23851
- }, [formatEntryForCopy]);
23852
25076
  const handleSelectAllCaps = useCallback(() => {
23853
25077
  setActiveCaps(new Set(visibleCaps));
23854
25078
  }, [visibleCaps]);
23855
25079
  const handleSelectNoCaps = useCallback(() => {
23856
25080
  setActiveCaps(/* @__PURE__ */ new Set());
23857
25081
  }, []);
25082
+ const toggleNode = useCallback((path, currentlyOpen) => {
25083
+ setCollapseOverrides((prev) => {
25084
+ const next = new Map(prev);
25085
+ next.set(path, !currentlyOpen);
25086
+ return next;
25087
+ });
25088
+ }, []);
25089
+ const isPathOpen = useCallback((path, depth) => {
25090
+ const override = collapseOverrides.get(path);
25091
+ if (override !== void 0) return override;
25092
+ return depth < DEFAULT_EXPAND_DEPTH;
25093
+ }, [collapseOverrides]);
25094
+ const [copiedAll, setCopiedAll] = useState(false);
25095
+ const handleCopyAll = useCallback(() => {
25096
+ const out = {};
25097
+ for (const capName of filteredCapNames) {
25098
+ const state = capStates.get(capName);
25099
+ if (state) out[capName] = state.slice;
25100
+ }
25101
+ navigator.clipboard.writeText(JSON.stringify(out, null, 2)).then(() => {
25102
+ setCopiedAll(true);
25103
+ setTimeout(() => setCopiedAll(false), 1500);
25104
+ });
25105
+ }, [filteredCapNames, capStates]);
23858
25106
  const filteredPopoverCaps = useMemo(() => {
23859
25107
  const needle = filterSearch.trim().toLowerCase();
23860
25108
  if (!needle) return visibleCaps;
@@ -23873,6 +25121,7 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", limi
23873
25121
  document.addEventListener("mousedown", onDocClick);
23874
25122
  return () => document.removeEventListener("mousedown", onDocClick);
23875
25123
  }, [filterOpen]);
25124
+ const searchNeedle = searchText.trim().toLowerCase();
23876
25125
  return /* @__PURE__ */ jsxs("div", {
23877
25126
  className: cn("h-full min-h-0 flex flex-col", className),
23878
25127
  onClick: (e) => e.stopPropagation(),
@@ -23889,18 +25138,6 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", limi
23889
25138
  label: "device",
23890
25139
  value: `#${deviceId}`
23891
25140
  }),
23892
- /* @__PURE__ */ jsxs("button", {
23893
- type: "button",
23894
- onClick: () => {
23895
- setAutoScroll((a) => !a);
23896
- if (!autoScroll && scrollRef.current) scrollRef.current.scrollTo({
23897
- top: 0,
23898
- behavior: "smooth"
23899
- });
23900
- },
23901
- className: cn("inline-flex items-center gap-1 text-[9px] px-1.5 py-0.5 rounded border transition-colors", autoScroll ? "border-border text-foreground-subtle hover:text-foreground" : "border-amber-500/30 bg-amber-500/10 text-amber-400"),
23902
- children: [/* @__PURE__ */ jsx(ArrowUpToLine, { className: "h-2.5 w-2.5" }), autoScroll ? "Auto" : "Paused"]
23903
- }),
23904
25141
  /* @__PURE__ */ jsxs("div", {
23905
25142
  className: "relative",
23906
25143
  ref: filterRootRef,
@@ -23908,7 +25145,7 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", limi
23908
25145
  type: "button",
23909
25146
  onClick: () => setFilterOpen((v) => !v),
23910
25147
  className: cn("inline-flex items-center gap-1 text-[9px] px-1.5 py-0.5 rounded border transition-colors", !allSelected ? "border-violet-500/30 bg-violet-500/10 text-violet-400" : "border-border text-foreground-subtle hover:text-foreground"),
23911
- title: "Pick caps to track",
25148
+ title: "Pick caps to show",
23912
25149
  children: [
23913
25150
  /* @__PURE__ */ jsx(Funnel, { className: "h-2.5 w-2.5" }),
23914
25151
  "Filter (",
@@ -23933,19 +25170,12 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", limi
23933
25170
  title: "Reset filters",
23934
25171
  children: [/* @__PURE__ */ jsx(RotateCcw, { className: "h-2.5 w-2.5" }), "Reset"]
23935
25172
  }),
23936
- /* @__PURE__ */ jsxs("button", {
23937
- type: "button",
23938
- onClick: handleClear,
23939
- className: "inline-flex items-center gap-1 text-[9px] px-1.5 py-0.5 rounded border border-border text-foreground-subtle hover:text-foreground transition-colors",
23940
- title: "Clear visible entries",
23941
- children: [/* @__PURE__ */ jsx(Trash2, { className: "h-2.5 w-2.5" }), "Clear"]
23942
- }),
23943
25173
  /* @__PURE__ */ jsxs("button", {
23944
25174
  type: "button",
23945
25175
  onClick: handleCopyAll,
23946
- disabled: allEntries.length === 0,
23947
- className: cn("inline-flex items-center gap-1 text-[9px] px-1.5 py-0.5 rounded border transition-colors", copiedAll ? "border-emerald-500/30 bg-emerald-500/10 text-emerald-400" : "border-border text-foreground-subtle hover:text-foreground", allEntries.length === 0 && "opacity-40 cursor-not-allowed"),
23948
- title: "Copy all visible state changes",
25176
+ disabled: filteredCapNames.length === 0,
25177
+ className: cn("inline-flex items-center gap-1 text-[9px] px-1.5 py-0.5 rounded border transition-colors", copiedAll ? "border-emerald-500/30 bg-emerald-500/10 text-emerald-400" : "border-border text-foreground-subtle hover:text-foreground", filteredCapNames.length === 0 && "opacity-40 cursor-not-allowed"),
25178
+ title: "Copy visible state as JSON",
23949
25179
  children: [/* @__PURE__ */ jsx(Copy, { className: "h-2.5 w-2.5" }), copiedAll ? "Copied!" : "Copy"]
23950
25180
  }),
23951
25181
  /* @__PURE__ */ jsxs("div", {
@@ -23964,13 +25194,9 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", limi
23964
25194
  children: /* @__PURE__ */ jsx(X, { className: "h-2.5 w-2.5" })
23965
25195
  })]
23966
25196
  }),
23967
- liveEvents.length > 0 && /* @__PURE__ */ jsxs("span", {
23968
- className: "text-[9px] text-emerald-400 font-medium",
23969
- children: [
23970
- "+",
23971
- liveEvents.length,
23972
- " live"
23973
- ]
25197
+ /* @__PURE__ */ jsxs("span", {
25198
+ className: "text-[9px] text-emerald-400 font-medium inline-flex items-center gap-1",
25199
+ children: [/* @__PURE__ */ jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-emerald-400 animate-pulse" }), "live"]
23974
25200
  })
23975
25201
  ]
23976
25202
  }), onClose && /* @__PURE__ */ jsx("button", {
@@ -23980,90 +25206,172 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", limi
23980
25206
  children: /* @__PURE__ */ jsx(X, { className: "h-3.5 w-3.5" })
23981
25207
  })]
23982
25208
  }), /* @__PURE__ */ jsxs("div", {
23983
- ref: scrollRef,
23984
- className: cn("flex-1 min-h-0 overflow-auto text-[10px]", maxHeight),
25209
+ className: cn("flex-1 min-h-0 overflow-auto text-[10px] font-mono", maxHeight),
23985
25210
  children: [
23986
- isLoading && /* @__PURE__ */ jsxs("div", {
23987
- className: "flex items-center justify-center gap-2 py-6 text-foreground-subtle",
23988
- children: [/* @__PURE__ */ jsx(LoaderCircle, { className: "h-3.5 w-3.5 animate-spin" }), "Loading state changes..."]
25211
+ isLoading && capStates.size === 0 && /* @__PURE__ */ jsxs("div", {
25212
+ className: "flex items-center justify-center gap-2 py-6 text-foreground-subtle font-sans",
25213
+ children: [/* @__PURE__ */ jsx(LoaderCircle, { className: "h-3.5 w-3.5 animate-spin" }), "Loading device state..."]
23989
25214
  }),
23990
- !isLoading && allEntries.length === 0 && /* @__PURE__ */ jsx("div", {
23991
- className: "py-4 text-center text-foreground-subtle",
23992
- children: "No state changes"
25215
+ !isLoading && capStates.size === 0 && /* @__PURE__ */ jsx("div", {
25216
+ className: "py-4 text-center text-foreground-subtle font-sans",
25217
+ children: "No runtime state"
23993
25218
  }),
23994
- allEntries.length > 0 && /* @__PURE__ */ jsx("div", {
23995
- className: "divide-y divide-border/30",
23996
- children: allEntries.map((entry) => {
23997
- const expanded = expandedRows.has(entry.id);
23998
- const fields = Object.entries(entry.slice);
23999
- const summary = fields.slice(0, 3).map(([k, v]) => `${k}=${formatValue(v)}`).join(" · ");
24000
- return /* @__PURE__ */ jsxs("div", {
24001
- className: "flex items-start gap-2 px-3 py-1.5 hover:bg-surface-hover/30 cursor-pointer",
24002
- onClick: () => toggleRow(entry.id),
24003
- children: [
24004
- /* @__PURE__ */ jsx("span", {
24005
- className: "text-foreground-subtle whitespace-nowrap w-[70px] shrink-0 pt-0.5",
24006
- children: new Date(entry.timestamp).toLocaleTimeString()
24007
- }),
24008
- /* @__PURE__ */ jsx(RowCopyButton, {
24009
- copied: copiedRowId === entry.id,
24010
- onCopy: () => copyOne(entry)
24011
- }),
24012
- /* @__PURE__ */ jsxs("div", {
24013
- className: "flex-1 min-w-0",
24014
- children: [
24015
- /* @__PURE__ */ jsxs("div", {
24016
- className: "flex items-center gap-1.5",
24017
- children: [
24018
- expanded ? /* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3 text-foreground-subtle shrink-0" }) : /* @__PURE__ */ jsx(ChevronRight, { className: "h-3 w-3 text-foreground-subtle shrink-0" }),
24019
- /* @__PURE__ */ jsx("span", {
24020
- className: "inline-flex items-center gap-1 rounded-full border border-violet-500/30 bg-violet-500/15 text-violet-400 px-2 py-0.5 text-[9px] font-medium",
24021
- children: entry.capName
24022
- }),
24023
- fields.length > 3 && /* @__PURE__ */ jsxs("span", {
24024
- className: "text-[9px] text-foreground-subtle",
24025
- children: [
24026
- "+",
24027
- fields.length - 3,
24028
- " more"
24029
- ]
24030
- })
24031
- ]
24032
- }),
24033
- summary && /* @__PURE__ */ jsx("p", {
24034
- className: "text-foreground-subtle text-[10px] mt-0.5 ml-5 truncate",
24035
- children: summary
24036
- }),
24037
- expanded && /* @__PURE__ */ jsx("div", {
24038
- className: "mt-1 ml-5 text-[9px] bg-surface rounded px-2 py-1.5 space-y-0.5 font-mono overflow-x-auto",
24039
- children: fields.length === 0 ? /* @__PURE__ */ jsx("div", {
24040
- className: "text-foreground-subtle italic",
24041
- children: "empty slice"
24042
- }) : fields.map(([k, v]) => /* @__PURE__ */ jsxs("div", { children: [
24043
- /* @__PURE__ */ jsx("span", {
24044
- className: "text-primary/70",
24045
- children: k
24046
- }),
24047
- /* @__PURE__ */ jsx("span", {
24048
- className: "text-foreground-subtle",
24049
- children: ": "
24050
- }),
24051
- /* @__PURE__ */ jsx("span", {
24052
- className: "text-foreground break-all",
24053
- children: formatValue(v)
24054
- })
24055
- ] }, k))
24056
- })
24057
- ]
24058
- })
24059
- ]
24060
- }, entry.id);
25219
+ !isLoading && capStates.size > 0 && filteredCapNames.length === 0 && /* @__PURE__ */ jsx("div", {
25220
+ className: "py-4 text-center text-foreground-subtle font-sans",
25221
+ children: "No caps match the filter"
25222
+ }),
25223
+ filteredCapNames.length > 0 && /* @__PURE__ */ jsx("div", {
25224
+ className: "py-1",
25225
+ children: filteredCapNames.map((capName) => {
25226
+ const state = capStates.get(capName);
25227
+ if (!state) return null;
25228
+ return /* @__PURE__ */ jsx(CapNode, {
25229
+ capName,
25230
+ slice: state.slice,
25231
+ updatedAt: state.updatedAt,
25232
+ searchNeedle,
25233
+ isPathOpen,
25234
+ onToggle: toggleNode
25235
+ }, capName);
24061
25236
  })
24062
25237
  })
24063
25238
  ]
24064
25239
  })]
24065
25240
  });
24066
25241
  }
25242
+ /** Top-level tree node: one capability. Header carries the cap-name
25243
+ * pill + an "updated Ns ago" indicator; the body expands to the
25244
+ * slice's keys. */
25245
+ function CapNode({ capName, slice, updatedAt, searchNeedle, isPathOpen, onToggle }) {
25246
+ const path = capName;
25247
+ const open = isPathOpen(path, 0);
25248
+ const entries = Object.entries(slice);
25249
+ return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
25250
+ className: "flex items-center gap-1.5 px-3 py-1 hover:bg-surface-hover/30 cursor-pointer select-none",
25251
+ onClick: () => onToggle(path, open),
25252
+ children: [
25253
+ /* @__PURE__ */ jsx(Chevron, {
25254
+ open,
25255
+ hasChildren: entries.length > 0
25256
+ }),
25257
+ /* @__PURE__ */ jsx("span", {
25258
+ className: "inline-flex items-center gap-1 rounded-full border border-violet-500/30 bg-violet-500/15 text-violet-400 px-2 py-0.5 text-[9px] font-medium font-sans",
25259
+ children: capName
25260
+ }),
25261
+ /* @__PURE__ */ jsxs("span", {
25262
+ className: "text-[9px] text-foreground-subtle font-sans",
25263
+ children: [
25264
+ entries.length,
25265
+ " ",
25266
+ entries.length === 1 ? "key" : "keys"
25267
+ ]
25268
+ }),
25269
+ /* @__PURE__ */ jsxs("span", {
25270
+ className: "ml-auto text-[9px] text-foreground-subtle/70 font-sans tabular-nums",
25271
+ children: ["updated ", formatAge(updatedAt)]
25272
+ })
25273
+ ]
25274
+ }), open && /* @__PURE__ */ jsx("div", { children: entries.length === 0 ? /* @__PURE__ */ jsx(LeafEmpty, {
25275
+ depth: 1,
25276
+ label: "empty slice"
25277
+ }) : entries.map(([k, v]) => /* @__PURE__ */ jsx(TreeNode, {
25278
+ path: `${path}.${k}`,
25279
+ nodeKey: k,
25280
+ value: v,
25281
+ depth: 1,
25282
+ searchNeedle,
25283
+ isPathOpen,
25284
+ onToggle
25285
+ }, k)) })] });
25286
+ }
25287
+ /** Recursive tree node. A leaf renders `key: value`; an object or
25288
+ * array renders a collapsible branch whose children recurse one
25289
+ * level deeper. */
25290
+ function TreeNode({ path, nodeKey, value, depth, searchNeedle, isPathOpen, onToggle }) {
25291
+ const branch = asBranch(value);
25292
+ const indentStyle = { paddingLeft: `${12 + depth * 14}px` };
25293
+ if (!branch) return /* @__PURE__ */ jsxs("div", {
25294
+ className: "flex items-start gap-1 px-3 py-0.5",
25295
+ style: indentStyle,
25296
+ children: [
25297
+ /* @__PURE__ */ jsx("span", {
25298
+ className: cn("shrink-0", highlightCls(nodeKey, searchNeedle, "text-primary/70")),
25299
+ children: nodeKey
25300
+ }),
25301
+ /* @__PURE__ */ jsx("span", {
25302
+ className: "text-foreground-subtle",
25303
+ children: ":"
25304
+ }),
25305
+ /* @__PURE__ */ jsx("span", {
25306
+ className: cn("break-all", highlightCls(formatValue(value), searchNeedle, valueCls(value))),
25307
+ children: formatValue(value)
25308
+ })
25309
+ ]
25310
+ });
25311
+ const open = isPathOpen(path, depth);
25312
+ return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
25313
+ className: "flex items-center gap-1 px-3 py-0.5 hover:bg-surface-hover/30 cursor-pointer select-none",
25314
+ style: indentStyle,
25315
+ onClick: () => onToggle(path, open),
25316
+ children: [
25317
+ /* @__PURE__ */ jsx(Chevron, {
25318
+ open,
25319
+ hasChildren: branch.entries.length > 0
25320
+ }),
25321
+ /* @__PURE__ */ jsx("span", {
25322
+ className: highlightCls(nodeKey, searchNeedle, "text-primary/70"),
25323
+ children: nodeKey
25324
+ }),
25325
+ /* @__PURE__ */ jsx("span", {
25326
+ className: "text-foreground-subtle/70 text-[9px]",
25327
+ children: branch.summary
25328
+ })
25329
+ ]
25330
+ }), open && (branch.entries.length === 0 ? /* @__PURE__ */ jsx(LeafEmpty, {
25331
+ depth: depth + 1,
25332
+ label: branch.kind === "array" ? "empty array" : "empty object"
25333
+ }) : branch.entries.map(([k, v]) => /* @__PURE__ */ jsx(TreeNode, {
25334
+ path: `${path}.${k}`,
25335
+ nodeKey: k,
25336
+ value: v,
25337
+ depth: depth + 1,
25338
+ searchNeedle,
25339
+ isPathOpen,
25340
+ onToggle
25341
+ }, k)))] });
25342
+ }
25343
+ /** Placeholder row for an object/array/slice with no entries. */
25344
+ function LeafEmpty({ depth, label }) {
25345
+ return /* @__PURE__ */ jsx("div", {
25346
+ className: "px-3 py-0.5 text-foreground-subtle/60 italic",
25347
+ style: { paddingLeft: `${12 + depth * 14}px` },
25348
+ children: label
25349
+ });
25350
+ }
25351
+ /** Expand/collapse chevron. Renders a fixed-width spacer when the node
25352
+ * has no children so leaf and branch keys stay column-aligned. */
25353
+ function Chevron({ open, hasChildren }) {
25354
+ if (!hasChildren) return /* @__PURE__ */ jsx("span", { className: "inline-block w-3 shrink-0" });
25355
+ return open ? /* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3 text-foreground-subtle shrink-0" }) : /* @__PURE__ */ jsx(ChevronRight, { className: "h-3 w-3 text-foreground-subtle shrink-0" });
25356
+ }
25357
+ /** Classify a value as an expandable branch (plain object or array)
25358
+ * or `null` for a leaf. Arrays expose index → element entries. */
25359
+ function asBranch(value) {
25360
+ if (Array.isArray(value)) return {
25361
+ kind: "array",
25362
+ entries: value.map((v, i) => [String(i), v]),
25363
+ summary: `[${value.length}]`
25364
+ };
25365
+ if (value && typeof value === "object") {
25366
+ const entries = Object.entries(value);
25367
+ return {
25368
+ kind: "object",
25369
+ entries,
25370
+ summary: `{${entries.length}}`
25371
+ };
25372
+ }
25373
+ return null;
25374
+ }
24067
25375
  function formatValue(v) {
24068
25376
  if (v === null) return "null";
24069
25377
  if (v === void 0) return "undefined";
@@ -24075,9 +25383,46 @@ function formatValue(v) {
24075
25383
  return String(v);
24076
25384
  }
24077
25385
  }
25386
+ /** Tailwind colour for a leaf value by JS type. */
25387
+ function valueCls(v) {
25388
+ if (v === null || v === void 0) return "text-foreground-subtle/60";
25389
+ if (typeof v === "number") return "text-amber-400";
25390
+ if (typeof v === "boolean") return v ? "text-emerald-400" : "text-rose-400";
25391
+ return "text-foreground";
25392
+ }
25393
+ /** Append a highlight background when `text` matches the live search
25394
+ * needle, so search hits are visible inside the tree. */
25395
+ function highlightCls(text, needle, base) {
25396
+ if (needle && text.toLowerCase().includes(needle)) return cn(base, "bg-violet-500/20 rounded px-0.5");
25397
+ return base;
25398
+ }
25399
+ /** Deep search a slice — true when `needle` appears in any key or any
25400
+ * stringified leaf value anywhere in the tree. */
25401
+ function sliceMatchesSearch(value, needle) {
25402
+ if (Array.isArray(value)) return value.some((v) => sliceMatchesSearch(v, needle));
25403
+ if (value && typeof value === "object") {
25404
+ for (const [k, v] of Object.entries(value)) {
25405
+ if (k.toLowerCase().includes(needle)) return true;
25406
+ if (sliceMatchesSearch(v, needle)) return true;
25407
+ }
25408
+ return false;
25409
+ }
25410
+ return formatValue(value).toLowerCase().includes(needle);
25411
+ }
25412
+ /** Human "Ns ago" / "Nm ago" age label for the per-cap freshness
25413
+ * indicator. */
25414
+ function formatAge(updatedAt) {
25415
+ const deltaMs = Date.now() - updatedAt;
25416
+ if (deltaMs < 5e3) return "just now";
25417
+ const secs = Math.floor(deltaMs / 1e3);
25418
+ if (secs < 60) return `${secs}s ago`;
25419
+ const mins = Math.floor(secs / 60);
25420
+ if (mins < 60) return `${mins}m ago`;
25421
+ return `${Math.floor(mins / 60)}h ago`;
25422
+ }
24078
25423
  function ScopeBadge({ label, value }) {
24079
25424
  return /* @__PURE__ */ jsxs("span", {
24080
- className: "text-[9px] px-1.5 py-0.5 rounded-full bg-primary/10 text-primary font-medium",
25425
+ className: "text-[9px] px-1.5 py-0.5 rounded-full bg-primary/10 text-primary font-medium font-sans",
24081
25426
  children: [
24082
25427
  label,
24083
25428
  ": ",
@@ -24086,16 +25431,13 @@ function ScopeBadge({ label, value }) {
24086
25431
  });
24087
25432
  }
24088
25433
  /**
24089
- * Cap-name multiselect popover — same shape as
24090
- * `CategoryFilterPopover` in event-stream.tsx (search box at top,
24091
- * Select All / None / Defaults buttons, scrollable checkbox list).
24092
- * Caps don't have icons / styled badges like events do — we render
24093
- * a flat name + violet-pill bracket so the look stays consistent
24094
- * with the rest of the StateValuesStream rows.
25434
+ * Cap-name multiselect popover — same shape as `CategoryFilterPopover`
25435
+ * in event-stream.tsx (search box at top, Select All / None / Defaults
25436
+ * buttons, scrollable checkbox list).
24095
25437
  */
24096
25438
  function CapFilterPopover({ search, onSearchChange, caps, active, onToggle, onSelectAll, onSelectNone, onResetDefaults }) {
24097
25439
  return /* @__PURE__ */ jsxs("div", {
24098
- className: "absolute z-20 top-full mt-1 left-0 w-72 max-h-80 rounded-md border border-border bg-surface shadow-lg flex flex-col",
25440
+ className: "absolute z-20 top-full mt-1 left-0 w-72 max-h-80 rounded-md border border-border bg-surface shadow-lg flex flex-col font-sans",
24099
25441
  onClick: (e) => e.stopPropagation(),
24100
25442
  children: [
24101
25443
  /* @__PURE__ */ jsxs("div", {
@@ -24166,24 +25508,6 @@ function CapFilterPopover({ search, onSearchChange, caps, active, onToggle, onSe
24166
25508
  ]
24167
25509
  });
24168
25510
  }
24169
- /**
24170
- * Per-row Copy icon — same shape used in log-stream / event-stream
24171
- * so the three live-streams' rows are visually symmetric. Stops
24172
- * propagation so the click doesn't toggle the row's expand state.
24173
- * Flashes a Check for ~1s on success.
24174
- */
24175
- function RowCopyButton({ copied, onCopy }) {
24176
- return /* @__PURE__ */ jsx("button", {
24177
- type: "button",
24178
- title: "Copy this state change",
24179
- onClick: (e) => {
24180
- e.stopPropagation();
24181
- onCopy();
24182
- },
24183
- className: cn("rounded p-0.5 transition-colors mt-0.5", copied ? "text-emerald-400" : "text-foreground-subtle/60 hover:text-foreground hover:bg-surface-hover"),
24184
- children: copied ? /* @__PURE__ */ jsx(Check, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(Copy, { className: "h-3 w-3" })
24185
- });
24186
- }
24187
25511
  //#endregion
24188
25512
  //#region src/composites/device-activity-panel.tsx
24189
25513
  /**
@@ -24262,303 +25586,99 @@ function DeviceActivityPanel({ deviceId, defaultEventCategories, defaultStateCap
24262
25586
  showFilters: false,
24263
25587
  liveBuffer: logsBuffer
24264
25588
  }),
24265
- tab === "events" && /* @__PURE__ */ jsx(EventStream, {
24266
- deviceId,
24267
- defaultCategories: defaultEventCategories,
24268
- maxHeight,
24269
- showCategoryFilter: true,
24270
- liveBuffer: eventsBuffer
24271
- }),
24272
- tab === "state" && /* @__PURE__ */ jsx(StateValuesStream, {
24273
- deviceId,
24274
- defaultCaps: defaultStateCaps,
24275
- maxHeight,
24276
- liveBuffer: stateBuffer
24277
- })
24278
- ]
24279
- })]
24280
- });
24281
- }
24282
- //#endregion
24283
- //#region src/composites/confirm-action-button.tsx
24284
- /**
24285
- * ConfirmActionButton — destructive-action button with a modal confirm.
24286
- *
24287
- * Two-step UX for any operation operators shouldn't trigger by accident:
24288
- * reboot device, restart addon, regenerate token, etc. The trigger is a
24289
- * normal Button that opens a Dialog; confirming runs the async action,
24290
- * the trigger spinner-disables itself for the duration, and the dialog
24291
- * closes on success. Errors surface inline at the dialog's bottom.
24292
- *
24293
- * Generic on `TResult` so callers don't have to discard the return
24294
- * value. Pass an `icon` to render it inside the trigger (e.g. RotateCw
24295
- * for reboot, RefreshCw for restart).
24296
- */
24297
- function ConfirmActionButton({ label, icon: Icon, title = "Confirm action", description, confirmLabel, triggerVariant = "outline", confirmVariant = "danger", size = "sm", disabled, className, action, onSuccess }) {
24298
- const [open, setOpen] = useState(false);
24299
- const [running, setRunning] = useState(false);
24300
- const [error, setError] = useState(null);
24301
- const handleConfirm = async () => {
24302
- setError(null);
24303
- setRunning(true);
24304
- try {
24305
- const result = await action();
24306
- onSuccess?.(result);
24307
- setOpen(false);
24308
- } catch (err) {
24309
- setError(err instanceof Error ? err.message : String(err));
24310
- } finally {
24311
- setRunning(false);
24312
- }
24313
- };
24314
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(Button, {
24315
- type: "button",
24316
- variant: triggerVariant,
24317
- size,
24318
- disabled,
24319
- onClick: () => {
24320
- setError(null);
24321
- setOpen(true);
24322
- },
24323
- className,
24324
- children: [Icon && /* @__PURE__ */ jsx(Icon, { className: "h-3.5 w-3.5" }), label]
24325
- }), /* @__PURE__ */ jsx(Dialog, {
24326
- open,
24327
- onOpenChange: (next) => !running && setOpen(next),
24328
- children: /* @__PURE__ */ jsxs(DialogContent, { children: [
24329
- /* @__PURE__ */ jsxs(DialogHeader, { children: [/* @__PURE__ */ jsxs(DialogTitle, {
24330
- className: "flex items-center gap-2",
24331
- children: [/* @__PURE__ */ jsx(TriangleAlert, { className: "h-4 w-4 text-amber-400" }), title]
24332
- }), typeof description === "string" ? /* @__PURE__ */ jsx(DialogDescription, { children: description }) : description] }),
24333
- error && /* @__PURE__ */ jsx("div", {
24334
- className: cn("mt-2 px-3 py-2 rounded border border-danger/40 bg-danger/10", "text-xs text-danger"),
24335
- children: error
24336
- }),
24337
- /* @__PURE__ */ jsxs(DialogFooter, { children: [/* @__PURE__ */ jsx(Button, {
24338
- type: "button",
24339
- variant: "ghost",
24340
- size,
24341
- disabled: running,
24342
- onClick: () => setOpen(false),
24343
- children: "Cancel"
24344
- }), /* @__PURE__ */ jsxs(Button, {
24345
- type: "button",
24346
- variant: confirmVariant,
24347
- size,
24348
- disabled: running,
24349
- onClick: () => {
24350
- handleConfirm();
24351
- },
24352
- children: [running && /* @__PURE__ */ jsx(LoaderCircle, { className: "h-3.5 w-3.5 animate-spin" }), confirmLabel ?? label]
24353
- })] })
24354
- ] })
24355
- })] });
24356
- }
24357
- //#endregion
24358
- //#region src/composites/ptz-overlay.tsx
24359
- /**
24360
- * PTZOverlay — pan / tilt / zoom controls.
24361
- *
24362
- * Two visual variants driven by `mode`:
24363
- * - `'overlay'` (default): translucent dark pill positioned bottom-
24364
- * right of the camera viewport. Used as `extraOverlay` on the
24365
- * `CameraStreamPlayer` so operators can drive PTZ without leaving
24366
- * the live view. Opaque background + subtle ring keeps the d-pad
24367
- * legible against any frame.
24368
- * - `'panel'`: full-bleed inside a host container (e.g. the floating
24369
- * PTZ panel in DeviceDetail). No absolute positioning, no inner
24370
- * wrapper card — the host's chrome is the only frame. Inherits the
24371
- * surrounding `bg-surface` so the dark theme reads consistently
24372
- * instead of the previous always-dark-bubble look.
24373
- *
24374
- * Interaction model is identical across modes: short tap fires a
24375
- * discrete pulse (`move`); long press starts continuous motion until
24376
- * release (`startContinuous` + `stopContinuous` on pointer up).
24377
- */
24378
- function DPadButton({ direction, icon: Icon, disabled, className, variant, onMove, onStart, onStop }) {
24379
- const [pressedAt, setPressedAt] = useState(null);
24380
- const [continuous, setContinuous] = useState(false);
24381
- const handlePointerDown = useCallback((e) => {
24382
- if (disabled) return;
24383
- e.currentTarget.setPointerCapture(e.pointerId);
24384
- setPressedAt(Date.now());
24385
- setContinuous(false);
24386
- const timer = setTimeout(() => {
24387
- setContinuous(true);
24388
- onStart(direction);
24389
- }, 250);
24390
- e.currentTarget.dataset.timer = String(timer);
24391
- }, [
24392
- direction,
24393
- disabled,
24394
- onStart
24395
- ]);
24396
- const handlePointerUp = useCallback((e) => {
24397
- const timerId = Number(e.currentTarget.dataset.timer);
24398
- if (timerId) clearTimeout(timerId);
24399
- e.currentTarget.dataset.timer = "";
24400
- if (continuous) onStop();
24401
- else if (pressedAt !== null) onMove(direction);
24402
- setPressedAt(null);
24403
- setContinuous(false);
24404
- }, [
24405
- continuous,
24406
- direction,
24407
- onMove,
24408
- onStop,
24409
- pressedAt
24410
- ]);
24411
- const handlePointerCancel = useCallback((e) => {
24412
- const timerId = Number(e.currentTarget.dataset.timer);
24413
- if (timerId) clearTimeout(timerId);
24414
- e.currentTarget.dataset.timer = "";
24415
- if (continuous) onStop();
24416
- setPressedAt(null);
24417
- setContinuous(false);
24418
- }, [continuous, onStop]);
24419
- const sizeClass = variant === "panel" ? "h-9 w-9" : "h-7 w-7";
24420
- const iconSizeClass = variant === "panel" ? "h-4 w-4" : "h-3.5 w-3.5";
24421
- return /* @__PURE__ */ jsx("button", {
24422
- type: "button",
24423
- disabled,
24424
- onPointerDown: handlePointerDown,
24425
- onPointerUp: handlePointerUp,
24426
- onPointerCancel: handlePointerCancel,
24427
- className: cn("flex items-center justify-center rounded transition-colors disabled:opacity-40", variant === "panel" ? "text-foreground hover:bg-surface-hover active:bg-surface-hover/80" : "text-white hover:bg-white/15 active:bg-white/25", sizeClass, className),
24428
- title: direction,
24429
- children: /* @__PURE__ */ jsx(Icon, { className: iconSizeClass })
24430
- });
24431
- }
24432
- function PTZOverlay({ controls, mode = "overlay", showPresets, showZoom = true, showHome = true, className }) {
24433
- const { move, startContinuous, stopContinuous, zoom, goHome, goToPreset, presets, busy, error } = controls;
24434
- const [presetsOpen, setPresetsOpen] = useState(false);
24435
- const presetsVisible = (showPresets ?? presets.length > 0) && presets.length > 0;
24436
- const isPanel = mode === "panel";
24437
- const containerClass = isPanel ? "flex flex-col items-stretch gap-2 w-full h-full p-3" : "pointer-events-auto absolute top-3 right-3 z-10 flex flex-col items-end gap-2";
24438
- const rowClass = isPanel ? cn("flex items-center gap-3 rounded-lg p-2", busy && "ring-1 ring-primary/40") : cn("flex items-center gap-2 rounded-lg border border-white/30 bg-zinc-900/90 backdrop-blur-md p-2 shadow-lg shadow-black/40", busy && "ring-1 ring-primary/40");
24439
- const sideButtonSize = isPanel ? "h-9 w-9" : "h-7 w-7";
24440
- const sideIconSize = isPanel ? "h-4 w-4" : "h-3.5 w-3.5";
24441
- const sideButtonHover = isPanel ? "text-foreground hover:bg-surface-hover" : "text-white hover:bg-white/15";
24442
- const sepClass = isPanel ? "h-12 w-px bg-border mx-1" : "h-12 w-px bg-white/15 mx-1";
24443
- return /* @__PURE__ */ jsxs("div", {
24444
- className: cn(containerClass, className),
24445
- children: [error && /* @__PURE__ */ jsxs("div", {
24446
- className: "rounded bg-danger/90 px-2 py-1 text-[10px] font-medium text-white shadow-lg max-w-[200px] self-center",
24447
- children: ["PTZ: ", error]
24448
- }), /* @__PURE__ */ jsxs("div", {
24449
- className: cn(rowClass, isPanel && "justify-center"),
24450
- children: [
24451
- /* @__PURE__ */ jsxs("div", {
24452
- className: "grid grid-cols-3 gap-0.5",
24453
- children: [
24454
- /* @__PURE__ */ jsx("span", {
24455
- "aria-hidden": true,
24456
- className: isPanel ? "h-9 w-9" : "h-7 w-7"
24457
- }),
24458
- /* @__PURE__ */ jsx(DPadButton, {
24459
- direction: "up",
24460
- icon: ArrowUp,
24461
- variant: mode,
24462
- onMove: move,
24463
- onStart: startContinuous,
24464
- onStop: stopContinuous
24465
- }),
24466
- /* @__PURE__ */ jsx("span", {
24467
- "aria-hidden": true,
24468
- className: isPanel ? "h-9 w-9" : "h-7 w-7"
24469
- }),
24470
- /* @__PURE__ */ jsx(DPadButton, {
24471
- direction: "left",
24472
- icon: ArrowLeft,
24473
- variant: mode,
24474
- onMove: move,
24475
- onStart: startContinuous,
24476
- onStop: stopContinuous
24477
- }),
24478
- /* @__PURE__ */ jsx("span", {
24479
- "aria-hidden": true,
24480
- className: isPanel ? "h-9 w-9" : "h-7 w-7"
24481
- }),
24482
- /* @__PURE__ */ jsx(DPadButton, {
24483
- direction: "right",
24484
- icon: ArrowRight,
24485
- variant: mode,
24486
- onMove: move,
24487
- onStart: startContinuous,
24488
- onStop: stopContinuous
24489
- }),
24490
- /* @__PURE__ */ jsx("span", {
24491
- "aria-hidden": true,
24492
- className: isPanel ? "h-9 w-9" : "h-7 w-7"
24493
- }),
24494
- /* @__PURE__ */ jsx(DPadButton, {
24495
- direction: "down",
24496
- icon: ArrowDown,
24497
- variant: mode,
24498
- onMove: move,
24499
- onStart: startContinuous,
24500
- onStop: stopContinuous
24501
- }),
24502
- /* @__PURE__ */ jsx("span", {
24503
- "aria-hidden": true,
24504
- className: isPanel ? "h-9 w-9" : "h-7 w-7"
24505
- })
24506
- ]
24507
- }),
24508
- (showZoom || showHome) && /* @__PURE__ */ jsx("div", { className: sepClass }),
24509
- /* @__PURE__ */ jsxs("div", {
24510
- className: "flex flex-col gap-0.5",
24511
- children: [showZoom && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("button", {
24512
- type: "button",
24513
- onClick: () => zoom("in"),
24514
- disabled: busy,
24515
- title: "Zoom in",
24516
- className: cn("flex items-center justify-center rounded disabled:opacity-40", sideButtonSize, sideButtonHover),
24517
- children: /* @__PURE__ */ jsx(ZoomIn, { className: sideIconSize })
24518
- }), /* @__PURE__ */ jsx("button", {
24519
- type: "button",
24520
- onClick: () => zoom("out"),
24521
- disabled: busy,
24522
- title: "Zoom out",
24523
- className: cn("flex items-center justify-center rounded disabled:opacity-40", sideButtonSize, sideButtonHover),
24524
- children: /* @__PURE__ */ jsx(ZoomOut, { className: sideIconSize })
24525
- })] }), showHome && /* @__PURE__ */ jsx("button", {
24526
- type: "button",
24527
- onClick: () => goHome(),
24528
- disabled: busy,
24529
- title: "Go home",
24530
- className: cn("flex items-center justify-center rounded disabled:opacity-40", sideButtonSize, sideButtonHover),
24531
- children: /* @__PURE__ */ jsx(House, { className: sideIconSize })
24532
- })]
25589
+ tab === "events" && /* @__PURE__ */ jsx(EventStream, {
25590
+ deviceId,
25591
+ defaultCategories: defaultEventCategories,
25592
+ maxHeight,
25593
+ showCategoryFilter: true,
25594
+ liveBuffer: eventsBuffer
24533
25595
  }),
24534
- presetsVisible && /* @__PURE__ */ jsxs("div", {
24535
- className: "relative",
24536
- children: [/* @__PURE__ */ jsxs("button", {
24537
- type: "button",
24538
- onClick: () => setPresetsOpen((v) => !v),
24539
- disabled: busy,
24540
- className: cn("flex items-center gap-1 rounded px-2 text-[10px] disabled:opacity-40", isPanel ? "h-9 text-foreground hover:bg-surface-hover" : "h-7 text-white hover:bg-white/15"),
24541
- title: "Presets",
24542
- children: ["Presets", /* @__PURE__ */ jsx(ChevronDown, { className: cn("h-3 w-3 transition-transform", presetsOpen && "rotate-180") })]
24543
- }), presetsOpen && /* @__PURE__ */ jsx("div", {
24544
- className: cn("absolute right-0 min-w-[140px] rounded-lg shadow-xl py-1 z-20", isPanel ? "bottom-full mb-1 bg-surface border border-border" : "top-full mt-1 border border-white/30 bg-zinc-900/95 backdrop-blur-md"),
24545
- children: presets.map((p) => /* @__PURE__ */ jsx("button", {
24546
- type: "button",
24547
- onClick: () => {
24548
- goToPreset(p.id);
24549
- setPresetsOpen(false);
24550
- },
24551
- disabled: busy,
24552
- className: cn("block w-full px-3 py-1.5 text-left text-[10px] disabled:opacity-40", isPanel ? "text-foreground hover:bg-surface-hover" : "text-white hover:bg-white/15"),
24553
- children: p.name || p.id
24554
- }, p.id))
24555
- })]
25596
+ tab === "state" && /* @__PURE__ */ jsx(StateValuesStream, {
25597
+ deviceId,
25598
+ defaultCaps: defaultStateCaps,
25599
+ maxHeight,
25600
+ liveBuffer: stateBuffer
24556
25601
  })
24557
25602
  ]
24558
25603
  })]
24559
25604
  });
24560
25605
  }
24561
25606
  //#endregion
25607
+ //#region src/composites/confirm-action-button.tsx
25608
+ /**
25609
+ * ConfirmActionButton — destructive-action button with a modal confirm.
25610
+ *
25611
+ * Two-step UX for any operation operators shouldn't trigger by accident:
25612
+ * reboot device, restart addon, regenerate token, etc. The trigger is a
25613
+ * normal Button that opens a Dialog; confirming runs the async action,
25614
+ * the trigger spinner-disables itself for the duration, and the dialog
25615
+ * closes on success. Errors surface inline at the dialog's bottom.
25616
+ *
25617
+ * Generic on `TResult` so callers don't have to discard the return
25618
+ * value. Pass an `icon` to render it inside the trigger (e.g. RotateCw
25619
+ * for reboot, RefreshCw for restart).
25620
+ */
25621
+ function ConfirmActionButton({ label, icon: Icon, title = "Confirm action", description, confirmLabel, triggerVariant = "outline", confirmVariant = "danger", size = "sm", disabled, className, action, onSuccess }) {
25622
+ const [open, setOpen] = useState(false);
25623
+ const [running, setRunning] = useState(false);
25624
+ const [error, setError] = useState(null);
25625
+ const handleConfirm = async () => {
25626
+ setError(null);
25627
+ setRunning(true);
25628
+ try {
25629
+ const result = await action();
25630
+ onSuccess?.(result);
25631
+ setOpen(false);
25632
+ } catch (err) {
25633
+ setError(err instanceof Error ? err.message : String(err));
25634
+ } finally {
25635
+ setRunning(false);
25636
+ }
25637
+ };
25638
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(Button, {
25639
+ type: "button",
25640
+ variant: triggerVariant,
25641
+ size,
25642
+ disabled,
25643
+ onClick: () => {
25644
+ setError(null);
25645
+ setOpen(true);
25646
+ },
25647
+ className,
25648
+ children: [Icon && /* @__PURE__ */ jsx(Icon, { className: "h-3.5 w-3.5" }), label]
25649
+ }), /* @__PURE__ */ jsx(Dialog, {
25650
+ open,
25651
+ onOpenChange: (next) => !running && setOpen(next),
25652
+ children: /* @__PURE__ */ jsxs(DialogContent, { children: [
25653
+ /* @__PURE__ */ jsxs(DialogHeader, { children: [/* @__PURE__ */ jsxs(DialogTitle, {
25654
+ className: "flex items-center gap-2",
25655
+ children: [/* @__PURE__ */ jsx(TriangleAlert, { className: "h-4 w-4 text-amber-400" }), title]
25656
+ }), typeof description === "string" ? /* @__PURE__ */ jsx(DialogDescription, { children: description }) : description] }),
25657
+ error && /* @__PURE__ */ jsx("div", {
25658
+ className: cn("mt-2 px-3 py-2 rounded border border-danger/40 bg-danger/10", "text-xs text-danger"),
25659
+ children: error
25660
+ }),
25661
+ /* @__PURE__ */ jsxs(DialogFooter, { children: [/* @__PURE__ */ jsx(Button, {
25662
+ type: "button",
25663
+ variant: "ghost",
25664
+ size,
25665
+ disabled: running,
25666
+ onClick: () => setOpen(false),
25667
+ children: "Cancel"
25668
+ }), /* @__PURE__ */ jsxs(Button, {
25669
+ type: "button",
25670
+ variant: confirmVariant,
25671
+ size,
25672
+ disabled: running,
25673
+ onClick: () => {
25674
+ handleConfirm();
25675
+ },
25676
+ children: [running && /* @__PURE__ */ jsx(LoaderCircle, { className: "h-3.5 w-3.5 animate-spin" }), confirmLabel ?? label]
25677
+ })] })
25678
+ ] })
25679
+ })] });
25680
+ }
25681
+ //#endregion
24562
25682
  //#region src/composites/snapshot-button.tsx
24563
25683
  /**
24564
25684
  * SnapshotButton — operator-facing manual snapshot trigger.
@@ -25465,169 +26585,6 @@ function useDeviceWebrtc(trpc, deviceId, pollIntervalMs = 5e3) {
25465
26585
  };
25466
26586
  }
25467
26587
  //#endregion
25468
- //#region src/hooks/use-ptz.ts
25469
- /**
25470
- * usePTZ — PTZ control hook for device-scoped pan / tilt / zoom.
25471
- *
25472
- * Wraps the `ptz` capability methods through the canonical
25473
- * `useDeviceProxy` surface — every call goes through
25474
- * `dev.ptz?.<method>(...)` with deviceId/nodeId auto-injected. The
25475
- * hook stays bare-bones around state (`busy`, `error`, `presets`)
25476
- * so the operator-facing component (PTZOverlay) stays trivial.
25477
- *
25478
- * Returns:
25479
- * - `move(direction)`: discrete one-shot move (cap.move + cap.stop).
25480
- * Use for short pulse moves triggered by tapping a d-pad button.
25481
- * - `startContinuous(direction)` / `stopContinuous()`: gesture-driven
25482
- * continuous motion (`cap.continuousMove` + `cap.stop`). Use for
25483
- * long-press handlers on touch / mouse-down handlers on desktop.
25484
- * - `zoom('in' | 'out')`: discrete zoom step.
25485
- * - `goHome()`: jump to preset 0.
25486
- * - `presets` + `goToPreset(presetId)`: list and jump to named presets.
25487
- *
25488
- * Parametrised by the same `UseDeviceProxyTrpc` shape every other
25489
- * device hook uses — works under admin-ui (BackendClient.trpc) and
25490
- * addon pages (AddonPageProps.trpc) alike.
25491
- */
25492
- var DIRECTION_VECTORS = {
25493
- "up": {
25494
- pan: 0,
25495
- tilt: 1
25496
- },
25497
- "down": {
25498
- pan: 0,
25499
- tilt: -1
25500
- },
25501
- "left": {
25502
- pan: -1,
25503
- tilt: 0
25504
- },
25505
- "right": {
25506
- pan: 1,
25507
- tilt: 0
25508
- },
25509
- "up-left": {
25510
- pan: -1,
25511
- tilt: 1
25512
- },
25513
- "up-right": {
25514
- pan: 1,
25515
- tilt: 1
25516
- },
25517
- "down-left": {
25518
- pan: -1,
25519
- tilt: -1
25520
- },
25521
- "down-right": {
25522
- pan: 1,
25523
- tilt: -1
25524
- }
25525
- };
25526
- function usePTZ(trpc, deviceId, options) {
25527
- const defaultSpeed = options?.defaultSpeed ?? .5;
25528
- const pulseMs = options?.pulseMs ?? 250;
25529
- const enabled = options?.enabled ?? true;
25530
- const ptz = useDeviceProxy(trpc, enabled ? deviceId : null)?.ptz;
25531
- const [presets, setPresets] = useState([]);
25532
- const [busy, setBusy] = useState(false);
25533
- const [error, setError] = useState(null);
25534
- const refreshPresets = useCallback(async () => {
25535
- if (!enabled || !ptz) return;
25536
- try {
25537
- setPresets(await ptz.getPresets({}));
25538
- } catch (err) {
25539
- const msg = err instanceof Error ? err.message : String(err);
25540
- if (msg.includes("provider not available") || msg.includes("no 'ptz' binding")) return;
25541
- setError(msg);
25542
- }
25543
- }, [ptz, enabled]);
25544
- useEffect(() => {
25545
- refreshPresets();
25546
- }, [refreshPresets]);
25547
- const wrap = useCallback(async (fn) => {
25548
- if (!enabled || !ptz) return void 0;
25549
- setError(null);
25550
- setBusy(true);
25551
- try {
25552
- return await fn();
25553
- } catch (err) {
25554
- setError(err instanceof Error ? err.message : String(err));
25555
- return;
25556
- } finally {
25557
- setBusy(false);
25558
- }
25559
- }, [enabled, ptz]);
25560
- return {
25561
- move: useCallback(async (direction, speed) => {
25562
- if (!ptz) return;
25563
- const v = DIRECTION_VECTORS[direction];
25564
- const s = speed ?? defaultSpeed;
25565
- await wrap(async () => {
25566
- await ptz.move({
25567
- pan: v.pan,
25568
- tilt: v.tilt,
25569
- speed: s
25570
- });
25571
- await new Promise((r) => setTimeout(r, pulseMs));
25572
- await ptz.stop({});
25573
- });
25574
- }, [
25575
- ptz,
25576
- defaultSpeed,
25577
- pulseMs,
25578
- wrap
25579
- ]),
25580
- startContinuous: useCallback(async (direction, speed) => {
25581
- if (!ptz) return;
25582
- const v = DIRECTION_VECTORS[direction];
25583
- const s = speed ?? defaultSpeed;
25584
- await wrap(() => ptz.continuousMove({
25585
- pan: v.pan,
25586
- tilt: v.tilt,
25587
- speed: s
25588
- }));
25589
- }, [
25590
- ptz,
25591
- defaultSpeed,
25592
- wrap
25593
- ]),
25594
- stopContinuous: useCallback(async () => {
25595
- if (!ptz) return;
25596
- await wrap(() => ptz.stop({}));
25597
- }, [ptz, wrap]),
25598
- zoom: useCallback(async (direction, speed) => {
25599
- if (!ptz) return;
25600
- const z = direction === "in" ? 1 : -1;
25601
- const s = speed ?? defaultSpeed;
25602
- await wrap(async () => {
25603
- await ptz.move({
25604
- zoom: z,
25605
- speed: s
25606
- });
25607
- await new Promise((r) => setTimeout(r, pulseMs));
25608
- await ptz.stop({});
25609
- });
25610
- }, [
25611
- ptz,
25612
- defaultSpeed,
25613
- pulseMs,
25614
- wrap
25615
- ]),
25616
- goHome: useCallback(async () => {
25617
- if (!ptz) return;
25618
- await wrap(() => ptz.goHome({}));
25619
- }, [ptz, wrap]),
25620
- goToPreset: useCallback(async (presetId) => {
25621
- if (!ptz) return;
25622
- await wrap(() => ptz.goToPreset({ presetId }));
25623
- }, [ptz, wrap]),
25624
- presets,
25625
- refreshPresets,
25626
- busy,
25627
- error
25628
- };
25629
- }
25630
- //#endregion
25631
26588
  //#region src/hooks/use-doorbell-events.ts
25632
26589
  /**
25633
26590
  * useDoorbellEvents — subscribe to doorbell.onPressed across the cluster.
@@ -25849,6 +26806,6 @@ function useEventInvalidation(queryKey, categories) {
25849
26806
  ]);
25850
26807
  }
25851
26808
  //#endregion
25852
- export { AddonGlobalSettingsForm, AgentStepEditor, AppShell, AudioClassificationList, AudioLevelWaveform, AudioWaveform, BTN_COMPACT, BTN_COMPACT_DANGER, BTN_COMPACT_PRIMARY, BTN_COMPACT_WARNING, Badge, BatteryBadge, BottomSheet, Breadcrumb, Button, CHIP_ACTIVE, CHIP_BASE, CHIP_INACTIVE, CLASS_COLORS, CameraStreamPlayer, Card, Checkbox, CodeBlock, CollapsibleCard, ConfigFormBuilder, FormField as ConfigFormField, ConfigSchemaField, ConfirmActionButton, ConfirmDialogProvider, CopyButton, CustomFieldRenderersProvider, DEFAULT_COLOR, DataTable, DetectionCanvas, DetectionOverlay, DetectionResultTree, DevShell, DeviceActivityPanel, DeviceCard, DeviceContextProvider, DeviceExportPanel, DeviceGrid, DeviceItem, DeviceList, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, DiscoveryPanel, DoorbellRecentPanel, Dropdown, DropdownContent, DropdownItem, DropdownTrigger, EmptyState, ErrorBox, EventStream, FilterBar, FloatingEventStream, FloatingLogStream, FloatingPanel, FormField$1 as FormField, GRID_GAP, GRID_PAIRED, GRID_QUICK_STATS, INPUT_COMPACT, IconButton, ImageSelector, InferenceConfigSelector, Input, KebabMenu, KeyValueList, LIST_ROW, Label, LogStream, LoginForm, MobileDrawer, NodeMultiSelectField, NodePicker, NodeSelectField, PHASE_CONFIG, PTZOverlay, PageHeader, PhaseIcon, PipelineBuilder, PipelineRuntimeSelector, PipelineStep, PipelineTreeMatrix, PlayerOverlaysProvider, Popover, PopoverContent, PopoverTrigger, ProviderBadge, QrCode, ResponseLog, SECTION_BODY, SECTION_CARD, SECTION_HEADER, SPLIT_PANEL_OUTER, SPLIT_PANEL_SIDE, STACK_GAP, ScopePicker, ScrollArea, Select, SemanticBadge, Separator, Sidebar, SidebarItem, Skeleton, SlideOverPanel, SnapshotButton, StatCard, StateValuesStream, StatusBadge, StepTimings, StepTreeMaster, StreamBrokerSelector, StreamPanel, Switch, SystemProvider, TEXT_FIELD_LABEL, TEXT_HINT, TEXT_METRIC, TEXT_SECTION_LABEL, TEXT_VALUE, Tabs, TabsContent, TabsList, TabsTrigger, ThemeProvider, Tooltip, TooltipContent, TooltipTrigger, VersionBadge, WidgetRegistryProvider, WidgetSlot, ZoneEditingProvider, buildStepTreeFromSchema, cn, createSharedContext, createTheme, darkColors, defaultTheme, deriveDeviceKind, ensureMfHostInit, getClassColor, getPhaseVisual, isFieldVisible, lightColors, mirror, mountAddonPage, providerIcons, statusIcons, themeToCss, trpc, useAccessoriesGetStatus, useAddonPagesListPages, useAddonSettingsGetDeviceSettings, useAddonSettingsGetGlobalSettings, useAddonSettingsUpdateDeviceSettings, useAddonSettingsUpdateGlobalSettings, useAddonWidgetsListWidgets, useAddonsApplyAutoUpdateToAll, useAddonsCustom, useAddonsForceRefresh, useAddonsGetAddonAutoUpdate, useAddonsGetAutoUpdateSettings, useAddonsGetLastRestart, useAddonsGetLogs, useAddonsGetVersions, useAddonsInstallFromWorkspace, useAddonsInstallPackage, useAddonsIsWorkspaceAvailable, useAddonsList, useAddonsListCapabilityProviders, useAddonsListFrameworkPackages, useAddonsListPackages, useAddonsListUpdates, useAddonsListWorkspacePackages, useAddonsOnAddonLogs, useAddonsReloadPackages, useAddonsRestartAddon, useAddonsRestartServer, useAddonsRetryLoad, useAddonsRollbackPackage, useAddonsSearchAvailable, useAddonsSetAddonAutoUpdate, useAddonsSetAutoUpdateSettings, useAddonsSetCapabilityProviderEnabled, useAddonsUninstallPackage, useAddonsUpdateFrameworkPackage, useAddonsUpdatePackage, useAlertsDismiss, useAlertsEmit, useAlertsGetUnreadCount, useAlertsList, useAlertsMarkAllRead, useAlertsMarkRead, useAlertsUpdate, useAllWidgets, useAudioAnalysisApplyDeviceSettingsPatch, useAudioAnalysisGetDeviceLiveContribution, useAudioAnalysisGetDeviceSettingsContribution, useAudioAnalysisResolveDeviceSettings, useAudioAnalyzerAnalyseChunk, useAudioAnalyzerClassify, useAudioAnalyzerDispose, useAudioAnalyzerIsReady, useAudioAnalyzerReprobeAudioEngine, useAudioCodecCanHandle, useAudioCodecCloseSession, useAudioCodecCreateDecodeSession, useAudioCodecCreateEncodeSession, useAudioCodecFlushEncode, useAudioCodecListActiveSessions, useAudioCodecListSupportedCodecs, useAudioCodecPullEncoded, useAudioCodecPullPcm, useAudioCodecPushEncodedFrame, useAudioCodecPushPcm, useAudioMetricsGetCurrentSnapshot, useAudioMetricsGetHistory, useBackupDelete, useBackupGetEntries, useBackupList, useBackupListArchives, useBackupListDestinations, useBackupListLocations, useBackupPreviewSchedule, useBackupRestore, useBackupTrigger, useBackupUpsertDestinationPolicy, useBatteryGetStatus, useBrightnessGetStatus, useBrightnessSetBrightness, useCameraCredentialsGetCredentials, useCameraCredentialsGetStatus, useCameraStreamsGetBrokerStreams, useCameraStreamsGetCameraStreams, useCameraStreamsGetRtspEntries, useClusterNodes, useConfirm, useCustomFieldRenderer, useDebouncedString, useDecoderCreateSession, useDecoderDestroySession, useDecoderGetInfo, useDecoderGetStats, useDecoderListActiveSessions, useDecoderOpenStream, useDecoderPullFrames, useDecoderPushPacket, useDecoderReprobeHwaccel, useDecoderSupportsCodec, useDecoderUpdateConfig, useDetectionPipelineApplyDeviceSettingsPatch, useDetectionPipelineGetDeviceLiveContribution, useDetectionPipelineGetDeviceSettingsContribution, useDevShell, useDevice, useDeviceBattery, useDeviceCapability, useDeviceDetections, useDeviceDiscoveryAdoptDevice, useDeviceDiscoveryGetStatus, useDeviceDiscoveryListDiscovered, useDeviceDiscoveryRefreshDiscovery, useDeviceDiscoveryReleaseDevice, useDeviceExportApplyDeviceSettingsPatch, useDeviceExportExposeDevice, useDeviceExportGetDeviceLiveContribution, useDeviceExportGetDeviceSettingsContribution, useDeviceExportGetStatus, useDeviceExportListExposedDevices, useDeviceExportListSupportedDeviceKinds, useDeviceExportUnexposeDevice, useDeviceId, useDeviceManagerAddLocation, useDeviceManagerAdoptDevice, useDeviceManagerAllocateDeviceId, useDeviceManagerCreateDevice, useDeviceManagerDisable, useDeviceManagerDiscoverDevices, useDeviceManagerEnable, useDeviceManagerGetAllBindings, useDeviceManagerGetBindings, useDeviceManagerGetChildren, useDeviceManagerGetConfigSchema, useDeviceManagerGetCreationSchema, useDeviceManagerGetDevice, useDeviceManagerGetDeviceAggregate, useDeviceManagerGetDeviceLiveInfoAggregate, useDeviceManagerGetDeviceSettingsAggregate, useDeviceManagerGetDeviceStatusAggregate, useDeviceManagerGetSettingsSchema, useDeviceManagerGetStreamProfileMap, useDeviceManagerGetStreamSources, useDeviceManagerListAll, useDeviceManagerListBindableCapsForDeviceType, useDeviceManagerListLocations, useDeviceManagerListPersistedByAddon, useDeviceManagerListWrappersForCap, useDeviceManagerLoadConfig, useDeviceManagerLoadMeta, useDeviceManagerLoadRuntimeState, useDeviceManagerPersistConfig, useDeviceManagerProbeStreams, useDeviceManagerRegisterDevice, useDeviceManagerRemove, useDeviceManagerRemoveDevice, useDeviceManagerRemoveLocation, useDeviceManagerSetDisabled, useDeviceManagerSetLocation, useDeviceManagerSetMetadata, useDeviceManagerSetName, useDeviceManagerSetStreamProfileMap, useDeviceManagerSetWrapperActive, useDeviceManagerTestCreationField, useDeviceManagerTestField, useDeviceManagerUpdateConfig, useDeviceManagerUpdateDeviceField, useDeviceManagerUpdateDeviceFieldsBatch, useDeviceOpsGetConfigEntries, useDeviceOpsGetSettingsSchema, useDeviceOpsGetStreamSources, useDeviceOpsRemoveDevice, useDeviceOpsSetConfig, useDeviceProviderAdoptDiscoveredDevice, useDeviceProviderCreateDevice, useDeviceProviderDiscoverDevices, useDeviceProviderGetChildCreationSchema, useDeviceProviderGetDevices, useDeviceProviderGetStatus, useDeviceProviderStart, useDeviceProviderStop, useDeviceProviderSupportsDiscovery, useDeviceProviderSupportsManualCreation, useDeviceProviderTestCreationField, useDeviceProxy, useDeviceSnapshot, useDeviceSnapshotImage, useDeviceState, useDeviceStateGetAllSnapshots, useDeviceStateGetCapSlice, useDeviceStateGetSnapshot, useDeviceStateSetCapSlice, useDeviceStateSlice, useDeviceStatusGetStatus, useDeviceWebrtc, useDevices, useDoorbellEvents, useDoorbellGetStatus, useEventInvalidation, useEventStreamLatest, useEventStreamMap, useEventsGetEventClipUrl, useEventsGetEventThumbnail, useEventsGetEvents, useFeatureProbeGetStatus, useIntegrationsCreate, useIntegrationsDelete, useIntegrationsGet, useIntegrationsGetAvailableTypes, useIntegrationsGetByAddonId, useIntegrationsGetSettings, useIntegrationsList, useIntegrationsSetSettings, useIntegrationsTestConnection, useIntegrationsUpdate, useIntercomEndTalkSession, useIntercomGetStatus, useIntercomHandleAnswer, useIntercomPushTalkPcm, useIntercomStartSession, useIntercomStartTalkSession, useIntercomStopSession, useIsMidWidth, useIsMobile, useLiveBuffer, useLiveEvent, useLocalNetworkGetAllowedAddresses, useLocalNetworkGetConnectionEndpoints, useLocalNetworkGetPreferred, useLocalNetworkList, useLocalNetworkResetAllowlistToBestMatch, useLocalNetworkSetAllowedAddresses, useMeshNetworkGetStatus, useMeshNetworkJoin, useMeshNetworkLeave, useMeshNetworkListPeers, useMeshNetworkLogout, useMeshNetworkStartLogin, useMeshNetworkTestConnection, useMetricsProviderCollectSnapshot, useMetricsProviderGetAddonStats, useMetricsProviderGetCached, useMetricsProviderGetCpuTemperature, useMetricsProviderGetCurrent, useMetricsProviderGetDiskSpace, useMetricsProviderGetGpuInfo, useMetricsProviderGetProcessStats, useMetricsProviderKillProcess, useMetricsProviderListAddonInstances, useMetricsProviderListNodeProcesses, useMotionDetectionAnalyze, useMotionDetectionApplyDeviceSettingsPatch, useMotionDetectionGetDeviceLiveContribution, useMotionDetectionGetDeviceSettingsContribution, useMotionDetectionRemoveCamera, useMotionDetectionReset, useMotionGetStatus, useMotionIsDetected, useMotionTriggerGetStatus, useMotionTriggerSetMotionTrigger, useMqttBrokerAddBroker, useMqttBrokerGetBrokerConfig, useMqttBrokerGetStatus, useMqttBrokerListBrokers, useMqttBrokerRemoveBroker, useMqttBrokerStartEmbeddedBroker, useMqttBrokerStopEmbeddedBroker, useMqttBrokerTestConnection, useNativeObjectDetectionGetStatus, useNetworkAccessGetEndpoint, useNetworkAccessGetStatus, useNetworkAccessListEndpoints, useNetworkAccessStart, useNetworkAccessStop, useNetworkQualityGetAllStats, useNetworkQualityGetDeviceStats, useNetworkQualityReportClientStats, useNodesClusterAddonStatus, useNodesDeployAddon, useNodesExecuteQuery, useNodesGetNodeAddons, useNodesRenameNode, useNodesRestartAddon, useNodesRestartNode, useNodesRestartProcess, useNodesSetProcessLogLevel, useNodesShutdownNode, useNodesTopology, useNodesUndeployAddon, useNotificationOutputSend, useNotificationOutputSendTest, useOptionalSystem, useOptionalWidgetRegistry, useOsdGetStatus, useOsdSetOverlay, usePTZ, usePipelineAnalyticsApplyDeviceSettingsPatch, usePipelineAnalyticsClearTracks, usePipelineAnalyticsGetActiveTracks, usePipelineAnalyticsGetAudioEvents, usePipelineAnalyticsGetDeviceLiveContribution, usePipelineAnalyticsGetDeviceSettingsContribution, usePipelineAnalyticsGetEventMedia, usePipelineAnalyticsGetMotionEvents, usePipelineAnalyticsGetObjectEvents, usePipelineAnalyticsGetTrack, usePipelineAnalyticsGetTrackMedia, usePipelineAnalyticsListTracks, usePipelineExecutorCacheFrameInPool, usePipelineExecutorDeleteModel, usePipelineExecutorDeleteTemplate, usePipelineExecutorDetect, usePipelineExecutorDownloadModel, usePipelineExecutorGetAddonModels, usePipelineExecutorGetAudioCapabilities, usePipelineExecutorGetAvailableEngines, usePipelineExecutorGetCapabilities, usePipelineExecutorGetDefaultSteps, usePipelineExecutorGetDetectionConfigSchema, usePipelineExecutorGetEffectiveTuning, usePipelineExecutorGetGlobalPipelineConfig, usePipelineExecutorGetGlobalSteps, usePipelineExecutorGetOrchestratorConfigSchema, usePipelineExecutorGetReferenceAudio, usePipelineExecutorGetReferenceAudioFiles, usePipelineExecutorGetReferenceImage, usePipelineExecutorGetSchema, usePipelineExecutorGetSelectedEngine, usePipelineExecutorGetVideoPipelineSteps, usePipelineExecutorInferCached, usePipelineExecutorKillEngine, usePipelineExecutorListLoadedEngines, usePipelineExecutorListReferenceImages, usePipelineExecutorListTemplates, usePipelineExecutorReprobeEngine, usePipelineExecutorRunAudioTest, usePipelineExecutorRunPipeline, usePipelineExecutorRunPipelineBatch, usePipelineExecutorSaveTemplate, usePipelineExecutorSetVideoPipelineSteps, usePipelineExecutorSpinEngine, usePipelineExecutorUncacheFrame, usePipelineExecutorUpdateTemplate, usePipelineOrchestratorApplyDeviceSettingsPatch, usePipelineOrchestratorAssignAudio, usePipelineOrchestratorAssignDecoder, usePipelineOrchestratorAssignPipeline, usePipelineOrchestratorDeleteTemplate, usePipelineOrchestratorGetAgentLoad, usePipelineOrchestratorGetAgentSettings, usePipelineOrchestratorGetAudioAssignment, usePipelineOrchestratorGetAudioAssignments, usePipelineOrchestratorGetAudioNodeLoad, usePipelineOrchestratorGetCameraMetrics, usePipelineOrchestratorGetCameraSettings, usePipelineOrchestratorGetCameraStepOverrides, usePipelineOrchestratorGetCapabilityBindings, usePipelineOrchestratorGetDecoderAssignment, usePipelineOrchestratorGetDecoderAssignments, usePipelineOrchestratorGetDeviceLiveContribution, usePipelineOrchestratorGetDeviceSettingsContribution, usePipelineOrchestratorGetGlobalMetrics, usePipelineOrchestratorGetPipelineAssignment, usePipelineOrchestratorGetPipelineAssignments, usePipelineOrchestratorListAgentSettings, usePipelineOrchestratorListTemplates, usePipelineOrchestratorRebalance, usePipelineOrchestratorRemoveAgentSettings, usePipelineOrchestratorResolvePipeline, usePipelineOrchestratorSaveTemplate, usePipelineOrchestratorSetAgentAddonDefaults, usePipelineOrchestratorSetCameraPipelineForAgent, usePipelineOrchestratorSetCameraStepOverride, usePipelineOrchestratorSetCameraStepToggle, usePipelineOrchestratorSetCapabilityBinding, usePipelineOrchestratorUnassignAudio, usePipelineOrchestratorUnassignDecoder, usePipelineOrchestratorUnassignPipeline, usePipelineOrchestratorUpdateTemplate, usePipelineRunnerAttachCamera, usePipelineRunnerDetachCamera, usePipelineRunnerGetAllCameraMetrics, usePipelineRunnerGetCameraMetrics, usePipelineRunnerGetLocalCameras, usePipelineRunnerGetLocalLoad, usePipelineRunnerGetLocalMetrics, usePipelineRunnerReportMotion, usePlatformProbeGetCapabilities, usePlatformProbeGetHardware, usePlatformProbeGetHardwareEncoders, usePlatformProbeRefreshHardwareEncoders, usePlatformProbeResolveHwAccel, usePlatformProbeResolveInferenceConfig, usePlayerOverlayLayer, usePlayerOverlayLayers, usePlayerToolbarButton, usePlayerToolbarButtons, usePtzAutotrackGetSettings, usePtzAutotrackGetStatus, usePtzAutotrackSetEnabled, usePtzAutotrackSetSettings, usePtzContinuousMove, usePtzGetPosition, usePtzGetPresets, usePtzGetStatus, usePtzGoHome, usePtzGoToPreset, usePtzMove, usePtzStop, useRebootReboot, useRecordingEngineDisable, useRecordingEngineEnable, useRecordingEngineEstimateGlobalStorage, useRecordingEngineEstimateStorage, useRecordingEngineGetAvailability, useRecordingEngineGetConfig, useRecordingEngineGetMotionStats, useRecordingEngineGetPlaylist, useRecordingEngineGetPolicy, useRecordingEngineGetPolicyStatus, useRecordingEngineGetRetentionConfig, useRecordingEngineGetSegments, useRecordingEngineGetStatus, useRecordingEngineGetStorageUsage, useRecordingEngineGetThumbnail, useRecordingEngineSetPolicy, useRecordingEngineUpdateConfig, useRecordingEngineUpdateRetentionConfig, useRecordingGetPlaybackUrl, useRecordingGetSegments, useRecordingGetThumbnailAt, useSettingsStoreCount, useSettingsStoreDeclareCollection, useSettingsStoreDelete, useSettingsStoreGet, useSettingsStoreInsert, useSettingsStoreIsEmpty, useSettingsStoreQuery, useSettingsStoreSet, useSettingsStoreUpdate, useSnapshotApplyDeviceSettingsPatch, useSnapshotGetDeviceLiveContribution, useSnapshotGetDeviceSettingsContribution, useSnapshotGetSnapshot, useSnapshotGetStatus, useSnapshotInvalidateCache, useSnapshotProviderGetSnapshot, useSnapshotProviderSupportsDevice, useStorageAbortUpload, useStorageBeginDownload, useStorageBeginUpload, useStorageDelete, useStorageDeleteLocation, useStorageEndDownload, useStorageExists, useStorageFinalizeUpload, useStorageGetAvailableSpace, useStorageGetDefaultLocation, useStorageList, useStorageListLocations, useStorageListProviders, useStorageRead, useStorageReadChunk, useStorageResolve, useStorageTestConfig, useStorageTestLocation, useStorageUpsertLocation, useStorageWrite, useStorageWriteChunk, useStreamBrokerApplyDeviceSettingsPatch, useStreamBrokerAssignProfile, useStreamBrokerGetAllRtspEntries, useStreamBrokerGetBroker, useStreamBrokerGetBrokerStats, useStreamBrokerGetDeviceLiveContribution, useStreamBrokerGetDeviceSettingsContribution, useStreamBrokerGetPreBufferInfo, useStreamBrokerGetRtspEntry, useStreamBrokerGetRtspPort, useStreamBrokerGetStreamUrl, useStreamBrokerGetStreamWithCodec, useStreamBrokerIsRtspEnabled, useStreamBrokerKillClient, useStreamBrokerListAllCameraStreams, useStreamBrokerListAllProfileSlots, useStreamBrokerListClients, useStreamBrokerPublishCameraStream, useStreamBrokerRegenerateRtspToken, useStreamBrokerReleaseStreamWithCodec, useStreamBrokerRestartProfile, useStreamBrokerRetractCameraStream, useStreamBrokerSetPreBufferDuration, useStreamBrokerSetRtspEnabled, useStreamBrokerUnassignProfile, useSwitchGetStatus, useSwitchSetState, useSystem, useSystemFeatureFlags, useSystemForceRetentionCleanup, useSystemGetRetentionConfig, useSystemHealth, useSystemInfo, useSystemMutation, useSystemNetworkAddresses, useSystemQuery, useSystemSetRetentionConfig, useThemeMode, useToastOnToast, useTurnProviderGetTurnServers, useUserManagementConfirmTotp, useUserManagementCreateApiKey, useUserManagementCreateScopedToken, useUserManagementCreateUser, useUserManagementDeleteUser, useUserManagementDisableTotp, useUserManagementGetTotpStatus, useUserManagementListApiKeys, useUserManagementListScopedTokens, useUserManagementListUsers, useUserManagementResetPassword, useUserManagementRevokeApiKey, useUserManagementRevokeScopedToken, useUserManagementSetUserScopes, useUserManagementSetupTotp, useUserManagementUpdateUser, useUserManagementValidateApiKey, useUserManagementValidateCredentials, useUserManagementValidateScopedToken, useUserManagementVerifyTotp, useWebrtcSessionCloseSession, useWebrtcSessionCreateSession, useWebrtcSessionHandleAnswer, useWebrtcSessionHandleOffer, useWebrtcSessionHasAdaptiveBitrate, useWebrtcSessionListStreams, useWidget, useWidgetMetadata, useWidgetRegistry, useZoneAnalyticsGetCameraHistory, useZoneAnalyticsGetCurrentSnapshot, useZoneAnalyticsGetUnzonedHistory, useZoneAnalyticsGetZoneHistory, useZoneEditing, useZoneRulesListRules, useZoneRulesSetRules, useZonesAddZone, useZonesListZones, useZonesRemoveZone, useZonesUpdateZone, validateScopes };
26809
+ export { AddonGlobalSettingsForm, AgentStepEditor, AppShell, AudioClassificationList, AudioLevelWaveform, AudioWaveform, AutotrackSection, BTN_COMPACT, BTN_COMPACT_DANGER, BTN_COMPACT_PRIMARY, BTN_COMPACT_WARNING, Badge, BatteryBadge, BottomSheet, Breadcrumb, Button, CHIP_ACTIVE, CHIP_BASE, CHIP_INACTIVE, CLASS_COLORS, CameraStreamPlayer, Card, Checkbox, CodeBlock, CollapsibleCard, ConfigFormBuilder, FormField as ConfigFormField, ConfigSchemaField, ConfirmActionButton, ConfirmDialogProvider, CopyButton, CustomFieldRenderersProvider, DEFAULT_COLOR, DataTable, DetectionCanvas, DetectionOverlay, DetectionResultTree, DevShell, DeviceActivityPanel, DeviceCard, DeviceContextProvider, DeviceExportPanel, DeviceGrid, DeviceItem, DeviceList, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, DiscoveryPanel, DoorbellRecentPanel, Dropdown, DropdownContent, DropdownItem, DropdownTrigger, EmptyState, ErrorBox, EventStream, FilterBar, FloatingEventStream, FloatingLogStream, FloatingPanel, FormField$1 as FormField, GRID_GAP, GRID_PAIRED, GRID_QUICK_STATS, HOST_WIDGETS, INPUT_COMPACT, IconButton, ImageSelector, InferenceConfigSelector, Input, KebabMenu, KeyValueList, LIST_ROW, Label, LogStream, LoginForm, MobileDrawer, MotionZonesSettings, NodeMultiSelectField, NodePicker, NodeSelectField, PHASE_CONFIG, PTZOverlay, PageHeader, PhaseIcon, PipelineBuilder, PipelineRuntimeSelector, PipelineStep, PipelineTreeMatrix, PlayerOverlaysProvider, Popover, PopoverContent, PopoverTrigger, ProviderBadge, PtzPanel, QrCode, ResponseLog, SECTION_BODY, SECTION_CARD, SECTION_HEADER, SPLIT_PANEL_OUTER, SPLIT_PANEL_SIDE, STACK_GAP, ScopePicker, ScrollArea, Select, SemanticBadge, Separator, Sidebar, SidebarItem, Skeleton, SlideOverPanel, SnapshotButton, StatCard, StateValuesStream, StatusBadge, StepTimings, StepTreeMaster, StreamBrokerSelector, StreamPanel, Switch, SystemProvider, TEXT_FIELD_LABEL, TEXT_HINT, TEXT_METRIC, TEXT_SECTION_LABEL, TEXT_VALUE, Tabs, TabsContent, TabsList, TabsTrigger, ThemeProvider, Tooltip, TooltipContent, TooltipTrigger, VersionBadge, WidgetRegistryProvider, WidgetSlot, ZoneEditingProvider, buildStepTreeFromSchema, cn, createSharedContext, createTheme, darkColors, defaultTheme, deriveDeviceKind, ensureMfHostInit, getClassColor, getPhaseVisual, isFieldVisible, lightColors, loadRemoteBundle, mirror, mountAddonPage, providerIcons, statusIcons, themeToCss, trpc, useAccessoriesGetStatus, useAddonPagesListPages, useAddonSettingsGetDeviceSettings, useAddonSettingsGetGlobalSettings, useAddonSettingsUpdateDeviceSettings, useAddonSettingsUpdateGlobalSettings, useAddonWidgetsListWidgets, useAddonsApplyAutoUpdateToAll, useAddonsCustom, useAddonsForceRefresh, useAddonsGetAddonAutoUpdate, useAddonsGetAutoUpdateSettings, useAddonsGetLastRestart, useAddonsGetLogs, useAddonsGetVersions, useAddonsInstallFromWorkspace, useAddonsInstallPackage, useAddonsIsWorkspaceAvailable, useAddonsList, useAddonsListCapabilityProviders, useAddonsListFrameworkPackages, useAddonsListPackages, useAddonsListUpdates, useAddonsListWorkspacePackages, useAddonsOnAddonLogs, useAddonsReloadPackages, useAddonsRestartAddon, useAddonsRestartServer, useAddonsRetryLoad, useAddonsRollbackPackage, useAddonsSearchAvailable, useAddonsSetAddonAutoUpdate, useAddonsSetAutoUpdateSettings, useAddonsSetCapabilityProviderEnabled, useAddonsUninstallPackage, useAddonsUpdateFrameworkPackage, useAddonsUpdatePackage, useAlertsDismiss, useAlertsEmit, useAlertsGetUnreadCount, useAlertsList, useAlertsMarkAllRead, useAlertsMarkRead, useAlertsUpdate, useAllWidgets, useAudioAnalysisApplyDeviceSettingsPatch, useAudioAnalysisGetDeviceLiveContribution, useAudioAnalysisGetDeviceSettingsContribution, useAudioAnalysisResolveDeviceSettings, useAudioAnalyzerAnalyseChunk, useAudioAnalyzerClassify, useAudioAnalyzerDispose, useAudioAnalyzerIsReady, useAudioAnalyzerReprobeAudioEngine, useAudioCodecCanHandle, useAudioCodecCloseSession, useAudioCodecCreateDecodeSession, useAudioCodecCreateEncodeSession, useAudioCodecFlushEncode, useAudioCodecListActiveSessions, useAudioCodecListSupportedCodecs, useAudioCodecPullEncoded, useAudioCodecPullPcm, useAudioCodecPushEncodedFrame, useAudioCodecPushPcm, useAudioMetricsGetCurrentSnapshot, useAudioMetricsGetHistory, useBackupDelete, useBackupGetEntries, useBackupList, useBackupListArchives, useBackupListDestinations, useBackupListLocations, useBackupPreviewSchedule, useBackupRestore, useBackupTrigger, useBackupUpsertDestinationPolicy, useBatteryGetStatus, useBrightnessGetStatus, useBrightnessSetBrightness, useCameraCredentialsGetCredentials, useCameraCredentialsGetStatus, useCameraStreamsGetBrokerStreams, useCameraStreamsGetCameraStreams, useCameraStreamsGetRtspEntries, useClusterNodes, useConfirm, useCustomFieldRenderer, useDebouncedString, useDecoderCreateSession, useDecoderDestroySession, useDecoderGetFrame, useDecoderGetInfo, useDecoderGetShmStats, useDecoderGetStats, useDecoderListActiveSessions, useDecoderOpenStream, useDecoderPullFrames, useDecoderPullHandles, useDecoderPushPacket, useDecoderReprobeHwaccel, useDecoderSupportsCodec, useDecoderUpdateConfig, useDetectionPipelineApplyDeviceSettingsPatch, useDetectionPipelineGetDeviceLiveContribution, useDetectionPipelineGetDeviceSettingsContribution, useDevShell, useDevice, useDeviceAutotrack, useDeviceBattery, useDeviceCapability, useDeviceDetections, useDeviceDiscoveryAdoptDevice, useDeviceDiscoveryGetStatus, useDeviceDiscoveryListDiscovered, useDeviceDiscoveryRefreshDiscovery, useDeviceDiscoveryReleaseDevice, useDeviceExportApplyDeviceSettingsPatch, useDeviceExportExposeDevice, useDeviceExportGetDeviceLiveContribution, useDeviceExportGetDeviceSettingsContribution, useDeviceExportGetStatus, useDeviceExportListExposedDevices, useDeviceExportListSupportedDeviceKinds, useDeviceExportUnexposeDevice, useDeviceId, useDeviceManagerAddLocation, useDeviceManagerAdoptDevice, useDeviceManagerAllocateDeviceId, useDeviceManagerCreateDevice, useDeviceManagerDisable, useDeviceManagerDiscoverDevices, useDeviceManagerEnable, useDeviceManagerGetAllBindings, useDeviceManagerGetBindings, useDeviceManagerGetChildren, useDeviceManagerGetConfigSchema, useDeviceManagerGetCreationSchema, useDeviceManagerGetDevice, useDeviceManagerGetDeviceAggregate, useDeviceManagerGetDeviceLiveInfoAggregate, useDeviceManagerGetDeviceSettingsAggregate, useDeviceManagerGetDeviceStatusAggregate, useDeviceManagerGetSettingsSchema, useDeviceManagerGetStreamProfileMap, useDeviceManagerGetStreamSources, useDeviceManagerListAll, useDeviceManagerListBindableCapsForDeviceType, useDeviceManagerListLocations, useDeviceManagerListPersistedByAddon, useDeviceManagerListWrappersForCap, useDeviceManagerLoadConfig, useDeviceManagerLoadMeta, useDeviceManagerLoadRuntimeState, useDeviceManagerPersistConfig, useDeviceManagerProbeStreams, useDeviceManagerRegisterDevice, useDeviceManagerRemove, useDeviceManagerRemoveDevice, useDeviceManagerRemoveLocation, useDeviceManagerSetDisabled, useDeviceManagerSetLocation, useDeviceManagerSetMetadata, useDeviceManagerSetName, useDeviceManagerSetStreamProfileMap, useDeviceManagerSetWrapperActive, useDeviceManagerTestCreationField, useDeviceManagerTestField, useDeviceManagerUpdateConfig, useDeviceManagerUpdateDeviceField, useDeviceManagerUpdateDeviceFieldsBatch, useDeviceOpsGetConfigEntries, useDeviceOpsGetSettingsSchema, useDeviceOpsGetStreamSources, useDeviceOpsRemoveDevice, useDeviceOpsSetConfig, useDeviceProviderAdoptDiscoveredDevice, useDeviceProviderCreateDevice, useDeviceProviderDiscoverDevices, useDeviceProviderGetChildCreationSchema, useDeviceProviderGetDevices, useDeviceProviderGetStatus, useDeviceProviderStart, useDeviceProviderStop, useDeviceProviderSupportsDiscovery, useDeviceProviderSupportsManualCreation, useDeviceProviderTestCreationField, useDeviceProxy, useDeviceSnapshot, useDeviceSnapshotImage, useDeviceState, useDeviceStateGetAllSnapshots, useDeviceStateGetCapSlice, useDeviceStateGetSnapshot, useDeviceStateSetCapSlice, useDeviceStateSlice, useDeviceStatusGetStatus, useDeviceWebrtc, useDevices, useDoorbellEvents, useDoorbellGetStatus, useEventInvalidation, useEventStreamLatest, useEventStreamMap, useEventsGetEventClipUrl, useEventsGetEventThumbnail, useEventsGetEvents, useFeatureProbeGetStatus, useIntegrationsCreate, useIntegrationsDelete, useIntegrationsGet, useIntegrationsGetAvailableTypes, useIntegrationsGetByAddonId, useIntegrationsGetSettings, useIntegrationsList, useIntegrationsSetSettings, useIntegrationsTestConnection, useIntegrationsUpdate, useIntercomEndTalkSession, useIntercomGetStatus, useIntercomHandleAnswer, useIntercomPushTalkPcm, useIntercomStartSession, useIntercomStartTalkSession, useIntercomStopSession, useIsMidWidth, useIsMobile, useLiveBuffer, useLiveEvent, useLocalNetworkGetAllowedAddresses, useLocalNetworkGetConnectionEndpoints, useLocalNetworkGetPreferred, useLocalNetworkList, useLocalNetworkResetAllowlistToBestMatch, useLocalNetworkSetAllowedAddresses, useMeshNetworkGetStatus, useMeshNetworkJoin, useMeshNetworkLeave, useMeshNetworkListPeers, useMeshNetworkLogout, useMeshNetworkStartLogin, useMeshNetworkTestConnection, useMetricsProviderCollectSnapshot, useMetricsProviderGetAddonStats, useMetricsProviderGetCached, useMetricsProviderGetCpuTemperature, useMetricsProviderGetCurrent, useMetricsProviderGetDiskSpace, useMetricsProviderGetGpuInfo, useMetricsProviderGetProcessStats, useMetricsProviderKillProcess, useMetricsProviderListAddonInstances, useMetricsProviderListNodeProcesses, useMotionDetectionAnalyze, useMotionDetectionApplyDeviceSettingsPatch, useMotionDetectionGetDeviceLiveContribution, useMotionDetectionGetDeviceSettingsContribution, useMotionDetectionRemoveCamera, useMotionDetectionReset, useMotionGetStatus, useMotionIsDetected, useMotionTriggerGetStatus, useMotionTriggerSetMotionTrigger, useMotionZonesGetOptions, useMotionZonesGetStatus, useMotionZonesSetZone, useMqttBrokerAddBroker, useMqttBrokerGetBrokerConfig, useMqttBrokerGetStatus, useMqttBrokerListBrokers, useMqttBrokerRemoveBroker, useMqttBrokerStartEmbeddedBroker, useMqttBrokerStopEmbeddedBroker, useMqttBrokerTestConnection, useNativeObjectDetectionGetStatus, useNetworkAccessGetEndpoint, useNetworkAccessGetStatus, useNetworkAccessListEndpoints, useNetworkAccessStart, useNetworkAccessStop, useNetworkQualityGetAllStats, useNetworkQualityGetDeviceStats, useNetworkQualityReportClientStats, useNodesClusterAddonStatus, useNodesDeployAddon, useNodesExecuteQuery, useNodesGetCapUsageGraph, useNodesGetNodeAddons, useNodesRenameNode, useNodesRestartAddon, useNodesRestartNode, useNodesRestartProcess, useNodesSetProcessLogLevel, useNodesShutdownNode, useNodesTopology, useNodesUndeployAddon, useNotificationOutputSend, useNotificationOutputSendTest, useOptionalSystem, useOptionalWidgetRegistry, useOsdGetStatus, useOsdSetOverlay, usePTZ, usePipelineAnalyticsApplyDeviceSettingsPatch, usePipelineAnalyticsClearTracks, usePipelineAnalyticsGetActiveTracks, usePipelineAnalyticsGetAudioEvents, usePipelineAnalyticsGetDeviceLiveContribution, usePipelineAnalyticsGetDeviceSettingsContribution, usePipelineAnalyticsGetEventMedia, usePipelineAnalyticsGetMotionEvents, usePipelineAnalyticsGetObjectEvents, usePipelineAnalyticsGetTrack, usePipelineAnalyticsGetTrackMedia, usePipelineAnalyticsListTracks, usePipelineExecutorCacheFrameInPool, usePipelineExecutorDeleteModel, usePipelineExecutorDeleteTemplate, usePipelineExecutorDetect, usePipelineExecutorDownloadModel, usePipelineExecutorGetAddonModels, usePipelineExecutorGetAudioCapabilities, usePipelineExecutorGetAvailableEngines, usePipelineExecutorGetCapabilities, usePipelineExecutorGetDefaultSteps, usePipelineExecutorGetDetectionConfigSchema, usePipelineExecutorGetEffectiveTuning, usePipelineExecutorGetGlobalPipelineConfig, usePipelineExecutorGetGlobalSteps, usePipelineExecutorGetOrchestratorConfigSchema, usePipelineExecutorGetReferenceAudio, usePipelineExecutorGetReferenceAudioFiles, usePipelineExecutorGetReferenceImage, usePipelineExecutorGetSchema, usePipelineExecutorGetSelectedEngine, usePipelineExecutorGetVideoPipelineSteps, usePipelineExecutorInferCached, usePipelineExecutorKillEngine, usePipelineExecutorListLoadedEngines, usePipelineExecutorListReferenceImages, usePipelineExecutorListTemplates, usePipelineExecutorReprobeEngine, usePipelineExecutorRunAudioTest, usePipelineExecutorRunPipeline, usePipelineExecutorRunPipelineBatch, usePipelineExecutorSaveTemplate, usePipelineExecutorSetVideoPipelineSteps, usePipelineExecutorSpinEngine, usePipelineExecutorUncacheFrame, usePipelineExecutorUpdateTemplate, usePipelineOrchestratorApplyDeviceSettingsPatch, usePipelineOrchestratorAssignAudio, usePipelineOrchestratorAssignDecoder, usePipelineOrchestratorAssignPipeline, usePipelineOrchestratorDeleteTemplate, usePipelineOrchestratorGetAgentLoad, usePipelineOrchestratorGetAgentSettings, usePipelineOrchestratorGetAudioAssignment, usePipelineOrchestratorGetAudioAssignments, usePipelineOrchestratorGetAudioNodeLoad, usePipelineOrchestratorGetCameraMetrics, usePipelineOrchestratorGetCameraSettings, usePipelineOrchestratorGetCameraStepOverrides, usePipelineOrchestratorGetCapabilityBindings, usePipelineOrchestratorGetDecoderAssignment, usePipelineOrchestratorGetDecoderAssignments, usePipelineOrchestratorGetDeviceLiveContribution, usePipelineOrchestratorGetDeviceSettingsContribution, usePipelineOrchestratorGetGlobalMetrics, usePipelineOrchestratorGetPipelineAssignment, usePipelineOrchestratorGetPipelineAssignments, usePipelineOrchestratorListAgentSettings, usePipelineOrchestratorListTemplates, usePipelineOrchestratorRebalance, usePipelineOrchestratorRemoveAgentSettings, usePipelineOrchestratorResolvePipeline, usePipelineOrchestratorSaveTemplate, usePipelineOrchestratorSetAgentAddonDefaults, usePipelineOrchestratorSetCameraPipelineForAgent, usePipelineOrchestratorSetCameraStepOverride, usePipelineOrchestratorSetCameraStepToggle, usePipelineOrchestratorSetCapabilityBinding, usePipelineOrchestratorUnassignAudio, usePipelineOrchestratorUnassignDecoder, usePipelineOrchestratorUnassignPipeline, usePipelineOrchestratorUpdateTemplate, usePipelineRunnerAttachCamera, usePipelineRunnerDetachCamera, usePipelineRunnerGetAllCameraMetrics, usePipelineRunnerGetCameraMetrics, usePipelineRunnerGetLocalCameras, usePipelineRunnerGetLocalLoad, usePipelineRunnerGetLocalMetrics, usePipelineRunnerReportMotion, usePlatformProbeGetCapabilities, usePlatformProbeGetHardware, usePlatformProbeGetHardwareEncoders, usePlatformProbeRefreshHardwareEncoders, usePlatformProbeResolveHwAccel, usePlatformProbeResolveInferenceConfig, usePlayerOverlayLayer, usePlayerOverlayLayers, usePlayerToolbarButton, usePlayerToolbarButtons, usePtzAutotrackGetSettings, usePtzAutotrackGetStatus, usePtzAutotrackSetEnabled, usePtzAutotrackSetSettings, usePtzContinuousMove, usePtzDeletePreset, usePtzGetOptions, usePtzGetPosition, usePtzGetPresets, usePtzGetStatus, usePtzGoHome, usePtzGoToPreset, usePtzMove, usePtzSavePreset, usePtzSetAutofocus, usePtzStop, useRebootReboot, useRecordingEngineDisable, useRecordingEngineEnable, useRecordingEngineEstimateGlobalStorage, useRecordingEngineEstimateStorage, useRecordingEngineGetAvailability, useRecordingEngineGetConfig, useRecordingEngineGetMotionStats, useRecordingEngineGetPlaylist, useRecordingEngineGetPolicy, useRecordingEngineGetPolicyStatus, useRecordingEngineGetRetentionConfig, useRecordingEngineGetSegments, useRecordingEngineGetStatus, useRecordingEngineGetStorageUsage, useRecordingEngineGetThumbnail, useRecordingEngineSetPolicy, useRecordingEngineUpdateConfig, useRecordingEngineUpdateRetentionConfig, useRecordingGetPlaybackUrl, useRecordingGetSegments, useRecordingGetThumbnailAt, useRemoteComponent, useSettingsStoreCount, useSettingsStoreDeclareCollection, useSettingsStoreDelete, useSettingsStoreGet, useSettingsStoreInsert, useSettingsStoreIsEmpty, useSettingsStoreQuery, useSettingsStoreSet, useSettingsStoreUpdate, useSnapshotApplyDeviceSettingsPatch, useSnapshotGetDeviceLiveContribution, useSnapshotGetDeviceSettingsContribution, useSnapshotGetSnapshot, useSnapshotGetStatus, useSnapshotInvalidateCache, useSnapshotProviderGetSnapshot, useSnapshotProviderSupportsDevice, useStorageAbortUpload, useStorageBeginDownload, useStorageBeginUpload, useStorageDelete, useStorageDeleteLocation, useStorageEndDownload, useStorageExists, useStorageFinalizeUpload, useStorageGetAvailableSpace, useStorageGetDefaultLocation, useStorageList, useStorageListLocations, useStorageListProviders, useStorageRead, useStorageReadChunk, useStorageResolve, useStorageTestConfig, useStorageTestLocation, useStorageUpsertLocation, useStorageWrite, useStorageWriteChunk, useStreamBrokerApplyDeviceSettingsPatch, useStreamBrokerAssignProfile, useStreamBrokerGetAllRtspEntries, useStreamBrokerGetBrokerStats, useStreamBrokerGetDeviceLiveContribution, useStreamBrokerGetDeviceSettingsContribution, useStreamBrokerGetPreBufferInfo, useStreamBrokerGetRtspEntry, useStreamBrokerGetRtspPort, useStreamBrokerGetStreamUrl, useStreamBrokerGetStreamWithCodec, useStreamBrokerIsRtspEnabled, useStreamBrokerKillClient, useStreamBrokerListAllCameraStreams, useStreamBrokerListAllProfileSlots, useStreamBrokerListClients, useStreamBrokerPublishCameraStream, useStreamBrokerPullAudioChunks, useStreamBrokerPullFrameHandles, useStreamBrokerRegenerateRtspToken, useStreamBrokerReleaseStreamWithCodec, useStreamBrokerRestartProfile, useStreamBrokerRetractCameraStream, useStreamBrokerSetPreBufferDuration, useStreamBrokerSetRtspEnabled, useStreamBrokerSubscribeAudioChunks, useStreamBrokerSubscribeFrames, useStreamBrokerUnassignProfile, useStreamBrokerUnsubscribeAudioChunks, useStreamBrokerUnsubscribeFrames, useStreamParamsGetConfigSchema, useStreamParamsGetOptions, useStreamParamsGetStatus, useStreamParamsSetProfile, useSwitchGetStatus, useSwitchSetState, useSystem, useSystemFeatureFlags, useSystemForceRetentionCleanup, useSystemGetRetentionConfig, useSystemHealth, useSystemInfo, useSystemMutation, useSystemNetworkAddresses, useSystemQuery, useSystemSetRetentionConfig, useThemeMode, useToastOnToast, useTurnProviderGetTurnServers, 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, useWebrtcSessionCloseSession, useWebrtcSessionCreateSession, useWebrtcSessionHandleAnswer, useWebrtcSessionHandleOffer, useWebrtcSessionHasAdaptiveBitrate, useWebrtcSessionListStreams, useWidget, useWidgetMetadata, useWidgetRegistry, useZoneAnalyticsGetCameraHistory, useZoneAnalyticsGetCurrentSnapshot, useZoneAnalyticsGetUnzonedHistory, useZoneAnalyticsGetZoneHistory, useZoneEditing, useZoneRulesListRules, useZoneRulesSetRules, useZonesAddZone, useZonesListZones, useZonesRemoveZone, useZonesUpdateZone, validateScopes };
25853
26810
 
25854
26811
  //# sourceMappingURL=index.js.map