@camstack/ui-library 0.1.51 → 0.1.53

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"))
@@ -15782,6 +15788,8 @@ var useAddonsGetLastRestart = trpc.addons.getLastRestart.useQuery;
15782
15788
  var useAddonsListFrameworkPackages = trpc.addons.listFrameworkPackages.useQuery;
15783
15789
  /** Generated alias around `trpc.addons.listCapabilityProviders.useQuery`. */
15784
15790
  var useAddonsListCapabilityProviders = trpc.addons.listCapabilityProviders.useQuery;
15791
+ /** Generated alias around `trpc.addons.setCapabilityProviderEnabled.useMutation`. */
15792
+ var useAddonsSetCapabilityProviderEnabled = trpc.addons.setCapabilityProviderEnabled.useMutation;
15785
15793
  /** Generated alias around `trpc.addons.updateFrameworkPackage.useMutation`. */
15786
15794
  var useAddonsUpdateFrameworkPackage = trpc.addons.updateFrameworkPackage.useMutation;
15787
15795
  /** Generated alias around `trpc.addons.getVersions.useQuery`. */
@@ -15872,10 +15880,6 @@ var useAudioCodecListActiveSessions = trpc.audioCodec.listActiveSessions.useQuer
15872
15880
  var useAudioMetricsGetCurrentSnapshot = trpc.audioMetrics.getCurrentSnapshot.useQuery;
15873
15881
  /** Generated alias around `trpc.audioMetrics.getHistory.useQuery`. */
15874
15882
  var useAudioMetricsGetHistory = trpc.audioMetrics.getHistory.useQuery;
15875
- /** Generated alias around `trpc.authentication.listProviders.useQuery`. */
15876
- var useAuthenticationListProviders = trpc.authentication.listProviders.useQuery;
15877
- /** Generated alias around `trpc.authentication.setProviderEnabled.useMutation`. */
15878
- var useAuthenticationSetProviderEnabled = trpc.authentication.setProviderEnabled.useMutation;
15879
15883
  /** Generated alias around `trpc.backup.listDestinations.useQuery`. */
15880
15884
  var useBackupListDestinations = trpc.backup.listDestinations.useQuery;
15881
15885
  /** Generated alias around `trpc.backup.trigger.useMutation`. */
@@ -15926,6 +15930,12 @@ var useDecoderPushPacket = trpc.decoder.pushPacket.useQuery;
15926
15930
  var useDecoderOpenStream = trpc.decoder.openStream.useQuery;
15927
15931
  /** Generated alias around `trpc.decoder.pullFrames.useQuery`. */
15928
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;
15929
15939
  /** Generated alias around `trpc.decoder.updateConfig.useQuery`. */
15930
15940
  var useDecoderUpdateConfig = trpc.decoder.updateConfig.useQuery;
15931
15941
  /** Generated alias around `trpc.decoder.getStats.useQuery`. */
@@ -16168,18 +16178,6 @@ var useMeshNetworkLogout = trpc.meshNetwork.logout.useMutation;
16168
16178
  var useMeshNetworkListPeers = trpc.meshNetwork.listPeers.useQuery;
16169
16179
  /** Generated alias around `trpc.meshNetwork.testConnection.useMutation`. */
16170
16180
  var useMeshNetworkTestConnection = trpc.meshNetwork.testConnection.useMutation;
16171
- /** Generated alias around `trpc.meshOrchestrator.listProviders.useQuery`. */
16172
- var useMeshOrchestratorListProviders = trpc.meshOrchestrator.listProviders.useQuery;
16173
- /** Generated alias around `trpc.meshOrchestrator.joinProvider.useMutation`. */
16174
- var useMeshOrchestratorJoinProvider = trpc.meshOrchestrator.joinProvider.useMutation;
16175
- /** Generated alias around `trpc.meshOrchestrator.leaveProvider.useMutation`. */
16176
- var useMeshOrchestratorLeaveProvider = trpc.meshOrchestrator.leaveProvider.useMutation;
16177
- /** Generated alias around `trpc.meshOrchestrator.startLoginProvider.useMutation`. */
16178
- var useMeshOrchestratorStartLoginProvider = trpc.meshOrchestrator.startLoginProvider.useMutation;
16179
- /** Generated alias around `trpc.meshOrchestrator.logoutProvider.useMutation`. */
16180
- var useMeshOrchestratorLogoutProvider = trpc.meshOrchestrator.logoutProvider.useMutation;
16181
- /** Generated alias around `trpc.meshOrchestrator.listProviderPeers.useQuery`. */
16182
- var useMeshOrchestratorListProviderPeers = trpc.meshOrchestrator.listProviderPeers.useQuery;
16183
16181
  /** Generated alias around `trpc.metricsProvider.collectSnapshot.useQuery`. */
16184
16182
  var useMetricsProviderCollectSnapshot = trpc.metricsProvider.collectSnapshot.useQuery;
16185
16183
  /** Generated alias around `trpc.metricsProvider.getCached.useQuery`. */
@@ -16222,6 +16220,12 @@ var useMotionDetectionApplyDeviceSettingsPatch = trpc.motionDetection.applyDevic
16222
16220
  var useMotionTriggerSetMotionTrigger = trpc.motionTrigger.setMotionTrigger.useMutation;
16223
16221
  /** Generated alias around `trpc.motionTrigger.getStatus.useQuery`. */
16224
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;
16225
16229
  /** Generated alias around `trpc.mqttBroker.listBrokers.useQuery`. */
16226
16230
  var useMqttBrokerListBrokers = trpc.mqttBroker.listBrokers.useQuery;
16227
16231
  /** Generated alias around `trpc.mqttBroker.getBrokerConfig.useQuery`. */
@@ -16240,6 +16244,16 @@ var useMqttBrokerStopEmbeddedBroker = trpc.mqttBroker.stopEmbeddedBroker.useMuta
16240
16244
  var useMqttBrokerGetStatus = trpc.mqttBroker.getStatus.useQuery;
16241
16245
  /** Generated alias around `trpc.nativeObjectDetection.getStatus.useQuery`. */
16242
16246
  var useNativeObjectDetectionGetStatus = trpc.nativeObjectDetection.getStatus.useQuery;
16247
+ /** Generated alias around `trpc.networkAccess.start.useMutation`. */
16248
+ var useNetworkAccessStart = trpc.networkAccess.start.useMutation;
16249
+ /** Generated alias around `trpc.networkAccess.stop.useMutation`. */
16250
+ var useNetworkAccessStop = trpc.networkAccess.stop.useMutation;
16251
+ /** Generated alias around `trpc.networkAccess.getEndpoint.useQuery`. */
16252
+ var useNetworkAccessGetEndpoint = trpc.networkAccess.getEndpoint.useQuery;
16253
+ /** Generated alias around `trpc.networkAccess.getStatus.useQuery`. */
16254
+ var useNetworkAccessGetStatus = trpc.networkAccess.getStatus.useQuery;
16255
+ /** Generated alias around `trpc.networkAccess.listEndpoints.useQuery`. */
16256
+ var useNetworkAccessListEndpoints = trpc.networkAccess.listEndpoints.useQuery;
16243
16257
  /** Generated alias around `trpc.networkQuality.getDeviceStats.useQuery`. */
16244
16258
  var useNetworkQualityGetDeviceStats = trpc.networkQuality.getDeviceStats.useQuery;
16245
16259
  /** Generated alias around `trpc.networkQuality.getAllStats.useQuery`. */
@@ -16264,6 +16278,8 @@ var useNodesShutdownNode = trpc.nodes.shutdownNode.useMutation;
16264
16278
  var useNodesRenameNode = trpc.nodes.renameNode.useMutation;
16265
16279
  /** Generated alias around `trpc.nodes.clusterAddonStatus.useQuery`. */
16266
16280
  var useNodesClusterAddonStatus = trpc.nodes.clusterAddonStatus.useQuery;
16281
+ /** Generated alias around `trpc.nodes.getCapUsageGraph.useQuery`. */
16282
+ var useNodesGetCapUsageGraph = trpc.nodes.getCapUsageGraph.useQuery;
16267
16283
  /** Generated alias around `trpc.nodes.getNodeAddons.useQuery`. */
16268
16284
  var useNodesGetNodeAddons = trpc.nodes.getNodeAddons.useQuery;
16269
16285
  /** Generated alias around `trpc.nodes.setProcessLogLevel.useMutation`. */
@@ -16482,10 +16498,18 @@ var usePtzStop = trpc.ptz.stop.useMutation;
16482
16498
  var usePtzGetPresets = trpc.ptz.getPresets.useQuery;
16483
16499
  /** Generated alias around `trpc.ptz.goToPreset.useMutation`. */
16484
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;
16485
16507
  /** Generated alias around `trpc.ptz.goHome.useMutation`. */
16486
16508
  var usePtzGoHome = trpc.ptz.goHome.useMutation;
16487
16509
  /** Generated alias around `trpc.ptz.getPosition.useQuery`. */
16488
16510
  var usePtzGetPosition = trpc.ptz.getPosition.useQuery;
16511
+ /** Generated alias around `trpc.ptz.setAutofocus.useMutation`. */
16512
+ var usePtzSetAutofocus = trpc.ptz.setAutofocus.useMutation;
16489
16513
  /** Generated alias around `trpc.ptz.getStatus.useQuery`. */
16490
16514
  var usePtzGetStatus = trpc.ptz.getStatus.useQuery;
16491
16515
  /** Generated alias around `trpc.ptzAutotrack.getStatus.useQuery`. */
@@ -16540,12 +16564,6 @@ var useRecordingEngineGetRetentionConfig = trpc.recordingEngine.getRetentionConf
16540
16564
  var useRecordingEngineUpdateRetentionConfig = trpc.recordingEngine.updateRetentionConfig.useMutation;
16541
16565
  /** Generated alias around `trpc.recordingEngine.getMotionStats.useQuery`. */
16542
16566
  var useRecordingEngineGetMotionStats = trpc.recordingEngine.getMotionStats.useQuery;
16543
- /** Generated alias around `trpc.remoteAccess.listProviders.useQuery`. */
16544
- var useRemoteAccessListProviders = trpc.remoteAccess.listProviders.useQuery;
16545
- /** Generated alias around `trpc.remoteAccess.startProvider.useMutation`. */
16546
- var useRemoteAccessStartProvider = trpc.remoteAccess.startProvider.useMutation;
16547
- /** Generated alias around `trpc.remoteAccess.stopProvider.useMutation`. */
16548
- var useRemoteAccessStopProvider = trpc.remoteAccess.stopProvider.useMutation;
16549
16567
  /** Generated alias around `trpc.settingsStore.get.useQuery`. */
16550
16568
  var useSettingsStoreGet = trpc.settingsStore.get.useQuery;
16551
16569
  /** Generated alias around `trpc.settingsStore.set.useMutation`. */
@@ -16648,8 +16666,18 @@ var useStreamBrokerGetStreamUrl = trpc.streamBroker.getStreamUrl.useQuery;
16648
16666
  var useStreamBrokerGetStreamWithCodec = trpc.streamBroker.getStreamWithCodec.useMutation;
16649
16667
  /** Generated alias around `trpc.streamBroker.releaseStreamWithCodec.useMutation`. */
16650
16668
  var useStreamBrokerReleaseStreamWithCodec = trpc.streamBroker.releaseStreamWithCodec.useMutation;
16651
- /** Generated alias around `trpc.streamBroker.getBroker.useQuery`. */
16652
- 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;
16653
16681
  /** Generated alias around `trpc.streamBroker.setPreBufferDuration.useMutation`. */
16654
16682
  var useStreamBrokerSetPreBufferDuration = trpc.streamBroker.setPreBufferDuration.useMutation;
16655
16683
  /** Generated alias around `trpc.streamBroker.getPreBufferInfo.useQuery`. */
@@ -16672,6 +16700,14 @@ var useStreamBrokerGetDeviceSettingsContribution = trpc.streamBroker.getDeviceSe
16672
16700
  var useStreamBrokerGetDeviceLiveContribution = trpc.streamBroker.getDeviceLiveContribution.useQuery;
16673
16701
  /** Generated alias around `trpc.streamBroker.applyDeviceSettingsPatch.useMutation`. */
16674
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;
16675
16711
  /** Generated alias around `trpc.switch.setState.useMutation`. */
16676
16712
  var useSwitchSetState = trpc.switch.setState.useMutation;
16677
16713
  /** Generated alias around `trpc.switch.getStatus.useQuery`. */
@@ -16692,12 +16728,6 @@ var useSystemSetRetentionConfig = trpc.system.setRetentionConfig.useMutation;
16692
16728
  var useSystemForceRetentionCleanup = trpc.system.forceRetentionCleanup.useMutation;
16693
16729
  /** Generated alias around `trpc.toast.onToast.useSubscription`. */
16694
16730
  var useToastOnToast = trpc.toast.onToast.useSubscription;
16695
- /** Generated alias around `trpc.turnOrchestrator.listProviders.useQuery`. */
16696
- var useTurnOrchestratorListProviders = trpc.turnOrchestrator.listProviders.useQuery;
16697
- /** Generated alias around `trpc.turnOrchestrator.getAllServers.useQuery`. */
16698
- var useTurnOrchestratorGetAllServers = trpc.turnOrchestrator.getAllServers.useQuery;
16699
- /** Generated alias around `trpc.turnOrchestrator.setProviderEnabled.useMutation`. */
16700
- var useTurnOrchestratorSetProviderEnabled = trpc.turnOrchestrator.setProviderEnabled.useMutation;
16701
16731
  /** Generated alias around `trpc.turnProvider.getTurnServers.useQuery`. */
16702
16732
  var useTurnProviderGetTurnServers = trpc.turnProvider.getTurnServers.useQuery;
16703
16733
  /** Generated alias around `trpc.userManagement.listUsers.useQuery`. */
@@ -16740,6 +16770,18 @@ var useUserManagementDisableTotp = trpc.userManagement.disableTotp.useMutation;
16740
16770
  var useUserManagementGetTotpStatus = trpc.userManagement.getTotpStatus.useQuery;
16741
16771
  /** Generated alias around `trpc.userManagement.verifyTotp.useMutation`. */
16742
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;
16743
16785
  /** Generated alias around `trpc.webrtcSession.listStreams.useQuery`. */
16744
16786
  var useWebrtcSessionListStreams = trpc.webrtcSession.listStreams.useQuery;
16745
16787
  /** Generated alias around `trpc.webrtcSession.createSession.useMutation`. */
@@ -16783,8 +16825,16 @@ var useZonesUpdateZone = trpc.zones.updateZone.useMutation;
16783
16825
  * `vite.widgets.config.ts`). At runtime we fetch the public list of
16784
16826
  * addon-contributed widgets via `useAddonWidgetsListWidgets()`, register
16785
16827
  * each remote (`registerRemotes`) once, then resolve a widget by
16786
- * loading the exposed `./widgets` module (`loadRemote`) whose default
16787
- * 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.
16788
16838
  *
16789
16839
  * Why MF instead of raw ESM:
16790
16840
  * - automatic dedup of shared deps (react/react-dom/@tanstack/etc.)
@@ -16794,9 +16844,7 @@ var useZonesUpdateZone = trpc.zones.updateZone.useMutation;
16794
16844
  * wins by default).
16795
16845
  * - cross-bundle context invariant preserved automatically: every
16796
16846
  * remote's `@camstack/ui-library` import resolves to the host's
16797
- * instance, so `createContext()` references match across bundles
16798
- * (the duplicate-Context bug that motivated `createSharedContext`
16799
- * falls out for free).
16847
+ * instance, so `createContext()` references match across bundles.
16800
16848
  *
16801
16849
  * Live-update — listens to `addon.widget-ready` so newly-loaded addons
16802
16850
  * surface their widgets without a manual page reload (the aggregator
@@ -16807,11 +16855,11 @@ var useZonesUpdateZone = trpc.zones.updateZone.useMutation;
16807
16855
  */
16808
16856
  var WidgetRegistryContext = createSharedContext("camstack:widget-registry", null);
16809
16857
  /**
16810
- * Process-global cache for resolved widget bundles, keyed by
16811
- * `remoteName` (every remote exposes a single `./widgets` module whose
16812
- * default export is the `Record<stableId, Component>` map). Survives
16813
- * provider remount — avoids a full re-fetch + re-init of the MF remote
16814
- * 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.
16815
16863
  */
16816
16864
  var bundleModuleCache = /* @__PURE__ */ new Map();
16817
16865
  var bundleInflight = /* @__PURE__ */ new Map();
@@ -16823,6 +16871,10 @@ var bundleInflight = /* @__PURE__ */ new Map();
16823
16871
  * update.
16824
16872
  */
16825
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
+ }
16826
16878
  function isRemoteWidgetsModule(value) {
16827
16879
  if (!value || typeof value !== "object") return false;
16828
16880
  const def = value.default;
@@ -16831,8 +16883,7 @@ function isRemoteWidgetsModule(value) {
16831
16883
  /**
16832
16884
  * Diagnostic-only string for the "Got: …" tail in the not-a-record
16833
16885
  * error. Returns a JSON-friendly snapshot of the module's keys + proto
16834
- * name without exposing the value itself (the value is the unknown
16835
- * thing the module factory produced, may be huge or contain PII).
16886
+ * name without exposing the value itself.
16836
16887
  */
16837
16888
  function describeRemoteShape(mod) {
16838
16889
  if (!mod || typeof mod !== "object") return typeof mod;
@@ -16846,13 +16897,19 @@ function describeRemoteShape(mod) {
16846
16897
  };
16847
16898
  }
16848
16899
  /**
16849
- * Register the MF remote (idempotent) and load its `./widgets` module.
16850
- * 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.
16851
16907
  */
16852
- async function loadRemoteBundle(remoteName, entryUrl) {
16853
- const cached = bundleModuleCache.get(remoteName);
16908
+ async function loadRemoteBundle(remoteName, exposedModule, entryUrl) {
16909
+ const cacheKey = bundleCacheKey(remoteName, exposedModule);
16910
+ const cached = bundleModuleCache.get(cacheKey);
16854
16911
  if (cached) return cached;
16855
- const inflight = bundleInflight.get(remoteName);
16912
+ const inflight = bundleInflight.get(cacheKey);
16856
16913
  if (inflight) return inflight;
16857
16914
  if (!registeredRemotes.has(remoteName)) {
16858
16915
  ensureMfHostInit();
@@ -16863,22 +16920,26 @@ async function loadRemoteBundle(remoteName, entryUrl) {
16863
16920
  }], { force: false });
16864
16921
  registeredRemotes.add(remoteName);
16865
16922
  }
16866
- const promise = loadRemote(`${remoteName}/widgets`).then((mod) => {
16923
+ const promise = loadRemote(`${remoteName}/${exposedModule.startsWith("./") ? exposedModule.slice(2) : exposedModule}`).then((mod) => {
16867
16924
  if (!isRemoteWidgetsModule(mod)) {
16868
16925
  const shape = describeRemoteShape(mod);
16869
- 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)}`);
16870
16927
  }
16871
16928
  const map = mod.default;
16872
- bundleModuleCache.set(remoteName, map);
16873
- bundleInflight.delete(remoteName);
16929
+ bundleModuleCache.set(cacheKey, map);
16930
+ bundleInflight.delete(cacheKey);
16874
16931
  return map;
16875
16932
  }).catch((err) => {
16876
- bundleInflight.delete(remoteName);
16933
+ bundleInflight.delete(cacheKey);
16877
16934
  throw err;
16878
16935
  });
16879
- bundleInflight.set(remoteName, promise);
16936
+ bundleInflight.set(cacheKey, promise);
16880
16937
  return promise;
16881
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
+ }
16882
16943
  var BOOT_WINDOW_MS = 3e4;
16883
16944
  var POLL_INTERVAL_MS = 2e3;
16884
16945
  function WidgetRegistryProvider({ children }) {
@@ -16898,21 +16959,24 @@ function WidgetRegistryProvider({ children }) {
16898
16959
  queryClient.invalidateQueries({ queryKey: [["addonWidgets", "listWidgets"]] });
16899
16960
  });
16900
16961
  const [resolvedTick, setResolvedTick] = useState(0);
16962
+ const widgets = useMemo(() => rawWidgets ?? [], [rawWidgets]);
16901
16963
  useEffect(() => {
16902
- if (!rawWidgets) return;
16964
+ if (widgets.length === 0) return;
16903
16965
  let cancelled = false;
16904
- const seenRemotes = /* @__PURE__ */ new Set();
16905
- for (const w of rawWidgets) {
16906
- if (seenRemotes.has(w.remoteName)) continue;
16907
- seenRemotes.add(w.remoteName);
16908
- 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;
16909
16972
  const entryUrl = w.bundleUrl;
16910
- loadRemoteBundle(w.remoteName, entryUrl).then(() => {
16973
+ loadRemoteBundle(w.remote.remoteName, w.remote.exposedModule, entryUrl).then(() => {
16911
16974
  if (!cancelled) setResolvedTick((t) => t + 1);
16912
16975
  }).catch((err) => {
16913
16976
  const reason = err instanceof Error ? err.message : String(err);
16914
16977
  (typeof globalThis !== "undefined" ? globalThis.console : void 0)?.error?.("[WidgetRegistry] Failed to load widget remote", {
16915
- remoteName: w.remoteName,
16978
+ remoteName: w.remote.remoteName,
16979
+ exposedModule: w.remote.exposedModule,
16916
16980
  entryUrl,
16917
16981
  reason
16918
16982
  });
@@ -16921,19 +16985,26 @@ function WidgetRegistryProvider({ children }) {
16921
16985
  return () => {
16922
16986
  cancelled = true;
16923
16987
  };
16924
- }, [rawWidgets]);
16988
+ }, [widgets]);
16925
16989
  const registry = useMemo(() => {
16926
- const widgets = rawWidgets ?? [];
16927
16990
  const byId = /* @__PURE__ */ new Map();
16928
- 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
+ }
16929
16996
  const toMetadata = (widgetId, entry) => ({
16930
16997
  widgetId,
16931
16998
  addonId: entry.addonId,
16932
16999
  stableId: entry.stableId,
17000
+ tab: entry.tab,
17001
+ subTab: entry.subTab,
16933
17002
  label: entry.label,
17003
+ order: entry.order,
17004
+ kind: entry.kind,
17005
+ remote: entry.remote,
16934
17006
  description: entry.description,
16935
17007
  icon: entry.icon,
16936
- remoteName: entry.remoteName,
16937
17008
  bundleUrl: entry.bundleUrl,
16938
17009
  hosts: entry.hosts,
16939
17010
  requires: entry.requires,
@@ -16946,9 +17017,9 @@ function WidgetRegistryProvider({ children }) {
16946
17017
  resolve: (widgetId) => {
16947
17018
  const entry = byId.get(widgetId);
16948
17019
  if (!entry) return void 0;
16949
- const bundle = bundleModuleCache.get(entry.remoteName);
17020
+ const bundle = peekBundle(entry.remote.remoteName, entry.remote.exposedModule);
16950
17021
  if (!bundle) return null;
16951
- const Component = bundle[entry.stableId];
17022
+ const Component = bundle[entry.remote.componentKey ?? entry.stableId];
16952
17023
  if (!Component) return void 0;
16953
17024
  return Component;
16954
17025
  },
@@ -16961,15 +17032,23 @@ function WidgetRegistryProvider({ children }) {
16961
17032
  const out = [];
16962
17033
  for (const [widgetId, entry] of byId) out.push(toMetadata(widgetId, entry));
16963
17034
  return out;
16964
- }
17035
+ },
17036
+ entryUrlFor: (remoteName) => entryUrlByRemote.get(remoteName)
16965
17037
  };
16966
- }, [rawWidgets, resolvedTick]);
17038
+ }, [widgets, resolvedTick]);
16967
17039
  return /* @__PURE__ */ jsx(WidgetRegistryContext.Provider, {
16968
17040
  value: registry,
16969
17041
  children
16970
17042
  });
16971
17043
  }
16972
- /** 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
+ */
16973
17052
  function useWidget(widgetId) {
16974
17053
  return useWidgetRegistry().resolve(widgetId);
16975
17054
  }
@@ -16983,6 +17062,68 @@ function useWidgetMetadata(widgetId) {
16983
17062
  function useAllWidgets() {
16984
17063
  return useWidgetRegistry().listAll();
16985
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
+ }
16986
17127
  /** Read the registry instance — throws when no provider is mounted. */
16987
17128
  function useWidgetRegistry() {
16988
17129
  const ctx = useOptionalWidgetRegistry();
@@ -16997,163 +17138,1429 @@ function useContextSafe(ctx) {
16997
17138
  return useContext(ctx);
16998
17139
  }
16999
17140
  //#endregion
17000
- //#region src/composites/widget-slot.tsx
17141
+ //#region src/hooks/use-ptz.ts
17001
17142
  /**
17002
- * <WidgetSlot>single host-side mount point for any addon-contributed
17003
- * widget. Consumers reference a widget by its public id
17004
- * (`<addonId>/<stableId>`), the slot looks the component up in the
17005
- * shared `WidgetRegistry`, validates host context against the widget's
17006
- * `requires` metadata, and renders one of:
17007
- * - skeleton placeholder while the bundle is still loading,
17008
- * - inline error fallback when the widget id is unknown OR the host
17009
- * didn't supply a required context (`deviceContext` /
17010
- * `integrationContext`),
17011
- * - the resolved component otherwise.
17143
+ * usePTZPTZ control hook for device-scoped pan / tilt / zoom.
17012
17144
  *
17013
- * The slot is intentionally STUPID no styling beyond the skeleton/
17014
- * error fallback. Layout (card vs inline, sizing) is the host's
17015
- * 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.
17016
17164
  */
17017
- function WidgetSlot(props) {
17018
- const { widgetId, host = "device-tab", config, deviceId, integrationId, instanceId, size, columns, rows } = props;
17019
- const Component = useWidget(widgetId);
17020
- const metadata = useWidgetMetadata(widgetId);
17021
- const resolvedInstanceId = useMemo(() => instanceId ?? widgetId, [instanceId, widgetId]);
17022
- if (Component === void 0 && metadata === void 0) return /* @__PURE__ */ jsx(WidgetMissingError, {
17023
- widgetId,
17024
- reason: "unknown"
17025
- });
17026
- if (Component === void 0) return /* @__PURE__ */ jsx(WidgetMissingError, {
17027
- widgetId,
17028
- reason: "missing-export"
17029
- });
17030
- if (Component === null) return /* @__PURE__ */ jsx(WidgetSkeleton, {});
17031
- if (metadata) {
17032
- if (metadata.requires.deviceContext && deviceId === void 0) return /* @__PURE__ */ jsx(WidgetMissingError, {
17033
- widgetId,
17034
- reason: "missing-device-context"
17035
- });
17036
- if (metadata.requires.integrationContext && integrationId === void 0) return /* @__PURE__ */ jsx(WidgetMissingError, {
17037
- widgetId,
17038
- reason: "missing-integration-context"
17039
- });
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
17040
17197
  }
17041
- return /* @__PURE__ */ jsx(Component, {
17042
- instanceId: resolvedInstanceId,
17043
- host,
17044
- config,
17045
- deviceId,
17046
- integrationId,
17047
- size,
17048
- columns,
17049
- rows
17050
- });
17051
- }
17052
- function WidgetSkeleton() {
17053
- return /* @__PURE__ */ jsxs("div", {
17054
- className: "rounded-lg border border-border bg-surface/40 p-4 animate-pulse",
17055
- children: [
17056
- /* @__PURE__ */ jsx("div", { className: "h-3 w-24 bg-foreground-subtle/20 rounded mb-2" }),
17057
- /* @__PURE__ */ jsx("div", { className: "h-2 w-full bg-foreground-subtle/10 rounded mb-1" }),
17058
- /* @__PURE__ */ jsx("div", { className: "h-2 w-3/4 bg-foreground-subtle/10 rounded" })
17059
- ]
17060
- });
17061
- }
17062
- function WidgetMissingError({ widgetId, reason }) {
17063
- return /* @__PURE__ */ jsx("div", {
17064
- className: "rounded-lg border border-warning/30 bg-warning/10 px-3 py-2 text-xs text-warning",
17065
- 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`
17066
- });
17067
- }
17068
- //#endregion
17069
- //#region src/composites/config-form-field.tsx
17070
- 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";
17071
- var LABEL_CLASS = "block text-[11px] font-medium text-foreground mb-1";
17072
- var DESC_CLASS = "text-[10px] text-foreground-subtle mt-0.5";
17073
- function FieldWrapper({ label, description, required, span, children, translationFn }) {
17074
- const colSpanClass = span === 2 ? "col-span-2" : span === 3 ? "col-span-3" : span === 4 ? "col-span-4" : "col-span-1";
17075
- const resolvedLabel = resolveLabel(label, translationFn);
17076
- const resolvedDescription = resolveLabel(description, translationFn);
17077
- return /* @__PURE__ */ jsxs("div", {
17078
- className: colSpanClass,
17079
- children: [
17080
- resolvedLabel !== void 0 && resolvedLabel !== "" && /* @__PURE__ */ jsxs("label", {
17081
- className: LABEL_CLASS,
17082
- children: [resolvedLabel, required && /* @__PURE__ */ jsx("span", {
17083
- className: "text-danger ml-0.5",
17084
- children: "*"
17085
- })]
17086
- }),
17087
- children,
17088
- resolvedDescription && /* @__PURE__ */ jsx("p", {
17089
- className: DESC_CLASS,
17090
- children: resolvedDescription
17091
- })
17092
- ]
17093
- });
17094
- }
17095
- function TextField({ field, value, onChange, disabled, translationFn }) {
17096
- return /* @__PURE__ */ jsx(FieldWrapper, {
17097
- label: field.label,
17098
- description: field.description,
17099
- required: field.required,
17100
- span: field.span,
17101
- translationFn,
17102
- children: /* @__PURE__ */ jsx("input", {
17103
- type: field.inputType ?? "text",
17104
- className: INPUT_CLASS,
17105
- value: value === void 0 || value === null ? "" : String(value),
17106
- placeholder: field.placeholder,
17107
- maxLength: field.maxLength,
17108
- pattern: field.pattern,
17109
- disabled: disabled || field.disabled,
17110
- onChange: (e) => onChange(e.target.value)
17111
- })
17112
- });
17113
- }
17114
- function NumberField({ field, value, onChange, disabled, translationFn }) {
17115
- const [local, setLocal] = useState(value === void 0 || value === null ? "" : String(value));
17116
- 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]);
17117
17230
  useEffect(() => {
17118
- if (focusedRef.current) return;
17119
- setLocal(value === void 0 || value === null ? "" : String(value));
17120
- }, [value]);
17121
- const handleChange = (raw) => {
17122
- setLocal(raw);
17123
- if (raw === "" || raw === "-") {
17124
- 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));
17125
17242
  return;
17243
+ } finally {
17244
+ setBusy(false);
17126
17245
  }
17127
- const parsed = Number(raw);
17128
- if (Number.isNaN(parsed)) return;
17129
- onChange(parsed);
17130
- };
17131
- return /* @__PURE__ */ jsx(FieldWrapper, {
17132
- label: field.label,
17133
- description: field.description,
17134
- required: field.required,
17135
- span: field.span,
17136
- translationFn,
17137
- children: /* @__PURE__ */ jsxs("div", {
17138
- className: "flex items-center gap-1",
17139
- children: [/* @__PURE__ */ jsx("input", {
17140
- type: "number",
17141
- className: INPUT_CLASS,
17142
- value: local,
17143
- placeholder: field.placeholder,
17144
- min: field.min,
17145
- max: field.max,
17146
- step: field.step,
17147
- disabled: disabled || field.disabled,
17148
- onFocus: () => {
17149
- focusedRef.current = true;
17150
- },
17151
- onBlur: () => {
17152
- focusedRef.current = false;
17153
- },
17154
- onChange: (e) => handleChange(e.target.value)
17155
- }), field.unit && /* @__PURE__ */ jsx("span", {
17156
- 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",
17157
18564
  children: field.unit
17158
18565
  })]
17159
18566
  })
@@ -18262,18 +19669,12 @@ function AddonActionButtonField({ field, values, disabled, onAction }) {
18262
19669
  }
18263
19670
  function WidgetField({ field }) {
18264
19671
  const deviceId = useDeviceId();
18265
- return /* @__PURE__ */ jsx(FieldWrapper, {
18266
- label: field.label,
18267
- description: field.description,
18268
- required: field.required,
18269
- span: field.span,
18270
- children: /* @__PURE__ */ jsx(WidgetSlot, {
18271
- widgetId: field.widgetId,
18272
- host: "device-tab",
18273
- config: field.widgetConfig,
18274
- deviceId: deviceId ?? void 0,
18275
- instanceId: field.key
18276
- })
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
18277
19678
  });
18278
19679
  }
18279
19680
  function formatReadonlyValue(value, unit) {
@@ -18764,7 +20165,7 @@ function ConfigFormBuilder({ schema, values, onChange, disabled, translationFn,
18764
20165
  * + patch loop. `onAfterChange` fires once the patch is applied so
18765
20166
  * consumers can re-read any derived state.
18766
20167
  */
18767
- var Chevron = ({ open }) => /* @__PURE__ */ jsx("svg", {
20168
+ var Chevron$1 = ({ open }) => /* @__PURE__ */ jsx("svg", {
18768
20169
  className: `h-3 w-3 transition-transform ${open ? "rotate-90" : ""}`,
18769
20170
  viewBox: "0 0 24 24",
18770
20171
  fill: "none",
@@ -18929,7 +20330,7 @@ function AddonGlobalSettingsForm({ trpc, addonId, nodeId, title, disabled, onAft
18929
20330
  className: "w-full px-4 py-2 flex items-center gap-2 hover:bg-surface-hover text-left",
18930
20331
  "aria-expanded": open,
18931
20332
  children: [
18932
- /* @__PURE__ */ jsx(Chevron, { open }),
20333
+ /* @__PURE__ */ jsx(Chevron$1, { open }),
18933
20334
  /* @__PURE__ */ jsx("h3", {
18934
20335
  className: "text-xs font-semibold text-foreground uppercase tracking-wide flex-1",
18935
20336
  children: title ?? addonId
@@ -19500,183 +20901,37 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
19500
20901
  className: "h-3.5 w-3.5",
19501
20902
  viewBox: "0 0 24 24",
19502
20903
  fill: "none",
19503
- stroke: "currentColor",
19504
- strokeWidth: "2",
19505
- strokeLinecap: "round",
19506
- 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" })
19507
- })
19508
- })]
19509
- })]
19510
- })
19511
- ]
19512
- });
19513
- }
19514
- function ToolbarButton$1({ onClick, title, children }) {
19515
- return /* @__PURE__ */ jsx("button", {
19516
- onClick,
19517
- title,
19518
- className: "rounded-full p-1.5 text-white/80 hover:text-white hover:bg-white/20 transition-colors",
19519
- children
19520
- });
19521
- }
19522
- async function waitForIceGathering(pc) {
19523
- if (pc.iceGatheringState === "complete") return;
19524
- return new Promise((resolve) => {
19525
- const handler = () => {
19526
- if (pc.iceGatheringState === "complete") {
19527
- pc.removeEventListener("icegatheringstatechange", handler);
19528
- resolve();
19529
- }
19530
- };
19531
- pc.addEventListener("icegatheringstatechange", handler);
19532
- setTimeout(resolve, 5e3);
19533
- });
19534
- }
19535
- //#endregion
19536
- //#region src/contexts/player-overlays.tsx
19537
- /**
19538
- * Player overlay registry — pluggable layers + toolbar buttons for
19539
- * the device-detail live-frame `StreamPanel`.
19540
- *
19541
- * Why a registry: previously `StreamPanelContent` hardcoded each
19542
- * overlay (PTZ, intercom toggle, zone editor) and threaded the
19543
- * editing state by hand. Adding a new feature (audio waveform,
19544
- * detection bbox tracker, recording controls, …) meant another
19545
- * round of plumbing through the host file and StreamPanel's prop
19546
- * surface. The registry lets a sibling component (Detection tab,
19547
- * Live Stats tab, an addon-page extension) declare its own layer
19548
- * imperatively via a hook, and the host renders whatever's
19549
- * registered.
19550
- *
19551
- * Two registries live side-by-side:
19552
- *
19553
- * 1. **layers** — absolute-positioned React nodes drawn over the
19554
- * video frame (zones polygon canvas, motion bbox overlay,
19555
- * audio waveform). Rendered ordered by `order` (lower first
19556
- * → bottom; higher → top); the last-registered layer wins
19557
- * ties.
19558
- *
19559
- * 2. **toolbar buttons** — controls surfaced in the player's
19560
- * always-visible toolbar cluster (next to Intercom + Play/Stop).
19561
- * Each carries an icon, label, controlled `active` flag, and
19562
- * a click handler. The host (StreamPanel) renders them
19563
- * uniformly; tone variants stay simple (`'default' | 'primary'`).
19564
- *
19565
- * Lifecycle: hooks register their layer / button on mount and
19566
- * unregister on unmount, so swapping tabs (e.g. leaving Detection)
19567
- * automatically tears down the registration. Re-registering with
19568
- * the same `id` replaces the prior entry — meaning a single hook
19569
- * call can update its overlay/button props on every render without
19570
- * leaking entries.
19571
- *
19572
- * Provider scope: typically wraps the whole device-detail subtree
19573
- * so the StreamPanel + every tab share the same registry. One
19574
- * provider per `deviceId`; switching device IDs unmounts the
19575
- * provider naturally.
19576
- */
19577
- var PlayerOverlaysStateContext = createSharedContext("camstack:player-overlays-state", null);
19578
- var PlayerOverlaysActionsContext = createSharedContext("camstack:player-overlays-actions", null);
19579
- function PlayerOverlaysProvider({ children }) {
19580
- const [layers, setLayers] = useState(() => /* @__PURE__ */ new Map());
19581
- const [buttons, setButtons] = useState(() => /* @__PURE__ */ new Map());
19582
- const setLayer = useCallback((layer) => {
19583
- setLayers((prev) => {
19584
- const next = new Map(prev);
19585
- next.set(layer.id, layer);
19586
- return next;
19587
- });
19588
- }, []);
19589
- const removeLayer = useCallback((id) => {
19590
- setLayers((prev) => {
19591
- if (!prev.has(id)) return prev;
19592
- const next = new Map(prev);
19593
- next.delete(id);
19594
- return next;
19595
- });
19596
- }, []);
19597
- const setButton = useCallback((button) => {
19598
- setButtons((prev) => {
19599
- const next = new Map(prev);
19600
- next.set(button.id, button);
19601
- return next;
19602
- });
19603
- }, []);
19604
- const removeButton = useCallback((id) => {
19605
- setButtons((prev) => {
19606
- if (!prev.has(id)) return prev;
19607
- const next = new Map(prev);
19608
- next.delete(id);
19609
- return next;
19610
- });
19611
- }, []);
19612
- const stateValue = useMemo(() => ({
19613
- layers,
19614
- buttons
19615
- }), [layers, buttons]);
19616
- const actionsValue = useMemo(() => ({
19617
- setLayer,
19618
- removeLayer,
19619
- setButton,
19620
- removeButton
19621
- }), [
19622
- setLayer,
19623
- removeLayer,
19624
- setButton,
19625
- removeButton
19626
- ]);
19627
- return /* @__PURE__ */ jsx(PlayerOverlaysStateContext.Provider, {
19628
- value: stateValue,
19629
- children: /* @__PURE__ */ jsx(PlayerOverlaysActionsContext.Provider, {
19630
- value: actionsValue,
19631
- children
19632
- })
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
+ ]
19633
20913
  });
19634
20914
  }
19635
- /** Snapshot of registered layers ordered by `order` (asc, ties resolved
19636
- * by insertion order of the underlying Map). Returns `[]` outside a
19637
- * provider. */
19638
- function usePlayerOverlayLayers() {
19639
- const state = useContext(PlayerOverlaysStateContext);
19640
- return useMemo(() => {
19641
- if (!state) return [];
19642
- return [...state.layers.values()].sort((a, b) => a.order - b.order);
19643
- }, [state]);
19644
- }
19645
- /** Snapshot of registered toolbar buttons, ordered by `order` (asc). */
19646
- function usePlayerToolbarButtons() {
19647
- const state = useContext(PlayerOverlaysStateContext);
19648
- return useMemo(() => {
19649
- if (!state) return [];
19650
- return [...state.buttons.values()].sort((a, b) => a.order - b.order);
19651
- }, [state]);
19652
- }
19653
- /**
19654
- * Register an overlay layer for the lifetime of the calling component.
19655
- * Re-registers on every render with the latest spec; auto-unregisters
19656
- * on unmount. Pass `null` to skip registration when the layer is
19657
- * conditionally enabled (the hook still runs every render — keeps
19658
- * react-hook order stable across spec === null toggles).
19659
- *
19660
- * Callers that build the spec inline should memoise it (`useMemo`)
19661
- * to avoid re-registering on every parent render — context-write
19662
- * effects depend on referential equality of the spec object.
19663
- */
19664
- function usePlayerOverlayLayer(spec) {
19665
- const actions = useContext(PlayerOverlaysActionsContext);
19666
- useEffect(() => {
19667
- if (!actions || !spec) return void 0;
19668
- actions.setLayer(spec);
19669
- return () => actions.removeLayer(spec.id);
19670
- }, [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
+ });
19671
20922
  }
19672
- /** Same shape as `usePlayerOverlayLayer`, scoped to toolbar buttons. */
19673
- function usePlayerToolbarButton(spec) {
19674
- const actions = useContext(PlayerOverlaysActionsContext);
19675
- useEffect(() => {
19676
- if (!actions || !spec) return void 0;
19677
- actions.setButton(spec);
19678
- return () => actions.removeButton(spec.id);
19679
- }, [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
+ });
19680
20935
  }
19681
20936
  //#endregion
19682
20937
  //#region src/composites/stream-panel.tsx
@@ -21484,12 +22739,11 @@ function CopyButton({ value, label, className, disabled }) {
21484
22739
  * Alexa / HAP / MQTT specifics here. Mount it next to an export addon's
21485
22740
  * standard settings form (e.g. in the addon-settings modal).
21486
22741
  *
21487
- * Routing note: `device-export` is a collection cap; the codegen'd
21488
- * router resolves `{ nodeId }` to the first registered provider. Export
21489
- * addons are all `hub-only`, so the panel queries `nodeId: 'hub'`. When
21490
- * multiple device-export addons are co-installed the cap-router today
21491
- * resolves to the first provider only (per-provider routing is a
21492
- * separate follow-up) — identical behaviour to the pages it replaces.
22742
+ * Routing note: `device-export` is a collection cap. Export addons are
22743
+ * all `hub-only`, so the panel queries `nodeId: 'hub'`. It also passes
22744
+ * `addonId` so the codegen'd cap-router resolves THIS addon's provider
22745
+ * within the local collection — when multiple device-export addons are
22746
+ * co-installed each panel shows its own provider, not provider[0].
21493
22747
  */
21494
22748
  var ExposedDeviceArraySchema = z.array(ExposedDeviceSchema);
21495
22749
  var STATUS_POLL_INTERVAL_MS = 1e4;
@@ -21580,11 +22834,17 @@ function SetupSection({ setup }) {
21580
22834
  */
21581
22835
  function DeviceExportPanel({ addonId, onOpenDevice }) {
21582
22836
  const queryClient = useQueryClient();
21583
- const statusQuery = useDeviceExportGetStatus({ nodeId: "hub" }, {
22837
+ const statusQuery = useDeviceExportGetStatus({
22838
+ nodeId: "hub",
22839
+ addonId
22840
+ }, {
21584
22841
  refetchInterval: STATUS_POLL_INTERVAL_MS,
21585
22842
  retry: false
21586
22843
  });
21587
- const exposedQuery = useDeviceExportListExposedDevices({ nodeId: "hub" }, {
22844
+ const exposedQuery = useDeviceExportListExposedDevices({
22845
+ nodeId: "hub",
22846
+ addonId
22847
+ }, {
21588
22848
  refetchInterval: STATUS_POLL_INTERVAL_MS,
21589
22849
  retry: false
21590
22850
  });
@@ -21656,13 +22916,18 @@ function DeviceExportPanel({ addonId, onOpenDevice }) {
21656
22916
  disabled: unexposeMutation.isPending,
21657
22917
  onClick: () => unexposeMutation.mutate({
21658
22918
  deviceId: row.deviceId,
21659
- nodeId: "hub"
22919
+ nodeId: "hub",
22920
+ addonId
21660
22921
  }),
21661
22922
  children: [/* @__PURE__ */ jsx(Unlink2, { className: "h-3.5 w-3.5 mr-1" }), "Unexpose"]
21662
22923
  })]
21663
22924
  })
21664
22925
  }
21665
- ], [onOpenDevice, unexposeMutation]);
22926
+ ], [
22927
+ onOpenDevice,
22928
+ unexposeMutation,
22929
+ addonId
22930
+ ]);
21666
22931
  return /* @__PURE__ */ jsxs("div", {
21667
22932
  className: "rounded-lg border border-border bg-surface overflow-hidden",
21668
22933
  children: [
@@ -22152,7 +23417,7 @@ var LEVEL_OPTIONS = [
22152
23417
  "error"
22153
23418
  ];
22154
23419
  /** Max live entries kept in the ring buffer. */
22155
- var MAX_LIVE_ENTRIES$2 = 500;
23420
+ var MAX_LIVE_ENTRIES$1 = 500;
22156
23421
  function passesLevel(logLevel, filterLevel) {
22157
23422
  if (!filterLevel) return true;
22158
23423
  return (LEVEL_SEVERITY[logLevel] ?? 0) >= (LEVEL_SEVERITY[filterLevel] ?? 0);
@@ -22181,7 +23446,7 @@ function LogStream({ agentId: propsAgentId, addonId: propsAddonId, deviceId: pro
22181
23446
  integrationId,
22182
23447
  requestId
22183
23448
  ]);
22184
- const fallbackBuffer = useLiveBuffer(MAX_LIVE_ENTRIES$2);
23449
+ const fallbackBuffer = useLiveBuffer(MAX_LIVE_ENTRIES$1);
22185
23450
  const buffer = externalBuffer ?? fallbackBuffer;
22186
23451
  const liveLogs = buffer.entries;
22187
23452
  const [clearedAt, setClearedAt] = useState(0);
@@ -22508,7 +23773,7 @@ function LogStream({ agentId: propsAgentId, addonId: propsAddonId, deviceId: pro
22508
23773
  }),
22509
23774
  /* @__PURE__ */ jsx("td", {
22510
23775
  className: "px-1 py-1 align-top w-5",
22511
- children: /* @__PURE__ */ jsx(RowCopyButton$2, {
23776
+ children: /* @__PURE__ */ jsx(RowCopyButton$1, {
22512
23777
  copied: copiedRowKey === key,
22513
23778
  onCopy: () => copyOne(log, key)
22514
23779
  })
@@ -22547,7 +23812,7 @@ function ScopeBadge$2({ label, value }) {
22547
23812
  * "did it work" affordance — same UX language the toolbar Copy
22548
23813
  * button already uses for its `Copied!` label.
22549
23814
  */
22550
- function RowCopyButton$2({ copied, onCopy }) {
23815
+ function RowCopyButton$1({ copied, onCopy }) {
22551
23816
  return /* @__PURE__ */ jsx("button", {
22552
23817
  type: "button",
22553
23818
  title: "Copy this row",
@@ -22872,7 +24137,7 @@ function hasContent(entry) {
22872
24137
  if (cat === EventCategory.DetectionResult || cat === EventCategory.PipelineAudioInferenceResult) return summarizeEvent(cat, entry.data) !== null;
22873
24138
  return true;
22874
24139
  }
22875
- var MAX_LIVE_ENTRIES$1 = 300;
24140
+ var MAX_LIVE_ENTRIES = 300;
22876
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 }) {
22877
24142
  const defaultsArray = useMemo(() => defaultCategories ?? legacyCategories ?? DEFAULT_EVENT_CATEGORIES, [defaultCategories, legacyCategories]);
22878
24143
  const defaultsKey = useMemo(() => [...defaultsArray].sort().join(","), [defaultsArray]);
@@ -22884,7 +24149,7 @@ function EventStream({ agentId, addonId, deviceId, defaultCategories, categories
22884
24149
  setActiveCategories(new Set(defaultsArray));
22885
24150
  }
22886
24151
  }, [defaultsKey, defaultsArray]);
22887
- const fallbackBuffer = useLiveBuffer(MAX_LIVE_ENTRIES$1);
24152
+ const fallbackBuffer = useLiveBuffer(MAX_LIVE_ENTRIES);
22888
24153
  const buffer = externalBuffer ?? fallbackBuffer;
22889
24154
  const liveEvents = buffer.entries;
22890
24155
  const [autoScroll, setAutoScroll] = useState(true);
@@ -23208,7 +24473,7 @@ function EventStream({ agentId, addonId, deviceId, defaultCategories, categories
23208
24473
  className: "text-foreground-subtle whitespace-nowrap w-[70px] shrink-0 pt-0.5",
23209
24474
  children: new Date(event.timestamp).toLocaleTimeString()
23210
24475
  }),
23211
- /* @__PURE__ */ jsx(RowCopyButton$1, {
24476
+ /* @__PURE__ */ jsx(RowCopyButton, {
23212
24477
  copied: copiedRowId === event.id,
23213
24478
  onCopy: () => copyOne(event)
23214
24479
  }),
@@ -23398,7 +24663,7 @@ function ScopeBadge$1({ label, value }) {
23398
24663
  * click doesn't toggle the row's expand state. Flashes a Check
23399
24664
  * for ~1s after copy.
23400
24665
  */
23401
- function RowCopyButton$1({ copied, onCopy }) {
24666
+ function RowCopyButton({ copied, onCopy }) {
23402
24667
  return /* @__PURE__ */ jsx("button", {
23403
24668
  type: "button",
23404
24669
  title: "Copy this event",
@@ -23665,162 +24930,137 @@ function StatusBadge$1({ status }) {
23665
24930
  //#endregion
23666
24931
  //#region src/composites/state-values-stream.tsx
23667
24932
  /**
23668
- * 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).
23669
24937
  *
23670
- * Subscribes to `EventCategory.DeviceStateChanged` events scoped to a
23671
- * single device. Each row shows: timestamp, capName, slice JSON.
23672
- * 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).
23673
24951
  *
23674
- * Live subscription is mounted only while this component is rendered,
24952
+ * The live subscription is mounted only while this component renders,
23675
24953
  * so the parent can mount/unmount it on tab switches and the tRPC
23676
24954
  * subscription is torn down automatically when hidden.
23677
24955
  */
23678
- function isStateChangeForDevice(raw, deviceId) {
23679
- if (!raw || typeof raw !== "object") return false;
23680
- const e = raw;
23681
- if (e.category !== EventCategory.DeviceStateChanged) return false;
23682
- const data = e.data;
23683
- if (!data || typeof data !== "object") return false;
23684
- if (data.deviceId !== deviceId) return false;
23685
- if (typeof data.capName !== "string") return false;
23686
- if (!data.slice || typeof data.slice !== "object") return false;
23687
- return true;
23688
- }
23689
- 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) {
23690
24959
  if (!raw || typeof raw !== "object") return null;
23691
24960
  const e = raw;
23692
- const id = typeof e.id === "string" ? e.id : null;
23693
- const timestamp = typeof e.timestamp === "string" ? e.timestamp : null;
24961
+ if (e.category !== EventCategory.DeviceStateChanged) return null;
23694
24962
  const data = e.data;
23695
- if (!id || !timestamp || !data || typeof data !== "object") return null;
23696
- 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;
23697
24967
  return {
23698
- id,
23699
- timestamp,
23700
24968
  capName: data.capName,
23701
24969
  slice: data.slice
23702
24970
  };
23703
24971
  }
23704
- var MAX_LIVE_ENTRIES = 300;
24972
+ function isSystemSnapshot(raw) {
24973
+ return !!raw && typeof raw === "object" && !Array.isArray(raw);
24974
+ }
23705
24975
  /**
23706
24976
  * Every device-scoped cap name, sorted alphabetically — same shape
23707
24977
  * as `ALL_EVENT_CATEGORIES` in event-stream.tsx so the filter
23708
24978
  * surface is symmetric across the three live-streams (Logs, Events,
23709
- * State). Computed once at module load; the user can pick from this
23710
- * full list rather than only the caps that have already emitted at
23711
- * least one slice change in the current session.
24979
+ * State). Computed once at module load.
23712
24980
  */
23713
24981
  var ALL_DEVICE_CAP_NAMES = Object.freeze([...new Set(ALL_CAPABILITY_DEFINITIONS.filter((c) => c.scope === "device").map((c) => c.name))].sort());
23714
- 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 }) {
23715
24987
  const [activeCaps, setActiveCaps] = useState(useMemo(() => new Set(defaultCaps ?? []), [defaultCaps]));
23716
24988
  const [searchText, setSearchText] = useState("");
23717
- const [autoScroll, setAutoScroll] = useState(true);
23718
- const [expandedRows, setExpandedRows] = useState(/* @__PURE__ */ new Set());
23719
- const [clearedAt, setClearedAt] = useState(0);
23720
24989
  const [filterOpen, setFilterOpen] = useState(false);
23721
24990
  const [filterSearch, setFilterSearch] = useState("");
23722
24991
  const filterRootRef = useRef(null);
23723
- const fallbackBuffer = useLiveBuffer(MAX_LIVE_ENTRIES);
23724
- const buffer = externalBuffer ?? fallbackBuffer;
23725
- const liveEvents = buffer.entries;
23726
- 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);
23727
24995
  const prevDeviceRef = useRef(deviceId);
23728
24996
  useEffect(() => {
23729
24997
  if (prevDeviceRef.current !== deviceId) {
23730
24998
  prevDeviceRef.current = deviceId;
23731
- buffer.reset();
23732
- setExpandedRows(/* @__PURE__ */ new Set());
23733
- setClearedAt(0);
24999
+ setCapStates(/* @__PURE__ */ new Map());
25000
+ setCollapseOverrides(/* @__PURE__ */ new Map());
23734
25001
  }
23735
- }, [deviceId, buffer]);
23736
- const recentInput = useMemo(() => ({
23737
- deviceId,
23738
- category: EventCategory.DeviceStateChanged,
23739
- limit
23740
- }), [deviceId, limit]);
23741
- const { data: initialEvents, isLoading } = trpc.systemEvents.getRecent.useQuery(recentInput, { staleTime: 3e4 });
23742
- 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]);
23743
25024
  useEffect(() => {
23744
- activeCapsRef.current = activeCaps;
23745
- }, [activeCaps]);
25025
+ seededRef.current = false;
25026
+ }, [deviceId]);
23746
25027
  trpc.systemEvents.subscribe.useSubscription({
23747
25028
  deviceId,
23748
25029
  category: EventCategory.DeviceStateChanged
23749
25030
  }, { onData: (raw) => {
23750
- if (!isStateChangeForDevice(raw, deviceId)) return;
23751
- const entry = toEntry(raw);
23752
- if (!entry) return;
23753
- const active = activeCapsRef.current;
23754
- if (active.size > 0 && !active.has(entry.capName)) return;
23755
- 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
+ });
23756
25041
  } });
23757
- const prevLiveCountRef = useRef(liveEvents.length);
23758
25042
  useEffect(() => {
23759
- const el = scrollRef.current;
23760
- if (!el) {
23761
- prevLiveCountRef.current = liveEvents.length;
23762
- return;
23763
- }
23764
- const prevCount = prevLiveCountRef.current;
23765
- prevLiveCountRef.current = liveEvents.length;
23766
- if (liveEvents.length <= prevCount) return;
23767
- if (autoScroll && el.scrollTop <= 8) el.scrollTo({ top: 0 });
23768
- else if (!autoScroll && el.scrollTop > 0) {
23769
- const firstRow = el.firstElementChild?.firstElementChild;
23770
- const rowHeight = firstRow instanceof HTMLElement ? firstRow.offsetHeight : 28;
23771
- const newCount = liveEvents.length - prevCount;
23772
- el.scrollTop += rowHeight * newCount;
23773
- }
23774
- }, [liveEvents.length, autoScroll]);
23775
- const allEntries = useMemo(() => {
23776
- const byId = /* @__PURE__ */ new Map();
23777
- for (const e of initialEvents ?? []) {
23778
- const entry = toEntry(e);
23779
- if (entry) byId.set(entry.id, entry);
23780
- }
23781
- for (const e of liveEvents) byId.set(e.id, e);
23782
- let list = [...byId.values()];
23783
- if (clearedAt > 0) list = list.filter((e) => new Date(e.timestamp).getTime() > clearedAt);
23784
- 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(() => {
23785
25052
  const needle = searchText.trim().toLowerCase();
23786
- if (needle) list = list.filter((e) => {
23787
- if (e.capName.toLowerCase().includes(needle)) return true;
23788
- try {
23789
- if (new Date(e.timestamp).toLocaleTimeString().toLowerCase().includes(needle)) return true;
23790
- } catch {}
23791
- try {
23792
- if (Object.entries(e.slice).map(([k, v]) => `${k}=${formatValue(v)}`).join(" · ").toLowerCase().includes(needle)) return true;
23793
- } catch {}
23794
- try {
23795
- if (JSON.stringify(e.slice).toLowerCase().includes(needle)) return true;
23796
- } catch {}
23797
- 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);
23798
25058
  });
23799
- return list.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
23800
25059
  }, [
23801
- initialEvents,
23802
- liveEvents,
25060
+ capStates,
23803
25061
  activeCaps,
23804
- searchText,
23805
- clearedAt
25062
+ searchText
23806
25063
  ]);
23807
- const visibleCaps = useMemo(() => {
23808
- const caps = new Set(ALL_DEVICE_CAP_NAMES);
23809
- for (const e of initialEvents ?? []) {
23810
- const entry = toEntry(e);
23811
- if (entry) caps.add(entry.capName);
23812
- }
23813
- for (const e of liveEvents) caps.add(e.capName);
23814
- return [...caps].sort();
23815
- }, [initialEvents, liveEvents]);
23816
- const toggleRow = useCallback((id) => {
23817
- setExpandedRows((prev) => {
23818
- const next = new Set(prev);
23819
- if (next.has(id)) next.delete(id);
23820
- else next.add(id);
23821
- return next;
23822
- });
23823
- }, []);
23824
25064
  const toggleCap = useCallback((cap) => {
23825
25065
  setActiveCaps((prev) => {
23826
25066
  const next = new Set(prev);
@@ -23833,34 +25073,36 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", limi
23833
25073
  setActiveCaps(new Set(defaultCaps ?? []));
23834
25074
  setSearchText("");
23835
25075
  }, [defaultCaps]);
23836
- const handleClear = useCallback(() => {
23837
- setClearedAt(Date.now());
23838
- buffer.reset();
23839
- }, [buffer]);
23840
- const [copiedAll, setCopiedAll] = useState(false);
23841
- const [copiedRowId, setCopiedRowId] = useState(null);
23842
- const formatEntryForCopy = useCallback((entry) => {
23843
- return `${new Date(entry.timestamp).toISOString()} [${entry.capName}] slice=${JSON.stringify(entry.slice)}`;
23844
- }, []);
23845
- const handleCopyAll = useCallback(() => {
23846
- const lines = allEntries.map(formatEntryForCopy);
23847
- navigator.clipboard.writeText(lines.join("\n")).then(() => {
23848
- setCopiedAll(true);
23849
- setTimeout(() => setCopiedAll(false), 1500);
23850
- });
23851
- }, [allEntries, formatEntryForCopy]);
23852
- const copyOne = useCallback((entry) => {
23853
- navigator.clipboard.writeText(formatEntryForCopy(entry)).then(() => {
23854
- setCopiedRowId(entry.id);
23855
- setTimeout(() => setCopiedRowId((prev) => prev === entry.id ? null : prev), 1200);
23856
- });
23857
- }, [formatEntryForCopy]);
23858
25076
  const handleSelectAllCaps = useCallback(() => {
23859
25077
  setActiveCaps(new Set(visibleCaps));
23860
25078
  }, [visibleCaps]);
23861
25079
  const handleSelectNoCaps = useCallback(() => {
23862
25080
  setActiveCaps(/* @__PURE__ */ new Set());
23863
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]);
23864
25106
  const filteredPopoverCaps = useMemo(() => {
23865
25107
  const needle = filterSearch.trim().toLowerCase();
23866
25108
  if (!needle) return visibleCaps;
@@ -23879,6 +25121,7 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", limi
23879
25121
  document.addEventListener("mousedown", onDocClick);
23880
25122
  return () => document.removeEventListener("mousedown", onDocClick);
23881
25123
  }, [filterOpen]);
25124
+ const searchNeedle = searchText.trim().toLowerCase();
23882
25125
  return /* @__PURE__ */ jsxs("div", {
23883
25126
  className: cn("h-full min-h-0 flex flex-col", className),
23884
25127
  onClick: (e) => e.stopPropagation(),
@@ -23895,18 +25138,6 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", limi
23895
25138
  label: "device",
23896
25139
  value: `#${deviceId}`
23897
25140
  }),
23898
- /* @__PURE__ */ jsxs("button", {
23899
- type: "button",
23900
- onClick: () => {
23901
- setAutoScroll((a) => !a);
23902
- if (!autoScroll && scrollRef.current) scrollRef.current.scrollTo({
23903
- top: 0,
23904
- behavior: "smooth"
23905
- });
23906
- },
23907
- 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"),
23908
- children: [/* @__PURE__ */ jsx(ArrowUpToLine, { className: "h-2.5 w-2.5" }), autoScroll ? "Auto" : "Paused"]
23909
- }),
23910
25141
  /* @__PURE__ */ jsxs("div", {
23911
25142
  className: "relative",
23912
25143
  ref: filterRootRef,
@@ -23914,7 +25145,7 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", limi
23914
25145
  type: "button",
23915
25146
  onClick: () => setFilterOpen((v) => !v),
23916
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"),
23917
- title: "Pick caps to track",
25148
+ title: "Pick caps to show",
23918
25149
  children: [
23919
25150
  /* @__PURE__ */ jsx(Funnel, { className: "h-2.5 w-2.5" }),
23920
25151
  "Filter (",
@@ -23939,19 +25170,12 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", limi
23939
25170
  title: "Reset filters",
23940
25171
  children: [/* @__PURE__ */ jsx(RotateCcw, { className: "h-2.5 w-2.5" }), "Reset"]
23941
25172
  }),
23942
- /* @__PURE__ */ jsxs("button", {
23943
- type: "button",
23944
- onClick: handleClear,
23945
- 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",
23946
- title: "Clear visible entries",
23947
- children: [/* @__PURE__ */ jsx(Trash2, { className: "h-2.5 w-2.5" }), "Clear"]
23948
- }),
23949
25173
  /* @__PURE__ */ jsxs("button", {
23950
25174
  type: "button",
23951
25175
  onClick: handleCopyAll,
23952
- disabled: allEntries.length === 0,
23953
- 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"),
23954
- 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",
23955
25179
  children: [/* @__PURE__ */ jsx(Copy, { className: "h-2.5 w-2.5" }), copiedAll ? "Copied!" : "Copy"]
23956
25180
  }),
23957
25181
  /* @__PURE__ */ jsxs("div", {
@@ -23970,13 +25194,9 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", limi
23970
25194
  children: /* @__PURE__ */ jsx(X, { className: "h-2.5 w-2.5" })
23971
25195
  })]
23972
25196
  }),
23973
- liveEvents.length > 0 && /* @__PURE__ */ jsxs("span", {
23974
- className: "text-[9px] text-emerald-400 font-medium",
23975
- children: [
23976
- "+",
23977
- liveEvents.length,
23978
- " live"
23979
- ]
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"]
23980
25200
  })
23981
25201
  ]
23982
25202
  }), onClose && /* @__PURE__ */ jsx("button", {
@@ -23986,90 +25206,172 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", limi
23986
25206
  children: /* @__PURE__ */ jsx(X, { className: "h-3.5 w-3.5" })
23987
25207
  })]
23988
25208
  }), /* @__PURE__ */ jsxs("div", {
23989
- ref: scrollRef,
23990
- 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),
23991
25210
  children: [
23992
- isLoading && /* @__PURE__ */ jsxs("div", {
23993
- className: "flex items-center justify-center gap-2 py-6 text-foreground-subtle",
23994
- 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..."]
23995
25214
  }),
23996
- !isLoading && allEntries.length === 0 && /* @__PURE__ */ jsx("div", {
23997
- className: "py-4 text-center text-foreground-subtle",
23998
- 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"
23999
25218
  }),
24000
- allEntries.length > 0 && /* @__PURE__ */ jsx("div", {
24001
- className: "divide-y divide-border/30",
24002
- children: allEntries.map((entry) => {
24003
- const expanded = expandedRows.has(entry.id);
24004
- const fields = Object.entries(entry.slice);
24005
- const summary = fields.slice(0, 3).map(([k, v]) => `${k}=${formatValue(v)}`).join(" · ");
24006
- return /* @__PURE__ */ jsxs("div", {
24007
- className: "flex items-start gap-2 px-3 py-1.5 hover:bg-surface-hover/30 cursor-pointer",
24008
- onClick: () => toggleRow(entry.id),
24009
- children: [
24010
- /* @__PURE__ */ jsx("span", {
24011
- className: "text-foreground-subtle whitespace-nowrap w-[70px] shrink-0 pt-0.5",
24012
- children: new Date(entry.timestamp).toLocaleTimeString()
24013
- }),
24014
- /* @__PURE__ */ jsx(RowCopyButton, {
24015
- copied: copiedRowId === entry.id,
24016
- onCopy: () => copyOne(entry)
24017
- }),
24018
- /* @__PURE__ */ jsxs("div", {
24019
- className: "flex-1 min-w-0",
24020
- children: [
24021
- /* @__PURE__ */ jsxs("div", {
24022
- className: "flex items-center gap-1.5",
24023
- children: [
24024
- 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" }),
24025
- /* @__PURE__ */ jsx("span", {
24026
- 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",
24027
- children: entry.capName
24028
- }),
24029
- fields.length > 3 && /* @__PURE__ */ jsxs("span", {
24030
- className: "text-[9px] text-foreground-subtle",
24031
- children: [
24032
- "+",
24033
- fields.length - 3,
24034
- " more"
24035
- ]
24036
- })
24037
- ]
24038
- }),
24039
- summary && /* @__PURE__ */ jsx("p", {
24040
- className: "text-foreground-subtle text-[10px] mt-0.5 ml-5 truncate",
24041
- children: summary
24042
- }),
24043
- expanded && /* @__PURE__ */ jsx("div", {
24044
- className: "mt-1 ml-5 text-[9px] bg-surface rounded px-2 py-1.5 space-y-0.5 font-mono overflow-x-auto",
24045
- children: fields.length === 0 ? /* @__PURE__ */ jsx("div", {
24046
- className: "text-foreground-subtle italic",
24047
- children: "empty slice"
24048
- }) : fields.map(([k, v]) => /* @__PURE__ */ jsxs("div", { children: [
24049
- /* @__PURE__ */ jsx("span", {
24050
- className: "text-primary/70",
24051
- children: k
24052
- }),
24053
- /* @__PURE__ */ jsx("span", {
24054
- className: "text-foreground-subtle",
24055
- children: ": "
24056
- }),
24057
- /* @__PURE__ */ jsx("span", {
24058
- className: "text-foreground break-all",
24059
- children: formatValue(v)
24060
- })
24061
- ] }, k))
24062
- })
24063
- ]
24064
- })
24065
- ]
24066
- }, 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);
24067
25236
  })
24068
25237
  })
24069
25238
  ]
24070
25239
  })]
24071
25240
  });
24072
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
+ }
24073
25375
  function formatValue(v) {
24074
25376
  if (v === null) return "null";
24075
25377
  if (v === void 0) return "undefined";
@@ -24081,9 +25383,46 @@ function formatValue(v) {
24081
25383
  return String(v);
24082
25384
  }
24083
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
+ }
24084
25423
  function ScopeBadge({ label, value }) {
24085
25424
  return /* @__PURE__ */ jsxs("span", {
24086
- 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",
24087
25426
  children: [
24088
25427
  label,
24089
25428
  ": ",
@@ -24092,16 +25431,13 @@ function ScopeBadge({ label, value }) {
24092
25431
  });
24093
25432
  }
24094
25433
  /**
24095
- * Cap-name multiselect popover — same shape as
24096
- * `CategoryFilterPopover` in event-stream.tsx (search box at top,
24097
- * Select All / None / Defaults buttons, scrollable checkbox list).
24098
- * Caps don't have icons / styled badges like events do — we render
24099
- * a flat name + violet-pill bracket so the look stays consistent
24100
- * 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).
24101
25437
  */
24102
25438
  function CapFilterPopover({ search, onSearchChange, caps, active, onToggle, onSelectAll, onSelectNone, onResetDefaults }) {
24103
25439
  return /* @__PURE__ */ jsxs("div", {
24104
- 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",
24105
25441
  onClick: (e) => e.stopPropagation(),
24106
25442
  children: [
24107
25443
  /* @__PURE__ */ jsxs("div", {
@@ -24172,24 +25508,6 @@ function CapFilterPopover({ search, onSearchChange, caps, active, onToggle, onSe
24172
25508
  ]
24173
25509
  });
24174
25510
  }
24175
- /**
24176
- * Per-row Copy icon — same shape used in log-stream / event-stream
24177
- * so the three live-streams' rows are visually symmetric. Stops
24178
- * propagation so the click doesn't toggle the row's expand state.
24179
- * Flashes a Check for ~1s on success.
24180
- */
24181
- function RowCopyButton({ copied, onCopy }) {
24182
- return /* @__PURE__ */ jsx("button", {
24183
- type: "button",
24184
- title: "Copy this state change",
24185
- onClick: (e) => {
24186
- e.stopPropagation();
24187
- onCopy();
24188
- },
24189
- 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"),
24190
- children: copied ? /* @__PURE__ */ jsx(Check, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(Copy, { className: "h-3 w-3" })
24191
- });
24192
- }
24193
25511
  //#endregion
24194
25512
  //#region src/composites/device-activity-panel.tsx
24195
25513
  /**
@@ -24268,303 +25586,99 @@ function DeviceActivityPanel({ deviceId, defaultEventCategories, defaultStateCap
24268
25586
  showFilters: false,
24269
25587
  liveBuffer: logsBuffer
24270
25588
  }),
24271
- tab === "events" && /* @__PURE__ */ jsx(EventStream, {
24272
- deviceId,
24273
- defaultCategories: defaultEventCategories,
24274
- maxHeight,
24275
- showCategoryFilter: true,
24276
- liveBuffer: eventsBuffer
24277
- }),
24278
- tab === "state" && /* @__PURE__ */ jsx(StateValuesStream, {
24279
- deviceId,
24280
- defaultCaps: defaultStateCaps,
24281
- maxHeight,
24282
- liveBuffer: stateBuffer
24283
- })
24284
- ]
24285
- })]
24286
- });
24287
- }
24288
- //#endregion
24289
- //#region src/composites/confirm-action-button.tsx
24290
- /**
24291
- * ConfirmActionButton — destructive-action button with a modal confirm.
24292
- *
24293
- * Two-step UX for any operation operators shouldn't trigger by accident:
24294
- * reboot device, restart addon, regenerate token, etc. The trigger is a
24295
- * normal Button that opens a Dialog; confirming runs the async action,
24296
- * the trigger spinner-disables itself for the duration, and the dialog
24297
- * closes on success. Errors surface inline at the dialog's bottom.
24298
- *
24299
- * Generic on `TResult` so callers don't have to discard the return
24300
- * value. Pass an `icon` to render it inside the trigger (e.g. RotateCw
24301
- * for reboot, RefreshCw for restart).
24302
- */
24303
- function ConfirmActionButton({ label, icon: Icon, title = "Confirm action", description, confirmLabel, triggerVariant = "outline", confirmVariant = "danger", size = "sm", disabled, className, action, onSuccess }) {
24304
- const [open, setOpen] = useState(false);
24305
- const [running, setRunning] = useState(false);
24306
- const [error, setError] = useState(null);
24307
- const handleConfirm = async () => {
24308
- setError(null);
24309
- setRunning(true);
24310
- try {
24311
- const result = await action();
24312
- onSuccess?.(result);
24313
- setOpen(false);
24314
- } catch (err) {
24315
- setError(err instanceof Error ? err.message : String(err));
24316
- } finally {
24317
- setRunning(false);
24318
- }
24319
- };
24320
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(Button, {
24321
- type: "button",
24322
- variant: triggerVariant,
24323
- size,
24324
- disabled,
24325
- onClick: () => {
24326
- setError(null);
24327
- setOpen(true);
24328
- },
24329
- className,
24330
- children: [Icon && /* @__PURE__ */ jsx(Icon, { className: "h-3.5 w-3.5" }), label]
24331
- }), /* @__PURE__ */ jsx(Dialog, {
24332
- open,
24333
- onOpenChange: (next) => !running && setOpen(next),
24334
- children: /* @__PURE__ */ jsxs(DialogContent, { children: [
24335
- /* @__PURE__ */ jsxs(DialogHeader, { children: [/* @__PURE__ */ jsxs(DialogTitle, {
24336
- className: "flex items-center gap-2",
24337
- children: [/* @__PURE__ */ jsx(TriangleAlert, { className: "h-4 w-4 text-amber-400" }), title]
24338
- }), typeof description === "string" ? /* @__PURE__ */ jsx(DialogDescription, { children: description }) : description] }),
24339
- error && /* @__PURE__ */ jsx("div", {
24340
- className: cn("mt-2 px-3 py-2 rounded border border-danger/40 bg-danger/10", "text-xs text-danger"),
24341
- children: error
24342
- }),
24343
- /* @__PURE__ */ jsxs(DialogFooter, { children: [/* @__PURE__ */ jsx(Button, {
24344
- type: "button",
24345
- variant: "ghost",
24346
- size,
24347
- disabled: running,
24348
- onClick: () => setOpen(false),
24349
- children: "Cancel"
24350
- }), /* @__PURE__ */ jsxs(Button, {
24351
- type: "button",
24352
- variant: confirmVariant,
24353
- size,
24354
- disabled: running,
24355
- onClick: () => {
24356
- handleConfirm();
24357
- },
24358
- children: [running && /* @__PURE__ */ jsx(LoaderCircle, { className: "h-3.5 w-3.5 animate-spin" }), confirmLabel ?? label]
24359
- })] })
24360
- ] })
24361
- })] });
24362
- }
24363
- //#endregion
24364
- //#region src/composites/ptz-overlay.tsx
24365
- /**
24366
- * PTZOverlay — pan / tilt / zoom controls.
24367
- *
24368
- * Two visual variants driven by `mode`:
24369
- * - `'overlay'` (default): translucent dark pill positioned bottom-
24370
- * right of the camera viewport. Used as `extraOverlay` on the
24371
- * `CameraStreamPlayer` so operators can drive PTZ without leaving
24372
- * the live view. Opaque background + subtle ring keeps the d-pad
24373
- * legible against any frame.
24374
- * - `'panel'`: full-bleed inside a host container (e.g. the floating
24375
- * PTZ panel in DeviceDetail). No absolute positioning, no inner
24376
- * wrapper card — the host's chrome is the only frame. Inherits the
24377
- * surrounding `bg-surface` so the dark theme reads consistently
24378
- * instead of the previous always-dark-bubble look.
24379
- *
24380
- * Interaction model is identical across modes: short tap fires a
24381
- * discrete pulse (`move`); long press starts continuous motion until
24382
- * release (`startContinuous` + `stopContinuous` on pointer up).
24383
- */
24384
- function DPadButton({ direction, icon: Icon, disabled, className, variant, onMove, onStart, onStop }) {
24385
- const [pressedAt, setPressedAt] = useState(null);
24386
- const [continuous, setContinuous] = useState(false);
24387
- const handlePointerDown = useCallback((e) => {
24388
- if (disabled) return;
24389
- e.currentTarget.setPointerCapture(e.pointerId);
24390
- setPressedAt(Date.now());
24391
- setContinuous(false);
24392
- const timer = setTimeout(() => {
24393
- setContinuous(true);
24394
- onStart(direction);
24395
- }, 250);
24396
- e.currentTarget.dataset.timer = String(timer);
24397
- }, [
24398
- direction,
24399
- disabled,
24400
- onStart
24401
- ]);
24402
- const handlePointerUp = useCallback((e) => {
24403
- const timerId = Number(e.currentTarget.dataset.timer);
24404
- if (timerId) clearTimeout(timerId);
24405
- e.currentTarget.dataset.timer = "";
24406
- if (continuous) onStop();
24407
- else if (pressedAt !== null) onMove(direction);
24408
- setPressedAt(null);
24409
- setContinuous(false);
24410
- }, [
24411
- continuous,
24412
- direction,
24413
- onMove,
24414
- onStop,
24415
- pressedAt
24416
- ]);
24417
- const handlePointerCancel = useCallback((e) => {
24418
- const timerId = Number(e.currentTarget.dataset.timer);
24419
- if (timerId) clearTimeout(timerId);
24420
- e.currentTarget.dataset.timer = "";
24421
- if (continuous) onStop();
24422
- setPressedAt(null);
24423
- setContinuous(false);
24424
- }, [continuous, onStop]);
24425
- const sizeClass = variant === "panel" ? "h-9 w-9" : "h-7 w-7";
24426
- const iconSizeClass = variant === "panel" ? "h-4 w-4" : "h-3.5 w-3.5";
24427
- return /* @__PURE__ */ jsx("button", {
24428
- type: "button",
24429
- disabled,
24430
- onPointerDown: handlePointerDown,
24431
- onPointerUp: handlePointerUp,
24432
- onPointerCancel: handlePointerCancel,
24433
- 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),
24434
- title: direction,
24435
- children: /* @__PURE__ */ jsx(Icon, { className: iconSizeClass })
24436
- });
24437
- }
24438
- function PTZOverlay({ controls, mode = "overlay", showPresets, showZoom = true, showHome = true, className }) {
24439
- const { move, startContinuous, stopContinuous, zoom, goHome, goToPreset, presets, busy, error } = controls;
24440
- const [presetsOpen, setPresetsOpen] = useState(false);
24441
- const presetsVisible = (showPresets ?? presets.length > 0) && presets.length > 0;
24442
- const isPanel = mode === "panel";
24443
- 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";
24444
- 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");
24445
- const sideButtonSize = isPanel ? "h-9 w-9" : "h-7 w-7";
24446
- const sideIconSize = isPanel ? "h-4 w-4" : "h-3.5 w-3.5";
24447
- const sideButtonHover = isPanel ? "text-foreground hover:bg-surface-hover" : "text-white hover:bg-white/15";
24448
- const sepClass = isPanel ? "h-12 w-px bg-border mx-1" : "h-12 w-px bg-white/15 mx-1";
24449
- return /* @__PURE__ */ jsxs("div", {
24450
- className: cn(containerClass, className),
24451
- children: [error && /* @__PURE__ */ jsxs("div", {
24452
- className: "rounded bg-danger/90 px-2 py-1 text-[10px] font-medium text-white shadow-lg max-w-[200px] self-center",
24453
- children: ["PTZ: ", error]
24454
- }), /* @__PURE__ */ jsxs("div", {
24455
- className: cn(rowClass, isPanel && "justify-center"),
24456
- children: [
24457
- /* @__PURE__ */ jsxs("div", {
24458
- className: "grid grid-cols-3 gap-0.5",
24459
- children: [
24460
- /* @__PURE__ */ jsx("span", {
24461
- "aria-hidden": true,
24462
- className: isPanel ? "h-9 w-9" : "h-7 w-7"
24463
- }),
24464
- /* @__PURE__ */ jsx(DPadButton, {
24465
- direction: "up",
24466
- icon: ArrowUp,
24467
- variant: mode,
24468
- onMove: move,
24469
- onStart: startContinuous,
24470
- onStop: stopContinuous
24471
- }),
24472
- /* @__PURE__ */ jsx("span", {
24473
- "aria-hidden": true,
24474
- className: isPanel ? "h-9 w-9" : "h-7 w-7"
24475
- }),
24476
- /* @__PURE__ */ jsx(DPadButton, {
24477
- direction: "left",
24478
- icon: ArrowLeft,
24479
- variant: mode,
24480
- onMove: move,
24481
- onStart: startContinuous,
24482
- onStop: stopContinuous
24483
- }),
24484
- /* @__PURE__ */ jsx("span", {
24485
- "aria-hidden": true,
24486
- className: isPanel ? "h-9 w-9" : "h-7 w-7"
24487
- }),
24488
- /* @__PURE__ */ jsx(DPadButton, {
24489
- direction: "right",
24490
- icon: ArrowRight,
24491
- variant: mode,
24492
- onMove: move,
24493
- onStart: startContinuous,
24494
- onStop: stopContinuous
24495
- }),
24496
- /* @__PURE__ */ jsx("span", {
24497
- "aria-hidden": true,
24498
- className: isPanel ? "h-9 w-9" : "h-7 w-7"
24499
- }),
24500
- /* @__PURE__ */ jsx(DPadButton, {
24501
- direction: "down",
24502
- icon: ArrowDown,
24503
- variant: mode,
24504
- onMove: move,
24505
- onStart: startContinuous,
24506
- onStop: stopContinuous
24507
- }),
24508
- /* @__PURE__ */ jsx("span", {
24509
- "aria-hidden": true,
24510
- className: isPanel ? "h-9 w-9" : "h-7 w-7"
24511
- })
24512
- ]
24513
- }),
24514
- (showZoom || showHome) && /* @__PURE__ */ jsx("div", { className: sepClass }),
24515
- /* @__PURE__ */ jsxs("div", {
24516
- className: "flex flex-col gap-0.5",
24517
- children: [showZoom && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("button", {
24518
- type: "button",
24519
- onClick: () => zoom("in"),
24520
- disabled: busy,
24521
- title: "Zoom in",
24522
- className: cn("flex items-center justify-center rounded disabled:opacity-40", sideButtonSize, sideButtonHover),
24523
- children: /* @__PURE__ */ jsx(ZoomIn, { className: sideIconSize })
24524
- }), /* @__PURE__ */ jsx("button", {
24525
- type: "button",
24526
- onClick: () => zoom("out"),
24527
- disabled: busy,
24528
- title: "Zoom out",
24529
- className: cn("flex items-center justify-center rounded disabled:opacity-40", sideButtonSize, sideButtonHover),
24530
- children: /* @__PURE__ */ jsx(ZoomOut, { className: sideIconSize })
24531
- })] }), showHome && /* @__PURE__ */ jsx("button", {
24532
- type: "button",
24533
- onClick: () => goHome(),
24534
- disabled: busy,
24535
- title: "Go home",
24536
- className: cn("flex items-center justify-center rounded disabled:opacity-40", sideButtonSize, sideButtonHover),
24537
- children: /* @__PURE__ */ jsx(House, { className: sideIconSize })
24538
- })]
25589
+ tab === "events" && /* @__PURE__ */ jsx(EventStream, {
25590
+ deviceId,
25591
+ defaultCategories: defaultEventCategories,
25592
+ maxHeight,
25593
+ showCategoryFilter: true,
25594
+ liveBuffer: eventsBuffer
24539
25595
  }),
24540
- presetsVisible && /* @__PURE__ */ jsxs("div", {
24541
- className: "relative",
24542
- children: [/* @__PURE__ */ jsxs("button", {
24543
- type: "button",
24544
- onClick: () => setPresetsOpen((v) => !v),
24545
- disabled: busy,
24546
- 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"),
24547
- title: "Presets",
24548
- children: ["Presets", /* @__PURE__ */ jsx(ChevronDown, { className: cn("h-3 w-3 transition-transform", presetsOpen && "rotate-180") })]
24549
- }), presetsOpen && /* @__PURE__ */ jsx("div", {
24550
- 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"),
24551
- children: presets.map((p) => /* @__PURE__ */ jsx("button", {
24552
- type: "button",
24553
- onClick: () => {
24554
- goToPreset(p.id);
24555
- setPresetsOpen(false);
24556
- },
24557
- disabled: busy,
24558
- 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"),
24559
- children: p.name || p.id
24560
- }, p.id))
24561
- })]
25596
+ tab === "state" && /* @__PURE__ */ jsx(StateValuesStream, {
25597
+ deviceId,
25598
+ defaultCaps: defaultStateCaps,
25599
+ maxHeight,
25600
+ liveBuffer: stateBuffer
24562
25601
  })
24563
25602
  ]
24564
25603
  })]
24565
25604
  });
24566
25605
  }
24567
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
24568
25682
  //#region src/composites/snapshot-button.tsx
24569
25683
  /**
24570
25684
  * SnapshotButton — operator-facing manual snapshot trigger.
@@ -25471,169 +26585,6 @@ function useDeviceWebrtc(trpc, deviceId, pollIntervalMs = 5e3) {
25471
26585
  };
25472
26586
  }
25473
26587
  //#endregion
25474
- //#region src/hooks/use-ptz.ts
25475
- /**
25476
- * usePTZ — PTZ control hook for device-scoped pan / tilt / zoom.
25477
- *
25478
- * Wraps the `ptz` capability methods through the canonical
25479
- * `useDeviceProxy` surface — every call goes through
25480
- * `dev.ptz?.<method>(...)` with deviceId/nodeId auto-injected. The
25481
- * hook stays bare-bones around state (`busy`, `error`, `presets`)
25482
- * so the operator-facing component (PTZOverlay) stays trivial.
25483
- *
25484
- * Returns:
25485
- * - `move(direction)`: discrete one-shot move (cap.move + cap.stop).
25486
- * Use for short pulse moves triggered by tapping a d-pad button.
25487
- * - `startContinuous(direction)` / `stopContinuous()`: gesture-driven
25488
- * continuous motion (`cap.continuousMove` + `cap.stop`). Use for
25489
- * long-press handlers on touch / mouse-down handlers on desktop.
25490
- * - `zoom('in' | 'out')`: discrete zoom step.
25491
- * - `goHome()`: jump to preset 0.
25492
- * - `presets` + `goToPreset(presetId)`: list and jump to named presets.
25493
- *
25494
- * Parametrised by the same `UseDeviceProxyTrpc` shape every other
25495
- * device hook uses — works under admin-ui (BackendClient.trpc) and
25496
- * addon pages (AddonPageProps.trpc) alike.
25497
- */
25498
- var DIRECTION_VECTORS = {
25499
- "up": {
25500
- pan: 0,
25501
- tilt: 1
25502
- },
25503
- "down": {
25504
- pan: 0,
25505
- tilt: -1
25506
- },
25507
- "left": {
25508
- pan: -1,
25509
- tilt: 0
25510
- },
25511
- "right": {
25512
- pan: 1,
25513
- tilt: 0
25514
- },
25515
- "up-left": {
25516
- pan: -1,
25517
- tilt: 1
25518
- },
25519
- "up-right": {
25520
- pan: 1,
25521
- tilt: 1
25522
- },
25523
- "down-left": {
25524
- pan: -1,
25525
- tilt: -1
25526
- },
25527
- "down-right": {
25528
- pan: 1,
25529
- tilt: -1
25530
- }
25531
- };
25532
- function usePTZ(trpc, deviceId, options) {
25533
- const defaultSpeed = options?.defaultSpeed ?? .5;
25534
- const pulseMs = options?.pulseMs ?? 250;
25535
- const enabled = options?.enabled ?? true;
25536
- const ptz = useDeviceProxy(trpc, enabled ? deviceId : null)?.ptz;
25537
- const [presets, setPresets] = useState([]);
25538
- const [busy, setBusy] = useState(false);
25539
- const [error, setError] = useState(null);
25540
- const refreshPresets = useCallback(async () => {
25541
- if (!enabled || !ptz) return;
25542
- try {
25543
- setPresets(await ptz.getPresets({}));
25544
- } catch (err) {
25545
- const msg = err instanceof Error ? err.message : String(err);
25546
- if (msg.includes("provider not available") || msg.includes("no 'ptz' binding")) return;
25547
- setError(msg);
25548
- }
25549
- }, [ptz, enabled]);
25550
- useEffect(() => {
25551
- refreshPresets();
25552
- }, [refreshPresets]);
25553
- const wrap = useCallback(async (fn) => {
25554
- if (!enabled || !ptz) return void 0;
25555
- setError(null);
25556
- setBusy(true);
25557
- try {
25558
- return await fn();
25559
- } catch (err) {
25560
- setError(err instanceof Error ? err.message : String(err));
25561
- return;
25562
- } finally {
25563
- setBusy(false);
25564
- }
25565
- }, [enabled, ptz]);
25566
- return {
25567
- move: useCallback(async (direction, speed) => {
25568
- if (!ptz) return;
25569
- const v = DIRECTION_VECTORS[direction];
25570
- const s = speed ?? defaultSpeed;
25571
- await wrap(async () => {
25572
- await ptz.move({
25573
- pan: v.pan,
25574
- tilt: v.tilt,
25575
- speed: s
25576
- });
25577
- await new Promise((r) => setTimeout(r, pulseMs));
25578
- await ptz.stop({});
25579
- });
25580
- }, [
25581
- ptz,
25582
- defaultSpeed,
25583
- pulseMs,
25584
- wrap
25585
- ]),
25586
- startContinuous: useCallback(async (direction, speed) => {
25587
- if (!ptz) return;
25588
- const v = DIRECTION_VECTORS[direction];
25589
- const s = speed ?? defaultSpeed;
25590
- await wrap(() => ptz.continuousMove({
25591
- pan: v.pan,
25592
- tilt: v.tilt,
25593
- speed: s
25594
- }));
25595
- }, [
25596
- ptz,
25597
- defaultSpeed,
25598
- wrap
25599
- ]),
25600
- stopContinuous: useCallback(async () => {
25601
- if (!ptz) return;
25602
- await wrap(() => ptz.stop({}));
25603
- }, [ptz, wrap]),
25604
- zoom: useCallback(async (direction, speed) => {
25605
- if (!ptz) return;
25606
- const z = direction === "in" ? 1 : -1;
25607
- const s = speed ?? defaultSpeed;
25608
- await wrap(async () => {
25609
- await ptz.move({
25610
- zoom: z,
25611
- speed: s
25612
- });
25613
- await new Promise((r) => setTimeout(r, pulseMs));
25614
- await ptz.stop({});
25615
- });
25616
- }, [
25617
- ptz,
25618
- defaultSpeed,
25619
- pulseMs,
25620
- wrap
25621
- ]),
25622
- goHome: useCallback(async () => {
25623
- if (!ptz) return;
25624
- await wrap(() => ptz.goHome({}));
25625
- }, [ptz, wrap]),
25626
- goToPreset: useCallback(async (presetId) => {
25627
- if (!ptz) return;
25628
- await wrap(() => ptz.goToPreset({ presetId }));
25629
- }, [ptz, wrap]),
25630
- presets,
25631
- refreshPresets,
25632
- busy,
25633
- error
25634
- };
25635
- }
25636
- //#endregion
25637
26588
  //#region src/hooks/use-doorbell-events.ts
25638
26589
  /**
25639
26590
  * useDoorbellEvents — subscribe to doorbell.onPressed across the cluster.
@@ -25855,6 +26806,6 @@ function useEventInvalidation(queryKey, categories) {
25855
26806
  ]);
25856
26807
  }
25857
26808
  //#endregion
25858
- 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, 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, useAuthenticationListProviders, useAuthenticationSetProviderEnabled, 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, useMeshOrchestratorJoinProvider, useMeshOrchestratorLeaveProvider, useMeshOrchestratorListProviderPeers, useMeshOrchestratorListProviders, useMeshOrchestratorLogoutProvider, useMeshOrchestratorStartLoginProvider, 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, 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, useRemoteAccessListProviders, useRemoteAccessStartProvider, useRemoteAccessStopProvider, 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, useTurnOrchestratorGetAllServers, useTurnOrchestratorListProviders, useTurnOrchestratorSetProviderEnabled, 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 };
25859
26810
 
25860
26811
  //# sourceMappingURL=index.js.map