@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/composites/cap-settings/AutotrackSection.d.ts +5 -0
- package/dist/composites/cap-settings/MotionGridCanvas.d.ts +9 -0
- package/dist/composites/cap-settings/MotionZonesSettings.d.ts +2 -0
- package/dist/composites/cap-settings/PtzPanel.d.ts +2 -0
- package/dist/composites/cap-settings/index.d.ts +14 -0
- package/dist/composites/index.d.ts +2 -0
- package/dist/composites/state-values-stream.d.ts +14 -18
- package/dist/contexts/widget-registry.d.ts +67 -6
- package/dist/generated/system-hooks.d.ts +66 -30
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/use-device-autotrack.d.ts +10 -0
- package/dist/hooks/use-ptz.d.ts +29 -1
- package/dist/index.cjs +2140 -1162
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2099 -1148
- package/dist/index.js.map +1 -1
- package/dist/widgets/host-widgets.d.ts +9 -0
- package/package.json +9 -1
package/dist/index.cjs
CHANGED
|
@@ -37,6 +37,10 @@ let _trpc_client = require("@trpc/client");
|
|
|
37
37
|
_trpc_client = __toESM(_trpc_client, 1);
|
|
38
38
|
let _trpc_react_query = require("@trpc/react-query");
|
|
39
39
|
_trpc_react_query = __toESM(_trpc_react_query, 1);
|
|
40
|
+
let react_konva = require("react-konva");
|
|
41
|
+
react_konva = __toESM(react_konva, 1);
|
|
42
|
+
let konva = require("konva");
|
|
43
|
+
konva = __toESM(konva, 1);
|
|
40
44
|
let _camstack_types = require("@camstack/types");
|
|
41
45
|
let _camstack_sdk = require("@camstack/sdk");
|
|
42
46
|
let zod = require("zod");
|
|
@@ -6875,6 +6879,8 @@ function populateShareCache() {
|
|
|
6875
6879
|
cache.share["@tanstack/react-query"] ??= _tanstack_react_query;
|
|
6876
6880
|
cache.share["@trpc/client"] ??= _trpc_client;
|
|
6877
6881
|
cache.share["@trpc/react-query"] ??= _trpc_react_query;
|
|
6882
|
+
cache.share["react-konva"] ??= react_konva;
|
|
6883
|
+
cache.share["konva"] ??= konva;
|
|
6878
6884
|
cache.share["@camstack/ui-library"] ??= readHostGlobal("__camstackUiLibrary");
|
|
6879
6885
|
cache.share["@camstack/sdk"] ??= readHostGlobal("__camstackSdk");
|
|
6880
6886
|
cache.share["@camstack/types"] ??= readHostGlobal("__camstackTypes");
|
|
@@ -6893,6 +6899,8 @@ function ensureMfHostInit() {
|
|
|
6893
6899
|
"@tanstack/react-query": npmShared(() => _tanstack_react_query),
|
|
6894
6900
|
"@trpc/client": npmShared(() => _trpc_client),
|
|
6895
6901
|
"@trpc/react-query": npmShared(() => _trpc_react_query),
|
|
6902
|
+
"react-konva": npmShared(() => react_konva),
|
|
6903
|
+
"konva": npmShared(() => konva),
|
|
6896
6904
|
"@camstack/ui-library": npmShared(() => readHostGlobal("__camstackUiLibrary")),
|
|
6897
6905
|
"@camstack/sdk": npmShared(() => readHostGlobal("__camstackSdk")),
|
|
6898
6906
|
"@camstack/types": npmShared(() => readHostGlobal("__camstackTypes"))
|
|
@@ -15806,6 +15814,8 @@ var useAddonsGetLastRestart = trpc.addons.getLastRestart.useQuery;
|
|
|
15806
15814
|
var useAddonsListFrameworkPackages = trpc.addons.listFrameworkPackages.useQuery;
|
|
15807
15815
|
/** Generated alias around `trpc.addons.listCapabilityProviders.useQuery`. */
|
|
15808
15816
|
var useAddonsListCapabilityProviders = trpc.addons.listCapabilityProviders.useQuery;
|
|
15817
|
+
/** Generated alias around `trpc.addons.setCapabilityProviderEnabled.useMutation`. */
|
|
15818
|
+
var useAddonsSetCapabilityProviderEnabled = trpc.addons.setCapabilityProviderEnabled.useMutation;
|
|
15809
15819
|
/** Generated alias around `trpc.addons.updateFrameworkPackage.useMutation`. */
|
|
15810
15820
|
var useAddonsUpdateFrameworkPackage = trpc.addons.updateFrameworkPackage.useMutation;
|
|
15811
15821
|
/** Generated alias around `trpc.addons.getVersions.useQuery`. */
|
|
@@ -15896,10 +15906,6 @@ var useAudioCodecListActiveSessions = trpc.audioCodec.listActiveSessions.useQuer
|
|
|
15896
15906
|
var useAudioMetricsGetCurrentSnapshot = trpc.audioMetrics.getCurrentSnapshot.useQuery;
|
|
15897
15907
|
/** Generated alias around `trpc.audioMetrics.getHistory.useQuery`. */
|
|
15898
15908
|
var useAudioMetricsGetHistory = trpc.audioMetrics.getHistory.useQuery;
|
|
15899
|
-
/** Generated alias around `trpc.authentication.listProviders.useQuery`. */
|
|
15900
|
-
var useAuthenticationListProviders = trpc.authentication.listProviders.useQuery;
|
|
15901
|
-
/** Generated alias around `trpc.authentication.setProviderEnabled.useMutation`. */
|
|
15902
|
-
var useAuthenticationSetProviderEnabled = trpc.authentication.setProviderEnabled.useMutation;
|
|
15903
15909
|
/** Generated alias around `trpc.backup.listDestinations.useQuery`. */
|
|
15904
15910
|
var useBackupListDestinations = trpc.backup.listDestinations.useQuery;
|
|
15905
15911
|
/** Generated alias around `trpc.backup.trigger.useMutation`. */
|
|
@@ -15950,6 +15956,12 @@ var useDecoderPushPacket = trpc.decoder.pushPacket.useQuery;
|
|
|
15950
15956
|
var useDecoderOpenStream = trpc.decoder.openStream.useQuery;
|
|
15951
15957
|
/** Generated alias around `trpc.decoder.pullFrames.useQuery`. */
|
|
15952
15958
|
var useDecoderPullFrames = trpc.decoder.pullFrames.useQuery;
|
|
15959
|
+
/** Generated alias around `trpc.decoder.pullHandles.useQuery`. */
|
|
15960
|
+
var useDecoderPullHandles = trpc.decoder.pullHandles.useQuery;
|
|
15961
|
+
/** Generated alias around `trpc.decoder.getFrame.useQuery`. */
|
|
15962
|
+
var useDecoderGetFrame = trpc.decoder.getFrame.useQuery;
|
|
15963
|
+
/** Generated alias around `trpc.decoder.getShmStats.useQuery`. */
|
|
15964
|
+
var useDecoderGetShmStats = trpc.decoder.getShmStats.useQuery;
|
|
15953
15965
|
/** Generated alias around `trpc.decoder.updateConfig.useQuery`. */
|
|
15954
15966
|
var useDecoderUpdateConfig = trpc.decoder.updateConfig.useQuery;
|
|
15955
15967
|
/** Generated alias around `trpc.decoder.getStats.useQuery`. */
|
|
@@ -16192,18 +16204,6 @@ var useMeshNetworkLogout = trpc.meshNetwork.logout.useMutation;
|
|
|
16192
16204
|
var useMeshNetworkListPeers = trpc.meshNetwork.listPeers.useQuery;
|
|
16193
16205
|
/** Generated alias around `trpc.meshNetwork.testConnection.useMutation`. */
|
|
16194
16206
|
var useMeshNetworkTestConnection = trpc.meshNetwork.testConnection.useMutation;
|
|
16195
|
-
/** Generated alias around `trpc.meshOrchestrator.listProviders.useQuery`. */
|
|
16196
|
-
var useMeshOrchestratorListProviders = trpc.meshOrchestrator.listProviders.useQuery;
|
|
16197
|
-
/** Generated alias around `trpc.meshOrchestrator.joinProvider.useMutation`. */
|
|
16198
|
-
var useMeshOrchestratorJoinProvider = trpc.meshOrchestrator.joinProvider.useMutation;
|
|
16199
|
-
/** Generated alias around `trpc.meshOrchestrator.leaveProvider.useMutation`. */
|
|
16200
|
-
var useMeshOrchestratorLeaveProvider = trpc.meshOrchestrator.leaveProvider.useMutation;
|
|
16201
|
-
/** Generated alias around `trpc.meshOrchestrator.startLoginProvider.useMutation`. */
|
|
16202
|
-
var useMeshOrchestratorStartLoginProvider = trpc.meshOrchestrator.startLoginProvider.useMutation;
|
|
16203
|
-
/** Generated alias around `trpc.meshOrchestrator.logoutProvider.useMutation`. */
|
|
16204
|
-
var useMeshOrchestratorLogoutProvider = trpc.meshOrchestrator.logoutProvider.useMutation;
|
|
16205
|
-
/** Generated alias around `trpc.meshOrchestrator.listProviderPeers.useQuery`. */
|
|
16206
|
-
var useMeshOrchestratorListProviderPeers = trpc.meshOrchestrator.listProviderPeers.useQuery;
|
|
16207
16207
|
/** Generated alias around `trpc.metricsProvider.collectSnapshot.useQuery`. */
|
|
16208
16208
|
var useMetricsProviderCollectSnapshot = trpc.metricsProvider.collectSnapshot.useQuery;
|
|
16209
16209
|
/** Generated alias around `trpc.metricsProvider.getCached.useQuery`. */
|
|
@@ -16246,6 +16246,12 @@ var useMotionDetectionApplyDeviceSettingsPatch = trpc.motionDetection.applyDevic
|
|
|
16246
16246
|
var useMotionTriggerSetMotionTrigger = trpc.motionTrigger.setMotionTrigger.useMutation;
|
|
16247
16247
|
/** Generated alias around `trpc.motionTrigger.getStatus.useQuery`. */
|
|
16248
16248
|
var useMotionTriggerGetStatus = trpc.motionTrigger.getStatus.useQuery;
|
|
16249
|
+
/** Generated alias around `trpc.motionZones.getOptions.useQuery`. */
|
|
16250
|
+
var useMotionZonesGetOptions = trpc.motionZones.getOptions.useQuery;
|
|
16251
|
+
/** Generated alias around `trpc.motionZones.setZone.useMutation`. */
|
|
16252
|
+
var useMotionZonesSetZone = trpc.motionZones.setZone.useMutation;
|
|
16253
|
+
/** Generated alias around `trpc.motionZones.getStatus.useQuery`. */
|
|
16254
|
+
var useMotionZonesGetStatus = trpc.motionZones.getStatus.useQuery;
|
|
16249
16255
|
/** Generated alias around `trpc.mqttBroker.listBrokers.useQuery`. */
|
|
16250
16256
|
var useMqttBrokerListBrokers = trpc.mqttBroker.listBrokers.useQuery;
|
|
16251
16257
|
/** Generated alias around `trpc.mqttBroker.getBrokerConfig.useQuery`. */
|
|
@@ -16264,6 +16270,16 @@ var useMqttBrokerStopEmbeddedBroker = trpc.mqttBroker.stopEmbeddedBroker.useMuta
|
|
|
16264
16270
|
var useMqttBrokerGetStatus = trpc.mqttBroker.getStatus.useQuery;
|
|
16265
16271
|
/** Generated alias around `trpc.nativeObjectDetection.getStatus.useQuery`. */
|
|
16266
16272
|
var useNativeObjectDetectionGetStatus = trpc.nativeObjectDetection.getStatus.useQuery;
|
|
16273
|
+
/** Generated alias around `trpc.networkAccess.start.useMutation`. */
|
|
16274
|
+
var useNetworkAccessStart = trpc.networkAccess.start.useMutation;
|
|
16275
|
+
/** Generated alias around `trpc.networkAccess.stop.useMutation`. */
|
|
16276
|
+
var useNetworkAccessStop = trpc.networkAccess.stop.useMutation;
|
|
16277
|
+
/** Generated alias around `trpc.networkAccess.getEndpoint.useQuery`. */
|
|
16278
|
+
var useNetworkAccessGetEndpoint = trpc.networkAccess.getEndpoint.useQuery;
|
|
16279
|
+
/** Generated alias around `trpc.networkAccess.getStatus.useQuery`. */
|
|
16280
|
+
var useNetworkAccessGetStatus = trpc.networkAccess.getStatus.useQuery;
|
|
16281
|
+
/** Generated alias around `trpc.networkAccess.listEndpoints.useQuery`. */
|
|
16282
|
+
var useNetworkAccessListEndpoints = trpc.networkAccess.listEndpoints.useQuery;
|
|
16267
16283
|
/** Generated alias around `trpc.networkQuality.getDeviceStats.useQuery`. */
|
|
16268
16284
|
var useNetworkQualityGetDeviceStats = trpc.networkQuality.getDeviceStats.useQuery;
|
|
16269
16285
|
/** Generated alias around `trpc.networkQuality.getAllStats.useQuery`. */
|
|
@@ -16288,6 +16304,8 @@ var useNodesShutdownNode = trpc.nodes.shutdownNode.useMutation;
|
|
|
16288
16304
|
var useNodesRenameNode = trpc.nodes.renameNode.useMutation;
|
|
16289
16305
|
/** Generated alias around `trpc.nodes.clusterAddonStatus.useQuery`. */
|
|
16290
16306
|
var useNodesClusterAddonStatus = trpc.nodes.clusterAddonStatus.useQuery;
|
|
16307
|
+
/** Generated alias around `trpc.nodes.getCapUsageGraph.useQuery`. */
|
|
16308
|
+
var useNodesGetCapUsageGraph = trpc.nodes.getCapUsageGraph.useQuery;
|
|
16291
16309
|
/** Generated alias around `trpc.nodes.getNodeAddons.useQuery`. */
|
|
16292
16310
|
var useNodesGetNodeAddons = trpc.nodes.getNodeAddons.useQuery;
|
|
16293
16311
|
/** Generated alias around `trpc.nodes.setProcessLogLevel.useMutation`. */
|
|
@@ -16506,10 +16524,18 @@ var usePtzStop = trpc.ptz.stop.useMutation;
|
|
|
16506
16524
|
var usePtzGetPresets = trpc.ptz.getPresets.useQuery;
|
|
16507
16525
|
/** Generated alias around `trpc.ptz.goToPreset.useMutation`. */
|
|
16508
16526
|
var usePtzGoToPreset = trpc.ptz.goToPreset.useMutation;
|
|
16527
|
+
/** Generated alias around `trpc.ptz.savePreset.useMutation`. */
|
|
16528
|
+
var usePtzSavePreset = trpc.ptz.savePreset.useMutation;
|
|
16529
|
+
/** Generated alias around `trpc.ptz.deletePreset.useMutation`. */
|
|
16530
|
+
var usePtzDeletePreset = trpc.ptz.deletePreset.useMutation;
|
|
16531
|
+
/** Generated alias around `trpc.ptz.getOptions.useQuery`. */
|
|
16532
|
+
var usePtzGetOptions = trpc.ptz.getOptions.useQuery;
|
|
16509
16533
|
/** Generated alias around `trpc.ptz.goHome.useMutation`. */
|
|
16510
16534
|
var usePtzGoHome = trpc.ptz.goHome.useMutation;
|
|
16511
16535
|
/** Generated alias around `trpc.ptz.getPosition.useQuery`. */
|
|
16512
16536
|
var usePtzGetPosition = trpc.ptz.getPosition.useQuery;
|
|
16537
|
+
/** Generated alias around `trpc.ptz.setAutofocus.useMutation`. */
|
|
16538
|
+
var usePtzSetAutofocus = trpc.ptz.setAutofocus.useMutation;
|
|
16513
16539
|
/** Generated alias around `trpc.ptz.getStatus.useQuery`. */
|
|
16514
16540
|
var usePtzGetStatus = trpc.ptz.getStatus.useQuery;
|
|
16515
16541
|
/** Generated alias around `trpc.ptzAutotrack.getStatus.useQuery`. */
|
|
@@ -16564,12 +16590,6 @@ var useRecordingEngineGetRetentionConfig = trpc.recordingEngine.getRetentionConf
|
|
|
16564
16590
|
var useRecordingEngineUpdateRetentionConfig = trpc.recordingEngine.updateRetentionConfig.useMutation;
|
|
16565
16591
|
/** Generated alias around `trpc.recordingEngine.getMotionStats.useQuery`. */
|
|
16566
16592
|
var useRecordingEngineGetMotionStats = trpc.recordingEngine.getMotionStats.useQuery;
|
|
16567
|
-
/** Generated alias around `trpc.remoteAccess.listProviders.useQuery`. */
|
|
16568
|
-
var useRemoteAccessListProviders = trpc.remoteAccess.listProviders.useQuery;
|
|
16569
|
-
/** Generated alias around `trpc.remoteAccess.startProvider.useMutation`. */
|
|
16570
|
-
var useRemoteAccessStartProvider = trpc.remoteAccess.startProvider.useMutation;
|
|
16571
|
-
/** Generated alias around `trpc.remoteAccess.stopProvider.useMutation`. */
|
|
16572
|
-
var useRemoteAccessStopProvider = trpc.remoteAccess.stopProvider.useMutation;
|
|
16573
16593
|
/** Generated alias around `trpc.settingsStore.get.useQuery`. */
|
|
16574
16594
|
var useSettingsStoreGet = trpc.settingsStore.get.useQuery;
|
|
16575
16595
|
/** Generated alias around `trpc.settingsStore.set.useMutation`. */
|
|
@@ -16672,8 +16692,18 @@ var useStreamBrokerGetStreamUrl = trpc.streamBroker.getStreamUrl.useQuery;
|
|
|
16672
16692
|
var useStreamBrokerGetStreamWithCodec = trpc.streamBroker.getStreamWithCodec.useMutation;
|
|
16673
16693
|
/** Generated alias around `trpc.streamBroker.releaseStreamWithCodec.useMutation`. */
|
|
16674
16694
|
var useStreamBrokerReleaseStreamWithCodec = trpc.streamBroker.releaseStreamWithCodec.useMutation;
|
|
16675
|
-
/** Generated alias around `trpc.streamBroker.
|
|
16676
|
-
var
|
|
16695
|
+
/** Generated alias around `trpc.streamBroker.subscribeAudioChunks.useMutation`. */
|
|
16696
|
+
var useStreamBrokerSubscribeAudioChunks = trpc.streamBroker.subscribeAudioChunks.useMutation;
|
|
16697
|
+
/** Generated alias around `trpc.streamBroker.pullAudioChunks.useQuery`. */
|
|
16698
|
+
var useStreamBrokerPullAudioChunks = trpc.streamBroker.pullAudioChunks.useQuery;
|
|
16699
|
+
/** Generated alias around `trpc.streamBroker.unsubscribeAudioChunks.useMutation`. */
|
|
16700
|
+
var useStreamBrokerUnsubscribeAudioChunks = trpc.streamBroker.unsubscribeAudioChunks.useMutation;
|
|
16701
|
+
/** Generated alias around `trpc.streamBroker.subscribeFrames.useMutation`. */
|
|
16702
|
+
var useStreamBrokerSubscribeFrames = trpc.streamBroker.subscribeFrames.useMutation;
|
|
16703
|
+
/** Generated alias around `trpc.streamBroker.pullFrameHandles.useQuery`. */
|
|
16704
|
+
var useStreamBrokerPullFrameHandles = trpc.streamBroker.pullFrameHandles.useQuery;
|
|
16705
|
+
/** Generated alias around `trpc.streamBroker.unsubscribeFrames.useMutation`. */
|
|
16706
|
+
var useStreamBrokerUnsubscribeFrames = trpc.streamBroker.unsubscribeFrames.useMutation;
|
|
16677
16707
|
/** Generated alias around `trpc.streamBroker.setPreBufferDuration.useMutation`. */
|
|
16678
16708
|
var useStreamBrokerSetPreBufferDuration = trpc.streamBroker.setPreBufferDuration.useMutation;
|
|
16679
16709
|
/** Generated alias around `trpc.streamBroker.getPreBufferInfo.useQuery`. */
|
|
@@ -16696,6 +16726,14 @@ var useStreamBrokerGetDeviceSettingsContribution = trpc.streamBroker.getDeviceSe
|
|
|
16696
16726
|
var useStreamBrokerGetDeviceLiveContribution = trpc.streamBroker.getDeviceLiveContribution.useQuery;
|
|
16697
16727
|
/** Generated alias around `trpc.streamBroker.applyDeviceSettingsPatch.useMutation`. */
|
|
16698
16728
|
var useStreamBrokerApplyDeviceSettingsPatch = trpc.streamBroker.applyDeviceSettingsPatch.useMutation;
|
|
16729
|
+
/** Generated alias around `trpc.streamParams.getOptions.useQuery`. */
|
|
16730
|
+
var useStreamParamsGetOptions = trpc.streamParams.getOptions.useQuery;
|
|
16731
|
+
/** Generated alias around `trpc.streamParams.setProfile.useMutation`. */
|
|
16732
|
+
var useStreamParamsSetProfile = trpc.streamParams.setProfile.useMutation;
|
|
16733
|
+
/** Generated alias around `trpc.streamParams.getConfigSchema.useQuery`. */
|
|
16734
|
+
var useStreamParamsGetConfigSchema = trpc.streamParams.getConfigSchema.useQuery;
|
|
16735
|
+
/** Generated alias around `trpc.streamParams.getStatus.useQuery`. */
|
|
16736
|
+
var useStreamParamsGetStatus = trpc.streamParams.getStatus.useQuery;
|
|
16699
16737
|
/** Generated alias around `trpc.switch.setState.useMutation`. */
|
|
16700
16738
|
var useSwitchSetState = trpc.switch.setState.useMutation;
|
|
16701
16739
|
/** Generated alias around `trpc.switch.getStatus.useQuery`. */
|
|
@@ -16716,12 +16754,6 @@ var useSystemSetRetentionConfig = trpc.system.setRetentionConfig.useMutation;
|
|
|
16716
16754
|
var useSystemForceRetentionCleanup = trpc.system.forceRetentionCleanup.useMutation;
|
|
16717
16755
|
/** Generated alias around `trpc.toast.onToast.useSubscription`. */
|
|
16718
16756
|
var useToastOnToast = trpc.toast.onToast.useSubscription;
|
|
16719
|
-
/** Generated alias around `trpc.turnOrchestrator.listProviders.useQuery`. */
|
|
16720
|
-
var useTurnOrchestratorListProviders = trpc.turnOrchestrator.listProviders.useQuery;
|
|
16721
|
-
/** Generated alias around `trpc.turnOrchestrator.getAllServers.useQuery`. */
|
|
16722
|
-
var useTurnOrchestratorGetAllServers = trpc.turnOrchestrator.getAllServers.useQuery;
|
|
16723
|
-
/** Generated alias around `trpc.turnOrchestrator.setProviderEnabled.useMutation`. */
|
|
16724
|
-
var useTurnOrchestratorSetProviderEnabled = trpc.turnOrchestrator.setProviderEnabled.useMutation;
|
|
16725
16757
|
/** Generated alias around `trpc.turnProvider.getTurnServers.useQuery`. */
|
|
16726
16758
|
var useTurnProviderGetTurnServers = trpc.turnProvider.getTurnServers.useQuery;
|
|
16727
16759
|
/** Generated alias around `trpc.userManagement.listUsers.useQuery`. */
|
|
@@ -16764,6 +16796,18 @@ var useUserManagementDisableTotp = trpc.userManagement.disableTotp.useMutation;
|
|
|
16764
16796
|
var useUserManagementGetTotpStatus = trpc.userManagement.getTotpStatus.useQuery;
|
|
16765
16797
|
/** Generated alias around `trpc.userManagement.verifyTotp.useMutation`. */
|
|
16766
16798
|
var useUserManagementVerifyTotp = trpc.userManagement.verifyTotp.useMutation;
|
|
16799
|
+
/** Generated alias around `trpc.userManagement.oauthIssueCode.useMutation`. */
|
|
16800
|
+
var useUserManagementOauthIssueCode = trpc.userManagement.oauthIssueCode.useMutation;
|
|
16801
|
+
/** Generated alias around `trpc.userManagement.oauthExchangeCode.useMutation`. */
|
|
16802
|
+
var useUserManagementOauthExchangeCode = trpc.userManagement.oauthExchangeCode.useMutation;
|
|
16803
|
+
/** Generated alias around `trpc.userManagement.oauthRefresh.useMutation`. */
|
|
16804
|
+
var useUserManagementOauthRefresh = trpc.userManagement.oauthRefresh.useMutation;
|
|
16805
|
+
/** Generated alias around `trpc.userManagement.oauthVerifyAccessToken.useQuery`. */
|
|
16806
|
+
var useUserManagementOauthVerifyAccessToken = trpc.userManagement.oauthVerifyAccessToken.useQuery;
|
|
16807
|
+
/** Generated alias around `trpc.userManagement.listOauthSessions.useQuery`. */
|
|
16808
|
+
var useUserManagementListOauthSessions = trpc.userManagement.listOauthSessions.useQuery;
|
|
16809
|
+
/** Generated alias around `trpc.userManagement.revokeOauthSession.useMutation`. */
|
|
16810
|
+
var useUserManagementRevokeOauthSession = trpc.userManagement.revokeOauthSession.useMutation;
|
|
16767
16811
|
/** Generated alias around `trpc.webrtcSession.listStreams.useQuery`. */
|
|
16768
16812
|
var useWebrtcSessionListStreams = trpc.webrtcSession.listStreams.useQuery;
|
|
16769
16813
|
/** Generated alias around `trpc.webrtcSession.createSession.useMutation`. */
|
|
@@ -16807,8 +16851,16 @@ var useZonesUpdateZone = trpc.zones.updateZone.useMutation;
|
|
|
16807
16851
|
* `vite.widgets.config.ts`). At runtime we fetch the public list of
|
|
16808
16852
|
* addon-contributed widgets via `useAddonWidgetsListWidgets()`, register
|
|
16809
16853
|
* each remote (`registerRemotes`) once, then resolve a widget by
|
|
16810
|
-
* loading the exposed
|
|
16811
|
-
*
|
|
16854
|
+
* loading the exposed module (`loadRemote`) whose default export is a
|
|
16855
|
+
* `Record<componentKey, ComponentType>` map.
|
|
16856
|
+
*
|
|
16857
|
+
* Unified UI-contribution model (Task 10) — a widget declaration IS a
|
|
16858
|
+
* `UiContribution` with `kind:'remote'`. Both the device-detail path
|
|
16859
|
+
* (`ContributionRenderer`) and the dashboard render widgets through the
|
|
16860
|
+
* same `loadRemoteBundle` MF path. `loadRemoteBundle` is exported and
|
|
16861
|
+
* generic over `(remoteName, exposedModule, entryUrl)` so any
|
|
16862
|
+
* `UiContributionRemote` descriptor resolves without an aggregator-keyed
|
|
16863
|
+
* lookup.
|
|
16812
16864
|
*
|
|
16813
16865
|
* Why MF instead of raw ESM:
|
|
16814
16866
|
* - automatic dedup of shared deps (react/react-dom/@tanstack/etc.)
|
|
@@ -16818,9 +16870,7 @@ var useZonesUpdateZone = trpc.zones.updateZone.useMutation;
|
|
|
16818
16870
|
* wins by default).
|
|
16819
16871
|
* - cross-bundle context invariant preserved automatically: every
|
|
16820
16872
|
* remote's `@camstack/ui-library` import resolves to the host's
|
|
16821
|
-
* instance, so `createContext()` references match across bundles
|
|
16822
|
-
* (the duplicate-Context bug that motivated `createSharedContext`
|
|
16823
|
-
* falls out for free).
|
|
16873
|
+
* instance, so `createContext()` references match across bundles.
|
|
16824
16874
|
*
|
|
16825
16875
|
* Live-update — listens to `addon.widget-ready` so newly-loaded addons
|
|
16826
16876
|
* surface their widgets without a manual page reload (the aggregator
|
|
@@ -16831,11 +16881,11 @@ var useZonesUpdateZone = trpc.zones.updateZone.useMutation;
|
|
|
16831
16881
|
*/
|
|
16832
16882
|
var WidgetRegistryContext = createSharedContext("camstack:widget-registry", null);
|
|
16833
16883
|
/**
|
|
16834
|
-
* Process-global cache for resolved
|
|
16835
|
-
*
|
|
16836
|
-
* default
|
|
16837
|
-
* provider remount — avoids a full re-fetch + re-init of the
|
|
16838
|
-
* when admin-ui's tree cycles.
|
|
16884
|
+
* Process-global cache for resolved remote bundles, keyed by
|
|
16885
|
+
* `<remoteName>::<exposedModule>`. Each cached value is the exposed
|
|
16886
|
+
* module's default-record map (`Record<componentKey, Component>`).
|
|
16887
|
+
* Survives provider remount — avoids a full re-fetch + re-init of the
|
|
16888
|
+
* MF remote when admin-ui's tree cycles.
|
|
16839
16889
|
*/
|
|
16840
16890
|
var bundleModuleCache = /* @__PURE__ */ new Map();
|
|
16841
16891
|
var bundleInflight = /* @__PURE__ */ new Map();
|
|
@@ -16847,6 +16897,10 @@ var bundleInflight = /* @__PURE__ */ new Map();
|
|
|
16847
16897
|
* update.
|
|
16848
16898
|
*/
|
|
16849
16899
|
var registeredRemotes = /* @__PURE__ */ new Set();
|
|
16900
|
+
/** Cache key for `bundleModuleCache` — one entry per `(remote, module)`. */
|
|
16901
|
+
function bundleCacheKey(remoteName, exposedModule) {
|
|
16902
|
+
return `${remoteName}::${exposedModule}`;
|
|
16903
|
+
}
|
|
16850
16904
|
function isRemoteWidgetsModule(value) {
|
|
16851
16905
|
if (!value || typeof value !== "object") return false;
|
|
16852
16906
|
const def = value.default;
|
|
@@ -16855,8 +16909,7 @@ function isRemoteWidgetsModule(value) {
|
|
|
16855
16909
|
/**
|
|
16856
16910
|
* Diagnostic-only string for the "Got: …" tail in the not-a-record
|
|
16857
16911
|
* error. Returns a JSON-friendly snapshot of the module's keys + proto
|
|
16858
|
-
* name without exposing the value itself
|
|
16859
|
-
* thing the module factory produced, may be huge or contain PII).
|
|
16912
|
+
* name without exposing the value itself.
|
|
16860
16913
|
*/
|
|
16861
16914
|
function describeRemoteShape(mod) {
|
|
16862
16915
|
if (!mod || typeof mod !== "object") return typeof mod;
|
|
@@ -16870,13 +16923,19 @@ function describeRemoteShape(mod) {
|
|
|
16870
16923
|
};
|
|
16871
16924
|
}
|
|
16872
16925
|
/**
|
|
16873
|
-
* Register the MF remote (idempotent) and load its
|
|
16874
|
-
* Returns the resolved `Record<
|
|
16926
|
+
* Register the MF remote (idempotent) and load one of its exposed
|
|
16927
|
+
* modules. Returns the resolved `Record<componentKey, Component>` map
|
|
16928
|
+
* (the exposed module's `default` export).
|
|
16929
|
+
*
|
|
16930
|
+
* Exported so the unified `ContributionRenderer` `kind:'remote'` path
|
|
16931
|
+
* can load an arbitrary `{ remoteName, exposedModule }` descriptor —
|
|
16932
|
+
* the same path the `WidgetRegistry` uses internally for widgets.
|
|
16875
16933
|
*/
|
|
16876
|
-
async function loadRemoteBundle(remoteName, entryUrl) {
|
|
16877
|
-
const
|
|
16934
|
+
async function loadRemoteBundle(remoteName, exposedModule, entryUrl) {
|
|
16935
|
+
const cacheKey = bundleCacheKey(remoteName, exposedModule);
|
|
16936
|
+
const cached = bundleModuleCache.get(cacheKey);
|
|
16878
16937
|
if (cached) return cached;
|
|
16879
|
-
const inflight = bundleInflight.get(
|
|
16938
|
+
const inflight = bundleInflight.get(cacheKey);
|
|
16880
16939
|
if (inflight) return inflight;
|
|
16881
16940
|
if (!registeredRemotes.has(remoteName)) {
|
|
16882
16941
|
ensureMfHostInit();
|
|
@@ -16887,22 +16946,26 @@ async function loadRemoteBundle(remoteName, entryUrl) {
|
|
|
16887
16946
|
}], { force: false });
|
|
16888
16947
|
registeredRemotes.add(remoteName);
|
|
16889
16948
|
}
|
|
16890
|
-
const promise = loadRemote(`${remoteName}
|
|
16949
|
+
const promise = loadRemote(`${remoteName}/${exposedModule.startsWith("./") ? exposedModule.slice(2) : exposedModule}`).then((mod) => {
|
|
16891
16950
|
if (!isRemoteWidgetsModule(mod)) {
|
|
16892
16951
|
const shape = describeRemoteShape(mod);
|
|
16893
|
-
throw new Error(`
|
|
16952
|
+
throw new Error(`Remote ${remoteName} (${entryUrl}) does not expose a default record on '${exposedModule}'. Got: ${JSON.stringify(shape)}`);
|
|
16894
16953
|
}
|
|
16895
16954
|
const map = mod.default;
|
|
16896
|
-
bundleModuleCache.set(
|
|
16897
|
-
bundleInflight.delete(
|
|
16955
|
+
bundleModuleCache.set(cacheKey, map);
|
|
16956
|
+
bundleInflight.delete(cacheKey);
|
|
16898
16957
|
return map;
|
|
16899
16958
|
}).catch((err) => {
|
|
16900
|
-
bundleInflight.delete(
|
|
16959
|
+
bundleInflight.delete(cacheKey);
|
|
16901
16960
|
throw err;
|
|
16902
16961
|
});
|
|
16903
|
-
bundleInflight.set(
|
|
16962
|
+
bundleInflight.set(cacheKey, promise);
|
|
16904
16963
|
return promise;
|
|
16905
16964
|
}
|
|
16965
|
+
/** Synchronous peek at the resolved-bundle cache — `undefined` if not yet loaded. */
|
|
16966
|
+
function peekBundle(remoteName, exposedModule) {
|
|
16967
|
+
return bundleModuleCache.get(bundleCacheKey(remoteName, exposedModule));
|
|
16968
|
+
}
|
|
16906
16969
|
var BOOT_WINDOW_MS = 3e4;
|
|
16907
16970
|
var POLL_INTERVAL_MS = 2e3;
|
|
16908
16971
|
function WidgetRegistryProvider({ children }) {
|
|
@@ -16922,21 +16985,24 @@ function WidgetRegistryProvider({ children }) {
|
|
|
16922
16985
|
queryClient.invalidateQueries({ queryKey: [["addonWidgets", "listWidgets"]] });
|
|
16923
16986
|
});
|
|
16924
16987
|
const [resolvedTick, setResolvedTick] = (0, react$1.useState)(0);
|
|
16988
|
+
const widgets = (0, react$1.useMemo)(() => rawWidgets ?? [], [rawWidgets]);
|
|
16925
16989
|
(0, react$1.useEffect)(() => {
|
|
16926
|
-
if (
|
|
16990
|
+
if (widgets.length === 0) return;
|
|
16927
16991
|
let cancelled = false;
|
|
16928
|
-
const
|
|
16929
|
-
for (const w of
|
|
16930
|
-
|
|
16931
|
-
|
|
16932
|
-
|
|
16992
|
+
const seen = /* @__PURE__ */ new Set();
|
|
16993
|
+
for (const w of widgets) {
|
|
16994
|
+
const key = bundleCacheKey(w.remote.remoteName, w.remote.exposedModule);
|
|
16995
|
+
if (seen.has(key)) continue;
|
|
16996
|
+
seen.add(key);
|
|
16997
|
+
if (bundleModuleCache.has(key)) continue;
|
|
16933
16998
|
const entryUrl = w.bundleUrl;
|
|
16934
|
-
loadRemoteBundle(w.remoteName, entryUrl).then(() => {
|
|
16999
|
+
loadRemoteBundle(w.remote.remoteName, w.remote.exposedModule, entryUrl).then(() => {
|
|
16935
17000
|
if (!cancelled) setResolvedTick((t) => t + 1);
|
|
16936
17001
|
}).catch((err) => {
|
|
16937
17002
|
const reason = err instanceof Error ? err.message : String(err);
|
|
16938
17003
|
(typeof globalThis !== "undefined" ? globalThis.console : void 0)?.error?.("[WidgetRegistry] Failed to load widget remote", {
|
|
16939
|
-
remoteName: w.remoteName,
|
|
17004
|
+
remoteName: w.remote.remoteName,
|
|
17005
|
+
exposedModule: w.remote.exposedModule,
|
|
16940
17006
|
entryUrl,
|
|
16941
17007
|
reason
|
|
16942
17008
|
});
|
|
@@ -16945,19 +17011,26 @@ function WidgetRegistryProvider({ children }) {
|
|
|
16945
17011
|
return () => {
|
|
16946
17012
|
cancelled = true;
|
|
16947
17013
|
};
|
|
16948
|
-
}, [
|
|
17014
|
+
}, [widgets]);
|
|
16949
17015
|
const registry = (0, react$1.useMemo)(() => {
|
|
16950
|
-
const widgets = rawWidgets ?? [];
|
|
16951
17016
|
const byId = /* @__PURE__ */ new Map();
|
|
16952
|
-
|
|
17017
|
+
const entryUrlByRemote = /* @__PURE__ */ new Map();
|
|
17018
|
+
for (const w of widgets) {
|
|
17019
|
+
byId.set(`${w.addonId}/${w.stableId}`, w);
|
|
17020
|
+
entryUrlByRemote.set(w.remote.remoteName, w.bundleUrl);
|
|
17021
|
+
}
|
|
16953
17022
|
const toMetadata = (widgetId, entry) => ({
|
|
16954
17023
|
widgetId,
|
|
16955
17024
|
addonId: entry.addonId,
|
|
16956
17025
|
stableId: entry.stableId,
|
|
17026
|
+
tab: entry.tab,
|
|
17027
|
+
subTab: entry.subTab,
|
|
16957
17028
|
label: entry.label,
|
|
17029
|
+
order: entry.order,
|
|
17030
|
+
kind: entry.kind,
|
|
17031
|
+
remote: entry.remote,
|
|
16958
17032
|
description: entry.description,
|
|
16959
17033
|
icon: entry.icon,
|
|
16960
|
-
remoteName: entry.remoteName,
|
|
16961
17034
|
bundleUrl: entry.bundleUrl,
|
|
16962
17035
|
hosts: entry.hosts,
|
|
16963
17036
|
requires: entry.requires,
|
|
@@ -16970,9 +17043,9 @@ function WidgetRegistryProvider({ children }) {
|
|
|
16970
17043
|
resolve: (widgetId) => {
|
|
16971
17044
|
const entry = byId.get(widgetId);
|
|
16972
17045
|
if (!entry) return void 0;
|
|
16973
|
-
const bundle =
|
|
17046
|
+
const bundle = peekBundle(entry.remote.remoteName, entry.remote.exposedModule);
|
|
16974
17047
|
if (!bundle) return null;
|
|
16975
|
-
const Component = bundle[entry.stableId];
|
|
17048
|
+
const Component = bundle[entry.remote.componentKey ?? entry.stableId];
|
|
16976
17049
|
if (!Component) return void 0;
|
|
16977
17050
|
return Component;
|
|
16978
17051
|
},
|
|
@@ -16985,15 +17058,23 @@ function WidgetRegistryProvider({ children }) {
|
|
|
16985
17058
|
const out = [];
|
|
16986
17059
|
for (const [widgetId, entry] of byId) out.push(toMetadata(widgetId, entry));
|
|
16987
17060
|
return out;
|
|
16988
|
-
}
|
|
17061
|
+
},
|
|
17062
|
+
entryUrlFor: (remoteName) => entryUrlByRemote.get(remoteName)
|
|
16989
17063
|
};
|
|
16990
|
-
}, [
|
|
17064
|
+
}, [widgets, resolvedTick]);
|
|
16991
17065
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetRegistryContext.Provider, {
|
|
16992
17066
|
value: registry,
|
|
16993
17067
|
children
|
|
16994
17068
|
});
|
|
16995
17069
|
}
|
|
16996
|
-
/**
|
|
17070
|
+
/**
|
|
17071
|
+
* Returns the registered widget component, `null` while loading, or
|
|
17072
|
+
* `undefined` if unknown.
|
|
17073
|
+
*
|
|
17074
|
+
* @deprecated Unused by all render paths since the unified
|
|
17075
|
+
* UI-contribution model (Task 10). Use `useRemoteComponent` /
|
|
17076
|
+
* `<WidgetSlot>` instead. Kept for external addons that may import it.
|
|
17077
|
+
*/
|
|
16997
17078
|
function useWidget(widgetId) {
|
|
16998
17079
|
return useWidgetRegistry().resolve(widgetId);
|
|
16999
17080
|
}
|
|
@@ -17007,6 +17088,68 @@ function useWidgetMetadata(widgetId) {
|
|
|
17007
17088
|
function useAllWidgets() {
|
|
17008
17089
|
return useWidgetRegistry().listAll();
|
|
17009
17090
|
}
|
|
17091
|
+
/**
|
|
17092
|
+
* Resolve a `kind:'remote'` `UiContributionRemote` descriptor to a
|
|
17093
|
+
* React component. Drives the unified `ContributionRenderer`
|
|
17094
|
+
* `kind:'remote'` path — both device-detail contributions and dashboard
|
|
17095
|
+
* widgets resolve through the same MF `loadRemoteBundle` loader.
|
|
17096
|
+
*
|
|
17097
|
+
* Returns:
|
|
17098
|
+
* - `undefined` — the remote couldn't be resolved (no entry URL known,
|
|
17099
|
+
* or the remote exposed no component for `componentKey`),
|
|
17100
|
+
* - `null` — the bundle is still loading,
|
|
17101
|
+
* - the component otherwise.
|
|
17102
|
+
*
|
|
17103
|
+
* `entryUrl` may be passed explicitly; when omitted it's resolved from
|
|
17104
|
+
* the registry's aggregator-stamped `bundleUrl` for `remote.remoteName`.
|
|
17105
|
+
*/
|
|
17106
|
+
function useRemoteComponent(remote, entryUrl) {
|
|
17107
|
+
const reg = useOptionalWidgetRegistry();
|
|
17108
|
+
const resolvedEntryUrl = entryUrl ?? reg?.entryUrlFor(remote.remoteName);
|
|
17109
|
+
const [tick, setTick] = (0, react$1.useState)(0);
|
|
17110
|
+
const [loadFailed, setLoadFailed] = (0, react$1.useState)(false);
|
|
17111
|
+
(0, react$1.useEffect)(() => {
|
|
17112
|
+
if (!resolvedEntryUrl) return;
|
|
17113
|
+
setLoadFailed(false);
|
|
17114
|
+
if (peekBundle(remote.remoteName, remote.exposedModule)) return;
|
|
17115
|
+
let cancelled = false;
|
|
17116
|
+
loadRemoteBundle(remote.remoteName, remote.exposedModule, resolvedEntryUrl).then(() => {
|
|
17117
|
+
if (!cancelled) setTick((t) => t + 1);
|
|
17118
|
+
}).catch((err) => {
|
|
17119
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
17120
|
+
(typeof globalThis !== "undefined" ? globalThis.console : void 0)?.error?.("[WidgetRegistry] Failed to load remote component", {
|
|
17121
|
+
remoteName: remote.remoteName,
|
|
17122
|
+
exposedModule: remote.exposedModule,
|
|
17123
|
+
entryUrl: resolvedEntryUrl,
|
|
17124
|
+
reason
|
|
17125
|
+
});
|
|
17126
|
+
if (!cancelled) setLoadFailed(true);
|
|
17127
|
+
});
|
|
17128
|
+
return () => {
|
|
17129
|
+
cancelled = true;
|
|
17130
|
+
};
|
|
17131
|
+
}, [
|
|
17132
|
+
remote.remoteName,
|
|
17133
|
+
remote.exposedModule,
|
|
17134
|
+
resolvedEntryUrl
|
|
17135
|
+
]);
|
|
17136
|
+
return (0, react$1.useMemo)(() => {
|
|
17137
|
+
if (!resolvedEntryUrl) return void 0;
|
|
17138
|
+
if (loadFailed) return void 0;
|
|
17139
|
+
const bundle = peekBundle(remote.remoteName, remote.exposedModule);
|
|
17140
|
+
if (!bundle) return null;
|
|
17141
|
+
const componentKey = remote.componentKey;
|
|
17142
|
+
if (componentKey === void 0) return Object.values(bundle)[0] ?? void 0;
|
|
17143
|
+
return bundle[componentKey] ?? void 0;
|
|
17144
|
+
}, [
|
|
17145
|
+
remote.remoteName,
|
|
17146
|
+
remote.exposedModule,
|
|
17147
|
+
remote.componentKey,
|
|
17148
|
+
resolvedEntryUrl,
|
|
17149
|
+
tick,
|
|
17150
|
+
loadFailed
|
|
17151
|
+
]);
|
|
17152
|
+
}
|
|
17010
17153
|
/** Read the registry instance — throws when no provider is mounted. */
|
|
17011
17154
|
function useWidgetRegistry() {
|
|
17012
17155
|
const ctx = useOptionalWidgetRegistry();
|
|
@@ -17021,163 +17164,1429 @@ function useContextSafe(ctx) {
|
|
|
17021
17164
|
return (0, react$1.useContext)(ctx);
|
|
17022
17165
|
}
|
|
17023
17166
|
//#endregion
|
|
17024
|
-
//#region src/
|
|
17167
|
+
//#region src/hooks/use-ptz.ts
|
|
17025
17168
|
/**
|
|
17026
|
-
*
|
|
17027
|
-
* widget. Consumers reference a widget by its public id
|
|
17028
|
-
* (`<addonId>/<stableId>`), the slot looks the component up in the
|
|
17029
|
-
* shared `WidgetRegistry`, validates host context against the widget's
|
|
17030
|
-
* `requires` metadata, and renders one of:
|
|
17031
|
-
* - skeleton placeholder while the bundle is still loading,
|
|
17032
|
-
* - inline error fallback when the widget id is unknown OR the host
|
|
17033
|
-
* didn't supply a required context (`deviceContext` /
|
|
17034
|
-
* `integrationContext`),
|
|
17035
|
-
* - the resolved component otherwise.
|
|
17169
|
+
* usePTZ — PTZ control hook for device-scoped pan / tilt / zoom.
|
|
17036
17170
|
*
|
|
17037
|
-
*
|
|
17038
|
-
*
|
|
17039
|
-
*
|
|
17171
|
+
* Wraps the `ptz` capability methods through the canonical
|
|
17172
|
+
* `useDeviceProxy` surface — every call goes through
|
|
17173
|
+
* `dev.ptz?.<method>(...)` with deviceId/nodeId auto-injected. The
|
|
17174
|
+
* hook stays bare-bones around state (`busy`, `error`, `presets`)
|
|
17175
|
+
* so the operator-facing component (PTZOverlay) stays trivial.
|
|
17176
|
+
*
|
|
17177
|
+
* Returns:
|
|
17178
|
+
* - `move(direction)`: discrete one-shot move (cap.move + cap.stop).
|
|
17179
|
+
* Use for short pulse moves triggered by tapping a d-pad button.
|
|
17180
|
+
* - `startContinuous(direction)` / `stopContinuous()`: gesture-driven
|
|
17181
|
+
* continuous motion (`cap.continuousMove` + `cap.stop`). Use for
|
|
17182
|
+
* long-press handlers on touch / mouse-down handlers on desktop.
|
|
17183
|
+
* - `zoom('in' | 'out')`: discrete zoom step.
|
|
17184
|
+
* - `goHome()`: jump to preset 0.
|
|
17185
|
+
* - `presets` + `goToPreset(presetId)`: list and jump to named presets.
|
|
17186
|
+
*
|
|
17187
|
+
* Parametrised by the same `UseDeviceProxyTrpc` shape every other
|
|
17188
|
+
* device hook uses — works under admin-ui (BackendClient.trpc) and
|
|
17189
|
+
* addon pages (AddonPageProps.trpc) alike.
|
|
17040
17190
|
*/
|
|
17041
|
-
|
|
17042
|
-
|
|
17043
|
-
|
|
17044
|
-
|
|
17045
|
-
|
|
17046
|
-
|
|
17047
|
-
|
|
17048
|
-
|
|
17049
|
-
}
|
|
17050
|
-
|
|
17051
|
-
|
|
17052
|
-
|
|
17053
|
-
}
|
|
17054
|
-
|
|
17055
|
-
|
|
17056
|
-
|
|
17057
|
-
|
|
17058
|
-
|
|
17059
|
-
|
|
17060
|
-
|
|
17061
|
-
|
|
17062
|
-
|
|
17063
|
-
|
|
17191
|
+
var DIRECTION_VECTORS = {
|
|
17192
|
+
"up": {
|
|
17193
|
+
pan: 0,
|
|
17194
|
+
tilt: 1
|
|
17195
|
+
},
|
|
17196
|
+
"down": {
|
|
17197
|
+
pan: 0,
|
|
17198
|
+
tilt: -1
|
|
17199
|
+
},
|
|
17200
|
+
"left": {
|
|
17201
|
+
pan: -1,
|
|
17202
|
+
tilt: 0
|
|
17203
|
+
},
|
|
17204
|
+
"right": {
|
|
17205
|
+
pan: 1,
|
|
17206
|
+
tilt: 0
|
|
17207
|
+
},
|
|
17208
|
+
"up-left": {
|
|
17209
|
+
pan: -1,
|
|
17210
|
+
tilt: 1
|
|
17211
|
+
},
|
|
17212
|
+
"up-right": {
|
|
17213
|
+
pan: 1,
|
|
17214
|
+
tilt: 1
|
|
17215
|
+
},
|
|
17216
|
+
"down-left": {
|
|
17217
|
+
pan: -1,
|
|
17218
|
+
tilt: -1
|
|
17219
|
+
},
|
|
17220
|
+
"down-right": {
|
|
17221
|
+
pan: 1,
|
|
17222
|
+
tilt: -1
|
|
17064
17223
|
}
|
|
17065
|
-
|
|
17066
|
-
|
|
17067
|
-
|
|
17068
|
-
|
|
17069
|
-
|
|
17070
|
-
|
|
17071
|
-
|
|
17072
|
-
|
|
17073
|
-
|
|
17074
|
-
|
|
17075
|
-
|
|
17076
|
-
|
|
17077
|
-
|
|
17078
|
-
|
|
17079
|
-
|
|
17080
|
-
|
|
17081
|
-
|
|
17082
|
-
|
|
17083
|
-
|
|
17084
|
-
|
|
17085
|
-
|
|
17086
|
-
|
|
17087
|
-
|
|
17088
|
-
|
|
17089
|
-
|
|
17090
|
-
|
|
17091
|
-
}
|
|
17092
|
-
|
|
17093
|
-
|
|
17094
|
-
|
|
17095
|
-
|
|
17096
|
-
|
|
17097
|
-
function FieldWrapper({ label, description, required, span, children, translationFn }) {
|
|
17098
|
-
const colSpanClass = span === 2 ? "col-span-2" : span === 3 ? "col-span-3" : span === 4 ? "col-span-4" : "col-span-1";
|
|
17099
|
-
const resolvedLabel = resolveLabel(label, translationFn);
|
|
17100
|
-
const resolvedDescription = resolveLabel(description, translationFn);
|
|
17101
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17102
|
-
className: colSpanClass,
|
|
17103
|
-
children: [
|
|
17104
|
-
resolvedLabel !== void 0 && resolvedLabel !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
17105
|
-
className: LABEL_CLASS,
|
|
17106
|
-
children: [resolvedLabel, required && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
17107
|
-
className: "text-danger ml-0.5",
|
|
17108
|
-
children: "*"
|
|
17109
|
-
})]
|
|
17110
|
-
}),
|
|
17111
|
-
children,
|
|
17112
|
-
resolvedDescription && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
17113
|
-
className: DESC_CLASS,
|
|
17114
|
-
children: resolvedDescription
|
|
17115
|
-
})
|
|
17116
|
-
]
|
|
17117
|
-
});
|
|
17118
|
-
}
|
|
17119
|
-
function TextField({ field, value, onChange, disabled, translationFn }) {
|
|
17120
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldWrapper, {
|
|
17121
|
-
label: field.label,
|
|
17122
|
-
description: field.description,
|
|
17123
|
-
required: field.required,
|
|
17124
|
-
span: field.span,
|
|
17125
|
-
translationFn,
|
|
17126
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
17127
|
-
type: field.inputType ?? "text",
|
|
17128
|
-
className: INPUT_CLASS,
|
|
17129
|
-
value: value === void 0 || value === null ? "" : String(value),
|
|
17130
|
-
placeholder: field.placeholder,
|
|
17131
|
-
maxLength: field.maxLength,
|
|
17132
|
-
pattern: field.pattern,
|
|
17133
|
-
disabled: disabled || field.disabled,
|
|
17134
|
-
onChange: (e) => onChange(e.target.value)
|
|
17135
|
-
})
|
|
17136
|
-
});
|
|
17137
|
-
}
|
|
17138
|
-
function NumberField({ field, value, onChange, disabled, translationFn }) {
|
|
17139
|
-
const [local, setLocal] = (0, react$1.useState)(value === void 0 || value === null ? "" : String(value));
|
|
17140
|
-
const focusedRef = (0, react$1.useRef)(false);
|
|
17224
|
+
};
|
|
17225
|
+
function usePTZ(trpc, deviceId, hookOptions) {
|
|
17226
|
+
const defaultSpeed = hookOptions?.defaultSpeed ?? .5;
|
|
17227
|
+
const pulseMs = hookOptions?.pulseMs ?? 250;
|
|
17228
|
+
const enabled = hookOptions?.enabled ?? true;
|
|
17229
|
+
const ptz = useDeviceProxy(trpc, enabled ? deviceId : null)?.ptz;
|
|
17230
|
+
const [presets, setPresets] = (0, react$1.useState)([]);
|
|
17231
|
+
const [options, setOptions] = (0, react$1.useState)(null);
|
|
17232
|
+
const [busy, setBusy] = (0, react$1.useState)(false);
|
|
17233
|
+
const [error, setError] = (0, react$1.useState)(null);
|
|
17234
|
+
const isAbsentProvider = (err) => {
|
|
17235
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
17236
|
+
return msg.includes("provider not available") || msg.includes("no 'ptz' binding");
|
|
17237
|
+
};
|
|
17238
|
+
const refreshPresets = (0, react$1.useCallback)(async () => {
|
|
17239
|
+
if (!enabled || !ptz) return;
|
|
17240
|
+
try {
|
|
17241
|
+
setPresets(await ptz.getPresets({}));
|
|
17242
|
+
} catch (err) {
|
|
17243
|
+
if (isAbsentProvider(err)) return;
|
|
17244
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
17245
|
+
}
|
|
17246
|
+
}, [ptz, enabled]);
|
|
17247
|
+
const refreshOptions = (0, react$1.useCallback)(async () => {
|
|
17248
|
+
if (!enabled || !ptz) return;
|
|
17249
|
+
try {
|
|
17250
|
+
setOptions(await ptz.getOptions({}));
|
|
17251
|
+
} catch (err) {
|
|
17252
|
+
if (isAbsentProvider(err)) return;
|
|
17253
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
17254
|
+
}
|
|
17255
|
+
}, [ptz, enabled]);
|
|
17141
17256
|
(0, react$1.useEffect)(() => {
|
|
17142
|
-
|
|
17143
|
-
|
|
17144
|
-
}, [
|
|
17145
|
-
const
|
|
17146
|
-
|
|
17147
|
-
|
|
17148
|
-
|
|
17257
|
+
refreshPresets();
|
|
17258
|
+
refreshOptions();
|
|
17259
|
+
}, [refreshPresets, refreshOptions]);
|
|
17260
|
+
const wrap = (0, react$1.useCallback)(async (fn) => {
|
|
17261
|
+
if (!enabled || !ptz) return void 0;
|
|
17262
|
+
setError(null);
|
|
17263
|
+
setBusy(true);
|
|
17264
|
+
try {
|
|
17265
|
+
return await fn();
|
|
17266
|
+
} catch (err) {
|
|
17267
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
17149
17268
|
return;
|
|
17269
|
+
} finally {
|
|
17270
|
+
setBusy(false);
|
|
17150
17271
|
}
|
|
17151
|
-
|
|
17152
|
-
|
|
17153
|
-
|
|
17154
|
-
|
|
17155
|
-
|
|
17156
|
-
|
|
17157
|
-
|
|
17158
|
-
|
|
17159
|
-
|
|
17160
|
-
|
|
17161
|
-
|
|
17162
|
-
|
|
17163
|
-
|
|
17164
|
-
|
|
17165
|
-
|
|
17166
|
-
|
|
17167
|
-
|
|
17168
|
-
|
|
17169
|
-
|
|
17170
|
-
|
|
17171
|
-
|
|
17172
|
-
|
|
17173
|
-
|
|
17174
|
-
|
|
17175
|
-
|
|
17176
|
-
|
|
17177
|
-
|
|
17178
|
-
|
|
17179
|
-
|
|
17180
|
-
|
|
17272
|
+
}, [enabled, ptz]);
|
|
17273
|
+
return {
|
|
17274
|
+
move: (0, react$1.useCallback)(async (direction, speed) => {
|
|
17275
|
+
if (!ptz) return;
|
|
17276
|
+
const v = DIRECTION_VECTORS[direction];
|
|
17277
|
+
const s = speed ?? defaultSpeed;
|
|
17278
|
+
await wrap(async () => {
|
|
17279
|
+
await ptz.move({
|
|
17280
|
+
pan: v.pan,
|
|
17281
|
+
tilt: v.tilt,
|
|
17282
|
+
speed: s
|
|
17283
|
+
});
|
|
17284
|
+
await new Promise((r) => setTimeout(r, pulseMs));
|
|
17285
|
+
await ptz.stop({});
|
|
17286
|
+
});
|
|
17287
|
+
}, [
|
|
17288
|
+
ptz,
|
|
17289
|
+
defaultSpeed,
|
|
17290
|
+
pulseMs,
|
|
17291
|
+
wrap
|
|
17292
|
+
]),
|
|
17293
|
+
startContinuous: (0, react$1.useCallback)(async (direction, speed) => {
|
|
17294
|
+
if (!ptz) return;
|
|
17295
|
+
const v = DIRECTION_VECTORS[direction];
|
|
17296
|
+
const s = speed ?? defaultSpeed;
|
|
17297
|
+
await wrap(() => ptz.continuousMove({
|
|
17298
|
+
pan: v.pan,
|
|
17299
|
+
tilt: v.tilt,
|
|
17300
|
+
speed: s
|
|
17301
|
+
}));
|
|
17302
|
+
}, [
|
|
17303
|
+
ptz,
|
|
17304
|
+
defaultSpeed,
|
|
17305
|
+
wrap
|
|
17306
|
+
]),
|
|
17307
|
+
stopContinuous: (0, react$1.useCallback)(async () => {
|
|
17308
|
+
if (!ptz) return;
|
|
17309
|
+
await wrap(() => ptz.stop({}));
|
|
17310
|
+
}, [ptz, wrap]),
|
|
17311
|
+
zoom: (0, react$1.useCallback)(async (direction, speed) => {
|
|
17312
|
+
if (!ptz) return;
|
|
17313
|
+
const z = direction === "in" ? 1 : -1;
|
|
17314
|
+
const s = speed ?? defaultSpeed;
|
|
17315
|
+
await wrap(async () => {
|
|
17316
|
+
await ptz.move({
|
|
17317
|
+
zoom: z,
|
|
17318
|
+
speed: s
|
|
17319
|
+
});
|
|
17320
|
+
await new Promise((r) => setTimeout(r, pulseMs));
|
|
17321
|
+
await ptz.stop({});
|
|
17322
|
+
});
|
|
17323
|
+
}, [
|
|
17324
|
+
ptz,
|
|
17325
|
+
defaultSpeed,
|
|
17326
|
+
pulseMs,
|
|
17327
|
+
wrap
|
|
17328
|
+
]),
|
|
17329
|
+
goHome: (0, react$1.useCallback)(async () => {
|
|
17330
|
+
if (!ptz) return;
|
|
17331
|
+
await wrap(() => ptz.goHome({}));
|
|
17332
|
+
}, [ptz, wrap]),
|
|
17333
|
+
goToPreset: (0, react$1.useCallback)(async (presetId) => {
|
|
17334
|
+
if (!ptz) return;
|
|
17335
|
+
await wrap(() => ptz.goToPreset({ presetId }));
|
|
17336
|
+
}, [ptz, wrap]),
|
|
17337
|
+
savePreset: (0, react$1.useCallback)(async (name) => {
|
|
17338
|
+
if (!ptz) return void 0;
|
|
17339
|
+
return wrap(async () => {
|
|
17340
|
+
const usedIds = new Set(presets.map((p) => Number(p.id)).filter((n) => Number.isInteger(n) && n >= 0));
|
|
17341
|
+
let nextId = 0;
|
|
17342
|
+
while (usedIds.has(nextId)) nextId += 1;
|
|
17343
|
+
const presetId = String(nextId);
|
|
17344
|
+
await ptz.savePreset({
|
|
17345
|
+
presetId,
|
|
17346
|
+
name
|
|
17347
|
+
});
|
|
17348
|
+
await refreshPresets();
|
|
17349
|
+
return presetId;
|
|
17350
|
+
});
|
|
17351
|
+
}, [
|
|
17352
|
+
ptz,
|
|
17353
|
+
presets,
|
|
17354
|
+
refreshPresets,
|
|
17355
|
+
wrap
|
|
17356
|
+
]),
|
|
17357
|
+
deletePreset: (0, react$1.useCallback)(async (presetId) => {
|
|
17358
|
+
if (!ptz) return;
|
|
17359
|
+
await wrap(async () => {
|
|
17360
|
+
await ptz.deletePreset({ presetId });
|
|
17361
|
+
await refreshPresets();
|
|
17362
|
+
});
|
|
17363
|
+
}, [
|
|
17364
|
+
ptz,
|
|
17365
|
+
refreshPresets,
|
|
17366
|
+
wrap
|
|
17367
|
+
]),
|
|
17368
|
+
presets,
|
|
17369
|
+
refreshPresets,
|
|
17370
|
+
options,
|
|
17371
|
+
busy,
|
|
17372
|
+
error
|
|
17373
|
+
};
|
|
17374
|
+
}
|
|
17375
|
+
//#endregion
|
|
17376
|
+
//#region src/composites/ptz-overlay.tsx
|
|
17377
|
+
/**
|
|
17378
|
+
* PTZOverlay — pan / tilt / zoom controls.
|
|
17379
|
+
*
|
|
17380
|
+
* Two visual variants driven by `mode`:
|
|
17381
|
+
* - `'overlay'` (default): translucent dark pill positioned bottom-
|
|
17382
|
+
* right of the camera viewport. Used as `extraOverlay` on the
|
|
17383
|
+
* `CameraStreamPlayer` so operators can drive PTZ without leaving
|
|
17384
|
+
* the live view. Opaque background + subtle ring keeps the d-pad
|
|
17385
|
+
* legible against any frame.
|
|
17386
|
+
* - `'panel'`: full-bleed inside a host container (e.g. the floating
|
|
17387
|
+
* PTZ panel in DeviceDetail). No absolute positioning, no inner
|
|
17388
|
+
* wrapper card — the host's chrome is the only frame. Inherits the
|
|
17389
|
+
* surrounding `bg-surface` so the dark theme reads consistently
|
|
17390
|
+
* instead of the previous always-dark-bubble look.
|
|
17391
|
+
*
|
|
17392
|
+
* Interaction model is identical across modes: short tap fires a
|
|
17393
|
+
* discrete pulse (`move`); long press starts continuous motion until
|
|
17394
|
+
* release (`startContinuous` + `stopContinuous` on pointer up).
|
|
17395
|
+
*/
|
|
17396
|
+
function DPadButton({ direction, icon: Icon, disabled, className, variant, onMove, onStart, onStop }) {
|
|
17397
|
+
const [pressedAt, setPressedAt] = (0, react$1.useState)(null);
|
|
17398
|
+
const [continuous, setContinuous] = (0, react$1.useState)(false);
|
|
17399
|
+
const handlePointerDown = (0, react$1.useCallback)((e) => {
|
|
17400
|
+
if (disabled) return;
|
|
17401
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
17402
|
+
setPressedAt(Date.now());
|
|
17403
|
+
setContinuous(false);
|
|
17404
|
+
const timer = setTimeout(() => {
|
|
17405
|
+
setContinuous(true);
|
|
17406
|
+
onStart(direction);
|
|
17407
|
+
}, 250);
|
|
17408
|
+
e.currentTarget.dataset.timer = String(timer);
|
|
17409
|
+
}, [
|
|
17410
|
+
direction,
|
|
17411
|
+
disabled,
|
|
17412
|
+
onStart
|
|
17413
|
+
]);
|
|
17414
|
+
const handlePointerUp = (0, react$1.useCallback)((e) => {
|
|
17415
|
+
const timerId = Number(e.currentTarget.dataset.timer);
|
|
17416
|
+
if (timerId) clearTimeout(timerId);
|
|
17417
|
+
e.currentTarget.dataset.timer = "";
|
|
17418
|
+
if (continuous) onStop();
|
|
17419
|
+
else if (pressedAt !== null) onMove(direction);
|
|
17420
|
+
setPressedAt(null);
|
|
17421
|
+
setContinuous(false);
|
|
17422
|
+
}, [
|
|
17423
|
+
continuous,
|
|
17424
|
+
direction,
|
|
17425
|
+
onMove,
|
|
17426
|
+
onStop,
|
|
17427
|
+
pressedAt
|
|
17428
|
+
]);
|
|
17429
|
+
const handlePointerCancel = (0, react$1.useCallback)((e) => {
|
|
17430
|
+
const timerId = Number(e.currentTarget.dataset.timer);
|
|
17431
|
+
if (timerId) clearTimeout(timerId);
|
|
17432
|
+
e.currentTarget.dataset.timer = "";
|
|
17433
|
+
if (continuous) onStop();
|
|
17434
|
+
setPressedAt(null);
|
|
17435
|
+
setContinuous(false);
|
|
17436
|
+
}, [continuous, onStop]);
|
|
17437
|
+
const sizeClass = variant === "panel" ? "h-9 w-9" : "h-7 w-7";
|
|
17438
|
+
const iconSizeClass = variant === "panel" ? "h-4 w-4" : "h-3.5 w-3.5";
|
|
17439
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
17440
|
+
type: "button",
|
|
17441
|
+
disabled,
|
|
17442
|
+
onPointerDown: handlePointerDown,
|
|
17443
|
+
onPointerUp: handlePointerUp,
|
|
17444
|
+
onPointerCancel: handlePointerCancel,
|
|
17445
|
+
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),
|
|
17446
|
+
title: direction,
|
|
17447
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { className: iconSizeClass })
|
|
17448
|
+
});
|
|
17449
|
+
}
|
|
17450
|
+
function PTZOverlay({ controls, mode = "overlay", showPresets, showZoom = true, showHome = true, className }) {
|
|
17451
|
+
const { move, startContinuous, stopContinuous, zoom, goHome, goToPreset, savePreset, deletePreset, presets, options, busy, error } = controls;
|
|
17452
|
+
const confirm = useConfirm();
|
|
17453
|
+
const [presetsOpen, setPresetsOpen] = (0, react$1.useState)(false);
|
|
17454
|
+
const [presetName, setPresetName] = (0, react$1.useState)("");
|
|
17455
|
+
const presetsVisible = (showPresets ?? presets.length > 0) && presets.length > 0;
|
|
17456
|
+
const isPanel = mode === "panel";
|
|
17457
|
+
const presetManagementVisible = isPanel && (options?.supportsPresets ?? false);
|
|
17458
|
+
const maxPresetsReached = options?.maxPresets !== void 0 && presets.length >= options.maxPresets;
|
|
17459
|
+
const handleSavePreset = (0, react$1.useCallback)(async () => {
|
|
17460
|
+
const name = presetName.trim();
|
|
17461
|
+
if (!name || busy || maxPresetsReached) return;
|
|
17462
|
+
if (await savePreset(name) !== void 0) setPresetName("");
|
|
17463
|
+
}, [
|
|
17464
|
+
presetName,
|
|
17465
|
+
busy,
|
|
17466
|
+
maxPresetsReached,
|
|
17467
|
+
savePreset
|
|
17468
|
+
]);
|
|
17469
|
+
const handleDeletePreset = (0, react$1.useCallback)(async (presetId, label) => {
|
|
17470
|
+
if (!await confirm({
|
|
17471
|
+
title: "Delete preset",
|
|
17472
|
+
message: `Delete PTZ preset "${label}"? This removes it from the camera.`,
|
|
17473
|
+
confirmLabel: "Delete",
|
|
17474
|
+
variant: "danger"
|
|
17475
|
+
})) return;
|
|
17476
|
+
await deletePreset(presetId);
|
|
17477
|
+
}, [confirm, deletePreset]);
|
|
17478
|
+
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";
|
|
17479
|
+
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");
|
|
17480
|
+
const sideButtonSize = isPanel ? "h-9 w-9" : "h-7 w-7";
|
|
17481
|
+
const sideIconSize = isPanel ? "h-4 w-4" : "h-3.5 w-3.5";
|
|
17482
|
+
const sideButtonHover = isPanel ? "text-foreground hover:bg-surface-hover" : "text-white hover:bg-white/15";
|
|
17483
|
+
const sepClass = isPanel ? "h-12 w-px bg-border mx-1" : "h-12 w-px bg-white/15 mx-1";
|
|
17484
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17485
|
+
className: cn(containerClass, className),
|
|
17486
|
+
children: [
|
|
17487
|
+
error && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17488
|
+
className: "rounded bg-danger/90 px-2 py-1 text-[10px] font-medium text-white shadow-lg max-w-[200px] self-center",
|
|
17489
|
+
children: ["PTZ: ", error]
|
|
17490
|
+
}),
|
|
17491
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17492
|
+
className: cn(rowClass, isPanel && "justify-center"),
|
|
17493
|
+
children: [
|
|
17494
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17495
|
+
className: "grid grid-cols-3 gap-0.5",
|
|
17496
|
+
children: [
|
|
17497
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
17498
|
+
"aria-hidden": true,
|
|
17499
|
+
className: isPanel ? "h-9 w-9" : "h-7 w-7"
|
|
17500
|
+
}),
|
|
17501
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DPadButton, {
|
|
17502
|
+
direction: "up",
|
|
17503
|
+
icon: ArrowUp,
|
|
17504
|
+
variant: mode,
|
|
17505
|
+
onMove: move,
|
|
17506
|
+
onStart: startContinuous,
|
|
17507
|
+
onStop: stopContinuous
|
|
17508
|
+
}),
|
|
17509
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
17510
|
+
"aria-hidden": true,
|
|
17511
|
+
className: isPanel ? "h-9 w-9" : "h-7 w-7"
|
|
17512
|
+
}),
|
|
17513
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DPadButton, {
|
|
17514
|
+
direction: "left",
|
|
17515
|
+
icon: ArrowLeft,
|
|
17516
|
+
variant: mode,
|
|
17517
|
+
onMove: move,
|
|
17518
|
+
onStart: startContinuous,
|
|
17519
|
+
onStop: stopContinuous
|
|
17520
|
+
}),
|
|
17521
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
17522
|
+
"aria-hidden": true,
|
|
17523
|
+
className: isPanel ? "h-9 w-9" : "h-7 w-7"
|
|
17524
|
+
}),
|
|
17525
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DPadButton, {
|
|
17526
|
+
direction: "right",
|
|
17527
|
+
icon: ArrowRight,
|
|
17528
|
+
variant: mode,
|
|
17529
|
+
onMove: move,
|
|
17530
|
+
onStart: startContinuous,
|
|
17531
|
+
onStop: stopContinuous
|
|
17532
|
+
}),
|
|
17533
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
17534
|
+
"aria-hidden": true,
|
|
17535
|
+
className: isPanel ? "h-9 w-9" : "h-7 w-7"
|
|
17536
|
+
}),
|
|
17537
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DPadButton, {
|
|
17538
|
+
direction: "down",
|
|
17539
|
+
icon: ArrowDown,
|
|
17540
|
+
variant: mode,
|
|
17541
|
+
onMove: move,
|
|
17542
|
+
onStart: startContinuous,
|
|
17543
|
+
onStop: stopContinuous
|
|
17544
|
+
}),
|
|
17545
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
17546
|
+
"aria-hidden": true,
|
|
17547
|
+
className: isPanel ? "h-9 w-9" : "h-7 w-7"
|
|
17548
|
+
})
|
|
17549
|
+
]
|
|
17550
|
+
}),
|
|
17551
|
+
(showZoom || showHome) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: sepClass }),
|
|
17552
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17553
|
+
className: "flex flex-col gap-0.5",
|
|
17554
|
+
children: [showZoom && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
17555
|
+
type: "button",
|
|
17556
|
+
onClick: () => zoom("in"),
|
|
17557
|
+
disabled: busy,
|
|
17558
|
+
title: "Zoom in",
|
|
17559
|
+
className: cn("flex items-center justify-center rounded disabled:opacity-40", sideButtonSize, sideButtonHover),
|
|
17560
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZoomIn, { className: sideIconSize })
|
|
17561
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
17562
|
+
type: "button",
|
|
17563
|
+
onClick: () => zoom("out"),
|
|
17564
|
+
disabled: busy,
|
|
17565
|
+
title: "Zoom out",
|
|
17566
|
+
className: cn("flex items-center justify-center rounded disabled:opacity-40", sideButtonSize, sideButtonHover),
|
|
17567
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZoomOut, { className: sideIconSize })
|
|
17568
|
+
})] }), showHome && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
17569
|
+
type: "button",
|
|
17570
|
+
onClick: () => goHome(),
|
|
17571
|
+
disabled: busy,
|
|
17572
|
+
title: "Go home",
|
|
17573
|
+
className: cn("flex items-center justify-center rounded disabled:opacity-40", sideButtonSize, sideButtonHover),
|
|
17574
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(House, { className: sideIconSize })
|
|
17575
|
+
})]
|
|
17576
|
+
}),
|
|
17577
|
+
presetsVisible && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17578
|
+
className: "relative",
|
|
17579
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
17580
|
+
type: "button",
|
|
17581
|
+
onClick: () => setPresetsOpen((v) => !v),
|
|
17582
|
+
disabled: busy,
|
|
17583
|
+
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"),
|
|
17584
|
+
title: "Presets",
|
|
17585
|
+
children: ["Presets", /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronDown, { className: cn("h-3 w-3 transition-transform", presetsOpen && "rotate-180") })]
|
|
17586
|
+
}), presetsOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
17587
|
+
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"),
|
|
17588
|
+
children: presets.map((p) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17589
|
+
className: cn("group flex items-center", isPanel ? "hover:bg-surface-hover" : "hover:bg-white/15"),
|
|
17590
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
17591
|
+
type: "button",
|
|
17592
|
+
onClick: () => {
|
|
17593
|
+
goToPreset(p.id);
|
|
17594
|
+
setPresetsOpen(false);
|
|
17595
|
+
},
|
|
17596
|
+
disabled: busy,
|
|
17597
|
+
className: cn("flex-1 px-3 py-1.5 text-left text-[10px] disabled:opacity-40", isPanel ? "text-foreground" : "text-white"),
|
|
17598
|
+
children: p.name || p.id
|
|
17599
|
+
}), presetManagementVisible && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
17600
|
+
type: "button",
|
|
17601
|
+
onClick: () => void handleDeletePreset(p.id, p.name || p.id),
|
|
17602
|
+
disabled: busy,
|
|
17603
|
+
title: `Delete preset ${p.name || p.id}`,
|
|
17604
|
+
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"),
|
|
17605
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(X, { className: "h-3 w-3" })
|
|
17606
|
+
})]
|
|
17607
|
+
}, p.id))
|
|
17608
|
+
})]
|
|
17609
|
+
})
|
|
17610
|
+
]
|
|
17611
|
+
}),
|
|
17612
|
+
presetManagementVisible && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17613
|
+
className: "flex flex-col gap-1 rounded-lg border border-border p-2",
|
|
17614
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17615
|
+
className: "flex items-center gap-2",
|
|
17616
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
17617
|
+
type: "text",
|
|
17618
|
+
value: presetName,
|
|
17619
|
+
onChange: (e) => setPresetName(e.target.value),
|
|
17620
|
+
onKeyDown: (e) => {
|
|
17621
|
+
if (e.key === "Enter") handleSavePreset();
|
|
17622
|
+
},
|
|
17623
|
+
disabled: busy || maxPresetsReached,
|
|
17624
|
+
placeholder: "New preset name",
|
|
17625
|
+
maxLength: 64,
|
|
17626
|
+
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")
|
|
17627
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
17628
|
+
type: "button",
|
|
17629
|
+
onClick: () => void handleSavePreset(),
|
|
17630
|
+
disabled: busy || maxPresetsReached || presetName.trim().length === 0,
|
|
17631
|
+
title: maxPresetsReached ? `Camera preset limit reached (${options?.maxPresets})` : "Save current position as a new preset",
|
|
17632
|
+
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"),
|
|
17633
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Plus, { className: "h-3 w-3" }), "Save current position"]
|
|
17634
|
+
})]
|
|
17635
|
+
}), maxPresetsReached && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
17636
|
+
className: "text-[10px] text-foreground-subtle",
|
|
17637
|
+
children: [
|
|
17638
|
+
"Camera preset limit reached (",
|
|
17639
|
+
options?.maxPresets,
|
|
17640
|
+
"). Delete a preset to add a new one."
|
|
17641
|
+
]
|
|
17642
|
+
})]
|
|
17643
|
+
})
|
|
17644
|
+
]
|
|
17645
|
+
});
|
|
17646
|
+
}
|
|
17647
|
+
//#endregion
|
|
17648
|
+
//#region src/composites/cap-settings/PtzPanel.tsx
|
|
17649
|
+
/**
|
|
17650
|
+
* PtzPanel — live pan/tilt/zoom controls for the `ptz` capability. A
|
|
17651
|
+
* cap-settings component (ui-library), mounted as a cap-contributed
|
|
17652
|
+
* top-level device-detail tab via the cap-UI contribution mechanism
|
|
17653
|
+
* (the `ptz` cap declares `ui: { tab: 'ptz', kind: 'static', ... }`).
|
|
17654
|
+
*
|
|
17655
|
+
* Consolidates the former admin-ui `PTZPanelContent`: `usePTZ` drives
|
|
17656
|
+
* the controls, `PTZOverlay` renders them (`mode='panel'`), and an
|
|
17657
|
+
* Autofocus toggle is shown only when `getOptions().hasAutofocus`.
|
|
17658
|
+
* Autotrack is its own cap-UI contribution (`ptz-autotrack`), mounted
|
|
17659
|
+
* independently by the contribution mechanism — not nested here.
|
|
17660
|
+
*/
|
|
17661
|
+
/** Autofocus toggle — only mounted when `hasAutofocus`. Reads the cap's
|
|
17662
|
+
* `getStatus().autofocus` and drives `setAutofocus`. */
|
|
17663
|
+
function AutofocusToggle({ deviceId }) {
|
|
17664
|
+
const dev = useDeviceProxy(useSystem().trpcClient, deviceId);
|
|
17665
|
+
const [enabled, setEnabled] = (0, react$1.useState)(null);
|
|
17666
|
+
const [busy, setBusy] = (0, react$1.useState)(false);
|
|
17667
|
+
(0, react$1.useEffect)(() => {
|
|
17668
|
+
if (!dev) return void 0;
|
|
17669
|
+
let cancelled = false;
|
|
17670
|
+
(async () => {
|
|
17671
|
+
try {
|
|
17672
|
+
const status = await dev.ptz?.getStatus({});
|
|
17673
|
+
if (cancelled || !status) return;
|
|
17674
|
+
setEnabled(status.autofocus);
|
|
17675
|
+
} catch {}
|
|
17676
|
+
})();
|
|
17677
|
+
return () => {
|
|
17678
|
+
cancelled = true;
|
|
17679
|
+
};
|
|
17680
|
+
}, [dev]);
|
|
17681
|
+
const toggle = (0, react$1.useCallback)(async () => {
|
|
17682
|
+
if (!dev?.ptz || enabled === null) return;
|
|
17683
|
+
setBusy(true);
|
|
17684
|
+
try {
|
|
17685
|
+
const next = !enabled;
|
|
17686
|
+
await dev.ptz.setAutofocus({ enabled: next });
|
|
17687
|
+
setEnabled(next);
|
|
17688
|
+
} catch (err) {
|
|
17689
|
+
console.error("ptz.setAutofocus failed", err);
|
|
17690
|
+
} finally {
|
|
17691
|
+
setBusy(false);
|
|
17692
|
+
}
|
|
17693
|
+
}, [dev, enabled]);
|
|
17694
|
+
if (enabled === null) return null;
|
|
17695
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17696
|
+
className: "flex items-center justify-between border-t border-border/40 mt-2 pt-2",
|
|
17697
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
17698
|
+
className: "text-[10.5px] font-semibold text-foreground",
|
|
17699
|
+
children: "Autofocus"
|
|
17700
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
17701
|
+
type: "button",
|
|
17702
|
+
onClick: () => void toggle(),
|
|
17703
|
+
disabled: busy,
|
|
17704
|
+
"aria-pressed": enabled,
|
|
17705
|
+
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`,
|
|
17706
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: `h-1.5 w-1.5 rounded-full ${enabled ? "bg-success" : "bg-foreground-subtle/40"}` }), enabled ? "On" : "Off"]
|
|
17707
|
+
})]
|
|
17708
|
+
});
|
|
17709
|
+
}
|
|
17710
|
+
function PtzPanel({ deviceId }) {
|
|
17711
|
+
const system = useSystem();
|
|
17712
|
+
const ready = Number.isFinite(deviceId);
|
|
17713
|
+
const controls = usePTZ(system.trpcClient, ready ? deviceId : 0, { enabled: ready });
|
|
17714
|
+
if (!ready) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17715
|
+
className: "flex items-center justify-center h-full text-[11px] text-foreground-subtle italic",
|
|
17716
|
+
children: [
|
|
17717
|
+
"Device #",
|
|
17718
|
+
deviceId,
|
|
17719
|
+
" not loaded."
|
|
17720
|
+
]
|
|
17721
|
+
});
|
|
17722
|
+
const hasAutofocus = controls.options?.hasAutofocus === true;
|
|
17723
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17724
|
+
className: "flex flex-col h-full overflow-y-auto",
|
|
17725
|
+
children: [
|
|
17726
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
17727
|
+
className: "text-[11px] font-semibold text-foreground-subtle uppercase tracking-wider mb-2",
|
|
17728
|
+
children: "PTZ"
|
|
17729
|
+
}),
|
|
17730
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PTZOverlay, {
|
|
17731
|
+
controls,
|
|
17732
|
+
mode: "panel"
|
|
17733
|
+
}),
|
|
17734
|
+
hasAutofocus && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AutofocusToggle, { deviceId })
|
|
17735
|
+
]
|
|
17736
|
+
});
|
|
17737
|
+
}
|
|
17738
|
+
//#endregion
|
|
17739
|
+
//#region src/hooks/use-device-autotrack.ts
|
|
17740
|
+
/**
|
|
17741
|
+
* `useDeviceAutotrack` — typed wrapper around the `ptz-autotrack` cap.
|
|
17742
|
+
*
|
|
17743
|
+
* Surface:
|
|
17744
|
+
* - `status` — current `{enabled, lastChangedAt, currentSettings}`,
|
|
17745
|
+
* polled at 5s intervals while the hook is mounted.
|
|
17746
|
+
* - `setEnabled(on)` — flip the on/off state.
|
|
17747
|
+
* - `setSettings(partial)` — patch one or more settings keys.
|
|
17748
|
+
* - `isPending` — true while a mutation is in flight.
|
|
17749
|
+
*
|
|
17750
|
+
* Returns `null` for `status` when the cap isn't available (device
|
|
17751
|
+
* doesn't support autotrack) — callers gate render on this OR on the
|
|
17752
|
+
* device's `PtzAutotrack` feature flag.
|
|
17753
|
+
*/
|
|
17754
|
+
function useDeviceAutotrack(deviceId) {
|
|
17755
|
+
const queryClient = (0, _tanstack_react_query.useQueryClient)();
|
|
17756
|
+
const statusQuery = usePtzAutotrackGetStatus({ deviceId: deviceId ?? 0 }, {
|
|
17757
|
+
enabled: deviceId !== null && Number.isFinite(deviceId),
|
|
17758
|
+
refetchInterval: 5e3,
|
|
17759
|
+
retry: 1
|
|
17760
|
+
});
|
|
17761
|
+
const setEnabledMutation = usePtzAutotrackSetEnabled({ onSuccess: () => {
|
|
17762
|
+
queryClient.invalidateQueries({ queryKey: [["ptzAutotrack"]] });
|
|
17763
|
+
} });
|
|
17764
|
+
const setSettingsMutation = usePtzAutotrackSetSettings({ onSuccess: () => {
|
|
17765
|
+
queryClient.invalidateQueries({ queryKey: [["ptzAutotrack"]] });
|
|
17766
|
+
} });
|
|
17767
|
+
const setEnabled = (0, react$1.useCallback)(async (on) => {
|
|
17768
|
+
if (deviceId === null) return;
|
|
17769
|
+
await setEnabledMutation.mutateAsync({
|
|
17770
|
+
deviceId,
|
|
17771
|
+
enabled: on
|
|
17772
|
+
});
|
|
17773
|
+
}, [deviceId, setEnabledMutation]);
|
|
17774
|
+
const setSettings = (0, react$1.useCallback)(async (patch) => {
|
|
17775
|
+
if (deviceId === null) return;
|
|
17776
|
+
await setSettingsMutation.mutateAsync({
|
|
17777
|
+
deviceId,
|
|
17778
|
+
settings: patch
|
|
17779
|
+
});
|
|
17780
|
+
}, [deviceId, setSettingsMutation]);
|
|
17781
|
+
const errorMsg = (() => {
|
|
17782
|
+
if (statusQuery.error) return statusQuery.error.message;
|
|
17783
|
+
if (setEnabledMutation.error) return setEnabledMutation.error.message;
|
|
17784
|
+
if (setSettingsMutation.error) return setSettingsMutation.error.message;
|
|
17785
|
+
return null;
|
|
17786
|
+
})();
|
|
17787
|
+
return {
|
|
17788
|
+
status: statusQuery.data ?? null,
|
|
17789
|
+
isLoading: statusQuery.isLoading,
|
|
17790
|
+
isPending: setEnabledMutation.isPending || setSettingsMutation.isPending,
|
|
17791
|
+
error: errorMsg,
|
|
17792
|
+
setEnabled,
|
|
17793
|
+
setSettings
|
|
17794
|
+
};
|
|
17795
|
+
}
|
|
17796
|
+
//#endregion
|
|
17797
|
+
//#region src/composites/cap-settings/AutotrackSection.tsx
|
|
17798
|
+
/**
|
|
17799
|
+
* Autotrack settings card — rendered inside the PTZ panel when the
|
|
17800
|
+
* device has `DeviceFeature.PtzAutotrack`. Three knobs (cross-vendor
|
|
17801
|
+
* schema): target type, stop delay, disappear delay, plus an enable /
|
|
17802
|
+
* disable toggle that drives the `ptz-autotrack` cap directly.
|
|
17803
|
+
*
|
|
17804
|
+
* Save semantics:
|
|
17805
|
+
* - Toggle (enable/disable) calls `setEnabled` immediately.
|
|
17806
|
+
* - Form fields debounce-save on blur — no explicit Save button.
|
|
17807
|
+
*
|
|
17808
|
+
* Vendor capability hints:
|
|
17809
|
+
* - The cap's status carries `currentSettings` from the camera's
|
|
17810
|
+
* own GET; rendered on every poll so the operator sees what's
|
|
17811
|
+
* really applied vs what they typed.
|
|
17812
|
+
* - When the firmware ignored a setting, the field stays editable.
|
|
17813
|
+
*/
|
|
17814
|
+
function AutotrackSection({ deviceId }) {
|
|
17815
|
+
const { status, isLoading, isPending, error, setEnabled, setSettings } = useDeviceAutotrack(Number.isFinite(deviceId) ? deviceId : null);
|
|
17816
|
+
const [draft, setDraft] = (0, react$1.useState)({
|
|
17817
|
+
targetType: "",
|
|
17818
|
+
stopDelaySeconds: 30,
|
|
17819
|
+
disappearDelaySeconds: 15
|
|
17820
|
+
});
|
|
17821
|
+
const [editing, setEditing] = (0, react$1.useState)(false);
|
|
17822
|
+
(0, react$1.useEffect)(() => {
|
|
17823
|
+
if (editing) return;
|
|
17824
|
+
const s = status?.currentSettings;
|
|
17825
|
+
if (!s) return;
|
|
17826
|
+
setDraft({
|
|
17827
|
+
targetType: s.targetType,
|
|
17828
|
+
stopDelaySeconds: s.stopDelaySeconds,
|
|
17829
|
+
disappearDelaySeconds: s.disappearDelaySeconds
|
|
17830
|
+
});
|
|
17831
|
+
}, [status, editing]);
|
|
17832
|
+
if (isLoading && !status) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17833
|
+
className: "flex items-center gap-2 px-3 py-2 text-[10.5px] text-foreground-subtle",
|
|
17834
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoaderCircle, { className: "h-3 w-3 animate-spin" }), "Loading autotrack…"]
|
|
17835
|
+
});
|
|
17836
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17837
|
+
className: "border-t border-border/40 mt-2 pt-2 px-1 space-y-2",
|
|
17838
|
+
children: [
|
|
17839
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17840
|
+
className: "flex items-center justify-between px-2",
|
|
17841
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h3", {
|
|
17842
|
+
className: "flex items-center gap-1.5 text-[11px] font-semibold text-foreground-subtle uppercase tracking-wider",
|
|
17843
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Crosshair, { className: "h-3 w-3 text-primary" }), "Autotrack"]
|
|
17844
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
17845
|
+
type: "button",
|
|
17846
|
+
onClick: () => {
|
|
17847
|
+
setEnabled(!(status?.enabled ?? false));
|
|
17848
|
+
},
|
|
17849
|
+
disabled: isPending,
|
|
17850
|
+
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`,
|
|
17851
|
+
"aria-pressed": status?.enabled ?? false,
|
|
17852
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: `h-1.5 w-1.5 rounded-full ${status?.enabled ? "bg-success" : "bg-foreground-subtle/40"}` }), status?.enabled ? "Enabled" : "Disabled"]
|
|
17853
|
+
})]
|
|
17854
|
+
}),
|
|
17855
|
+
error && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17856
|
+
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",
|
|
17857
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CircleAlert, { className: "h-3 w-3 flex-shrink-0 mt-0.5" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
17858
|
+
className: "leading-snug break-words",
|
|
17859
|
+
children: error
|
|
17860
|
+
})]
|
|
17861
|
+
}),
|
|
17862
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17863
|
+
className: "grid grid-cols-2 gap-2 px-2",
|
|
17864
|
+
children: [
|
|
17865
|
+
(status?.supportedTargetTypes?.length ?? 0) > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
17866
|
+
className: "col-span-2 flex flex-col gap-1",
|
|
17867
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
17868
|
+
className: "text-[10px] font-medium text-foreground-subtle",
|
|
17869
|
+
children: "Target type"
|
|
17870
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
17871
|
+
value: draft.targetType,
|
|
17872
|
+
onFocus: () => setEditing(true),
|
|
17873
|
+
onChange: (e) => setDraft((d) => ({
|
|
17874
|
+
...d,
|
|
17875
|
+
targetType: e.target.value
|
|
17876
|
+
})),
|
|
17877
|
+
onBlur: () => {
|
|
17878
|
+
setEditing(false);
|
|
17879
|
+
const current = status?.currentSettings?.targetType ?? "";
|
|
17880
|
+
if (draft.targetType !== current) setSettings({ targetType: draft.targetType });
|
|
17881
|
+
},
|
|
17882
|
+
disabled: isPending,
|
|
17883
|
+
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",
|
|
17884
|
+
children: (status?.supportedTargetTypes ?? []).map((opt) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
17885
|
+
value: opt.value,
|
|
17886
|
+
children: opt.label
|
|
17887
|
+
}, opt.value))
|
|
17888
|
+
})]
|
|
17889
|
+
}),
|
|
17890
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
17891
|
+
className: "flex flex-col gap-1",
|
|
17892
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
17893
|
+
className: "text-[10px] font-medium text-foreground-subtle",
|
|
17894
|
+
children: "Stop delay (s)"
|
|
17895
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
17896
|
+
type: "number",
|
|
17897
|
+
min: 0,
|
|
17898
|
+
max: 300,
|
|
17899
|
+
value: draft.stopDelaySeconds,
|
|
17900
|
+
onFocus: () => setEditing(true),
|
|
17901
|
+
onChange: (e) => setDraft((d) => ({
|
|
17902
|
+
...d,
|
|
17903
|
+
stopDelaySeconds: Number(e.target.value) || 0
|
|
17904
|
+
})),
|
|
17905
|
+
onBlur: () => {
|
|
17906
|
+
setEditing(false);
|
|
17907
|
+
const current = status?.currentSettings?.stopDelaySeconds ?? 30;
|
|
17908
|
+
if (draft.stopDelaySeconds !== current) setSettings({ stopDelaySeconds: draft.stopDelaySeconds });
|
|
17909
|
+
},
|
|
17910
|
+
disabled: isPending,
|
|
17911
|
+
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"
|
|
17912
|
+
})]
|
|
17913
|
+
}),
|
|
17914
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
17915
|
+
className: "flex flex-col gap-1",
|
|
17916
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
17917
|
+
className: "text-[10px] font-medium text-foreground-subtle",
|
|
17918
|
+
children: "Disappear delay (s)"
|
|
17919
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
17920
|
+
type: "number",
|
|
17921
|
+
min: 0,
|
|
17922
|
+
max: 300,
|
|
17923
|
+
value: draft.disappearDelaySeconds,
|
|
17924
|
+
onFocus: () => setEditing(true),
|
|
17925
|
+
onChange: (e) => setDraft((d) => ({
|
|
17926
|
+
...d,
|
|
17927
|
+
disappearDelaySeconds: Number(e.target.value) || 0
|
|
17928
|
+
})),
|
|
17929
|
+
onBlur: () => {
|
|
17930
|
+
setEditing(false);
|
|
17931
|
+
const current = status?.currentSettings?.disappearDelaySeconds ?? 15;
|
|
17932
|
+
if (draft.disappearDelaySeconds !== current) setSettings({ disappearDelaySeconds: draft.disappearDelaySeconds });
|
|
17933
|
+
},
|
|
17934
|
+
disabled: isPending,
|
|
17935
|
+
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"
|
|
17936
|
+
})]
|
|
17937
|
+
})
|
|
17938
|
+
]
|
|
17939
|
+
}),
|
|
17940
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
17941
|
+
className: "px-2 text-[9.5px] text-foreground-subtle leading-snug italic",
|
|
17942
|
+
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)."
|
|
17943
|
+
})
|
|
17944
|
+
]
|
|
17945
|
+
});
|
|
17946
|
+
}
|
|
17947
|
+
//#endregion
|
|
17948
|
+
//#region src/contexts/player-overlays.tsx
|
|
17949
|
+
/**
|
|
17950
|
+
* Player overlay registry — pluggable layers + toolbar buttons for
|
|
17951
|
+
* the device-detail live-frame `StreamPanel`.
|
|
17952
|
+
*
|
|
17953
|
+
* Why a registry: previously `StreamPanelContent` hardcoded each
|
|
17954
|
+
* overlay (PTZ, intercom toggle, zone editor) and threaded the
|
|
17955
|
+
* editing state by hand. Adding a new feature (audio waveform,
|
|
17956
|
+
* detection bbox tracker, recording controls, …) meant another
|
|
17957
|
+
* round of plumbing through the host file and StreamPanel's prop
|
|
17958
|
+
* surface. The registry lets a sibling component (Detection tab,
|
|
17959
|
+
* Live Stats tab, an addon-page extension) declare its own layer
|
|
17960
|
+
* imperatively via a hook, and the host renders whatever's
|
|
17961
|
+
* registered.
|
|
17962
|
+
*
|
|
17963
|
+
* Two registries live side-by-side:
|
|
17964
|
+
*
|
|
17965
|
+
* 1. **layers** — absolute-positioned React nodes drawn over the
|
|
17966
|
+
* video frame (zones polygon canvas, motion bbox overlay,
|
|
17967
|
+
* audio waveform). Rendered ordered by `order` (lower first
|
|
17968
|
+
* → bottom; higher → top); the last-registered layer wins
|
|
17969
|
+
* ties.
|
|
17970
|
+
*
|
|
17971
|
+
* 2. **toolbar buttons** — controls surfaced in the player's
|
|
17972
|
+
* always-visible toolbar cluster (next to Intercom + Play/Stop).
|
|
17973
|
+
* Each carries an icon, label, controlled `active` flag, and
|
|
17974
|
+
* a click handler. The host (StreamPanel) renders them
|
|
17975
|
+
* uniformly; tone variants stay simple (`'default' | 'primary'`).
|
|
17976
|
+
*
|
|
17977
|
+
* Lifecycle: hooks register their layer / button on mount and
|
|
17978
|
+
* unregister on unmount, so swapping tabs (e.g. leaving Detection)
|
|
17979
|
+
* automatically tears down the registration. Re-registering with
|
|
17980
|
+
* the same `id` replaces the prior entry — meaning a single hook
|
|
17981
|
+
* call can update its overlay/button props on every render without
|
|
17982
|
+
* leaking entries.
|
|
17983
|
+
*
|
|
17984
|
+
* Provider scope: typically wraps the whole device-detail subtree
|
|
17985
|
+
* so the StreamPanel + every tab share the same registry. One
|
|
17986
|
+
* provider per `deviceId`; switching device IDs unmounts the
|
|
17987
|
+
* provider naturally.
|
|
17988
|
+
*/
|
|
17989
|
+
var PlayerOverlaysStateContext = createSharedContext("camstack:player-overlays-state", null);
|
|
17990
|
+
var PlayerOverlaysActionsContext = createSharedContext("camstack:player-overlays-actions", null);
|
|
17991
|
+
function PlayerOverlaysProvider({ children }) {
|
|
17992
|
+
const [layers, setLayers] = (0, react$1.useState)(() => /* @__PURE__ */ new Map());
|
|
17993
|
+
const [buttons, setButtons] = (0, react$1.useState)(() => /* @__PURE__ */ new Map());
|
|
17994
|
+
const setLayer = (0, react$1.useCallback)((layer) => {
|
|
17995
|
+
setLayers((prev) => {
|
|
17996
|
+
const next = new Map(prev);
|
|
17997
|
+
next.set(layer.id, layer);
|
|
17998
|
+
return next;
|
|
17999
|
+
});
|
|
18000
|
+
}, []);
|
|
18001
|
+
const removeLayer = (0, react$1.useCallback)((id) => {
|
|
18002
|
+
setLayers((prev) => {
|
|
18003
|
+
if (!prev.has(id)) return prev;
|
|
18004
|
+
const next = new Map(prev);
|
|
18005
|
+
next.delete(id);
|
|
18006
|
+
return next;
|
|
18007
|
+
});
|
|
18008
|
+
}, []);
|
|
18009
|
+
const setButton = (0, react$1.useCallback)((button) => {
|
|
18010
|
+
setButtons((prev) => {
|
|
18011
|
+
const next = new Map(prev);
|
|
18012
|
+
next.set(button.id, button);
|
|
18013
|
+
return next;
|
|
18014
|
+
});
|
|
18015
|
+
}, []);
|
|
18016
|
+
const removeButton = (0, react$1.useCallback)((id) => {
|
|
18017
|
+
setButtons((prev) => {
|
|
18018
|
+
if (!prev.has(id)) return prev;
|
|
18019
|
+
const next = new Map(prev);
|
|
18020
|
+
next.delete(id);
|
|
18021
|
+
return next;
|
|
18022
|
+
});
|
|
18023
|
+
}, []);
|
|
18024
|
+
const stateValue = (0, react$1.useMemo)(() => ({
|
|
18025
|
+
layers,
|
|
18026
|
+
buttons
|
|
18027
|
+
}), [layers, buttons]);
|
|
18028
|
+
const actionsValue = (0, react$1.useMemo)(() => ({
|
|
18029
|
+
setLayer,
|
|
18030
|
+
removeLayer,
|
|
18031
|
+
setButton,
|
|
18032
|
+
removeButton
|
|
18033
|
+
}), [
|
|
18034
|
+
setLayer,
|
|
18035
|
+
removeLayer,
|
|
18036
|
+
setButton,
|
|
18037
|
+
removeButton
|
|
18038
|
+
]);
|
|
18039
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlayerOverlaysStateContext.Provider, {
|
|
18040
|
+
value: stateValue,
|
|
18041
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlayerOverlaysActionsContext.Provider, {
|
|
18042
|
+
value: actionsValue,
|
|
18043
|
+
children
|
|
18044
|
+
})
|
|
18045
|
+
});
|
|
18046
|
+
}
|
|
18047
|
+
/** Snapshot of registered layers ordered by `order` (asc, ties resolved
|
|
18048
|
+
* by insertion order of the underlying Map). Returns `[]` outside a
|
|
18049
|
+
* provider. */
|
|
18050
|
+
function usePlayerOverlayLayers() {
|
|
18051
|
+
const state = (0, react$1.useContext)(PlayerOverlaysStateContext);
|
|
18052
|
+
return (0, react$1.useMemo)(() => {
|
|
18053
|
+
if (!state) return [];
|
|
18054
|
+
return [...state.layers.values()].sort((a, b) => a.order - b.order);
|
|
18055
|
+
}, [state]);
|
|
18056
|
+
}
|
|
18057
|
+
/** Snapshot of registered toolbar buttons, ordered by `order` (asc). */
|
|
18058
|
+
function usePlayerToolbarButtons() {
|
|
18059
|
+
const state = (0, react$1.useContext)(PlayerOverlaysStateContext);
|
|
18060
|
+
return (0, react$1.useMemo)(() => {
|
|
18061
|
+
if (!state) return [];
|
|
18062
|
+
return [...state.buttons.values()].sort((a, b) => a.order - b.order);
|
|
18063
|
+
}, [state]);
|
|
18064
|
+
}
|
|
18065
|
+
/**
|
|
18066
|
+
* Register an overlay layer for the lifetime of the calling component.
|
|
18067
|
+
* Re-registers on every render with the latest spec; auto-unregisters
|
|
18068
|
+
* on unmount. Pass `null` to skip registration when the layer is
|
|
18069
|
+
* conditionally enabled (the hook still runs every render — keeps
|
|
18070
|
+
* react-hook order stable across spec === null toggles).
|
|
18071
|
+
*
|
|
18072
|
+
* Callers that build the spec inline should memoise it (`useMemo`)
|
|
18073
|
+
* to avoid re-registering on every parent render — context-write
|
|
18074
|
+
* effects depend on referential equality of the spec object.
|
|
18075
|
+
*/
|
|
18076
|
+
function usePlayerOverlayLayer(spec) {
|
|
18077
|
+
const actions = (0, react$1.useContext)(PlayerOverlaysActionsContext);
|
|
18078
|
+
(0, react$1.useEffect)(() => {
|
|
18079
|
+
if (!actions || !spec) return void 0;
|
|
18080
|
+
actions.setLayer(spec);
|
|
18081
|
+
return () => actions.removeLayer(spec.id);
|
|
18082
|
+
}, [actions, spec]);
|
|
18083
|
+
}
|
|
18084
|
+
/** Same shape as `usePlayerOverlayLayer`, scoped to toolbar buttons. */
|
|
18085
|
+
function usePlayerToolbarButton(spec) {
|
|
18086
|
+
const actions = (0, react$1.useContext)(PlayerOverlaysActionsContext);
|
|
18087
|
+
(0, react$1.useEffect)(() => {
|
|
18088
|
+
if (!actions || !spec) return void 0;
|
|
18089
|
+
actions.setButton(spec);
|
|
18090
|
+
return () => actions.removeButton(spec.id);
|
|
18091
|
+
}, [actions, spec]);
|
|
18092
|
+
}
|
|
18093
|
+
//#endregion
|
|
18094
|
+
//#region src/composites/cap-settings/MotionGridCanvas.tsx
|
|
18095
|
+
/**
|
|
18096
|
+
* MotionGridCanvas — the on-frame motion-zone grid editor.
|
|
18097
|
+
*
|
|
18098
|
+
* This component is *only* the editable lattice painted over the live
|
|
18099
|
+
* frame: a plain CSS-grid of `gridWidth × gridHeight` cells. It carries
|
|
18100
|
+
* NO controls — enabled / sensitivity / save / quick-actions all live
|
|
18101
|
+
* in the management bar inside the settings section (`MotionZonesTab`),
|
|
18102
|
+
* so the live frame stays unobstructed.
|
|
18103
|
+
*
|
|
18104
|
+
* Interaction:
|
|
18105
|
+
* - Click a cell → toggle it.
|
|
18106
|
+
* - Press + drag → paint every touched cell to whatever the
|
|
18107
|
+
* first-touched cell flipped TO (the standard grid-mask gesture).
|
|
18108
|
+
*
|
|
18109
|
+
* Purely controlled — every change is forwarded through `onCellsChange`;
|
|
18110
|
+
* the owning `MotionZonesTab` keeps the single source of truth.
|
|
18111
|
+
*/
|
|
18112
|
+
function MotionGridCanvas({ options, cells, onCellsChange }) {
|
|
18113
|
+
const { gridWidth, gridHeight } = options;
|
|
18114
|
+
const total = gridWidth * gridHeight;
|
|
18115
|
+
const paintingRef = (0, react$1.useRef)(false);
|
|
18116
|
+
const paintValueRef = (0, react$1.useRef)(true);
|
|
18117
|
+
const paintedRef = (0, react$1.useRef)(/* @__PURE__ */ new Set());
|
|
18118
|
+
const [, forceTick] = (0, react$1.useState)(0);
|
|
18119
|
+
const applyCell = (0, react$1.useCallback)((index, value) => {
|
|
18120
|
+
if (index < 0 || index >= total) return;
|
|
18121
|
+
if (cells[index] === value) return;
|
|
18122
|
+
const next = [...cells];
|
|
18123
|
+
while (next.length < total) next.push(false);
|
|
18124
|
+
next.length = total;
|
|
18125
|
+
next[index] = value;
|
|
18126
|
+
onCellsChange(next);
|
|
18127
|
+
}, [
|
|
18128
|
+
cells,
|
|
18129
|
+
total,
|
|
18130
|
+
onCellsChange
|
|
18131
|
+
]);
|
|
18132
|
+
const handlePointerDown = (0, react$1.useCallback)((index) => (e) => {
|
|
18133
|
+
e.preventDefault();
|
|
18134
|
+
const nextValue = !cells[index];
|
|
18135
|
+
paintingRef.current = true;
|
|
18136
|
+
paintValueRef.current = nextValue;
|
|
18137
|
+
paintedRef.current = new Set([index]);
|
|
18138
|
+
applyCell(index, nextValue);
|
|
18139
|
+
forceTick((t) => t + 1);
|
|
18140
|
+
}, [cells, applyCell]);
|
|
18141
|
+
const handlePointerEnter = (0, react$1.useCallback)((index) => () => {
|
|
18142
|
+
if (!paintingRef.current) return;
|
|
18143
|
+
if (paintedRef.current.has(index)) return;
|
|
18144
|
+
paintedRef.current.add(index);
|
|
18145
|
+
applyCell(index, paintValueRef.current);
|
|
18146
|
+
}, [applyCell]);
|
|
18147
|
+
const endPaint = (0, react$1.useCallback)(() => {
|
|
18148
|
+
if (!paintingRef.current) return;
|
|
18149
|
+
paintingRef.current = false;
|
|
18150
|
+
paintedRef.current = /* @__PURE__ */ new Set();
|
|
18151
|
+
}, []);
|
|
18152
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
18153
|
+
className: "absolute inset-0 select-none grid",
|
|
18154
|
+
style: {
|
|
18155
|
+
gridTemplateColumns: `repeat(${String(gridWidth)}, 1fr)`,
|
|
18156
|
+
gridTemplateRows: `repeat(${String(gridHeight)}, 1fr)`
|
|
18157
|
+
},
|
|
18158
|
+
onPointerUp: endPaint,
|
|
18159
|
+
onPointerLeave: endPaint,
|
|
18160
|
+
children: Array.from({ length: total }, (_, i) => {
|
|
18161
|
+
const active = cells[i] === true;
|
|
18162
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
18163
|
+
type: "button",
|
|
18164
|
+
"aria-label": `motion cell ${String(i)}`,
|
|
18165
|
+
"aria-pressed": active,
|
|
18166
|
+
onPointerDown: handlePointerDown(i),
|
|
18167
|
+
onPointerEnter: handlePointerEnter(i),
|
|
18168
|
+
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"
|
|
18169
|
+
}, i);
|
|
18170
|
+
})
|
|
18171
|
+
});
|
|
18172
|
+
}
|
|
18173
|
+
//#endregion
|
|
18174
|
+
//#region src/composites/cap-settings/MotionZonesSettings.tsx
|
|
18175
|
+
/**
|
|
18176
|
+
* MotionZonesSettings — management surface for the on-camera
|
|
18177
|
+
* `motion-zones` capability. A cap-settings component (ui-library),
|
|
18178
|
+
* mounted into the device-detail Config tab via the cap-UI
|
|
18179
|
+
* contribution mechanism (the `motion-zones` cap declares a `ui` block).
|
|
18180
|
+
*
|
|
18181
|
+
* Split of concerns (mirrors the analytics zones drawer):
|
|
18182
|
+
* - The editable GRID is painted over the live frame by
|
|
18183
|
+
* `MotionGridCanvas`, registered as a player-overlay layer only
|
|
18184
|
+
* while the operator has toggled "Edit grid" on (OFF by default).
|
|
18185
|
+
* - Every CONTROL lives here, in the settings section, NOT on the
|
|
18186
|
+
* frame: a quick-action bar (All on / All off / Invert), Save /
|
|
18187
|
+
* Revert, and the cell-count status.
|
|
18188
|
+
* - Detection enable + sensitivity are NOT edited here — they are
|
|
18189
|
+
* the camera's on-board motion master switch, owned by the
|
|
18190
|
+
* driver's "On-camera motion" settings section. The grid editor
|
|
18191
|
+
* only paints the mask (`setZone` with a `cells`-only patch;
|
|
18192
|
+
* enable / sensitivity are left untouched camera-side).
|
|
18193
|
+
*
|
|
18194
|
+
* The grid editor draws nothing on cameras without the `motion-zones`
|
|
18195
|
+
* cap — the fetch self-gates on a failed `getOptions`.
|
|
18196
|
+
*/
|
|
18197
|
+
/** "Camera doesn't expose the cap" — swallow so the editor stays
|
|
18198
|
+
* hidden on unsupported devices (mirrors `usePTZ`'s gate). */
|
|
18199
|
+
function isAbsentProvider(err) {
|
|
18200
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
18201
|
+
return msg.includes("provider not available") || msg.includes("motion-zones");
|
|
18202
|
+
}
|
|
18203
|
+
/** Coerce an arbitrary `cells` array to exactly `gridWidth*gridHeight`. */
|
|
18204
|
+
function normaliseCells(cells, options) {
|
|
18205
|
+
const total = options.gridWidth * options.gridHeight;
|
|
18206
|
+
const next = new Array(total);
|
|
18207
|
+
for (let i = 0; i < total; i += 1) next[i] = cells[i] === true;
|
|
18208
|
+
return next;
|
|
18209
|
+
}
|
|
18210
|
+
function cellsEqual(a, b) {
|
|
18211
|
+
if (a.length !== b.length) return false;
|
|
18212
|
+
for (let i = 0; i < a.length; i += 1) if (a[i] !== b[i]) return false;
|
|
18213
|
+
return true;
|
|
18214
|
+
}
|
|
18215
|
+
function MotionZonesSettings({ deviceId }) {
|
|
18216
|
+
const dev = useDeviceProxy(useSystem().trpcClient, deviceId);
|
|
18217
|
+
const [options, setOptions] = (0, react$1.useState)(null);
|
|
18218
|
+
const [unsupported, setUnsupported] = (0, react$1.useState)(false);
|
|
18219
|
+
const [cells, setCells] = (0, react$1.useState)(null);
|
|
18220
|
+
const [committed, setCommitted] = (0, react$1.useState)(null);
|
|
18221
|
+
const [saving, setSaving] = (0, react$1.useState)(false);
|
|
18222
|
+
const [editingGrid, setEditingGrid] = (0, react$1.useState)(false);
|
|
18223
|
+
const seededRef = (0, react$1.useRef)(false);
|
|
18224
|
+
(0, react$1.useEffect)(() => {
|
|
18225
|
+
if (!dev) return void 0;
|
|
18226
|
+
let cancelled = false;
|
|
18227
|
+
(async () => {
|
|
18228
|
+
try {
|
|
18229
|
+
const opts = await dev.motionZones?.getOptions({});
|
|
18230
|
+
if (cancelled || !opts) return;
|
|
18231
|
+
setOptions(opts);
|
|
18232
|
+
if (seededRef.current) return;
|
|
18233
|
+
const status = await dev.motionZones?.getStatus({});
|
|
18234
|
+
if (cancelled || !status) return;
|
|
18235
|
+
seededRef.current = true;
|
|
18236
|
+
const norm = normaliseCells(status.cells, opts);
|
|
18237
|
+
setCommitted(norm);
|
|
18238
|
+
setCells(norm);
|
|
18239
|
+
} catch (err) {
|
|
18240
|
+
if (cancelled) return;
|
|
18241
|
+
if (isAbsentProvider(err)) setUnsupported(true);
|
|
18242
|
+
else console.error("motion-zones load failed", err);
|
|
18243
|
+
}
|
|
18244
|
+
})();
|
|
18245
|
+
return () => {
|
|
18246
|
+
cancelled = true;
|
|
18247
|
+
};
|
|
18248
|
+
}, [dev]);
|
|
18249
|
+
const dirty = (0, react$1.useMemo)(() => cells !== null && committed !== null && !cellsEqual(cells, committed), [cells, committed]);
|
|
18250
|
+
const activeCount = (0, react$1.useMemo)(() => cells ? cells.reduce((n, c) => c ? n + 1 : n, 0) : 0, [cells]);
|
|
18251
|
+
const total = options ? options.gridWidth * options.gridHeight : 0;
|
|
18252
|
+
const setAll = (0, react$1.useCallback)((value) => {
|
|
18253
|
+
if (total > 0) setCells(new Array(total).fill(value));
|
|
18254
|
+
}, [total]);
|
|
18255
|
+
const invert = (0, react$1.useCallback)(() => {
|
|
18256
|
+
setCells((prev) => prev ? prev.map((c) => !c) : prev);
|
|
18257
|
+
}, []);
|
|
18258
|
+
const revert = (0, react$1.useCallback)(() => {
|
|
18259
|
+
if (committed) setCells([...committed]);
|
|
18260
|
+
}, [committed]);
|
|
18261
|
+
const save = (0, react$1.useCallback)(async () => {
|
|
18262
|
+
if (!cells || !dev?.motionZones || !options) return;
|
|
18263
|
+
setSaving(true);
|
|
18264
|
+
try {
|
|
18265
|
+
await dev.motionZones.setZone({ patch: { cells: [...cells] } });
|
|
18266
|
+
const fresh = await dev.motionZones.getStatus({});
|
|
18267
|
+
if (fresh) {
|
|
18268
|
+
const norm = normaliseCells(fresh.cells, options);
|
|
18269
|
+
setCommitted(norm);
|
|
18270
|
+
setCells(norm);
|
|
18271
|
+
}
|
|
18272
|
+
} catch (err) {
|
|
18273
|
+
console.error("motion-zones.setZone failed", err);
|
|
18274
|
+
} finally {
|
|
18275
|
+
setSaving(false);
|
|
18276
|
+
}
|
|
18277
|
+
}, [
|
|
18278
|
+
cells,
|
|
18279
|
+
dev,
|
|
18280
|
+
options
|
|
18281
|
+
]);
|
|
18282
|
+
usePlayerOverlayLayer((0, react$1.useMemo)(() => editingGrid && !unsupported && options && cells ? {
|
|
18283
|
+
id: "motion-zones",
|
|
18284
|
+
order: 110,
|
|
18285
|
+
node: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MotionGridCanvas, {
|
|
18286
|
+
options,
|
|
18287
|
+
cells,
|
|
18288
|
+
onCellsChange: setCells
|
|
18289
|
+
})
|
|
18290
|
+
} : null, [
|
|
18291
|
+
editingGrid,
|
|
18292
|
+
unsupported,
|
|
18293
|
+
options,
|
|
18294
|
+
cells
|
|
18295
|
+
]));
|
|
18296
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
18297
|
+
className: "flex flex-col gap-3",
|
|
18298
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
18299
|
+
className: "text-[11px] font-semibold text-foreground-subtle uppercase tracking-wider",
|
|
18300
|
+
children: "Motion Zones"
|
|
18301
|
+
}), unsupported ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
18302
|
+
className: `${TEXT_HINT} leading-relaxed`,
|
|
18303
|
+
children: "This camera doesn't expose an on-board motion-detection grid."
|
|
18304
|
+
}) : !(!unsupported && options !== null && cells !== null) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
18305
|
+
className: `${TEXT_HINT} leading-relaxed`,
|
|
18306
|
+
children: "Loading the camera's motion grid…"
|
|
18307
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
18308
|
+
className: `${TEXT_HINT} leading-relaxed`,
|
|
18309
|
+
children: [
|
|
18310
|
+
"Toggle",
|
|
18311
|
+
" ",
|
|
18312
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
|
|
18313
|
+
className: "text-foreground",
|
|
18314
|
+
children: "Edit grid"
|
|
18315
|
+
}),
|
|
18316
|
+
" to paint the region the camera watches for motion directly on the live frame, then ",
|
|
18317
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
|
|
18318
|
+
className: "text-foreground",
|
|
18319
|
+
children: "Save"
|
|
18320
|
+
}),
|
|
18321
|
+
" to push the mask to the camera. Detection on/off and sensitivity are set in the",
|
|
18322
|
+
" ",
|
|
18323
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
|
|
18324
|
+
className: "text-foreground",
|
|
18325
|
+
children: "On-camera motion"
|
|
18326
|
+
}),
|
|
18327
|
+
" section."
|
|
18328
|
+
]
|
|
18329
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
18330
|
+
className: "flex items-center gap-2 flex-wrap",
|
|
18331
|
+
children: [
|
|
18332
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
18333
|
+
type: "button",
|
|
18334
|
+
onClick: () => setEditingGrid((v) => !v),
|
|
18335
|
+
disabled: saving,
|
|
18336
|
+
"aria-pressed": editingGrid,
|
|
18337
|
+
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",
|
|
18338
|
+
children: editingGrid ? "Done editing" : "Edit grid"
|
|
18339
|
+
}),
|
|
18340
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
18341
|
+
type: "button",
|
|
18342
|
+
onClick: () => setAll(true),
|
|
18343
|
+
disabled: saving,
|
|
18344
|
+
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",
|
|
18345
|
+
children: "All on"
|
|
18346
|
+
}),
|
|
18347
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
18348
|
+
type: "button",
|
|
18349
|
+
onClick: () => setAll(false),
|
|
18350
|
+
disabled: saving,
|
|
18351
|
+
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",
|
|
18352
|
+
children: "All off"
|
|
18353
|
+
}),
|
|
18354
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
18355
|
+
type: "button",
|
|
18356
|
+
onClick: invert,
|
|
18357
|
+
disabled: saving,
|
|
18358
|
+
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",
|
|
18359
|
+
children: "Invert"
|
|
18360
|
+
}),
|
|
18361
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
18362
|
+
className: `${TEXT_HINT} ml-1 tabular-nums`,
|
|
18363
|
+
children: [
|
|
18364
|
+
activeCount,
|
|
18365
|
+
" / ",
|
|
18366
|
+
total,
|
|
18367
|
+
" cells · ",
|
|
18368
|
+
options.gridWidth,
|
|
18369
|
+
"×",
|
|
18370
|
+
options.gridHeight
|
|
18371
|
+
]
|
|
18372
|
+
}),
|
|
18373
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "flex-1" }),
|
|
18374
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
18375
|
+
type: "button",
|
|
18376
|
+
onClick: revert,
|
|
18377
|
+
disabled: saving || !dirty,
|
|
18378
|
+
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",
|
|
18379
|
+
children: "Revert"
|
|
18380
|
+
}),
|
|
18381
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
18382
|
+
type: "button",
|
|
18383
|
+
onClick: () => void save(),
|
|
18384
|
+
disabled: saving || !dirty,
|
|
18385
|
+
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",
|
|
18386
|
+
children: saving ? "Saving…" : "Save"
|
|
18387
|
+
})
|
|
18388
|
+
]
|
|
18389
|
+
})] })]
|
|
18390
|
+
});
|
|
18391
|
+
}
|
|
18392
|
+
//#endregion
|
|
18393
|
+
//#region src/widgets/host-widgets.ts
|
|
18394
|
+
/** Host widgets — ui-library React components embeddable as a
|
|
18395
|
+
* `type:'widget'` ConfigField. `WidgetSlot` resolves these BEFORE the
|
|
18396
|
+
* Module-Federation `WidgetRegistry`. ids are namespaced `host/<name>`. */
|
|
18397
|
+
var HOST_WIDGETS = {
|
|
18398
|
+
"host/motion-zones-grid": MotionZonesSettings,
|
|
18399
|
+
"host/ptz-panel": PtzPanel,
|
|
18400
|
+
"host/ptz-autotrack": AutotrackSection
|
|
18401
|
+
};
|
|
18402
|
+
//#endregion
|
|
18403
|
+
//#region src/composites/widget-slot.tsx
|
|
18404
|
+
/**
|
|
18405
|
+
* <WidgetSlot> — single host-side mount point for any addon-contributed
|
|
18406
|
+
* widget. Consumers reference a widget by its public id
|
|
18407
|
+
* (`<addonId>/<stableId>`), the slot looks the widget's metadata up in
|
|
18408
|
+
* the shared `WidgetRegistry`, resolves the component through the
|
|
18409
|
+
* unified `useRemoteComponent` Module-Federation loader (the SAME path
|
|
18410
|
+
* `ContributionRenderer`'s `kind:'remote'` branch uses), validates host
|
|
18411
|
+
* context against the widget's `requires` metadata, and renders one of:
|
|
18412
|
+
* - skeleton placeholder while the bundle is still loading,
|
|
18413
|
+
* - inline error fallback when the widget id is unknown OR the host
|
|
18414
|
+
* didn't supply a required context (`deviceContext` /
|
|
18415
|
+
* `integrationContext`),
|
|
18416
|
+
* - the resolved component otherwise.
|
|
18417
|
+
*
|
|
18418
|
+
* The slot is intentionally STUPID — no styling beyond the skeleton/
|
|
18419
|
+
* error fallback. Layout (card vs inline, sizing) is the host's
|
|
18420
|
+
* responsibility.
|
|
18421
|
+
*/
|
|
18422
|
+
function WidgetSlot(props) {
|
|
18423
|
+
const { widgetId, deviceId } = props;
|
|
18424
|
+
const HostComponent = HOST_WIDGETS[widgetId];
|
|
18425
|
+
if (HostComponent !== void 0) {
|
|
18426
|
+
if (deviceId === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetMissingError, {
|
|
18427
|
+
widgetId,
|
|
18428
|
+
reason: "missing-device-context"
|
|
18429
|
+
});
|
|
18430
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HostComponent, { deviceId });
|
|
18431
|
+
}
|
|
18432
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RemoteWidgetSlot, { ...props });
|
|
18433
|
+
}
|
|
18434
|
+
/**
|
|
18435
|
+
* Module-Federation remote-widget path — looks the widget's metadata up
|
|
18436
|
+
* in the shared `WidgetRegistry` and resolves it through `useRemoteComponent`.
|
|
18437
|
+
*/
|
|
18438
|
+
function RemoteWidgetSlot(props) {
|
|
18439
|
+
const { widgetId } = props;
|
|
18440
|
+
const metadata = useWidgetMetadata(widgetId);
|
|
18441
|
+
if (metadata === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetMissingError, {
|
|
18442
|
+
widgetId,
|
|
18443
|
+
reason: "unknown"
|
|
18444
|
+
});
|
|
18445
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResolvedWidgetSlot, {
|
|
18446
|
+
...props,
|
|
18447
|
+
metadata
|
|
18448
|
+
});
|
|
18449
|
+
}
|
|
18450
|
+
/**
|
|
18451
|
+
* Inner host — resolves the widget component through the unified
|
|
18452
|
+
* `useRemoteComponent` MF loader (shared with `ContributionRenderer`'s
|
|
18453
|
+
* `kind:'remote'` branch). Mounted only once a `metadata` descriptor is
|
|
18454
|
+
* known, so the hook always receives a valid `remote` descriptor.
|
|
18455
|
+
*/
|
|
18456
|
+
function ResolvedWidgetSlot(props) {
|
|
18457
|
+
const { widgetId, host = "device-tab", config, deviceId, integrationId, instanceId, size, columns, rows, metadata } = props;
|
|
18458
|
+
const Component = useRemoteComponent(metadata.remote, metadata.bundleUrl);
|
|
18459
|
+
const resolvedInstanceId = (0, react$1.useMemo)(() => instanceId ?? widgetId, [instanceId, widgetId]);
|
|
18460
|
+
if (Component === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetSkeleton, {});
|
|
18461
|
+
if (Component === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetMissingError, {
|
|
18462
|
+
widgetId,
|
|
18463
|
+
reason: "missing-export"
|
|
18464
|
+
});
|
|
18465
|
+
if (metadata.requires.deviceContext && deviceId === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetMissingError, {
|
|
18466
|
+
widgetId,
|
|
18467
|
+
reason: "missing-device-context"
|
|
18468
|
+
});
|
|
18469
|
+
if (metadata.requires.integrationContext && integrationId === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetMissingError, {
|
|
18470
|
+
widgetId,
|
|
18471
|
+
reason: "missing-integration-context"
|
|
18472
|
+
});
|
|
18473
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Component, {
|
|
18474
|
+
instanceId: resolvedInstanceId,
|
|
18475
|
+
host,
|
|
18476
|
+
config,
|
|
18477
|
+
deviceId,
|
|
18478
|
+
integrationId,
|
|
18479
|
+
size,
|
|
18480
|
+
columns,
|
|
18481
|
+
rows
|
|
18482
|
+
});
|
|
18483
|
+
}
|
|
18484
|
+
function WidgetSkeleton() {
|
|
18485
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
18486
|
+
className: "rounded-lg border border-border bg-surface/40 p-4 animate-pulse",
|
|
18487
|
+
children: [
|
|
18488
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "h-3 w-24 bg-foreground-subtle/20 rounded mb-2" }),
|
|
18489
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "h-2 w-full bg-foreground-subtle/10 rounded mb-1" }),
|
|
18490
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "h-2 w-3/4 bg-foreground-subtle/10 rounded" })
|
|
18491
|
+
]
|
|
18492
|
+
});
|
|
18493
|
+
}
|
|
18494
|
+
function WidgetMissingError({ widgetId, reason }) {
|
|
18495
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
18496
|
+
className: "rounded-lg border border-warning/30 bg-warning/10 px-3 py-2 text-xs text-warning",
|
|
18497
|
+
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`
|
|
18498
|
+
});
|
|
18499
|
+
}
|
|
18500
|
+
//#endregion
|
|
18501
|
+
//#region src/composites/config-form-field.tsx
|
|
18502
|
+
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";
|
|
18503
|
+
var LABEL_CLASS = "block text-[11px] font-medium text-foreground mb-1";
|
|
18504
|
+
var DESC_CLASS = "text-[10px] text-foreground-subtle mt-0.5";
|
|
18505
|
+
function FieldWrapper({ label, description, required, span, children, translationFn }) {
|
|
18506
|
+
const colSpanClass = span === 2 ? "col-span-2" : span === 3 ? "col-span-3" : span === 4 ? "col-span-4" : "col-span-1";
|
|
18507
|
+
const resolvedLabel = resolveLabel(label, translationFn);
|
|
18508
|
+
const resolvedDescription = resolveLabel(description, translationFn);
|
|
18509
|
+
const hasLabel = resolvedLabel !== void 0 && resolvedLabel !== "";
|
|
18510
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
18511
|
+
className: `${colSpanClass} min-w-0`,
|
|
18512
|
+
children: [
|
|
18513
|
+
hasLabel && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
18514
|
+
className: LABEL_CLASS,
|
|
18515
|
+
children: [resolvedLabel, required && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
18516
|
+
className: "text-danger ml-0.5",
|
|
18517
|
+
children: "*"
|
|
18518
|
+
})]
|
|
18519
|
+
}),
|
|
18520
|
+
children,
|
|
18521
|
+
resolvedDescription && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
18522
|
+
className: DESC_CLASS,
|
|
18523
|
+
children: resolvedDescription
|
|
18524
|
+
})
|
|
18525
|
+
]
|
|
18526
|
+
});
|
|
18527
|
+
}
|
|
18528
|
+
function TextField({ field, value, onChange, disabled, translationFn }) {
|
|
18529
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldWrapper, {
|
|
18530
|
+
label: field.label,
|
|
18531
|
+
description: field.description,
|
|
18532
|
+
required: field.required,
|
|
18533
|
+
span: field.span,
|
|
18534
|
+
translationFn,
|
|
18535
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
18536
|
+
type: field.inputType ?? "text",
|
|
18537
|
+
className: INPUT_CLASS,
|
|
18538
|
+
value: value === void 0 || value === null ? "" : String(value),
|
|
18539
|
+
placeholder: field.placeholder,
|
|
18540
|
+
maxLength: field.maxLength,
|
|
18541
|
+
pattern: field.pattern,
|
|
18542
|
+
disabled: disabled || field.disabled,
|
|
18543
|
+
onChange: (e) => onChange(e.target.value)
|
|
18544
|
+
})
|
|
18545
|
+
});
|
|
18546
|
+
}
|
|
18547
|
+
function NumberField({ field, value, onChange, disabled, translationFn }) {
|
|
18548
|
+
const [local, setLocal] = (0, react$1.useState)(value === void 0 || value === null ? "" : String(value));
|
|
18549
|
+
const focusedRef = (0, react$1.useRef)(false);
|
|
18550
|
+
(0, react$1.useEffect)(() => {
|
|
18551
|
+
if (focusedRef.current) return;
|
|
18552
|
+
setLocal(value === void 0 || value === null ? "" : String(value));
|
|
18553
|
+
}, [value]);
|
|
18554
|
+
const handleChange = (raw) => {
|
|
18555
|
+
setLocal(raw);
|
|
18556
|
+
if (raw === "" || raw === "-") {
|
|
18557
|
+
onChange(void 0);
|
|
18558
|
+
return;
|
|
18559
|
+
}
|
|
18560
|
+
const parsed = Number(raw);
|
|
18561
|
+
if (Number.isNaN(parsed)) return;
|
|
18562
|
+
onChange(parsed);
|
|
18563
|
+
};
|
|
18564
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldWrapper, {
|
|
18565
|
+
label: field.label,
|
|
18566
|
+
description: field.description,
|
|
18567
|
+
required: field.required,
|
|
18568
|
+
span: field.span,
|
|
18569
|
+
translationFn,
|
|
18570
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
18571
|
+
className: "flex items-center gap-1",
|
|
18572
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
18573
|
+
type: "number",
|
|
18574
|
+
className: INPUT_CLASS,
|
|
18575
|
+
value: local,
|
|
18576
|
+
placeholder: field.placeholder,
|
|
18577
|
+
min: field.min,
|
|
18578
|
+
max: field.max,
|
|
18579
|
+
step: field.step,
|
|
18580
|
+
disabled: disabled || field.disabled,
|
|
18581
|
+
onFocus: () => {
|
|
18582
|
+
focusedRef.current = true;
|
|
18583
|
+
},
|
|
18584
|
+
onBlur: () => {
|
|
18585
|
+
focusedRef.current = false;
|
|
18586
|
+
},
|
|
18587
|
+
onChange: (e) => handleChange(e.target.value)
|
|
18588
|
+
}), field.unit && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
18589
|
+
className: "text-xs text-foreground-subtle whitespace-nowrap",
|
|
17181
18590
|
children: field.unit
|
|
17182
18591
|
})]
|
|
17183
18592
|
})
|
|
@@ -18286,18 +19695,12 @@ function AddonActionButtonField({ field, values, disabled, onAction }) {
|
|
|
18286
19695
|
}
|
|
18287
19696
|
function WidgetField({ field }) {
|
|
18288
19697
|
const deviceId = useDeviceId();
|
|
18289
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
18290
|
-
|
|
18291
|
-
|
|
18292
|
-
|
|
18293
|
-
|
|
18294
|
-
|
|
18295
|
-
widgetId: field.widgetId,
|
|
18296
|
-
host: "device-tab",
|
|
18297
|
-
config: field.widgetConfig,
|
|
18298
|
-
deviceId: deviceId ?? void 0,
|
|
18299
|
-
instanceId: field.key
|
|
18300
|
-
})
|
|
19698
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetSlot, {
|
|
19699
|
+
widgetId: field.widgetId,
|
|
19700
|
+
host: "device-tab",
|
|
19701
|
+
config: field.widgetConfig,
|
|
19702
|
+
deviceId: deviceId ?? void 0,
|
|
19703
|
+
instanceId: field.key
|
|
18301
19704
|
});
|
|
18302
19705
|
}
|
|
18303
19706
|
function formatReadonlyValue(value, unit) {
|
|
@@ -18788,7 +20191,7 @@ function ConfigFormBuilder({ schema, values, onChange, disabled, translationFn,
|
|
|
18788
20191
|
* + patch loop. `onAfterChange` fires once the patch is applied so
|
|
18789
20192
|
* consumers can re-read any derived state.
|
|
18790
20193
|
*/
|
|
18791
|
-
var Chevron = ({ open }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
20194
|
+
var Chevron$1 = ({ open }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
18792
20195
|
className: `h-3 w-3 transition-transform ${open ? "rotate-90" : ""}`,
|
|
18793
20196
|
viewBox: "0 0 24 24",
|
|
18794
20197
|
fill: "none",
|
|
@@ -18953,7 +20356,7 @@ function AddonGlobalSettingsForm({ trpc, addonId, nodeId, title, disabled, onAft
|
|
|
18953
20356
|
className: "w-full px-4 py-2 flex items-center gap-2 hover:bg-surface-hover text-left",
|
|
18954
20357
|
"aria-expanded": open,
|
|
18955
20358
|
children: [
|
|
18956
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Chevron, { open }),
|
|
20359
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Chevron$1, { open }),
|
|
18957
20360
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
18958
20361
|
className: "text-xs font-semibold text-foreground uppercase tracking-wide flex-1",
|
|
18959
20362
|
children: title ?? addonId
|
|
@@ -19524,183 +20927,37 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
|
|
|
19524
20927
|
className: "h-3.5 w-3.5",
|
|
19525
20928
|
viewBox: "0 0 24 24",
|
|
19526
20929
|
fill: "none",
|
|
19527
|
-
stroke: "currentColor",
|
|
19528
|
-
strokeWidth: "2",
|
|
19529
|
-
strokeLinecap: "round",
|
|
19530
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.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" })
|
|
19531
|
-
})
|
|
19532
|
-
})]
|
|
19533
|
-
})]
|
|
19534
|
-
})
|
|
19535
|
-
]
|
|
19536
|
-
});
|
|
19537
|
-
}
|
|
19538
|
-
function ToolbarButton$1({ onClick, title, children }) {
|
|
19539
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
19540
|
-
onClick,
|
|
19541
|
-
title,
|
|
19542
|
-
className: "rounded-full p-1.5 text-white/80 hover:text-white hover:bg-white/20 transition-colors",
|
|
19543
|
-
children
|
|
19544
|
-
});
|
|
19545
|
-
}
|
|
19546
|
-
async function waitForIceGathering(pc) {
|
|
19547
|
-
if (pc.iceGatheringState === "complete") return;
|
|
19548
|
-
return new Promise((resolve) => {
|
|
19549
|
-
const handler = () => {
|
|
19550
|
-
if (pc.iceGatheringState === "complete") {
|
|
19551
|
-
pc.removeEventListener("icegatheringstatechange", handler);
|
|
19552
|
-
resolve();
|
|
19553
|
-
}
|
|
19554
|
-
};
|
|
19555
|
-
pc.addEventListener("icegatheringstatechange", handler);
|
|
19556
|
-
setTimeout(resolve, 5e3);
|
|
19557
|
-
});
|
|
19558
|
-
}
|
|
19559
|
-
//#endregion
|
|
19560
|
-
//#region src/contexts/player-overlays.tsx
|
|
19561
|
-
/**
|
|
19562
|
-
* Player overlay registry — pluggable layers + toolbar buttons for
|
|
19563
|
-
* the device-detail live-frame `StreamPanel`.
|
|
19564
|
-
*
|
|
19565
|
-
* Why a registry: previously `StreamPanelContent` hardcoded each
|
|
19566
|
-
* overlay (PTZ, intercom toggle, zone editor) and threaded the
|
|
19567
|
-
* editing state by hand. Adding a new feature (audio waveform,
|
|
19568
|
-
* detection bbox tracker, recording controls, …) meant another
|
|
19569
|
-
* round of plumbing through the host file and StreamPanel's prop
|
|
19570
|
-
* surface. The registry lets a sibling component (Detection tab,
|
|
19571
|
-
* Live Stats tab, an addon-page extension) declare its own layer
|
|
19572
|
-
* imperatively via a hook, and the host renders whatever's
|
|
19573
|
-
* registered.
|
|
19574
|
-
*
|
|
19575
|
-
* Two registries live side-by-side:
|
|
19576
|
-
*
|
|
19577
|
-
* 1. **layers** — absolute-positioned React nodes drawn over the
|
|
19578
|
-
* video frame (zones polygon canvas, motion bbox overlay,
|
|
19579
|
-
* audio waveform). Rendered ordered by `order` (lower first
|
|
19580
|
-
* → bottom; higher → top); the last-registered layer wins
|
|
19581
|
-
* ties.
|
|
19582
|
-
*
|
|
19583
|
-
* 2. **toolbar buttons** — controls surfaced in the player's
|
|
19584
|
-
* always-visible toolbar cluster (next to Intercom + Play/Stop).
|
|
19585
|
-
* Each carries an icon, label, controlled `active` flag, and
|
|
19586
|
-
* a click handler. The host (StreamPanel) renders them
|
|
19587
|
-
* uniformly; tone variants stay simple (`'default' | 'primary'`).
|
|
19588
|
-
*
|
|
19589
|
-
* Lifecycle: hooks register their layer / button on mount and
|
|
19590
|
-
* unregister on unmount, so swapping tabs (e.g. leaving Detection)
|
|
19591
|
-
* automatically tears down the registration. Re-registering with
|
|
19592
|
-
* the same `id` replaces the prior entry — meaning a single hook
|
|
19593
|
-
* call can update its overlay/button props on every render without
|
|
19594
|
-
* leaking entries.
|
|
19595
|
-
*
|
|
19596
|
-
* Provider scope: typically wraps the whole device-detail subtree
|
|
19597
|
-
* so the StreamPanel + every tab share the same registry. One
|
|
19598
|
-
* provider per `deviceId`; switching device IDs unmounts the
|
|
19599
|
-
* provider naturally.
|
|
19600
|
-
*/
|
|
19601
|
-
var PlayerOverlaysStateContext = createSharedContext("camstack:player-overlays-state", null);
|
|
19602
|
-
var PlayerOverlaysActionsContext = createSharedContext("camstack:player-overlays-actions", null);
|
|
19603
|
-
function PlayerOverlaysProvider({ children }) {
|
|
19604
|
-
const [layers, setLayers] = (0, react$1.useState)(() => /* @__PURE__ */ new Map());
|
|
19605
|
-
const [buttons, setButtons] = (0, react$1.useState)(() => /* @__PURE__ */ new Map());
|
|
19606
|
-
const setLayer = (0, react$1.useCallback)((layer) => {
|
|
19607
|
-
setLayers((prev) => {
|
|
19608
|
-
const next = new Map(prev);
|
|
19609
|
-
next.set(layer.id, layer);
|
|
19610
|
-
return next;
|
|
19611
|
-
});
|
|
19612
|
-
}, []);
|
|
19613
|
-
const removeLayer = (0, react$1.useCallback)((id) => {
|
|
19614
|
-
setLayers((prev) => {
|
|
19615
|
-
if (!prev.has(id)) return prev;
|
|
19616
|
-
const next = new Map(prev);
|
|
19617
|
-
next.delete(id);
|
|
19618
|
-
return next;
|
|
19619
|
-
});
|
|
19620
|
-
}, []);
|
|
19621
|
-
const setButton = (0, react$1.useCallback)((button) => {
|
|
19622
|
-
setButtons((prev) => {
|
|
19623
|
-
const next = new Map(prev);
|
|
19624
|
-
next.set(button.id, button);
|
|
19625
|
-
return next;
|
|
19626
|
-
});
|
|
19627
|
-
}, []);
|
|
19628
|
-
const removeButton = (0, react$1.useCallback)((id) => {
|
|
19629
|
-
setButtons((prev) => {
|
|
19630
|
-
if (!prev.has(id)) return prev;
|
|
19631
|
-
const next = new Map(prev);
|
|
19632
|
-
next.delete(id);
|
|
19633
|
-
return next;
|
|
19634
|
-
});
|
|
19635
|
-
}, []);
|
|
19636
|
-
const stateValue = (0, react$1.useMemo)(() => ({
|
|
19637
|
-
layers,
|
|
19638
|
-
buttons
|
|
19639
|
-
}), [layers, buttons]);
|
|
19640
|
-
const actionsValue = (0, react$1.useMemo)(() => ({
|
|
19641
|
-
setLayer,
|
|
19642
|
-
removeLayer,
|
|
19643
|
-
setButton,
|
|
19644
|
-
removeButton
|
|
19645
|
-
}), [
|
|
19646
|
-
setLayer,
|
|
19647
|
-
removeLayer,
|
|
19648
|
-
setButton,
|
|
19649
|
-
removeButton
|
|
19650
|
-
]);
|
|
19651
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlayerOverlaysStateContext.Provider, {
|
|
19652
|
-
value: stateValue,
|
|
19653
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlayerOverlaysActionsContext.Provider, {
|
|
19654
|
-
value: actionsValue,
|
|
19655
|
-
children
|
|
19656
|
-
})
|
|
20930
|
+
stroke: "currentColor",
|
|
20931
|
+
strokeWidth: "2",
|
|
20932
|
+
strokeLinecap: "round",
|
|
20933
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.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" })
|
|
20934
|
+
})
|
|
20935
|
+
})]
|
|
20936
|
+
})]
|
|
20937
|
+
})
|
|
20938
|
+
]
|
|
19657
20939
|
});
|
|
19658
20940
|
}
|
|
19659
|
-
|
|
19660
|
-
|
|
19661
|
-
|
|
19662
|
-
|
|
19663
|
-
|
|
19664
|
-
|
|
19665
|
-
|
|
19666
|
-
return [...state.layers.values()].sort((a, b) => a.order - b.order);
|
|
19667
|
-
}, [state]);
|
|
19668
|
-
}
|
|
19669
|
-
/** Snapshot of registered toolbar buttons, ordered by `order` (asc). */
|
|
19670
|
-
function usePlayerToolbarButtons() {
|
|
19671
|
-
const state = (0, react$1.useContext)(PlayerOverlaysStateContext);
|
|
19672
|
-
return (0, react$1.useMemo)(() => {
|
|
19673
|
-
if (!state) return [];
|
|
19674
|
-
return [...state.buttons.values()].sort((a, b) => a.order - b.order);
|
|
19675
|
-
}, [state]);
|
|
19676
|
-
}
|
|
19677
|
-
/**
|
|
19678
|
-
* Register an overlay layer for the lifetime of the calling component.
|
|
19679
|
-
* Re-registers on every render with the latest spec; auto-unregisters
|
|
19680
|
-
* on unmount. Pass `null` to skip registration when the layer is
|
|
19681
|
-
* conditionally enabled (the hook still runs every render — keeps
|
|
19682
|
-
* react-hook order stable across spec === null toggles).
|
|
19683
|
-
*
|
|
19684
|
-
* Callers that build the spec inline should memoise it (`useMemo`)
|
|
19685
|
-
* to avoid re-registering on every parent render — context-write
|
|
19686
|
-
* effects depend on referential equality of the spec object.
|
|
19687
|
-
*/
|
|
19688
|
-
function usePlayerOverlayLayer(spec) {
|
|
19689
|
-
const actions = (0, react$1.useContext)(PlayerOverlaysActionsContext);
|
|
19690
|
-
(0, react$1.useEffect)(() => {
|
|
19691
|
-
if (!actions || !spec) return void 0;
|
|
19692
|
-
actions.setLayer(spec);
|
|
19693
|
-
return () => actions.removeLayer(spec.id);
|
|
19694
|
-
}, [actions, spec]);
|
|
20941
|
+
function ToolbarButton$1({ onClick, title, children }) {
|
|
20942
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
20943
|
+
onClick,
|
|
20944
|
+
title,
|
|
20945
|
+
className: "rounded-full p-1.5 text-white/80 hover:text-white hover:bg-white/20 transition-colors",
|
|
20946
|
+
children
|
|
20947
|
+
});
|
|
19695
20948
|
}
|
|
19696
|
-
|
|
19697
|
-
|
|
19698
|
-
|
|
19699
|
-
|
|
19700
|
-
|
|
19701
|
-
|
|
19702
|
-
|
|
19703
|
-
|
|
20949
|
+
async function waitForIceGathering(pc) {
|
|
20950
|
+
if (pc.iceGatheringState === "complete") return;
|
|
20951
|
+
return new Promise((resolve) => {
|
|
20952
|
+
const handler = () => {
|
|
20953
|
+
if (pc.iceGatheringState === "complete") {
|
|
20954
|
+
pc.removeEventListener("icegatheringstatechange", handler);
|
|
20955
|
+
resolve();
|
|
20956
|
+
}
|
|
20957
|
+
};
|
|
20958
|
+
pc.addEventListener("icegatheringstatechange", handler);
|
|
20959
|
+
setTimeout(resolve, 5e3);
|
|
20960
|
+
});
|
|
19704
20961
|
}
|
|
19705
20962
|
//#endregion
|
|
19706
20963
|
//#region src/composites/stream-panel.tsx
|
|
@@ -21508,12 +22765,11 @@ function CopyButton({ value, label, className, disabled }) {
|
|
|
21508
22765
|
* Alexa / HAP / MQTT specifics here. Mount it next to an export addon's
|
|
21509
22766
|
* standard settings form (e.g. in the addon-settings modal).
|
|
21510
22767
|
*
|
|
21511
|
-
* Routing note: `device-export` is a collection cap
|
|
21512
|
-
*
|
|
21513
|
-
*
|
|
21514
|
-
* multiple device-export addons are
|
|
21515
|
-
*
|
|
21516
|
-
* separate follow-up) — identical behaviour to the pages it replaces.
|
|
22768
|
+
* Routing note: `device-export` is a collection cap. Export addons are
|
|
22769
|
+
* all `hub-only`, so the panel queries `nodeId: 'hub'`. It also passes
|
|
22770
|
+
* `addonId` so the codegen'd cap-router resolves THIS addon's provider
|
|
22771
|
+
* within the local collection — when multiple device-export addons are
|
|
22772
|
+
* co-installed each panel shows its own provider, not provider[0].
|
|
21517
22773
|
*/
|
|
21518
22774
|
var ExposedDeviceArraySchema = zod.z.array(_camstack_types.ExposedDeviceSchema);
|
|
21519
22775
|
var STATUS_POLL_INTERVAL_MS = 1e4;
|
|
@@ -21604,11 +22860,17 @@ function SetupSection({ setup }) {
|
|
|
21604
22860
|
*/
|
|
21605
22861
|
function DeviceExportPanel({ addonId, onOpenDevice }) {
|
|
21606
22862
|
const queryClient = (0, _tanstack_react_query.useQueryClient)();
|
|
21607
|
-
const statusQuery = useDeviceExportGetStatus({
|
|
22863
|
+
const statusQuery = useDeviceExportGetStatus({
|
|
22864
|
+
nodeId: "hub",
|
|
22865
|
+
addonId
|
|
22866
|
+
}, {
|
|
21608
22867
|
refetchInterval: STATUS_POLL_INTERVAL_MS,
|
|
21609
22868
|
retry: false
|
|
21610
22869
|
});
|
|
21611
|
-
const exposedQuery = useDeviceExportListExposedDevices({
|
|
22870
|
+
const exposedQuery = useDeviceExportListExposedDevices({
|
|
22871
|
+
nodeId: "hub",
|
|
22872
|
+
addonId
|
|
22873
|
+
}, {
|
|
21612
22874
|
refetchInterval: STATUS_POLL_INTERVAL_MS,
|
|
21613
22875
|
retry: false
|
|
21614
22876
|
});
|
|
@@ -21680,13 +22942,18 @@ function DeviceExportPanel({ addonId, onOpenDevice }) {
|
|
|
21680
22942
|
disabled: unexposeMutation.isPending,
|
|
21681
22943
|
onClick: () => unexposeMutation.mutate({
|
|
21682
22944
|
deviceId: row.deviceId,
|
|
21683
|
-
nodeId: "hub"
|
|
22945
|
+
nodeId: "hub",
|
|
22946
|
+
addonId
|
|
21684
22947
|
}),
|
|
21685
22948
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Unlink2, { className: "h-3.5 w-3.5 mr-1" }), "Unexpose"]
|
|
21686
22949
|
})]
|
|
21687
22950
|
})
|
|
21688
22951
|
}
|
|
21689
|
-
], [
|
|
22952
|
+
], [
|
|
22953
|
+
onOpenDevice,
|
|
22954
|
+
unexposeMutation,
|
|
22955
|
+
addonId
|
|
22956
|
+
]);
|
|
21690
22957
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
21691
22958
|
className: "rounded-lg border border-border bg-surface overflow-hidden",
|
|
21692
22959
|
children: [
|
|
@@ -22176,7 +23443,7 @@ var LEVEL_OPTIONS = [
|
|
|
22176
23443
|
"error"
|
|
22177
23444
|
];
|
|
22178
23445
|
/** Max live entries kept in the ring buffer. */
|
|
22179
|
-
var MAX_LIVE_ENTRIES$
|
|
23446
|
+
var MAX_LIVE_ENTRIES$1 = 500;
|
|
22180
23447
|
function passesLevel(logLevel, filterLevel) {
|
|
22181
23448
|
if (!filterLevel) return true;
|
|
22182
23449
|
return (LEVEL_SEVERITY[logLevel] ?? 0) >= (LEVEL_SEVERITY[filterLevel] ?? 0);
|
|
@@ -22205,7 +23472,7 @@ function LogStream({ agentId: propsAgentId, addonId: propsAddonId, deviceId: pro
|
|
|
22205
23472
|
integrationId,
|
|
22206
23473
|
requestId
|
|
22207
23474
|
]);
|
|
22208
|
-
const fallbackBuffer = useLiveBuffer(MAX_LIVE_ENTRIES$
|
|
23475
|
+
const fallbackBuffer = useLiveBuffer(MAX_LIVE_ENTRIES$1);
|
|
22209
23476
|
const buffer = externalBuffer ?? fallbackBuffer;
|
|
22210
23477
|
const liveLogs = buffer.entries;
|
|
22211
23478
|
const [clearedAt, setClearedAt] = (0, react$1.useState)(0);
|
|
@@ -22532,7 +23799,7 @@ function LogStream({ agentId: propsAgentId, addonId: propsAddonId, deviceId: pro
|
|
|
22532
23799
|
}),
|
|
22533
23800
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
22534
23801
|
className: "px-1 py-1 align-top w-5",
|
|
22535
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RowCopyButton$
|
|
23802
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RowCopyButton$1, {
|
|
22536
23803
|
copied: copiedRowKey === key,
|
|
22537
23804
|
onCopy: () => copyOne(log, key)
|
|
22538
23805
|
})
|
|
@@ -22571,7 +23838,7 @@ function ScopeBadge$2({ label, value }) {
|
|
|
22571
23838
|
* "did it work" affordance — same UX language the toolbar Copy
|
|
22572
23839
|
* button already uses for its `Copied!` label.
|
|
22573
23840
|
*/
|
|
22574
|
-
function RowCopyButton$
|
|
23841
|
+
function RowCopyButton$1({ copied, onCopy }) {
|
|
22575
23842
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
22576
23843
|
type: "button",
|
|
22577
23844
|
title: "Copy this row",
|
|
@@ -22896,7 +24163,7 @@ function hasContent(entry) {
|
|
|
22896
24163
|
if (cat === _camstack_types.EventCategory.DetectionResult || cat === _camstack_types.EventCategory.PipelineAudioInferenceResult) return summarizeEvent(cat, entry.data) !== null;
|
|
22897
24164
|
return true;
|
|
22898
24165
|
}
|
|
22899
|
-
var MAX_LIVE_ENTRIES
|
|
24166
|
+
var MAX_LIVE_ENTRIES = 300;
|
|
22900
24167
|
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 }) {
|
|
22901
24168
|
const defaultsArray = (0, react$1.useMemo)(() => defaultCategories ?? legacyCategories ?? DEFAULT_EVENT_CATEGORIES, [defaultCategories, legacyCategories]);
|
|
22902
24169
|
const defaultsKey = (0, react$1.useMemo)(() => [...defaultsArray].sort().join(","), [defaultsArray]);
|
|
@@ -22908,7 +24175,7 @@ function EventStream({ agentId, addonId, deviceId, defaultCategories, categories
|
|
|
22908
24175
|
setActiveCategories(new Set(defaultsArray));
|
|
22909
24176
|
}
|
|
22910
24177
|
}, [defaultsKey, defaultsArray]);
|
|
22911
|
-
const fallbackBuffer = useLiveBuffer(MAX_LIVE_ENTRIES
|
|
24178
|
+
const fallbackBuffer = useLiveBuffer(MAX_LIVE_ENTRIES);
|
|
22912
24179
|
const buffer = externalBuffer ?? fallbackBuffer;
|
|
22913
24180
|
const liveEvents = buffer.entries;
|
|
22914
24181
|
const [autoScroll, setAutoScroll] = (0, react$1.useState)(true);
|
|
@@ -23232,7 +24499,7 @@ function EventStream({ agentId, addonId, deviceId, defaultCategories, categories
|
|
|
23232
24499
|
className: "text-foreground-subtle whitespace-nowrap w-[70px] shrink-0 pt-0.5",
|
|
23233
24500
|
children: new Date(event.timestamp).toLocaleTimeString()
|
|
23234
24501
|
}),
|
|
23235
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RowCopyButton
|
|
24502
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RowCopyButton, {
|
|
23236
24503
|
copied: copiedRowId === event.id,
|
|
23237
24504
|
onCopy: () => copyOne(event)
|
|
23238
24505
|
}),
|
|
@@ -23422,7 +24689,7 @@ function ScopeBadge$1({ label, value }) {
|
|
|
23422
24689
|
* click doesn't toggle the row's expand state. Flashes a Check
|
|
23423
24690
|
* for ~1s after copy.
|
|
23424
24691
|
*/
|
|
23425
|
-
function RowCopyButton
|
|
24692
|
+
function RowCopyButton({ copied, onCopy }) {
|
|
23426
24693
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
23427
24694
|
type: "button",
|
|
23428
24695
|
title: "Copy this event",
|
|
@@ -23689,162 +24956,137 @@ function StatusBadge$1({ status }) {
|
|
|
23689
24956
|
//#endregion
|
|
23690
24957
|
//#region src/composites/state-values-stream.tsx
|
|
23691
24958
|
/**
|
|
23692
|
-
* StateValuesStream —
|
|
24959
|
+
* StateValuesStream — live current-state tree for ui-library.
|
|
24960
|
+
*
|
|
24961
|
+
* Renders a single device's CURRENT runtime-state as a scrollable,
|
|
24962
|
+
* collapsible hierarchical tree (NOT a chronological change log).
|
|
23693
24963
|
*
|
|
23694
|
-
*
|
|
23695
|
-
*
|
|
23696
|
-
*
|
|
24964
|
+
* - Seed: on mount, `trpc.deviceState.getAllSnapshots` returns the
|
|
24965
|
+
* whole-system snapshot; we pick out this device's entry and build
|
|
24966
|
+
* a `Map<capName, slice>` of its current state.
|
|
24967
|
+
* - Realtime: an `EventCategory.DeviceStateChanged` tRPC subscription
|
|
24968
|
+
* scoped to the device patches each cap's slice into the map in
|
|
24969
|
+
* place — the matching tree node re-renders, no rows are appended.
|
|
24970
|
+
* - Tree: top level = cap names (sorted, expanded by default); each
|
|
24971
|
+
* cap node expands to its slice keys; nested objects/arrays expand
|
|
24972
|
+
* recursively, deep nesting collapsed by default. A per-cap
|
|
24973
|
+
* "updated Ns ago" indicator surfaces realtime activity.
|
|
24974
|
+
* - Filter/search: cap-name multiselect popover + free-text search,
|
|
24975
|
+
* both applied to the tree (filter which caps show; search matches
|
|
24976
|
+
* keys/values anywhere in the slice).
|
|
23697
24977
|
*
|
|
23698
|
-
*
|
|
24978
|
+
* The live subscription is mounted only while this component renders,
|
|
23699
24979
|
* so the parent can mount/unmount it on tab switches and the tRPC
|
|
23700
24980
|
* subscription is torn down automatically when hidden.
|
|
23701
24981
|
*/
|
|
23702
|
-
|
|
23703
|
-
|
|
23704
|
-
|
|
23705
|
-
if (e.category !== _camstack_types.EventCategory.DeviceStateChanged) return false;
|
|
23706
|
-
const data = e.data;
|
|
23707
|
-
if (!data || typeof data !== "object") return false;
|
|
23708
|
-
if (data.deviceId !== deviceId) return false;
|
|
23709
|
-
if (typeof data.capName !== "string") return false;
|
|
23710
|
-
if (!data.slice || typeof data.slice !== "object") return false;
|
|
23711
|
-
return true;
|
|
23712
|
-
}
|
|
23713
|
-
function toEntry(raw) {
|
|
24982
|
+
/** Narrow an untrusted live-stream event to a `DeviceStateChanged`
|
|
24983
|
+
* for THIS device, returning the cap name + slice it carries. */
|
|
24984
|
+
function readStateChange(raw, deviceId) {
|
|
23714
24985
|
if (!raw || typeof raw !== "object") return null;
|
|
23715
24986
|
const e = raw;
|
|
23716
|
-
|
|
23717
|
-
const timestamp = typeof e.timestamp === "string" ? e.timestamp : null;
|
|
24987
|
+
if (e.category !== _camstack_types.EventCategory.DeviceStateChanged) return null;
|
|
23718
24988
|
const data = e.data;
|
|
23719
|
-
if (!
|
|
23720
|
-
if (
|
|
24989
|
+
if (!data || typeof data !== "object") return null;
|
|
24990
|
+
if (data.deviceId !== deviceId) return null;
|
|
24991
|
+
if (typeof data.capName !== "string") return null;
|
|
24992
|
+
if (!data.slice || typeof data.slice !== "object") return null;
|
|
23721
24993
|
return {
|
|
23722
|
-
id,
|
|
23723
|
-
timestamp,
|
|
23724
24994
|
capName: data.capName,
|
|
23725
24995
|
slice: data.slice
|
|
23726
24996
|
};
|
|
23727
24997
|
}
|
|
23728
|
-
|
|
24998
|
+
function isSystemSnapshot(raw) {
|
|
24999
|
+
return !!raw && typeof raw === "object" && !Array.isArray(raw);
|
|
25000
|
+
}
|
|
23729
25001
|
/**
|
|
23730
25002
|
* Every device-scoped cap name, sorted alphabetically — same shape
|
|
23731
25003
|
* as `ALL_EVENT_CATEGORIES` in event-stream.tsx so the filter
|
|
23732
25004
|
* surface is symmetric across the three live-streams (Logs, Events,
|
|
23733
|
-
* State). Computed once at module load
|
|
23734
|
-
* full list rather than only the caps that have already emitted at
|
|
23735
|
-
* least one slice change in the current session.
|
|
25005
|
+
* State). Computed once at module load.
|
|
23736
25006
|
*/
|
|
23737
25007
|
var ALL_DEVICE_CAP_NAMES = Object.freeze([...new Set(_camstack_types.ALL_CAPABILITY_DEFINITIONS.filter((c) => c.scope === "device").map((c) => c.name))].sort());
|
|
23738
|
-
|
|
25008
|
+
/** Depth at which tree nodes start collapsed. Cap nodes (depth 0) and
|
|
25009
|
+
* their direct slice keys (depth 1) render expanded; anything nested
|
|
25010
|
+
* deeper opens on demand. */
|
|
25011
|
+
var DEFAULT_EXPAND_DEPTH = 1;
|
|
25012
|
+
function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", onClose, className }) {
|
|
23739
25013
|
const [activeCaps, setActiveCaps] = (0, react$1.useState)((0, react$1.useMemo)(() => new Set(defaultCaps ?? []), [defaultCaps]));
|
|
23740
25014
|
const [searchText, setSearchText] = (0, react$1.useState)("");
|
|
23741
|
-
const [autoScroll, setAutoScroll] = (0, react$1.useState)(true);
|
|
23742
|
-
const [expandedRows, setExpandedRows] = (0, react$1.useState)(/* @__PURE__ */ new Set());
|
|
23743
|
-
const [clearedAt, setClearedAt] = (0, react$1.useState)(0);
|
|
23744
25015
|
const [filterOpen, setFilterOpen] = (0, react$1.useState)(false);
|
|
23745
25016
|
const [filterSearch, setFilterSearch] = (0, react$1.useState)("");
|
|
23746
25017
|
const filterRootRef = (0, react$1.useRef)(null);
|
|
23747
|
-
const
|
|
23748
|
-
const
|
|
23749
|
-
const
|
|
23750
|
-
const scrollRef = (0, react$1.useRef)(null);
|
|
25018
|
+
const [capStates, setCapStates] = (0, react$1.useState)(/* @__PURE__ */ new Map());
|
|
25019
|
+
const [collapseOverrides, setCollapseOverrides] = (0, react$1.useState)(/* @__PURE__ */ new Map());
|
|
25020
|
+
const [, setNowTick] = (0, react$1.useState)(0);
|
|
23751
25021
|
const prevDeviceRef = (0, react$1.useRef)(deviceId);
|
|
23752
25022
|
(0, react$1.useEffect)(() => {
|
|
23753
25023
|
if (prevDeviceRef.current !== deviceId) {
|
|
23754
25024
|
prevDeviceRef.current = deviceId;
|
|
23755
|
-
|
|
23756
|
-
|
|
23757
|
-
setClearedAt(0);
|
|
25025
|
+
setCapStates(/* @__PURE__ */ new Map());
|
|
25026
|
+
setCollapseOverrides(/* @__PURE__ */ new Map());
|
|
23758
25027
|
}
|
|
23759
|
-
}, [deviceId
|
|
23760
|
-
const
|
|
23761
|
-
|
|
23762
|
-
|
|
23763
|
-
|
|
23764
|
-
|
|
23765
|
-
|
|
23766
|
-
|
|
25028
|
+
}, [deviceId]);
|
|
25029
|
+
const { data: snapshot, isLoading } = trpc.deviceState.getAllSnapshots.useQuery({}, { staleTime: 3e4 });
|
|
25030
|
+
const seededRef = (0, react$1.useRef)(false);
|
|
25031
|
+
(0, react$1.useEffect)(() => {
|
|
25032
|
+
if (seededRef.current) return;
|
|
25033
|
+
if (!isSystemSnapshot(snapshot)) return;
|
|
25034
|
+
const deviceSlices = snapshot[String(deviceId)];
|
|
25035
|
+
seededRef.current = true;
|
|
25036
|
+
if (!deviceSlices) return;
|
|
25037
|
+
const now = Date.now();
|
|
25038
|
+
setCapStates((prev) => {
|
|
25039
|
+
const next = new Map(prev);
|
|
25040
|
+
for (const [capName, slice] of Object.entries(deviceSlices)) {
|
|
25041
|
+
if (next.has(capName)) continue;
|
|
25042
|
+
next.set(capName, {
|
|
25043
|
+
slice,
|
|
25044
|
+
updatedAt: now
|
|
25045
|
+
});
|
|
25046
|
+
}
|
|
25047
|
+
return next;
|
|
25048
|
+
});
|
|
25049
|
+
}, [snapshot, deviceId]);
|
|
23767
25050
|
(0, react$1.useEffect)(() => {
|
|
23768
|
-
|
|
23769
|
-
}, [
|
|
25051
|
+
seededRef.current = false;
|
|
25052
|
+
}, [deviceId]);
|
|
23770
25053
|
trpc.systemEvents.subscribe.useSubscription({
|
|
23771
25054
|
deviceId,
|
|
23772
25055
|
category: _camstack_types.EventCategory.DeviceStateChanged
|
|
23773
25056
|
}, { onData: (raw) => {
|
|
23774
|
-
|
|
23775
|
-
|
|
23776
|
-
|
|
23777
|
-
|
|
23778
|
-
|
|
23779
|
-
|
|
25057
|
+
const change = readStateChange(raw, deviceId);
|
|
25058
|
+
if (!change) return;
|
|
25059
|
+
setCapStates((prev) => {
|
|
25060
|
+
const next = new Map(prev);
|
|
25061
|
+
next.set(change.capName, {
|
|
25062
|
+
slice: change.slice,
|
|
25063
|
+
updatedAt: Date.now()
|
|
25064
|
+
});
|
|
25065
|
+
return next;
|
|
25066
|
+
});
|
|
23780
25067
|
} });
|
|
23781
|
-
const prevLiveCountRef = (0, react$1.useRef)(liveEvents.length);
|
|
23782
25068
|
(0, react$1.useEffect)(() => {
|
|
23783
|
-
const
|
|
23784
|
-
|
|
23785
|
-
|
|
23786
|
-
|
|
23787
|
-
|
|
23788
|
-
const
|
|
23789
|
-
|
|
23790
|
-
|
|
23791
|
-
|
|
23792
|
-
else if (!autoScroll && el.scrollTop > 0) {
|
|
23793
|
-
const firstRow = el.firstElementChild?.firstElementChild;
|
|
23794
|
-
const rowHeight = firstRow instanceof HTMLElement ? firstRow.offsetHeight : 28;
|
|
23795
|
-
const newCount = liveEvents.length - prevCount;
|
|
23796
|
-
el.scrollTop += rowHeight * newCount;
|
|
23797
|
-
}
|
|
23798
|
-
}, [liveEvents.length, autoScroll]);
|
|
23799
|
-
const allEntries = (0, react$1.useMemo)(() => {
|
|
23800
|
-
const byId = /* @__PURE__ */ new Map();
|
|
23801
|
-
for (const e of initialEvents ?? []) {
|
|
23802
|
-
const entry = toEntry(e);
|
|
23803
|
-
if (entry) byId.set(entry.id, entry);
|
|
23804
|
-
}
|
|
23805
|
-
for (const e of liveEvents) byId.set(e.id, e);
|
|
23806
|
-
let list = [...byId.values()];
|
|
23807
|
-
if (clearedAt > 0) list = list.filter((e) => new Date(e.timestamp).getTime() > clearedAt);
|
|
23808
|
-
if (activeCaps.size > 0) list = list.filter((e) => activeCaps.has(e.capName));
|
|
25069
|
+
const id = setInterval(() => setNowTick((t) => t + 1), 5e3);
|
|
25070
|
+
return () => clearInterval(id);
|
|
25071
|
+
}, []);
|
|
25072
|
+
const visibleCaps = (0, react$1.useMemo)(() => {
|
|
25073
|
+
const caps = new Set(ALL_DEVICE_CAP_NAMES);
|
|
25074
|
+
for (const capName of capStates.keys()) caps.add(capName);
|
|
25075
|
+
return [...caps].sort();
|
|
25076
|
+
}, [capStates]);
|
|
25077
|
+
const filteredCapNames = (0, react$1.useMemo)(() => {
|
|
23809
25078
|
const needle = searchText.trim().toLowerCase();
|
|
23810
|
-
|
|
23811
|
-
if (
|
|
23812
|
-
|
|
23813
|
-
|
|
23814
|
-
|
|
23815
|
-
try {
|
|
23816
|
-
if (Object.entries(e.slice).map(([k, v]) => `${k}=${formatValue(v)}`).join(" · ").toLowerCase().includes(needle)) return true;
|
|
23817
|
-
} catch {}
|
|
23818
|
-
try {
|
|
23819
|
-
if (JSON.stringify(e.slice).toLowerCase().includes(needle)) return true;
|
|
23820
|
-
} catch {}
|
|
23821
|
-
return false;
|
|
25079
|
+
return [...capStates.keys()].sort().filter((capName) => {
|
|
25080
|
+
if (activeCaps.size > 0 && !activeCaps.has(capName)) return false;
|
|
25081
|
+
if (!needle) return true;
|
|
25082
|
+
if (capName.toLowerCase().includes(needle)) return true;
|
|
25083
|
+
return sliceMatchesSearch(capStates.get(capName)?.slice ?? {}, needle);
|
|
23822
25084
|
});
|
|
23823
|
-
return list.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
23824
25085
|
}, [
|
|
23825
|
-
|
|
23826
|
-
liveEvents,
|
|
25086
|
+
capStates,
|
|
23827
25087
|
activeCaps,
|
|
23828
|
-
searchText
|
|
23829
|
-
clearedAt
|
|
25088
|
+
searchText
|
|
23830
25089
|
]);
|
|
23831
|
-
const visibleCaps = (0, react$1.useMemo)(() => {
|
|
23832
|
-
const caps = new Set(ALL_DEVICE_CAP_NAMES);
|
|
23833
|
-
for (const e of initialEvents ?? []) {
|
|
23834
|
-
const entry = toEntry(e);
|
|
23835
|
-
if (entry) caps.add(entry.capName);
|
|
23836
|
-
}
|
|
23837
|
-
for (const e of liveEvents) caps.add(e.capName);
|
|
23838
|
-
return [...caps].sort();
|
|
23839
|
-
}, [initialEvents, liveEvents]);
|
|
23840
|
-
const toggleRow = (0, react$1.useCallback)((id) => {
|
|
23841
|
-
setExpandedRows((prev) => {
|
|
23842
|
-
const next = new Set(prev);
|
|
23843
|
-
if (next.has(id)) next.delete(id);
|
|
23844
|
-
else next.add(id);
|
|
23845
|
-
return next;
|
|
23846
|
-
});
|
|
23847
|
-
}, []);
|
|
23848
25090
|
const toggleCap = (0, react$1.useCallback)((cap) => {
|
|
23849
25091
|
setActiveCaps((prev) => {
|
|
23850
25092
|
const next = new Set(prev);
|
|
@@ -23857,34 +25099,36 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", limi
|
|
|
23857
25099
|
setActiveCaps(new Set(defaultCaps ?? []));
|
|
23858
25100
|
setSearchText("");
|
|
23859
25101
|
}, [defaultCaps]);
|
|
23860
|
-
const handleClear = (0, react$1.useCallback)(() => {
|
|
23861
|
-
setClearedAt(Date.now());
|
|
23862
|
-
buffer.reset();
|
|
23863
|
-
}, [buffer]);
|
|
23864
|
-
const [copiedAll, setCopiedAll] = (0, react$1.useState)(false);
|
|
23865
|
-
const [copiedRowId, setCopiedRowId] = (0, react$1.useState)(null);
|
|
23866
|
-
const formatEntryForCopy = (0, react$1.useCallback)((entry) => {
|
|
23867
|
-
return `${new Date(entry.timestamp).toISOString()} [${entry.capName}] slice=${JSON.stringify(entry.slice)}`;
|
|
23868
|
-
}, []);
|
|
23869
|
-
const handleCopyAll = (0, react$1.useCallback)(() => {
|
|
23870
|
-
const lines = allEntries.map(formatEntryForCopy);
|
|
23871
|
-
navigator.clipboard.writeText(lines.join("\n")).then(() => {
|
|
23872
|
-
setCopiedAll(true);
|
|
23873
|
-
setTimeout(() => setCopiedAll(false), 1500);
|
|
23874
|
-
});
|
|
23875
|
-
}, [allEntries, formatEntryForCopy]);
|
|
23876
|
-
const copyOne = (0, react$1.useCallback)((entry) => {
|
|
23877
|
-
navigator.clipboard.writeText(formatEntryForCopy(entry)).then(() => {
|
|
23878
|
-
setCopiedRowId(entry.id);
|
|
23879
|
-
setTimeout(() => setCopiedRowId((prev) => prev === entry.id ? null : prev), 1200);
|
|
23880
|
-
});
|
|
23881
|
-
}, [formatEntryForCopy]);
|
|
23882
25102
|
const handleSelectAllCaps = (0, react$1.useCallback)(() => {
|
|
23883
25103
|
setActiveCaps(new Set(visibleCaps));
|
|
23884
25104
|
}, [visibleCaps]);
|
|
23885
25105
|
const handleSelectNoCaps = (0, react$1.useCallback)(() => {
|
|
23886
25106
|
setActiveCaps(/* @__PURE__ */ new Set());
|
|
23887
25107
|
}, []);
|
|
25108
|
+
const toggleNode = (0, react$1.useCallback)((path, currentlyOpen) => {
|
|
25109
|
+
setCollapseOverrides((prev) => {
|
|
25110
|
+
const next = new Map(prev);
|
|
25111
|
+
next.set(path, !currentlyOpen);
|
|
25112
|
+
return next;
|
|
25113
|
+
});
|
|
25114
|
+
}, []);
|
|
25115
|
+
const isPathOpen = (0, react$1.useCallback)((path, depth) => {
|
|
25116
|
+
const override = collapseOverrides.get(path);
|
|
25117
|
+
if (override !== void 0) return override;
|
|
25118
|
+
return depth < DEFAULT_EXPAND_DEPTH;
|
|
25119
|
+
}, [collapseOverrides]);
|
|
25120
|
+
const [copiedAll, setCopiedAll] = (0, react$1.useState)(false);
|
|
25121
|
+
const handleCopyAll = (0, react$1.useCallback)(() => {
|
|
25122
|
+
const out = {};
|
|
25123
|
+
for (const capName of filteredCapNames) {
|
|
25124
|
+
const state = capStates.get(capName);
|
|
25125
|
+
if (state) out[capName] = state.slice;
|
|
25126
|
+
}
|
|
25127
|
+
navigator.clipboard.writeText(JSON.stringify(out, null, 2)).then(() => {
|
|
25128
|
+
setCopiedAll(true);
|
|
25129
|
+
setTimeout(() => setCopiedAll(false), 1500);
|
|
25130
|
+
});
|
|
25131
|
+
}, [filteredCapNames, capStates]);
|
|
23888
25132
|
const filteredPopoverCaps = (0, react$1.useMemo)(() => {
|
|
23889
25133
|
const needle = filterSearch.trim().toLowerCase();
|
|
23890
25134
|
if (!needle) return visibleCaps;
|
|
@@ -23903,6 +25147,7 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", limi
|
|
|
23903
25147
|
document.addEventListener("mousedown", onDocClick);
|
|
23904
25148
|
return () => document.removeEventListener("mousedown", onDocClick);
|
|
23905
25149
|
}, [filterOpen]);
|
|
25150
|
+
const searchNeedle = searchText.trim().toLowerCase();
|
|
23906
25151
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
23907
25152
|
className: cn("h-full min-h-0 flex flex-col", className),
|
|
23908
25153
|
onClick: (e) => e.stopPropagation(),
|
|
@@ -23919,18 +25164,6 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", limi
|
|
|
23919
25164
|
label: "device",
|
|
23920
25165
|
value: `#${deviceId}`
|
|
23921
25166
|
}),
|
|
23922
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
23923
|
-
type: "button",
|
|
23924
|
-
onClick: () => {
|
|
23925
|
-
setAutoScroll((a) => !a);
|
|
23926
|
-
if (!autoScroll && scrollRef.current) scrollRef.current.scrollTo({
|
|
23927
|
-
top: 0,
|
|
23928
|
-
behavior: "smooth"
|
|
23929
|
-
});
|
|
23930
|
-
},
|
|
23931
|
-
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"),
|
|
23932
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ArrowUpToLine, { className: "h-2.5 w-2.5" }), autoScroll ? "Auto" : "Paused"]
|
|
23933
|
-
}),
|
|
23934
25167
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
23935
25168
|
className: "relative",
|
|
23936
25169
|
ref: filterRootRef,
|
|
@@ -23938,7 +25171,7 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", limi
|
|
|
23938
25171
|
type: "button",
|
|
23939
25172
|
onClick: () => setFilterOpen((v) => !v),
|
|
23940
25173
|
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"),
|
|
23941
|
-
title: "Pick caps to
|
|
25174
|
+
title: "Pick caps to show",
|
|
23942
25175
|
children: [
|
|
23943
25176
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Funnel, { className: "h-2.5 w-2.5" }),
|
|
23944
25177
|
"Filter (",
|
|
@@ -23963,19 +25196,12 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", limi
|
|
|
23963
25196
|
title: "Reset filters",
|
|
23964
25197
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RotateCcw, { className: "h-2.5 w-2.5" }), "Reset"]
|
|
23965
25198
|
}),
|
|
23966
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
23967
|
-
type: "button",
|
|
23968
|
-
onClick: handleClear,
|
|
23969
|
-
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",
|
|
23970
|
-
title: "Clear visible entries",
|
|
23971
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Trash2, { className: "h-2.5 w-2.5" }), "Clear"]
|
|
23972
|
-
}),
|
|
23973
25199
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
23974
25200
|
type: "button",
|
|
23975
25201
|
onClick: handleCopyAll,
|
|
23976
|
-
disabled:
|
|
23977
|
-
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",
|
|
23978
|
-
title: "Copy
|
|
25202
|
+
disabled: filteredCapNames.length === 0,
|
|
25203
|
+
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"),
|
|
25204
|
+
title: "Copy visible state as JSON",
|
|
23979
25205
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Copy, { className: "h-2.5 w-2.5" }), copiedAll ? "Copied!" : "Copy"]
|
|
23980
25206
|
}),
|
|
23981
25207
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -23994,13 +25220,9 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", limi
|
|
|
23994
25220
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(X, { className: "h-2.5 w-2.5" })
|
|
23995
25221
|
})]
|
|
23996
25222
|
}),
|
|
23997
|
-
|
|
23998
|
-
className: "text-[9px] text-emerald-400 font-medium",
|
|
23999
|
-
children: [
|
|
24000
|
-
"+",
|
|
24001
|
-
liveEvents.length,
|
|
24002
|
-
" live"
|
|
24003
|
-
]
|
|
25223
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
25224
|
+
className: "text-[9px] text-emerald-400 font-medium inline-flex items-center gap-1",
|
|
25225
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "h-1.5 w-1.5 rounded-full bg-emerald-400 animate-pulse" }), "live"]
|
|
24004
25226
|
})
|
|
24005
25227
|
]
|
|
24006
25228
|
}), onClose && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
@@ -24010,90 +25232,172 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", limi
|
|
|
24010
25232
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(X, { className: "h-3.5 w-3.5" })
|
|
24011
25233
|
})]
|
|
24012
25234
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
24013
|
-
|
|
24014
|
-
className: cn("flex-1 min-h-0 overflow-auto text-[10px]", maxHeight),
|
|
25235
|
+
className: cn("flex-1 min-h-0 overflow-auto text-[10px] font-mono", maxHeight),
|
|
24015
25236
|
children: [
|
|
24016
|
-
isLoading && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
24017
|
-
className: "flex items-center justify-center gap-2 py-6 text-foreground-subtle",
|
|
24018
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoaderCircle, { className: "h-3.5 w-3.5 animate-spin" }), "Loading state
|
|
25237
|
+
isLoading && capStates.size === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
25238
|
+
className: "flex items-center justify-center gap-2 py-6 text-foreground-subtle font-sans",
|
|
25239
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoaderCircle, { className: "h-3.5 w-3.5 animate-spin" }), "Loading device state..."]
|
|
24019
25240
|
}),
|
|
24020
|
-
!isLoading &&
|
|
24021
|
-
className: "py-4 text-center text-foreground-subtle",
|
|
24022
|
-
children: "No state
|
|
25241
|
+
!isLoading && capStates.size === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
25242
|
+
className: "py-4 text-center text-foreground-subtle font-sans",
|
|
25243
|
+
children: "No runtime state"
|
|
24023
25244
|
}),
|
|
24024
|
-
|
|
24025
|
-
className: "
|
|
24026
|
-
children:
|
|
24027
|
-
|
|
24028
|
-
|
|
24029
|
-
|
|
24030
|
-
|
|
24031
|
-
|
|
24032
|
-
|
|
24033
|
-
|
|
24034
|
-
|
|
24035
|
-
|
|
24036
|
-
|
|
24037
|
-
|
|
24038
|
-
|
|
24039
|
-
|
|
24040
|
-
|
|
24041
|
-
}),
|
|
24042
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
24043
|
-
className: "flex-1 min-w-0",
|
|
24044
|
-
children: [
|
|
24045
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
24046
|
-
className: "flex items-center gap-1.5",
|
|
24047
|
-
children: [
|
|
24048
|
-
expanded ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronDown, { className: "h-3 w-3 text-foreground-subtle shrink-0" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronRight, { className: "h-3 w-3 text-foreground-subtle shrink-0" }),
|
|
24049
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
24050
|
-
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",
|
|
24051
|
-
children: entry.capName
|
|
24052
|
-
}),
|
|
24053
|
-
fields.length > 3 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
24054
|
-
className: "text-[9px] text-foreground-subtle",
|
|
24055
|
-
children: [
|
|
24056
|
-
"+",
|
|
24057
|
-
fields.length - 3,
|
|
24058
|
-
" more"
|
|
24059
|
-
]
|
|
24060
|
-
})
|
|
24061
|
-
]
|
|
24062
|
-
}),
|
|
24063
|
-
summary && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
24064
|
-
className: "text-foreground-subtle text-[10px] mt-0.5 ml-5 truncate",
|
|
24065
|
-
children: summary
|
|
24066
|
-
}),
|
|
24067
|
-
expanded && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
24068
|
-
className: "mt-1 ml-5 text-[9px] bg-surface rounded px-2 py-1.5 space-y-0.5 font-mono overflow-x-auto",
|
|
24069
|
-
children: fields.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
24070
|
-
className: "text-foreground-subtle italic",
|
|
24071
|
-
children: "empty slice"
|
|
24072
|
-
}) : fields.map(([k, v]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
|
|
24073
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
24074
|
-
className: "text-primary/70",
|
|
24075
|
-
children: k
|
|
24076
|
-
}),
|
|
24077
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
24078
|
-
className: "text-foreground-subtle",
|
|
24079
|
-
children: ": "
|
|
24080
|
-
}),
|
|
24081
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
24082
|
-
className: "text-foreground break-all",
|
|
24083
|
-
children: formatValue(v)
|
|
24084
|
-
})
|
|
24085
|
-
] }, k))
|
|
24086
|
-
})
|
|
24087
|
-
]
|
|
24088
|
-
})
|
|
24089
|
-
]
|
|
24090
|
-
}, entry.id);
|
|
25245
|
+
!isLoading && capStates.size > 0 && filteredCapNames.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
25246
|
+
className: "py-4 text-center text-foreground-subtle font-sans",
|
|
25247
|
+
children: "No caps match the filter"
|
|
25248
|
+
}),
|
|
25249
|
+
filteredCapNames.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
25250
|
+
className: "py-1",
|
|
25251
|
+
children: filteredCapNames.map((capName) => {
|
|
25252
|
+
const state = capStates.get(capName);
|
|
25253
|
+
if (!state) return null;
|
|
25254
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CapNode, {
|
|
25255
|
+
capName,
|
|
25256
|
+
slice: state.slice,
|
|
25257
|
+
updatedAt: state.updatedAt,
|
|
25258
|
+
searchNeedle,
|
|
25259
|
+
isPathOpen,
|
|
25260
|
+
onToggle: toggleNode
|
|
25261
|
+
}, capName);
|
|
24091
25262
|
})
|
|
24092
25263
|
})
|
|
24093
25264
|
]
|
|
24094
25265
|
})]
|
|
24095
25266
|
});
|
|
24096
25267
|
}
|
|
25268
|
+
/** Top-level tree node: one capability. Header carries the cap-name
|
|
25269
|
+
* pill + an "updated Ns ago" indicator; the body expands to the
|
|
25270
|
+
* slice's keys. */
|
|
25271
|
+
function CapNode({ capName, slice, updatedAt, searchNeedle, isPathOpen, onToggle }) {
|
|
25272
|
+
const path = capName;
|
|
25273
|
+
const open = isPathOpen(path, 0);
|
|
25274
|
+
const entries = Object.entries(slice);
|
|
25275
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
25276
|
+
className: "flex items-center gap-1.5 px-3 py-1 hover:bg-surface-hover/30 cursor-pointer select-none",
|
|
25277
|
+
onClick: () => onToggle(path, open),
|
|
25278
|
+
children: [
|
|
25279
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Chevron, {
|
|
25280
|
+
open,
|
|
25281
|
+
hasChildren: entries.length > 0
|
|
25282
|
+
}),
|
|
25283
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
25284
|
+
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",
|
|
25285
|
+
children: capName
|
|
25286
|
+
}),
|
|
25287
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
25288
|
+
className: "text-[9px] text-foreground-subtle font-sans",
|
|
25289
|
+
children: [
|
|
25290
|
+
entries.length,
|
|
25291
|
+
" ",
|
|
25292
|
+
entries.length === 1 ? "key" : "keys"
|
|
25293
|
+
]
|
|
25294
|
+
}),
|
|
25295
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
25296
|
+
className: "ml-auto text-[9px] text-foreground-subtle/70 font-sans tabular-nums",
|
|
25297
|
+
children: ["updated ", formatAge(updatedAt)]
|
|
25298
|
+
})
|
|
25299
|
+
]
|
|
25300
|
+
}), open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: entries.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LeafEmpty, {
|
|
25301
|
+
depth: 1,
|
|
25302
|
+
label: "empty slice"
|
|
25303
|
+
}) : entries.map(([k, v]) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TreeNode, {
|
|
25304
|
+
path: `${path}.${k}`,
|
|
25305
|
+
nodeKey: k,
|
|
25306
|
+
value: v,
|
|
25307
|
+
depth: 1,
|
|
25308
|
+
searchNeedle,
|
|
25309
|
+
isPathOpen,
|
|
25310
|
+
onToggle
|
|
25311
|
+
}, k)) })] });
|
|
25312
|
+
}
|
|
25313
|
+
/** Recursive tree node. A leaf renders `key: value`; an object or
|
|
25314
|
+
* array renders a collapsible branch whose children recurse one
|
|
25315
|
+
* level deeper. */
|
|
25316
|
+
function TreeNode({ path, nodeKey, value, depth, searchNeedle, isPathOpen, onToggle }) {
|
|
25317
|
+
const branch = asBranch(value);
|
|
25318
|
+
const indentStyle = { paddingLeft: `${12 + depth * 14}px` };
|
|
25319
|
+
if (!branch) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
25320
|
+
className: "flex items-start gap-1 px-3 py-0.5",
|
|
25321
|
+
style: indentStyle,
|
|
25322
|
+
children: [
|
|
25323
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
25324
|
+
className: cn("shrink-0", highlightCls(nodeKey, searchNeedle, "text-primary/70")),
|
|
25325
|
+
children: nodeKey
|
|
25326
|
+
}),
|
|
25327
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
25328
|
+
className: "text-foreground-subtle",
|
|
25329
|
+
children: ":"
|
|
25330
|
+
}),
|
|
25331
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
25332
|
+
className: cn("break-all", highlightCls(formatValue(value), searchNeedle, valueCls(value))),
|
|
25333
|
+
children: formatValue(value)
|
|
25334
|
+
})
|
|
25335
|
+
]
|
|
25336
|
+
});
|
|
25337
|
+
const open = isPathOpen(path, depth);
|
|
25338
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
25339
|
+
className: "flex items-center gap-1 px-3 py-0.5 hover:bg-surface-hover/30 cursor-pointer select-none",
|
|
25340
|
+
style: indentStyle,
|
|
25341
|
+
onClick: () => onToggle(path, open),
|
|
25342
|
+
children: [
|
|
25343
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Chevron, {
|
|
25344
|
+
open,
|
|
25345
|
+
hasChildren: branch.entries.length > 0
|
|
25346
|
+
}),
|
|
25347
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
25348
|
+
className: highlightCls(nodeKey, searchNeedle, "text-primary/70"),
|
|
25349
|
+
children: nodeKey
|
|
25350
|
+
}),
|
|
25351
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
25352
|
+
className: "text-foreground-subtle/70 text-[9px]",
|
|
25353
|
+
children: branch.summary
|
|
25354
|
+
})
|
|
25355
|
+
]
|
|
25356
|
+
}), open && (branch.entries.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LeafEmpty, {
|
|
25357
|
+
depth: depth + 1,
|
|
25358
|
+
label: branch.kind === "array" ? "empty array" : "empty object"
|
|
25359
|
+
}) : branch.entries.map(([k, v]) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TreeNode, {
|
|
25360
|
+
path: `${path}.${k}`,
|
|
25361
|
+
nodeKey: k,
|
|
25362
|
+
value: v,
|
|
25363
|
+
depth: depth + 1,
|
|
25364
|
+
searchNeedle,
|
|
25365
|
+
isPathOpen,
|
|
25366
|
+
onToggle
|
|
25367
|
+
}, k)))] });
|
|
25368
|
+
}
|
|
25369
|
+
/** Placeholder row for an object/array/slice with no entries. */
|
|
25370
|
+
function LeafEmpty({ depth, label }) {
|
|
25371
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
25372
|
+
className: "px-3 py-0.5 text-foreground-subtle/60 italic",
|
|
25373
|
+
style: { paddingLeft: `${12 + depth * 14}px` },
|
|
25374
|
+
children: label
|
|
25375
|
+
});
|
|
25376
|
+
}
|
|
25377
|
+
/** Expand/collapse chevron. Renders a fixed-width spacer when the node
|
|
25378
|
+
* has no children so leaf and branch keys stay column-aligned. */
|
|
25379
|
+
function Chevron({ open, hasChildren }) {
|
|
25380
|
+
if (!hasChildren) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "inline-block w-3 shrink-0" });
|
|
25381
|
+
return open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronDown, { className: "h-3 w-3 text-foreground-subtle shrink-0" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronRight, { className: "h-3 w-3 text-foreground-subtle shrink-0" });
|
|
25382
|
+
}
|
|
25383
|
+
/** Classify a value as an expandable branch (plain object or array)
|
|
25384
|
+
* or `null` for a leaf. Arrays expose index → element entries. */
|
|
25385
|
+
function asBranch(value) {
|
|
25386
|
+
if (Array.isArray(value)) return {
|
|
25387
|
+
kind: "array",
|
|
25388
|
+
entries: value.map((v, i) => [String(i), v]),
|
|
25389
|
+
summary: `[${value.length}]`
|
|
25390
|
+
};
|
|
25391
|
+
if (value && typeof value === "object") {
|
|
25392
|
+
const entries = Object.entries(value);
|
|
25393
|
+
return {
|
|
25394
|
+
kind: "object",
|
|
25395
|
+
entries,
|
|
25396
|
+
summary: `{${entries.length}}`
|
|
25397
|
+
};
|
|
25398
|
+
}
|
|
25399
|
+
return null;
|
|
25400
|
+
}
|
|
24097
25401
|
function formatValue(v) {
|
|
24098
25402
|
if (v === null) return "null";
|
|
24099
25403
|
if (v === void 0) return "undefined";
|
|
@@ -24105,9 +25409,46 @@ function formatValue(v) {
|
|
|
24105
25409
|
return String(v);
|
|
24106
25410
|
}
|
|
24107
25411
|
}
|
|
25412
|
+
/** Tailwind colour for a leaf value by JS type. */
|
|
25413
|
+
function valueCls(v) {
|
|
25414
|
+
if (v === null || v === void 0) return "text-foreground-subtle/60";
|
|
25415
|
+
if (typeof v === "number") return "text-amber-400";
|
|
25416
|
+
if (typeof v === "boolean") return v ? "text-emerald-400" : "text-rose-400";
|
|
25417
|
+
return "text-foreground";
|
|
25418
|
+
}
|
|
25419
|
+
/** Append a highlight background when `text` matches the live search
|
|
25420
|
+
* needle, so search hits are visible inside the tree. */
|
|
25421
|
+
function highlightCls(text, needle, base) {
|
|
25422
|
+
if (needle && text.toLowerCase().includes(needle)) return cn(base, "bg-violet-500/20 rounded px-0.5");
|
|
25423
|
+
return base;
|
|
25424
|
+
}
|
|
25425
|
+
/** Deep search a slice — true when `needle` appears in any key or any
|
|
25426
|
+
* stringified leaf value anywhere in the tree. */
|
|
25427
|
+
function sliceMatchesSearch(value, needle) {
|
|
25428
|
+
if (Array.isArray(value)) return value.some((v) => sliceMatchesSearch(v, needle));
|
|
25429
|
+
if (value && typeof value === "object") {
|
|
25430
|
+
for (const [k, v] of Object.entries(value)) {
|
|
25431
|
+
if (k.toLowerCase().includes(needle)) return true;
|
|
25432
|
+
if (sliceMatchesSearch(v, needle)) return true;
|
|
25433
|
+
}
|
|
25434
|
+
return false;
|
|
25435
|
+
}
|
|
25436
|
+
return formatValue(value).toLowerCase().includes(needle);
|
|
25437
|
+
}
|
|
25438
|
+
/** Human "Ns ago" / "Nm ago" age label for the per-cap freshness
|
|
25439
|
+
* indicator. */
|
|
25440
|
+
function formatAge(updatedAt) {
|
|
25441
|
+
const deltaMs = Date.now() - updatedAt;
|
|
25442
|
+
if (deltaMs < 5e3) return "just now";
|
|
25443
|
+
const secs = Math.floor(deltaMs / 1e3);
|
|
25444
|
+
if (secs < 60) return `${secs}s ago`;
|
|
25445
|
+
const mins = Math.floor(secs / 60);
|
|
25446
|
+
if (mins < 60) return `${mins}m ago`;
|
|
25447
|
+
return `${Math.floor(mins / 60)}h ago`;
|
|
25448
|
+
}
|
|
24108
25449
|
function ScopeBadge({ label, value }) {
|
|
24109
25450
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
24110
|
-
className: "text-[9px] px-1.5 py-0.5 rounded-full bg-primary/10 text-primary font-medium",
|
|
25451
|
+
className: "text-[9px] px-1.5 py-0.5 rounded-full bg-primary/10 text-primary font-medium font-sans",
|
|
24111
25452
|
children: [
|
|
24112
25453
|
label,
|
|
24113
25454
|
": ",
|
|
@@ -24116,16 +25457,13 @@ function ScopeBadge({ label, value }) {
|
|
|
24116
25457
|
});
|
|
24117
25458
|
}
|
|
24118
25459
|
/**
|
|
24119
|
-
* Cap-name multiselect popover — same shape as
|
|
24120
|
-
*
|
|
24121
|
-
*
|
|
24122
|
-
* Caps don't have icons / styled badges like events do — we render
|
|
24123
|
-
* a flat name + violet-pill bracket so the look stays consistent
|
|
24124
|
-
* with the rest of the StateValuesStream rows.
|
|
25460
|
+
* Cap-name multiselect popover — same shape as `CategoryFilterPopover`
|
|
25461
|
+
* in event-stream.tsx (search box at top, Select All / None / Defaults
|
|
25462
|
+
* buttons, scrollable checkbox list).
|
|
24125
25463
|
*/
|
|
24126
25464
|
function CapFilterPopover({ search, onSearchChange, caps, active, onToggle, onSelectAll, onSelectNone, onResetDefaults }) {
|
|
24127
25465
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
24128
|
-
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",
|
|
25466
|
+
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",
|
|
24129
25467
|
onClick: (e) => e.stopPropagation(),
|
|
24130
25468
|
children: [
|
|
24131
25469
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -24196,24 +25534,6 @@ function CapFilterPopover({ search, onSearchChange, caps, active, onToggle, onSe
|
|
|
24196
25534
|
]
|
|
24197
25535
|
});
|
|
24198
25536
|
}
|
|
24199
|
-
/**
|
|
24200
|
-
* Per-row Copy icon — same shape used in log-stream / event-stream
|
|
24201
|
-
* so the three live-streams' rows are visually symmetric. Stops
|
|
24202
|
-
* propagation so the click doesn't toggle the row's expand state.
|
|
24203
|
-
* Flashes a Check for ~1s on success.
|
|
24204
|
-
*/
|
|
24205
|
-
function RowCopyButton({ copied, onCopy }) {
|
|
24206
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
24207
|
-
type: "button",
|
|
24208
|
-
title: "Copy this state change",
|
|
24209
|
-
onClick: (e) => {
|
|
24210
|
-
e.stopPropagation();
|
|
24211
|
-
onCopy();
|
|
24212
|
-
},
|
|
24213
|
-
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"),
|
|
24214
|
-
children: copied ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Check, { className: "h-3 w-3" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Copy, { className: "h-3 w-3" })
|
|
24215
|
-
});
|
|
24216
|
-
}
|
|
24217
25537
|
//#endregion
|
|
24218
25538
|
//#region src/composites/device-activity-panel.tsx
|
|
24219
25539
|
/**
|
|
@@ -24292,303 +25612,99 @@ function DeviceActivityPanel({ deviceId, defaultEventCategories, defaultStateCap
|
|
|
24292
25612
|
showFilters: false,
|
|
24293
25613
|
liveBuffer: logsBuffer
|
|
24294
25614
|
}),
|
|
24295
|
-
tab === "events" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EventStream, {
|
|
24296
|
-
deviceId,
|
|
24297
|
-
defaultCategories: defaultEventCategories,
|
|
24298
|
-
maxHeight,
|
|
24299
|
-
showCategoryFilter: true,
|
|
24300
|
-
liveBuffer: eventsBuffer
|
|
24301
|
-
}),
|
|
24302
|
-
tab === "state" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StateValuesStream, {
|
|
24303
|
-
deviceId,
|
|
24304
|
-
defaultCaps: defaultStateCaps,
|
|
24305
|
-
maxHeight,
|
|
24306
|
-
liveBuffer: stateBuffer
|
|
24307
|
-
})
|
|
24308
|
-
]
|
|
24309
|
-
})]
|
|
24310
|
-
});
|
|
24311
|
-
}
|
|
24312
|
-
//#endregion
|
|
24313
|
-
//#region src/composites/confirm-action-button.tsx
|
|
24314
|
-
/**
|
|
24315
|
-
* ConfirmActionButton — destructive-action button with a modal confirm.
|
|
24316
|
-
*
|
|
24317
|
-
* Two-step UX for any operation operators shouldn't trigger by accident:
|
|
24318
|
-
* reboot device, restart addon, regenerate token, etc. The trigger is a
|
|
24319
|
-
* normal Button that opens a Dialog; confirming runs the async action,
|
|
24320
|
-
* the trigger spinner-disables itself for the duration, and the dialog
|
|
24321
|
-
* closes on success. Errors surface inline at the dialog's bottom.
|
|
24322
|
-
*
|
|
24323
|
-
* Generic on `TResult` so callers don't have to discard the return
|
|
24324
|
-
* value. Pass an `icon` to render it inside the trigger (e.g. RotateCw
|
|
24325
|
-
* for reboot, RefreshCw for restart).
|
|
24326
|
-
*/
|
|
24327
|
-
function ConfirmActionButton({ label, icon: Icon, title = "Confirm action", description, confirmLabel, triggerVariant = "outline", confirmVariant = "danger", size = "sm", disabled, className, action, onSuccess }) {
|
|
24328
|
-
const [open, setOpen] = (0, react$1.useState)(false);
|
|
24329
|
-
const [running, setRunning] = (0, react$1.useState)(false);
|
|
24330
|
-
const [error, setError] = (0, react$1.useState)(null);
|
|
24331
|
-
const handleConfirm = async () => {
|
|
24332
|
-
setError(null);
|
|
24333
|
-
setRunning(true);
|
|
24334
|
-
try {
|
|
24335
|
-
const result = await action();
|
|
24336
|
-
onSuccess?.(result);
|
|
24337
|
-
setOpen(false);
|
|
24338
|
-
} catch (err) {
|
|
24339
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
24340
|
-
} finally {
|
|
24341
|
-
setRunning(false);
|
|
24342
|
-
}
|
|
24343
|
-
};
|
|
24344
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
|
|
24345
|
-
type: "button",
|
|
24346
|
-
variant: triggerVariant,
|
|
24347
|
-
size,
|
|
24348
|
-
disabled,
|
|
24349
|
-
onClick: () => {
|
|
24350
|
-
setError(null);
|
|
24351
|
-
setOpen(true);
|
|
24352
|
-
},
|
|
24353
|
-
className,
|
|
24354
|
-
children: [Icon && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { className: "h-3.5 w-3.5" }), label]
|
|
24355
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Dialog, {
|
|
24356
|
-
open,
|
|
24357
|
-
onOpenChange: (next) => !running && setOpen(next),
|
|
24358
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DialogContent, { children: [
|
|
24359
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DialogHeader, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DialogTitle, {
|
|
24360
|
-
className: "flex items-center gap-2",
|
|
24361
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TriangleAlert, { className: "h-4 w-4 text-amber-400" }), title]
|
|
24362
|
-
}), typeof description === "string" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DialogDescription, { children: description }) : description] }),
|
|
24363
|
-
error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
24364
|
-
className: cn("mt-2 px-3 py-2 rounded border border-danger/40 bg-danger/10", "text-xs text-danger"),
|
|
24365
|
-
children: error
|
|
24366
|
-
}),
|
|
24367
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DialogFooter, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
24368
|
-
type: "button",
|
|
24369
|
-
variant: "ghost",
|
|
24370
|
-
size,
|
|
24371
|
-
disabled: running,
|
|
24372
|
-
onClick: () => setOpen(false),
|
|
24373
|
-
children: "Cancel"
|
|
24374
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
|
|
24375
|
-
type: "button",
|
|
24376
|
-
variant: confirmVariant,
|
|
24377
|
-
size,
|
|
24378
|
-
disabled: running,
|
|
24379
|
-
onClick: () => {
|
|
24380
|
-
handleConfirm();
|
|
24381
|
-
},
|
|
24382
|
-
children: [running && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoaderCircle, { className: "h-3.5 w-3.5 animate-spin" }), confirmLabel ?? label]
|
|
24383
|
-
})] })
|
|
24384
|
-
] })
|
|
24385
|
-
})] });
|
|
24386
|
-
}
|
|
24387
|
-
//#endregion
|
|
24388
|
-
//#region src/composites/ptz-overlay.tsx
|
|
24389
|
-
/**
|
|
24390
|
-
* PTZOverlay — pan / tilt / zoom controls.
|
|
24391
|
-
*
|
|
24392
|
-
* Two visual variants driven by `mode`:
|
|
24393
|
-
* - `'overlay'` (default): translucent dark pill positioned bottom-
|
|
24394
|
-
* right of the camera viewport. Used as `extraOverlay` on the
|
|
24395
|
-
* `CameraStreamPlayer` so operators can drive PTZ without leaving
|
|
24396
|
-
* the live view. Opaque background + subtle ring keeps the d-pad
|
|
24397
|
-
* legible against any frame.
|
|
24398
|
-
* - `'panel'`: full-bleed inside a host container (e.g. the floating
|
|
24399
|
-
* PTZ panel in DeviceDetail). No absolute positioning, no inner
|
|
24400
|
-
* wrapper card — the host's chrome is the only frame. Inherits the
|
|
24401
|
-
* surrounding `bg-surface` so the dark theme reads consistently
|
|
24402
|
-
* instead of the previous always-dark-bubble look.
|
|
24403
|
-
*
|
|
24404
|
-
* Interaction model is identical across modes: short tap fires a
|
|
24405
|
-
* discrete pulse (`move`); long press starts continuous motion until
|
|
24406
|
-
* release (`startContinuous` + `stopContinuous` on pointer up).
|
|
24407
|
-
*/
|
|
24408
|
-
function DPadButton({ direction, icon: Icon, disabled, className, variant, onMove, onStart, onStop }) {
|
|
24409
|
-
const [pressedAt, setPressedAt] = (0, react$1.useState)(null);
|
|
24410
|
-
const [continuous, setContinuous] = (0, react$1.useState)(false);
|
|
24411
|
-
const handlePointerDown = (0, react$1.useCallback)((e) => {
|
|
24412
|
-
if (disabled) return;
|
|
24413
|
-
e.currentTarget.setPointerCapture(e.pointerId);
|
|
24414
|
-
setPressedAt(Date.now());
|
|
24415
|
-
setContinuous(false);
|
|
24416
|
-
const timer = setTimeout(() => {
|
|
24417
|
-
setContinuous(true);
|
|
24418
|
-
onStart(direction);
|
|
24419
|
-
}, 250);
|
|
24420
|
-
e.currentTarget.dataset.timer = String(timer);
|
|
24421
|
-
}, [
|
|
24422
|
-
direction,
|
|
24423
|
-
disabled,
|
|
24424
|
-
onStart
|
|
24425
|
-
]);
|
|
24426
|
-
const handlePointerUp = (0, react$1.useCallback)((e) => {
|
|
24427
|
-
const timerId = Number(e.currentTarget.dataset.timer);
|
|
24428
|
-
if (timerId) clearTimeout(timerId);
|
|
24429
|
-
e.currentTarget.dataset.timer = "";
|
|
24430
|
-
if (continuous) onStop();
|
|
24431
|
-
else if (pressedAt !== null) onMove(direction);
|
|
24432
|
-
setPressedAt(null);
|
|
24433
|
-
setContinuous(false);
|
|
24434
|
-
}, [
|
|
24435
|
-
continuous,
|
|
24436
|
-
direction,
|
|
24437
|
-
onMove,
|
|
24438
|
-
onStop,
|
|
24439
|
-
pressedAt
|
|
24440
|
-
]);
|
|
24441
|
-
const handlePointerCancel = (0, react$1.useCallback)((e) => {
|
|
24442
|
-
const timerId = Number(e.currentTarget.dataset.timer);
|
|
24443
|
-
if (timerId) clearTimeout(timerId);
|
|
24444
|
-
e.currentTarget.dataset.timer = "";
|
|
24445
|
-
if (continuous) onStop();
|
|
24446
|
-
setPressedAt(null);
|
|
24447
|
-
setContinuous(false);
|
|
24448
|
-
}, [continuous, onStop]);
|
|
24449
|
-
const sizeClass = variant === "panel" ? "h-9 w-9" : "h-7 w-7";
|
|
24450
|
-
const iconSizeClass = variant === "panel" ? "h-4 w-4" : "h-3.5 w-3.5";
|
|
24451
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
24452
|
-
type: "button",
|
|
24453
|
-
disabled,
|
|
24454
|
-
onPointerDown: handlePointerDown,
|
|
24455
|
-
onPointerUp: handlePointerUp,
|
|
24456
|
-
onPointerCancel: handlePointerCancel,
|
|
24457
|
-
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),
|
|
24458
|
-
title: direction,
|
|
24459
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { className: iconSizeClass })
|
|
24460
|
-
});
|
|
24461
|
-
}
|
|
24462
|
-
function PTZOverlay({ controls, mode = "overlay", showPresets, showZoom = true, showHome = true, className }) {
|
|
24463
|
-
const { move, startContinuous, stopContinuous, zoom, goHome, goToPreset, presets, busy, error } = controls;
|
|
24464
|
-
const [presetsOpen, setPresetsOpen] = (0, react$1.useState)(false);
|
|
24465
|
-
const presetsVisible = (showPresets ?? presets.length > 0) && presets.length > 0;
|
|
24466
|
-
const isPanel = mode === "panel";
|
|
24467
|
-
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";
|
|
24468
|
-
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");
|
|
24469
|
-
const sideButtonSize = isPanel ? "h-9 w-9" : "h-7 w-7";
|
|
24470
|
-
const sideIconSize = isPanel ? "h-4 w-4" : "h-3.5 w-3.5";
|
|
24471
|
-
const sideButtonHover = isPanel ? "text-foreground hover:bg-surface-hover" : "text-white hover:bg-white/15";
|
|
24472
|
-
const sepClass = isPanel ? "h-12 w-px bg-border mx-1" : "h-12 w-px bg-white/15 mx-1";
|
|
24473
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
24474
|
-
className: cn(containerClass, className),
|
|
24475
|
-
children: [error && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
24476
|
-
className: "rounded bg-danger/90 px-2 py-1 text-[10px] font-medium text-white shadow-lg max-w-[200px] self-center",
|
|
24477
|
-
children: ["PTZ: ", error]
|
|
24478
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
24479
|
-
className: cn(rowClass, isPanel && "justify-center"),
|
|
24480
|
-
children: [
|
|
24481
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
24482
|
-
className: "grid grid-cols-3 gap-0.5",
|
|
24483
|
-
children: [
|
|
24484
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
24485
|
-
"aria-hidden": true,
|
|
24486
|
-
className: isPanel ? "h-9 w-9" : "h-7 w-7"
|
|
24487
|
-
}),
|
|
24488
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DPadButton, {
|
|
24489
|
-
direction: "up",
|
|
24490
|
-
icon: ArrowUp,
|
|
24491
|
-
variant: mode,
|
|
24492
|
-
onMove: move,
|
|
24493
|
-
onStart: startContinuous,
|
|
24494
|
-
onStop: stopContinuous
|
|
24495
|
-
}),
|
|
24496
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
24497
|
-
"aria-hidden": true,
|
|
24498
|
-
className: isPanel ? "h-9 w-9" : "h-7 w-7"
|
|
24499
|
-
}),
|
|
24500
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DPadButton, {
|
|
24501
|
-
direction: "left",
|
|
24502
|
-
icon: ArrowLeft,
|
|
24503
|
-
variant: mode,
|
|
24504
|
-
onMove: move,
|
|
24505
|
-
onStart: startContinuous,
|
|
24506
|
-
onStop: stopContinuous
|
|
24507
|
-
}),
|
|
24508
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
24509
|
-
"aria-hidden": true,
|
|
24510
|
-
className: isPanel ? "h-9 w-9" : "h-7 w-7"
|
|
24511
|
-
}),
|
|
24512
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DPadButton, {
|
|
24513
|
-
direction: "right",
|
|
24514
|
-
icon: ArrowRight,
|
|
24515
|
-
variant: mode,
|
|
24516
|
-
onMove: move,
|
|
24517
|
-
onStart: startContinuous,
|
|
24518
|
-
onStop: stopContinuous
|
|
24519
|
-
}),
|
|
24520
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
24521
|
-
"aria-hidden": true,
|
|
24522
|
-
className: isPanel ? "h-9 w-9" : "h-7 w-7"
|
|
24523
|
-
}),
|
|
24524
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DPadButton, {
|
|
24525
|
-
direction: "down",
|
|
24526
|
-
icon: ArrowDown,
|
|
24527
|
-
variant: mode,
|
|
24528
|
-
onMove: move,
|
|
24529
|
-
onStart: startContinuous,
|
|
24530
|
-
onStop: stopContinuous
|
|
24531
|
-
}),
|
|
24532
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
24533
|
-
"aria-hidden": true,
|
|
24534
|
-
className: isPanel ? "h-9 w-9" : "h-7 w-7"
|
|
24535
|
-
})
|
|
24536
|
-
]
|
|
24537
|
-
}),
|
|
24538
|
-
(showZoom || showHome) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: sepClass }),
|
|
24539
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
24540
|
-
className: "flex flex-col gap-0.5",
|
|
24541
|
-
children: [showZoom && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
24542
|
-
type: "button",
|
|
24543
|
-
onClick: () => zoom("in"),
|
|
24544
|
-
disabled: busy,
|
|
24545
|
-
title: "Zoom in",
|
|
24546
|
-
className: cn("flex items-center justify-center rounded disabled:opacity-40", sideButtonSize, sideButtonHover),
|
|
24547
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZoomIn, { className: sideIconSize })
|
|
24548
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
24549
|
-
type: "button",
|
|
24550
|
-
onClick: () => zoom("out"),
|
|
24551
|
-
disabled: busy,
|
|
24552
|
-
title: "Zoom out",
|
|
24553
|
-
className: cn("flex items-center justify-center rounded disabled:opacity-40", sideButtonSize, sideButtonHover),
|
|
24554
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZoomOut, { className: sideIconSize })
|
|
24555
|
-
})] }), showHome && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
24556
|
-
type: "button",
|
|
24557
|
-
onClick: () => goHome(),
|
|
24558
|
-
disabled: busy,
|
|
24559
|
-
title: "Go home",
|
|
24560
|
-
className: cn("flex items-center justify-center rounded disabled:opacity-40", sideButtonSize, sideButtonHover),
|
|
24561
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(House, { className: sideIconSize })
|
|
24562
|
-
})]
|
|
25615
|
+
tab === "events" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EventStream, {
|
|
25616
|
+
deviceId,
|
|
25617
|
+
defaultCategories: defaultEventCategories,
|
|
25618
|
+
maxHeight,
|
|
25619
|
+
showCategoryFilter: true,
|
|
25620
|
+
liveBuffer: eventsBuffer
|
|
24563
25621
|
}),
|
|
24564
|
-
|
|
24565
|
-
|
|
24566
|
-
|
|
24567
|
-
|
|
24568
|
-
|
|
24569
|
-
disabled: busy,
|
|
24570
|
-
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"),
|
|
24571
|
-
title: "Presets",
|
|
24572
|
-
children: ["Presets", /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronDown, { className: cn("h-3 w-3 transition-transform", presetsOpen && "rotate-180") })]
|
|
24573
|
-
}), presetsOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
24574
|
-
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"),
|
|
24575
|
-
children: presets.map((p) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
24576
|
-
type: "button",
|
|
24577
|
-
onClick: () => {
|
|
24578
|
-
goToPreset(p.id);
|
|
24579
|
-
setPresetsOpen(false);
|
|
24580
|
-
},
|
|
24581
|
-
disabled: busy,
|
|
24582
|
-
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"),
|
|
24583
|
-
children: p.name || p.id
|
|
24584
|
-
}, p.id))
|
|
24585
|
-
})]
|
|
25622
|
+
tab === "state" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StateValuesStream, {
|
|
25623
|
+
deviceId,
|
|
25624
|
+
defaultCaps: defaultStateCaps,
|
|
25625
|
+
maxHeight,
|
|
25626
|
+
liveBuffer: stateBuffer
|
|
24586
25627
|
})
|
|
24587
25628
|
]
|
|
24588
25629
|
})]
|
|
24589
25630
|
});
|
|
24590
25631
|
}
|
|
24591
25632
|
//#endregion
|
|
25633
|
+
//#region src/composites/confirm-action-button.tsx
|
|
25634
|
+
/**
|
|
25635
|
+
* ConfirmActionButton — destructive-action button with a modal confirm.
|
|
25636
|
+
*
|
|
25637
|
+
* Two-step UX for any operation operators shouldn't trigger by accident:
|
|
25638
|
+
* reboot device, restart addon, regenerate token, etc. The trigger is a
|
|
25639
|
+
* normal Button that opens a Dialog; confirming runs the async action,
|
|
25640
|
+
* the trigger spinner-disables itself for the duration, and the dialog
|
|
25641
|
+
* closes on success. Errors surface inline at the dialog's bottom.
|
|
25642
|
+
*
|
|
25643
|
+
* Generic on `TResult` so callers don't have to discard the return
|
|
25644
|
+
* value. Pass an `icon` to render it inside the trigger (e.g. RotateCw
|
|
25645
|
+
* for reboot, RefreshCw for restart).
|
|
25646
|
+
*/
|
|
25647
|
+
function ConfirmActionButton({ label, icon: Icon, title = "Confirm action", description, confirmLabel, triggerVariant = "outline", confirmVariant = "danger", size = "sm", disabled, className, action, onSuccess }) {
|
|
25648
|
+
const [open, setOpen] = (0, react$1.useState)(false);
|
|
25649
|
+
const [running, setRunning] = (0, react$1.useState)(false);
|
|
25650
|
+
const [error, setError] = (0, react$1.useState)(null);
|
|
25651
|
+
const handleConfirm = async () => {
|
|
25652
|
+
setError(null);
|
|
25653
|
+
setRunning(true);
|
|
25654
|
+
try {
|
|
25655
|
+
const result = await action();
|
|
25656
|
+
onSuccess?.(result);
|
|
25657
|
+
setOpen(false);
|
|
25658
|
+
} catch (err) {
|
|
25659
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
25660
|
+
} finally {
|
|
25661
|
+
setRunning(false);
|
|
25662
|
+
}
|
|
25663
|
+
};
|
|
25664
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
|
|
25665
|
+
type: "button",
|
|
25666
|
+
variant: triggerVariant,
|
|
25667
|
+
size,
|
|
25668
|
+
disabled,
|
|
25669
|
+
onClick: () => {
|
|
25670
|
+
setError(null);
|
|
25671
|
+
setOpen(true);
|
|
25672
|
+
},
|
|
25673
|
+
className,
|
|
25674
|
+
children: [Icon && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { className: "h-3.5 w-3.5" }), label]
|
|
25675
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Dialog, {
|
|
25676
|
+
open,
|
|
25677
|
+
onOpenChange: (next) => !running && setOpen(next),
|
|
25678
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DialogContent, { children: [
|
|
25679
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DialogHeader, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DialogTitle, {
|
|
25680
|
+
className: "flex items-center gap-2",
|
|
25681
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TriangleAlert, { className: "h-4 w-4 text-amber-400" }), title]
|
|
25682
|
+
}), typeof description === "string" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DialogDescription, { children: description }) : description] }),
|
|
25683
|
+
error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
25684
|
+
className: cn("mt-2 px-3 py-2 rounded border border-danger/40 bg-danger/10", "text-xs text-danger"),
|
|
25685
|
+
children: error
|
|
25686
|
+
}),
|
|
25687
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DialogFooter, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
25688
|
+
type: "button",
|
|
25689
|
+
variant: "ghost",
|
|
25690
|
+
size,
|
|
25691
|
+
disabled: running,
|
|
25692
|
+
onClick: () => setOpen(false),
|
|
25693
|
+
children: "Cancel"
|
|
25694
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
|
|
25695
|
+
type: "button",
|
|
25696
|
+
variant: confirmVariant,
|
|
25697
|
+
size,
|
|
25698
|
+
disabled: running,
|
|
25699
|
+
onClick: () => {
|
|
25700
|
+
handleConfirm();
|
|
25701
|
+
},
|
|
25702
|
+
children: [running && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoaderCircle, { className: "h-3.5 w-3.5 animate-spin" }), confirmLabel ?? label]
|
|
25703
|
+
})] })
|
|
25704
|
+
] })
|
|
25705
|
+
})] });
|
|
25706
|
+
}
|
|
25707
|
+
//#endregion
|
|
24592
25708
|
//#region src/composites/snapshot-button.tsx
|
|
24593
25709
|
/**
|
|
24594
25710
|
* SnapshotButton — operator-facing manual snapshot trigger.
|
|
@@ -25495,169 +26611,6 @@ function useDeviceWebrtc(trpc, deviceId, pollIntervalMs = 5e3) {
|
|
|
25495
26611
|
};
|
|
25496
26612
|
}
|
|
25497
26613
|
//#endregion
|
|
25498
|
-
//#region src/hooks/use-ptz.ts
|
|
25499
|
-
/**
|
|
25500
|
-
* usePTZ — PTZ control hook for device-scoped pan / tilt / zoom.
|
|
25501
|
-
*
|
|
25502
|
-
* Wraps the `ptz` capability methods through the canonical
|
|
25503
|
-
* `useDeviceProxy` surface — every call goes through
|
|
25504
|
-
* `dev.ptz?.<method>(...)` with deviceId/nodeId auto-injected. The
|
|
25505
|
-
* hook stays bare-bones around state (`busy`, `error`, `presets`)
|
|
25506
|
-
* so the operator-facing component (PTZOverlay) stays trivial.
|
|
25507
|
-
*
|
|
25508
|
-
* Returns:
|
|
25509
|
-
* - `move(direction)`: discrete one-shot move (cap.move + cap.stop).
|
|
25510
|
-
* Use for short pulse moves triggered by tapping a d-pad button.
|
|
25511
|
-
* - `startContinuous(direction)` / `stopContinuous()`: gesture-driven
|
|
25512
|
-
* continuous motion (`cap.continuousMove` + `cap.stop`). Use for
|
|
25513
|
-
* long-press handlers on touch / mouse-down handlers on desktop.
|
|
25514
|
-
* - `zoom('in' | 'out')`: discrete zoom step.
|
|
25515
|
-
* - `goHome()`: jump to preset 0.
|
|
25516
|
-
* - `presets` + `goToPreset(presetId)`: list and jump to named presets.
|
|
25517
|
-
*
|
|
25518
|
-
* Parametrised by the same `UseDeviceProxyTrpc` shape every other
|
|
25519
|
-
* device hook uses — works under admin-ui (BackendClient.trpc) and
|
|
25520
|
-
* addon pages (AddonPageProps.trpc) alike.
|
|
25521
|
-
*/
|
|
25522
|
-
var DIRECTION_VECTORS = {
|
|
25523
|
-
"up": {
|
|
25524
|
-
pan: 0,
|
|
25525
|
-
tilt: 1
|
|
25526
|
-
},
|
|
25527
|
-
"down": {
|
|
25528
|
-
pan: 0,
|
|
25529
|
-
tilt: -1
|
|
25530
|
-
},
|
|
25531
|
-
"left": {
|
|
25532
|
-
pan: -1,
|
|
25533
|
-
tilt: 0
|
|
25534
|
-
},
|
|
25535
|
-
"right": {
|
|
25536
|
-
pan: 1,
|
|
25537
|
-
tilt: 0
|
|
25538
|
-
},
|
|
25539
|
-
"up-left": {
|
|
25540
|
-
pan: -1,
|
|
25541
|
-
tilt: 1
|
|
25542
|
-
},
|
|
25543
|
-
"up-right": {
|
|
25544
|
-
pan: 1,
|
|
25545
|
-
tilt: 1
|
|
25546
|
-
},
|
|
25547
|
-
"down-left": {
|
|
25548
|
-
pan: -1,
|
|
25549
|
-
tilt: -1
|
|
25550
|
-
},
|
|
25551
|
-
"down-right": {
|
|
25552
|
-
pan: 1,
|
|
25553
|
-
tilt: -1
|
|
25554
|
-
}
|
|
25555
|
-
};
|
|
25556
|
-
function usePTZ(trpc, deviceId, options) {
|
|
25557
|
-
const defaultSpeed = options?.defaultSpeed ?? .5;
|
|
25558
|
-
const pulseMs = options?.pulseMs ?? 250;
|
|
25559
|
-
const enabled = options?.enabled ?? true;
|
|
25560
|
-
const ptz = useDeviceProxy(trpc, enabled ? deviceId : null)?.ptz;
|
|
25561
|
-
const [presets, setPresets] = (0, react$1.useState)([]);
|
|
25562
|
-
const [busy, setBusy] = (0, react$1.useState)(false);
|
|
25563
|
-
const [error, setError] = (0, react$1.useState)(null);
|
|
25564
|
-
const refreshPresets = (0, react$1.useCallback)(async () => {
|
|
25565
|
-
if (!enabled || !ptz) return;
|
|
25566
|
-
try {
|
|
25567
|
-
setPresets(await ptz.getPresets({}));
|
|
25568
|
-
} catch (err) {
|
|
25569
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
25570
|
-
if (msg.includes("provider not available") || msg.includes("no 'ptz' binding")) return;
|
|
25571
|
-
setError(msg);
|
|
25572
|
-
}
|
|
25573
|
-
}, [ptz, enabled]);
|
|
25574
|
-
(0, react$1.useEffect)(() => {
|
|
25575
|
-
refreshPresets();
|
|
25576
|
-
}, [refreshPresets]);
|
|
25577
|
-
const wrap = (0, react$1.useCallback)(async (fn) => {
|
|
25578
|
-
if (!enabled || !ptz) return void 0;
|
|
25579
|
-
setError(null);
|
|
25580
|
-
setBusy(true);
|
|
25581
|
-
try {
|
|
25582
|
-
return await fn();
|
|
25583
|
-
} catch (err) {
|
|
25584
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
25585
|
-
return;
|
|
25586
|
-
} finally {
|
|
25587
|
-
setBusy(false);
|
|
25588
|
-
}
|
|
25589
|
-
}, [enabled, ptz]);
|
|
25590
|
-
return {
|
|
25591
|
-
move: (0, react$1.useCallback)(async (direction, speed) => {
|
|
25592
|
-
if (!ptz) return;
|
|
25593
|
-
const v = DIRECTION_VECTORS[direction];
|
|
25594
|
-
const s = speed ?? defaultSpeed;
|
|
25595
|
-
await wrap(async () => {
|
|
25596
|
-
await ptz.move({
|
|
25597
|
-
pan: v.pan,
|
|
25598
|
-
tilt: v.tilt,
|
|
25599
|
-
speed: s
|
|
25600
|
-
});
|
|
25601
|
-
await new Promise((r) => setTimeout(r, pulseMs));
|
|
25602
|
-
await ptz.stop({});
|
|
25603
|
-
});
|
|
25604
|
-
}, [
|
|
25605
|
-
ptz,
|
|
25606
|
-
defaultSpeed,
|
|
25607
|
-
pulseMs,
|
|
25608
|
-
wrap
|
|
25609
|
-
]),
|
|
25610
|
-
startContinuous: (0, react$1.useCallback)(async (direction, speed) => {
|
|
25611
|
-
if (!ptz) return;
|
|
25612
|
-
const v = DIRECTION_VECTORS[direction];
|
|
25613
|
-
const s = speed ?? defaultSpeed;
|
|
25614
|
-
await wrap(() => ptz.continuousMove({
|
|
25615
|
-
pan: v.pan,
|
|
25616
|
-
tilt: v.tilt,
|
|
25617
|
-
speed: s
|
|
25618
|
-
}));
|
|
25619
|
-
}, [
|
|
25620
|
-
ptz,
|
|
25621
|
-
defaultSpeed,
|
|
25622
|
-
wrap
|
|
25623
|
-
]),
|
|
25624
|
-
stopContinuous: (0, react$1.useCallback)(async () => {
|
|
25625
|
-
if (!ptz) return;
|
|
25626
|
-
await wrap(() => ptz.stop({}));
|
|
25627
|
-
}, [ptz, wrap]),
|
|
25628
|
-
zoom: (0, react$1.useCallback)(async (direction, speed) => {
|
|
25629
|
-
if (!ptz) return;
|
|
25630
|
-
const z = direction === "in" ? 1 : -1;
|
|
25631
|
-
const s = speed ?? defaultSpeed;
|
|
25632
|
-
await wrap(async () => {
|
|
25633
|
-
await ptz.move({
|
|
25634
|
-
zoom: z,
|
|
25635
|
-
speed: s
|
|
25636
|
-
});
|
|
25637
|
-
await new Promise((r) => setTimeout(r, pulseMs));
|
|
25638
|
-
await ptz.stop({});
|
|
25639
|
-
});
|
|
25640
|
-
}, [
|
|
25641
|
-
ptz,
|
|
25642
|
-
defaultSpeed,
|
|
25643
|
-
pulseMs,
|
|
25644
|
-
wrap
|
|
25645
|
-
]),
|
|
25646
|
-
goHome: (0, react$1.useCallback)(async () => {
|
|
25647
|
-
if (!ptz) return;
|
|
25648
|
-
await wrap(() => ptz.goHome({}));
|
|
25649
|
-
}, [ptz, wrap]),
|
|
25650
|
-
goToPreset: (0, react$1.useCallback)(async (presetId) => {
|
|
25651
|
-
if (!ptz) return;
|
|
25652
|
-
await wrap(() => ptz.goToPreset({ presetId }));
|
|
25653
|
-
}, [ptz, wrap]),
|
|
25654
|
-
presets,
|
|
25655
|
-
refreshPresets,
|
|
25656
|
-
busy,
|
|
25657
|
-
error
|
|
25658
|
-
};
|
|
25659
|
-
}
|
|
25660
|
-
//#endregion
|
|
25661
26614
|
//#region src/hooks/use-doorbell-events.ts
|
|
25662
26615
|
/**
|
|
25663
26616
|
* useDoorbellEvents — subscribe to doorbell.onPressed across the cluster.
|
|
@@ -25885,6 +26838,7 @@ exports.AppShell = AppShell;
|
|
|
25885
26838
|
exports.AudioClassificationList = AudioClassificationList;
|
|
25886
26839
|
exports.AudioLevelWaveform = AudioLevelWaveform;
|
|
25887
26840
|
exports.AudioWaveform = AudioWaveform;
|
|
26841
|
+
exports.AutotrackSection = AutotrackSection;
|
|
25888
26842
|
exports.BTN_COMPACT = BTN_COMPACT;
|
|
25889
26843
|
exports.BTN_COMPACT_DANGER = BTN_COMPACT_DANGER;
|
|
25890
26844
|
exports.BTN_COMPACT_PRIMARY = BTN_COMPACT_PRIMARY;
|
|
@@ -25947,6 +26901,7 @@ exports.FormField = FormField$1;
|
|
|
25947
26901
|
exports.GRID_GAP = GRID_GAP;
|
|
25948
26902
|
exports.GRID_PAIRED = GRID_PAIRED;
|
|
25949
26903
|
exports.GRID_QUICK_STATS = GRID_QUICK_STATS;
|
|
26904
|
+
exports.HOST_WIDGETS = HOST_WIDGETS;
|
|
25950
26905
|
exports.INPUT_COMPACT = INPUT_COMPACT;
|
|
25951
26906
|
exports.IconButton = IconButton;
|
|
25952
26907
|
exports.ImageSelector = ImageSelector;
|
|
@@ -25959,6 +26914,7 @@ exports.Label = Label;
|
|
|
25959
26914
|
exports.LogStream = LogStream;
|
|
25960
26915
|
exports.LoginForm = LoginForm;
|
|
25961
26916
|
exports.MobileDrawer = MobileDrawer;
|
|
26917
|
+
exports.MotionZonesSettings = MotionZonesSettings;
|
|
25962
26918
|
exports.NodeMultiSelectField = NodeMultiSelectField;
|
|
25963
26919
|
exports.NodePicker = NodePicker;
|
|
25964
26920
|
exports.NodeSelectField = NodeSelectField;
|
|
@@ -25975,6 +26931,7 @@ exports.Popover = Popover;
|
|
|
25975
26931
|
exports.PopoverContent = PopoverContent;
|
|
25976
26932
|
exports.PopoverTrigger = PopoverTrigger;
|
|
25977
26933
|
exports.ProviderBadge = ProviderBadge;
|
|
26934
|
+
exports.PtzPanel = PtzPanel;
|
|
25978
26935
|
exports.QrCode = QrCode;
|
|
25979
26936
|
exports.ResponseLog = ResponseLog;
|
|
25980
26937
|
exports.SECTION_BODY = SECTION_BODY;
|
|
@@ -26032,6 +26989,7 @@ exports.getClassColor = getClassColor;
|
|
|
26032
26989
|
exports.getPhaseVisual = getPhaseVisual;
|
|
26033
26990
|
exports.isFieldVisible = isFieldVisible;
|
|
26034
26991
|
exports.lightColors = require_theme_index.lightColors$1;
|
|
26992
|
+
exports.loadRemoteBundle = loadRemoteBundle;
|
|
26035
26993
|
exports.mirror = mirror;
|
|
26036
26994
|
exports.mountAddonPage = mountAddonPage;
|
|
26037
26995
|
exports.providerIcons = providerIcons;
|
|
@@ -26071,6 +27029,7 @@ exports.useAddonsRollbackPackage = useAddonsRollbackPackage;
|
|
|
26071
27029
|
exports.useAddonsSearchAvailable = useAddonsSearchAvailable;
|
|
26072
27030
|
exports.useAddonsSetAddonAutoUpdate = useAddonsSetAddonAutoUpdate;
|
|
26073
27031
|
exports.useAddonsSetAutoUpdateSettings = useAddonsSetAutoUpdateSettings;
|
|
27032
|
+
exports.useAddonsSetCapabilityProviderEnabled = useAddonsSetCapabilityProviderEnabled;
|
|
26074
27033
|
exports.useAddonsUninstallPackage = useAddonsUninstallPackage;
|
|
26075
27034
|
exports.useAddonsUpdateFrameworkPackage = useAddonsUpdateFrameworkPackage;
|
|
26076
27035
|
exports.useAddonsUpdatePackage = useAddonsUpdatePackage;
|
|
@@ -26104,8 +27063,6 @@ exports.useAudioCodecPushEncodedFrame = useAudioCodecPushEncodedFrame;
|
|
|
26104
27063
|
exports.useAudioCodecPushPcm = useAudioCodecPushPcm;
|
|
26105
27064
|
exports.useAudioMetricsGetCurrentSnapshot = useAudioMetricsGetCurrentSnapshot;
|
|
26106
27065
|
exports.useAudioMetricsGetHistory = useAudioMetricsGetHistory;
|
|
26107
|
-
exports.useAuthenticationListProviders = useAuthenticationListProviders;
|
|
26108
|
-
exports.useAuthenticationSetProviderEnabled = useAuthenticationSetProviderEnabled;
|
|
26109
27066
|
exports.useBackupDelete = useBackupDelete;
|
|
26110
27067
|
exports.useBackupGetEntries = useBackupGetEntries;
|
|
26111
27068
|
exports.useBackupList = useBackupList;
|
|
@@ -26130,11 +27087,14 @@ exports.useCustomFieldRenderer = useCustomFieldRenderer;
|
|
|
26130
27087
|
exports.useDebouncedString = useDebouncedString;
|
|
26131
27088
|
exports.useDecoderCreateSession = useDecoderCreateSession;
|
|
26132
27089
|
exports.useDecoderDestroySession = useDecoderDestroySession;
|
|
27090
|
+
exports.useDecoderGetFrame = useDecoderGetFrame;
|
|
26133
27091
|
exports.useDecoderGetInfo = useDecoderGetInfo;
|
|
27092
|
+
exports.useDecoderGetShmStats = useDecoderGetShmStats;
|
|
26134
27093
|
exports.useDecoderGetStats = useDecoderGetStats;
|
|
26135
27094
|
exports.useDecoderListActiveSessions = useDecoderListActiveSessions;
|
|
26136
27095
|
exports.useDecoderOpenStream = useDecoderOpenStream;
|
|
26137
27096
|
exports.useDecoderPullFrames = useDecoderPullFrames;
|
|
27097
|
+
exports.useDecoderPullHandles = useDecoderPullHandles;
|
|
26138
27098
|
exports.useDecoderPushPacket = useDecoderPushPacket;
|
|
26139
27099
|
exports.useDecoderReprobeHwaccel = useDecoderReprobeHwaccel;
|
|
26140
27100
|
exports.useDecoderSupportsCodec = useDecoderSupportsCodec;
|
|
@@ -26144,6 +27104,7 @@ exports.useDetectionPipelineGetDeviceLiveContribution = useDetectionPipelineGetD
|
|
|
26144
27104
|
exports.useDetectionPipelineGetDeviceSettingsContribution = useDetectionPipelineGetDeviceSettingsContribution;
|
|
26145
27105
|
exports.useDevShell = useDevShell;
|
|
26146
27106
|
exports.useDevice = useDevice;
|
|
27107
|
+
exports.useDeviceAutotrack = useDeviceAutotrack;
|
|
26147
27108
|
exports.useDeviceBattery = useDeviceBattery;
|
|
26148
27109
|
exports.useDeviceCapability = useDeviceCapability;
|
|
26149
27110
|
exports.useDeviceDetections = useDeviceDetections;
|
|
@@ -26277,12 +27238,6 @@ exports.useMeshNetworkListPeers = useMeshNetworkListPeers;
|
|
|
26277
27238
|
exports.useMeshNetworkLogout = useMeshNetworkLogout;
|
|
26278
27239
|
exports.useMeshNetworkStartLogin = useMeshNetworkStartLogin;
|
|
26279
27240
|
exports.useMeshNetworkTestConnection = useMeshNetworkTestConnection;
|
|
26280
|
-
exports.useMeshOrchestratorJoinProvider = useMeshOrchestratorJoinProvider;
|
|
26281
|
-
exports.useMeshOrchestratorLeaveProvider = useMeshOrchestratorLeaveProvider;
|
|
26282
|
-
exports.useMeshOrchestratorListProviderPeers = useMeshOrchestratorListProviderPeers;
|
|
26283
|
-
exports.useMeshOrchestratorListProviders = useMeshOrchestratorListProviders;
|
|
26284
|
-
exports.useMeshOrchestratorLogoutProvider = useMeshOrchestratorLogoutProvider;
|
|
26285
|
-
exports.useMeshOrchestratorStartLoginProvider = useMeshOrchestratorStartLoginProvider;
|
|
26286
27241
|
exports.useMetricsProviderCollectSnapshot = useMetricsProviderCollectSnapshot;
|
|
26287
27242
|
exports.useMetricsProviderGetAddonStats = useMetricsProviderGetAddonStats;
|
|
26288
27243
|
exports.useMetricsProviderGetCached = useMetricsProviderGetCached;
|
|
@@ -26304,6 +27259,9 @@ exports.useMotionGetStatus = useMotionGetStatus;
|
|
|
26304
27259
|
exports.useMotionIsDetected = useMotionIsDetected;
|
|
26305
27260
|
exports.useMotionTriggerGetStatus = useMotionTriggerGetStatus;
|
|
26306
27261
|
exports.useMotionTriggerSetMotionTrigger = useMotionTriggerSetMotionTrigger;
|
|
27262
|
+
exports.useMotionZonesGetOptions = useMotionZonesGetOptions;
|
|
27263
|
+
exports.useMotionZonesGetStatus = useMotionZonesGetStatus;
|
|
27264
|
+
exports.useMotionZonesSetZone = useMotionZonesSetZone;
|
|
26307
27265
|
exports.useMqttBrokerAddBroker = useMqttBrokerAddBroker;
|
|
26308
27266
|
exports.useMqttBrokerGetBrokerConfig = useMqttBrokerGetBrokerConfig;
|
|
26309
27267
|
exports.useMqttBrokerGetStatus = useMqttBrokerGetStatus;
|
|
@@ -26313,12 +27271,18 @@ exports.useMqttBrokerStartEmbeddedBroker = useMqttBrokerStartEmbeddedBroker;
|
|
|
26313
27271
|
exports.useMqttBrokerStopEmbeddedBroker = useMqttBrokerStopEmbeddedBroker;
|
|
26314
27272
|
exports.useMqttBrokerTestConnection = useMqttBrokerTestConnection;
|
|
26315
27273
|
exports.useNativeObjectDetectionGetStatus = useNativeObjectDetectionGetStatus;
|
|
27274
|
+
exports.useNetworkAccessGetEndpoint = useNetworkAccessGetEndpoint;
|
|
27275
|
+
exports.useNetworkAccessGetStatus = useNetworkAccessGetStatus;
|
|
27276
|
+
exports.useNetworkAccessListEndpoints = useNetworkAccessListEndpoints;
|
|
27277
|
+
exports.useNetworkAccessStart = useNetworkAccessStart;
|
|
27278
|
+
exports.useNetworkAccessStop = useNetworkAccessStop;
|
|
26316
27279
|
exports.useNetworkQualityGetAllStats = useNetworkQualityGetAllStats;
|
|
26317
27280
|
exports.useNetworkQualityGetDeviceStats = useNetworkQualityGetDeviceStats;
|
|
26318
27281
|
exports.useNetworkQualityReportClientStats = useNetworkQualityReportClientStats;
|
|
26319
27282
|
exports.useNodesClusterAddonStatus = useNodesClusterAddonStatus;
|
|
26320
27283
|
exports.useNodesDeployAddon = useNodesDeployAddon;
|
|
26321
27284
|
exports.useNodesExecuteQuery = useNodesExecuteQuery;
|
|
27285
|
+
exports.useNodesGetCapUsageGraph = useNodesGetCapUsageGraph;
|
|
26322
27286
|
exports.useNodesGetNodeAddons = useNodesGetNodeAddons;
|
|
26323
27287
|
exports.useNodesRenameNode = useNodesRenameNode;
|
|
26324
27288
|
exports.useNodesRestartAddon = useNodesRestartAddon;
|
|
@@ -26441,12 +27405,16 @@ exports.usePtzAutotrackGetStatus = usePtzAutotrackGetStatus;
|
|
|
26441
27405
|
exports.usePtzAutotrackSetEnabled = usePtzAutotrackSetEnabled;
|
|
26442
27406
|
exports.usePtzAutotrackSetSettings = usePtzAutotrackSetSettings;
|
|
26443
27407
|
exports.usePtzContinuousMove = usePtzContinuousMove;
|
|
27408
|
+
exports.usePtzDeletePreset = usePtzDeletePreset;
|
|
27409
|
+
exports.usePtzGetOptions = usePtzGetOptions;
|
|
26444
27410
|
exports.usePtzGetPosition = usePtzGetPosition;
|
|
26445
27411
|
exports.usePtzGetPresets = usePtzGetPresets;
|
|
26446
27412
|
exports.usePtzGetStatus = usePtzGetStatus;
|
|
26447
27413
|
exports.usePtzGoHome = usePtzGoHome;
|
|
26448
27414
|
exports.usePtzGoToPreset = usePtzGoToPreset;
|
|
26449
27415
|
exports.usePtzMove = usePtzMove;
|
|
27416
|
+
exports.usePtzSavePreset = usePtzSavePreset;
|
|
27417
|
+
exports.usePtzSetAutofocus = usePtzSetAutofocus;
|
|
26450
27418
|
exports.usePtzStop = usePtzStop;
|
|
26451
27419
|
exports.useRebootReboot = useRebootReboot;
|
|
26452
27420
|
exports.useRecordingEngineDisable = useRecordingEngineDisable;
|
|
@@ -26470,9 +27438,7 @@ exports.useRecordingEngineUpdateRetentionConfig = useRecordingEngineUpdateRetent
|
|
|
26470
27438
|
exports.useRecordingGetPlaybackUrl = useRecordingGetPlaybackUrl;
|
|
26471
27439
|
exports.useRecordingGetSegments = useRecordingGetSegments;
|
|
26472
27440
|
exports.useRecordingGetThumbnailAt = useRecordingGetThumbnailAt;
|
|
26473
|
-
exports.
|
|
26474
|
-
exports.useRemoteAccessStartProvider = useRemoteAccessStartProvider;
|
|
26475
|
-
exports.useRemoteAccessStopProvider = useRemoteAccessStopProvider;
|
|
27441
|
+
exports.useRemoteComponent = useRemoteComponent;
|
|
26476
27442
|
exports.useSettingsStoreCount = useSettingsStoreCount;
|
|
26477
27443
|
exports.useSettingsStoreDeclareCollection = useSettingsStoreDeclareCollection;
|
|
26478
27444
|
exports.useSettingsStoreDelete = useSettingsStoreDelete;
|
|
@@ -26514,7 +27480,6 @@ exports.useStorageWriteChunk = useStorageWriteChunk;
|
|
|
26514
27480
|
exports.useStreamBrokerApplyDeviceSettingsPatch = useStreamBrokerApplyDeviceSettingsPatch;
|
|
26515
27481
|
exports.useStreamBrokerAssignProfile = useStreamBrokerAssignProfile;
|
|
26516
27482
|
exports.useStreamBrokerGetAllRtspEntries = useStreamBrokerGetAllRtspEntries;
|
|
26517
|
-
exports.useStreamBrokerGetBroker = useStreamBrokerGetBroker;
|
|
26518
27483
|
exports.useStreamBrokerGetBrokerStats = useStreamBrokerGetBrokerStats;
|
|
26519
27484
|
exports.useStreamBrokerGetDeviceLiveContribution = useStreamBrokerGetDeviceLiveContribution;
|
|
26520
27485
|
exports.useStreamBrokerGetDeviceSettingsContribution = useStreamBrokerGetDeviceSettingsContribution;
|
|
@@ -26529,13 +27494,23 @@ exports.useStreamBrokerListAllCameraStreams = useStreamBrokerListAllCameraStream
|
|
|
26529
27494
|
exports.useStreamBrokerListAllProfileSlots = useStreamBrokerListAllProfileSlots;
|
|
26530
27495
|
exports.useStreamBrokerListClients = useStreamBrokerListClients;
|
|
26531
27496
|
exports.useStreamBrokerPublishCameraStream = useStreamBrokerPublishCameraStream;
|
|
27497
|
+
exports.useStreamBrokerPullAudioChunks = useStreamBrokerPullAudioChunks;
|
|
27498
|
+
exports.useStreamBrokerPullFrameHandles = useStreamBrokerPullFrameHandles;
|
|
26532
27499
|
exports.useStreamBrokerRegenerateRtspToken = useStreamBrokerRegenerateRtspToken;
|
|
26533
27500
|
exports.useStreamBrokerReleaseStreamWithCodec = useStreamBrokerReleaseStreamWithCodec;
|
|
26534
27501
|
exports.useStreamBrokerRestartProfile = useStreamBrokerRestartProfile;
|
|
26535
27502
|
exports.useStreamBrokerRetractCameraStream = useStreamBrokerRetractCameraStream;
|
|
26536
27503
|
exports.useStreamBrokerSetPreBufferDuration = useStreamBrokerSetPreBufferDuration;
|
|
26537
27504
|
exports.useStreamBrokerSetRtspEnabled = useStreamBrokerSetRtspEnabled;
|
|
27505
|
+
exports.useStreamBrokerSubscribeAudioChunks = useStreamBrokerSubscribeAudioChunks;
|
|
27506
|
+
exports.useStreamBrokerSubscribeFrames = useStreamBrokerSubscribeFrames;
|
|
26538
27507
|
exports.useStreamBrokerUnassignProfile = useStreamBrokerUnassignProfile;
|
|
27508
|
+
exports.useStreamBrokerUnsubscribeAudioChunks = useStreamBrokerUnsubscribeAudioChunks;
|
|
27509
|
+
exports.useStreamBrokerUnsubscribeFrames = useStreamBrokerUnsubscribeFrames;
|
|
27510
|
+
exports.useStreamParamsGetConfigSchema = useStreamParamsGetConfigSchema;
|
|
27511
|
+
exports.useStreamParamsGetOptions = useStreamParamsGetOptions;
|
|
27512
|
+
exports.useStreamParamsGetStatus = useStreamParamsGetStatus;
|
|
27513
|
+
exports.useStreamParamsSetProfile = useStreamParamsSetProfile;
|
|
26539
27514
|
exports.useSwitchGetStatus = useSwitchGetStatus;
|
|
26540
27515
|
exports.useSwitchSetState = useSwitchSetState;
|
|
26541
27516
|
exports.useSystem = useSystem;
|
|
@@ -26550,9 +27525,6 @@ exports.useSystemQuery = useSystemQuery;
|
|
|
26550
27525
|
exports.useSystemSetRetentionConfig = useSystemSetRetentionConfig;
|
|
26551
27526
|
exports.useThemeMode = require_theme_index.useThemeMode$1;
|
|
26552
27527
|
exports.useToastOnToast = useToastOnToast;
|
|
26553
|
-
exports.useTurnOrchestratorGetAllServers = useTurnOrchestratorGetAllServers;
|
|
26554
|
-
exports.useTurnOrchestratorListProviders = useTurnOrchestratorListProviders;
|
|
26555
|
-
exports.useTurnOrchestratorSetProviderEnabled = useTurnOrchestratorSetProviderEnabled;
|
|
26556
27528
|
exports.useTurnProviderGetTurnServers = useTurnProviderGetTurnServers;
|
|
26557
27529
|
exports.useUserManagementConfirmTotp = useUserManagementConfirmTotp;
|
|
26558
27530
|
exports.useUserManagementCreateApiKey = useUserManagementCreateApiKey;
|
|
@@ -26562,10 +27534,16 @@ exports.useUserManagementDeleteUser = useUserManagementDeleteUser;
|
|
|
26562
27534
|
exports.useUserManagementDisableTotp = useUserManagementDisableTotp;
|
|
26563
27535
|
exports.useUserManagementGetTotpStatus = useUserManagementGetTotpStatus;
|
|
26564
27536
|
exports.useUserManagementListApiKeys = useUserManagementListApiKeys;
|
|
27537
|
+
exports.useUserManagementListOauthSessions = useUserManagementListOauthSessions;
|
|
26565
27538
|
exports.useUserManagementListScopedTokens = useUserManagementListScopedTokens;
|
|
26566
27539
|
exports.useUserManagementListUsers = useUserManagementListUsers;
|
|
27540
|
+
exports.useUserManagementOauthExchangeCode = useUserManagementOauthExchangeCode;
|
|
27541
|
+
exports.useUserManagementOauthIssueCode = useUserManagementOauthIssueCode;
|
|
27542
|
+
exports.useUserManagementOauthRefresh = useUserManagementOauthRefresh;
|
|
27543
|
+
exports.useUserManagementOauthVerifyAccessToken = useUserManagementOauthVerifyAccessToken;
|
|
26567
27544
|
exports.useUserManagementResetPassword = useUserManagementResetPassword;
|
|
26568
27545
|
exports.useUserManagementRevokeApiKey = useUserManagementRevokeApiKey;
|
|
27546
|
+
exports.useUserManagementRevokeOauthSession = useUserManagementRevokeOauthSession;
|
|
26569
27547
|
exports.useUserManagementRevokeScopedToken = useUserManagementRevokeScopedToken;
|
|
26570
27548
|
exports.useUserManagementSetUserScopes = useUserManagementSetUserScopes;
|
|
26571
27549
|
exports.useUserManagementSetupTotp = useUserManagementSetupTotp;
|