@camstack/addon-pipeline 1.2.137 → 1.2.139

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.
Files changed (40) hide show
  1. package/dist/audio-analyzer/index.js +38 -86
  2. package/dist/audio-analyzer/index.mjs +36 -84
  3. package/dist/detection-pipeline/index.js +12 -12
  4. package/dist/detection-pipeline/index.mjs +5 -5
  5. package/dist/{dist-g0Or5_pR.mjs → dist-CTmOaCwF.mjs} +163 -3
  6. package/dist/{dist-4EMKvxCp.js → dist-WUZH-lAM.js} +168 -2
  7. package/dist/{event-loop-stall-monitor-CskL9Lgu.mjs → event-loop-stall-monitor-BMtR9qZR.mjs} +1 -1
  8. package/dist/{event-loop-stall-monitor-DDm07Ei6.js → event-loop-stall-monitor-CoM21UjL.js} +1 -1
  9. package/dist/{lazy-sharp-BlA9PxWk.js → lazy-sharp-1WF7ZnxZ.js} +1 -1
  10. package/dist/motion-wasm/index.js +2 -2
  11. package/dist/motion-wasm/index.mjs +1 -1
  12. package/dist/{node-BOHn8gpk.js → node-BAo2ivVu.js} +1 -1
  13. package/dist/{node-Bo1g8hLO.mjs → node-CQcgxO8l.mjs} +1 -1
  14. package/dist/pipeline-runner/index.js +55 -12
  15. package/dist/pipeline-runner/index.mjs +54 -11
  16. package/dist/{process-memory-DmrHWybb.mjs → process-memory-BPEKERfe.mjs} +1 -1
  17. package/dist/{process-memory-C3084DOt.js → process-memory-BZvkQCLP.js} +1 -1
  18. package/dist/recorder/index.js +6 -6
  19. package/dist/recorder/index.mjs +3 -3
  20. package/dist/{segment-demux-js-BrQ_A0oi.js → segment-demux-js-B1L1pfoc.js} +1 -1
  21. package/dist/{segment-demux-js-C-a2pghM.mjs → segment-demux-js-CubhFNwb.mjs} +1 -1
  22. package/dist/session-decode/decode-worker-child.js +115 -5
  23. package/dist/session-decode/decode-worker-child.mjs +114 -4
  24. package/dist/stream-broker/_stub.js +2 -2
  25. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-AH6OJ3hJ.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DJ4k5ZBb.mjs} +3 -3
  26. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-xdmO8wYp.mjs +26 -0
  27. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DPpl_mtg.mjs +26 -0
  28. package/dist/stream-broker/demux-worker-child.js +1 -1
  29. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  30. package/dist/stream-broker/{hostInit-XNyn-FTM.mjs → hostInit-oy-XDnw6.mjs} +3 -3
  31. package/dist/stream-broker/index.js +170 -62
  32. package/dist/stream-broker/index.mjs +170 -62
  33. package/dist/stream-broker/remoteEntry.js +1 -1
  34. package/dist/{worker-protocol-BLfaD5uC.js → worker-protocol-C-0LYdSt.js} +2 -1
  35. package/dist/{worker-protocol-vc8WWhjx.mjs → worker-protocol-DsuI4ifI.mjs} +2 -1
  36. package/package.json +5 -3
  37. package/dist/addon-utils-C6b5Tnia.js +0 -625
  38. package/dist/addon-utils-C8KTuPdE.mjs +0 -583
  39. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CQyiEd0l.mjs +0 -26
  40. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BrTM_tsb.mjs +0 -26
@@ -5932,6 +5932,40 @@ var BaseAddon = class {
5932
5932
  deviceSettingsSchema() {
5933
5933
  return null;
5934
5934
  }
5935
+ /**
5936
+ * INTEGRATION-LEVEL SETTINGS — declare which of this addon's global sections
5937
+ * ARE the configuration of its integration.
5938
+ *
5939
+ * Return the `ConfigSection.id`s, from {@link globalSettingsSchema}, that an
5940
+ * operator should find on the addon's integration page (System →
5941
+ * Integrations → <name>) rather than only in the cluster-wide list of every
5942
+ * addon. Empty (the default) means the addon has no integration-level
5943
+ * settings and no such surface is offered — this is opt-in, because whether
5944
+ * an addon's configuration IS its integration's configuration depends on the
5945
+ * nature of the integration.
5946
+ *
5947
+ * WHAT THIS IS NOT. It is not a scope. The selected sections keep living in
5948
+ * the ONE global schema, in the ONE addon store, written by the ONE
5949
+ * `updateGlobalSettings` path. There is deliberately no
5950
+ * `updateIntegrationSettings`: a second write path is how a surface acquires
5951
+ * a second store key, and this repo has shipped that twice (`btmPath@hub`,
5952
+ * D266). Selecting sections cannot introduce a key that selecting cannot.
5953
+ *
5954
+ * WHY IT IS A LIST OF SECTION IDS AND NOT A MARKER ON THE SECTION.
5955
+ * `ConfigFieldBase` used to carry `scope?: 'device' | 'global'` and it was
5956
+ * removed with the reason recorded at
5957
+ * `packages/types/src/interfaces/config-ui.ts:249` — *"a field's scope is
5958
+ * determined by WHICH schema it lives in, not by a field-level marker."* A
5959
+ * marker sprinkled across sections also has to borrow a field that already
5960
+ * means something else; borrowing `section.tab` put the literal word
5961
+ * "integration" into an operator-facing tab bar, because `tab` means "how to
5962
+ * GROUP this visually" and cannot also mean "where this lives" (D269
5963
+ * supersedes D268). One declaration, in one place, next to the schema whose
5964
+ * ids it names.
5965
+ */
5966
+ integrationSettingSections() {
5967
+ return [];
5968
+ }
5935
5969
  async getGlobalSettings(overlay, cap, nodeId) {
5936
5970
  const schema = this.globalSettingsSchema(cap);
5937
5971
  if (!schema) return { sections: [] };
@@ -5942,6 +5976,55 @@ var BaseAddon = class {
5942
5976
  } : projected);
5943
5977
  }
5944
5978
  /**
5979
+ * The integration-level view of this addon's settings: exactly the sections
5980
+ * named by {@link integrationSettingSections}, hydrated from the SAME store
5981
+ * `getGlobalSettings` reads, and narrowed to cluster-scoped fields.
5982
+ *
5983
+ * Returns `null` when the addon declared nothing — an addon that opts out has
5984
+ * no integration settings surface at all, rather than an empty one that reads
5985
+ * as a failed load.
5986
+ *
5987
+ * Three properties hold BY CONSTRUCTION, which is why they are here in core
5988
+ * and not in whichever UI happens to render this:
5989
+ *
5990
+ * 1. **One key.** The payload is a SUBSET of the global schema, so a field
5991
+ * shown here is the same field, with the same bare key, that the addon's
5992
+ * own page shows. There is no integration-specific writer — callers save
5993
+ * through `updateGlobalSettings` — so a second store key is unreachable,
5994
+ * not merely discouraged.
5995
+ * 2. **No node scope.** `perNode: true` fields are DROPPED. Their store key
5996
+ * is `<key>@<nodeId>` and an integration is not a node; whichever node
5997
+ * such a field silently picked would be a wrong answer for the operator
5998
+ * who opened the page (D266).
5999
+ * 3. **No silent typo.** A declared id that names no section throws. The
6000
+ * alternative — skip it — turns a rename into a surface that quietly
6001
+ * empties, which looks exactly like an addon with nothing to configure.
6002
+ */
6003
+ async getIntegrationSettings(nodeId) {
6004
+ const declared = this.integrationSettingSections();
6005
+ if (declared.length === 0) return null;
6006
+ const schema = this.globalSettingsSchema();
6007
+ if (!schema) throw new Error(`${this.constructor.name}: integrationSettingSections() names [${declared.join(", ")}] but globalSettingsSchema() returns null.`);
6008
+ const byId = new Map(schema.sections.map((section) => [section.id, section]));
6009
+ const sections = [];
6010
+ for (const id of declared) {
6011
+ const section = byId.get(id);
6012
+ if (!section) throw new Error(`${this.constructor.name}: integrationSettingSections() names unknown section "${id}". Known sections: [${[...byId.keys()].join(", ")}].`);
6013
+ const fields = dropPerNodeFields(section.fields);
6014
+ if (fields.length === 0) continue;
6015
+ sections.push({
6016
+ ...section,
6017
+ fields
6018
+ });
6019
+ }
6020
+ if (sections.length === 0) return null;
6021
+ const projected = await this.resolveGlobalStore(nodeId);
6022
+ return hydrateSchema({
6023
+ ...schema,
6024
+ sections
6025
+ }, projected);
6026
+ }
6027
+ /**
5945
6028
  * The raw addon store PROJECTED onto the target node's bare per-node keys:
5946
6029
  * every `perNode: true` field carries THAT node's scoped value on its bare
5947
6030
  * key (absent scoped key ⇒ key absent, so the schema `default` wins — no
@@ -6245,6 +6328,41 @@ var BaseAddon = class {
6245
6328
  * `hydrateSchema` does. Valueless structural fields (separator/info/…)
6246
6329
  * don't declare `perNode` and are excluded by the `in` narrowing.
6247
6330
  */
6331
+ /**
6332
+ * The same fields with every `perNode: true` one removed, recursing into layout
6333
+ * containers exactly as {@link collectPerNodeFieldKeys} does. A container left
6334
+ * with no child is dropped rather than rendered empty.
6335
+ *
6336
+ * Used by `getIntegrationSettings`: an integration is not a node, so a field
6337
+ * whose store key is `<key>@<nodeId>` has no node to belong to there.
6338
+ */
6339
+ function dropPerNodeFields(fields) {
6340
+ const kept = [];
6341
+ for (const field of fields) {
6342
+ if (field.type === "group") {
6343
+ const inner = dropPerNodeFields(field.fields);
6344
+ if (inner.length > 0) kept.push({
6345
+ ...field,
6346
+ fields: inner
6347
+ });
6348
+ continue;
6349
+ }
6350
+ if (field.type === "sub-tabs") {
6351
+ const tabs = field.tabs.map((tab) => ({
6352
+ ...tab,
6353
+ fields: dropPerNodeFields(tab.fields)
6354
+ })).filter((tab) => tab.fields.length > 0);
6355
+ if (tabs.length > 0) kept.push({
6356
+ ...field,
6357
+ tabs
6358
+ });
6359
+ continue;
6360
+ }
6361
+ if ("perNode" in field && field.perNode === true) continue;
6362
+ kept.push(field);
6363
+ }
6364
+ return kept;
6365
+ }
6248
6366
  function collectPerNodeFieldKeys(fields) {
6249
6367
  const collected = [];
6250
6368
  for (const field of fields) {
@@ -7173,6 +7291,40 @@ method(object({ deviceId: number() }), array(StreamSourceEntrySchema)), method(o
7173
7291
  input: unknown()
7174
7292
  }), unknown(), { kind: "mutation" }), method(object({ deviceId: number() }), _void(), { kind: "mutation" }), method(object({ deviceId: number() }), unknown().nullable()), method(object({ deviceId: number() }), RawStateResultSchema.nullable(), { auth: "protected" });
7175
7293
  /**
7294
+ * The ONE retry schedule for work that can fail simply because the node was
7295
+ * still booting.
7296
+ *
7297
+ * The measured failure (docs/design/boot-to-usable-camera.md, item E): a hub
7298
+ * boot that serialised its addon graph starved every driver dial on it. Fifteen
7299
+ * Hikvision devices logged `Initial probe did not complete — scheduling retry
7300
+ * chain {firstRetryMs=300000}` and then sat unprobed for five minutes, long
7301
+ * after the condition that caused the failure had gone. The Tailscale addon's
7302
+ * auto-rejoin carried a byte-identical schedule, armed from `initialize()`, for
7303
+ * the same reason.
7304
+ *
7305
+ * The shape that fixes it: **two cheap attempts first, then the long tail.**
7306
+ *
7307
+ * - 15 s / 60 s — the boot window. Anything that failed because the node was
7308
+ * busy is recovered here, before an operator can open the UI and see a
7309
+ * camera that is wrong about itself.
7310
+ * - 5 m → 90 m — the genuinely-offline case. A battery cam that is asleep, a
7311
+ * VLAN that is down, a peer that has not come back. Unchanged in intent from
7312
+ * the schedule this replaces (~3 h 30 m of coverage): here ~3 h 21 m, then
7313
+ * the chain ends and manual re-arm (reprobe / restart) is the only path.
7314
+ *
7315
+ * Bounded on purpose — see D6. A chain that never gives up is a busy loop with
7316
+ * extra steps.
7317
+ */
7318
+ var BOOT_RECOVERY_BACKOFF_MS = [
7319
+ 15e3,
7320
+ 6e4,
7321
+ 5 * 6e4,
7322
+ 15 * 6e4,
7323
+ 30 * 6e4,
7324
+ 60 * 6e4,
7325
+ 90 * 6e4
7326
+ ];
7327
+ /**
7176
7328
  * Type-safe JSON parsing helpers.
7177
7329
  *
7178
7330
  * `JSON.parse` is typed as `any` in lib.es5.d.ts, which triggers
@@ -10708,6 +10860,9 @@ method(object({
10708
10860
  kind: "mutation",
10709
10861
  auth: "admin"
10710
10862
  }), method(object({
10863
+ addonId: string(),
10864
+ nodeId: string().optional()
10865
+ }), SettingsSchemaWithValuesSchema.nullable()), method(object({
10711
10866
  addonId: string(),
10712
10867
  deviceId: number(),
10713
10868
  nodeId: string().optional()
@@ -11095,8 +11250,7 @@ var AUDIO_BACKEND_CHOICES = [
11095
11250
  ];
11096
11251
  var DEFAULT_AUDIO_ANALYZER_CONFIG = {
11097
11252
  audioBackend: "auto",
11098
- probedBestAudioBackend: "",
11099
- selectedAudioModel: ""
11253
+ probedBestAudioBackend: ""
11100
11254
  };
11101
11255
  var audioAnalyzerCapability = {
11102
11256
  name: "audio-analyzer",
@@ -30756,6 +30910,12 @@ Object.freeze({
30756
30910
  addonId: null,
30757
30911
  access: "view"
30758
30912
  },
30913
+ "addonSettings.getIntegrationSettings": {
30914
+ capName: "addon-settings",
30915
+ capScope: "system",
30916
+ addonId: null,
30917
+ access: "view"
30918
+ },
30759
30919
  "addonSettings.updateDeviceSettings": {
30760
30920
  capName: "addon-settings",
30761
30921
  capScope: "system",
@@ -39711,4 +39871,4 @@ function enumerateInferenceDevices(hw) {
39711
39871
  return out;
39712
39872
  }
39713
39873
  //#endregion
39714
- export { loadContributionCapability as $, union as $t, audioAnalysisCapability as A, CAM_PROFILE_ORDER as At, defineCustomActions as B, parseProfileBrokerId as Bt, RATE_CONTROL_RELAXED as C, errMsg as Ct, RingBuffer as D, invocationFromEncodeProfile as Dt, RecordingConfigSchema as E, buildFfmpegArgs as Et, createHwAccelCache as F, isEvent as Ft, egressTranscodeSharingKey as G, boolean as Gt, deriveDetailCropRect as H, sleep as Ht, createLogChannelsProvider as I, makeProfileBrokerId as It, evaluateSensorEdge as J, literal as Jt, egressTransportFromRequest as K, discriminatedUnion as Kt, customAction as L, makeSourceBrokerId as Lt, audioKindId as M, DeviceType as Mt, batteryCapability as N, createEvent as Nt, YAMNET_TO_MACRO as O, isSoftwareDecode as Ot, cameraStreamsCapability as P, hydrateSchema as Pt, isFirstLevelMacroClass as Q, string as Qt, declareLogChannel as R, nodePin as Rt, PoolMemoryWatchdog as S, webrtcSessionCapability as St, RECORDING_EXPORT_MAX_READ_BYTES as T, Fmp4BoxSplitter as Tt, deriveRecordingMode as U, _enum as Ut, deriveBatteryPresence as V, selectAssignedProfileSlots as Vt, detectionPipelineCapability as W, array as Wt, hfModelUrl as X, object as Xt, evaluateZoneRules as Y, number as Yt, inferModelProvider as Z, record as Zt, ExportRecordSchema as _, resolveRecordingProfiles as _t, COCO_80_LABELS as a, parseProcStatus as at, NativeLeaseSettingsSchema as b, streamBrokerCapability as bt, DEFAULT_CLUSTER_STEP_MODELS as c, pickDetailCropConvention as ct, DEFAULT_EVENTS_BAND_BUFFER_SEC as d, pipelineRunnerCapability as dt, EventCategory as en, logChannelsCapability as et, DEFAULT_FIRST_SIGHTING_FRESHNESS_MS as f, recordingCapability as ft, EncodeProfileSchema as g, resolvePoolMemoryPolicy as gt, EVENT_PAD_MS as h, resolveEgressDecodeHwAccel as ht, BatteryStatusSchema as i, overlayClusterStepSettings as it, audioAnalyzerCapability as j, DeviceFeature as jt, addonWidgetsSourceCapability as k, BaseAddon as kt, DEFAULT_CLUSTER_STEP_SETTINGS as l, pickNativeLeaseOverride as lt, DEVICE_BACKEND_TO_FORMAT as m, resolveClusterStepModelId as mt, AUDIO_BACKEND_CHOICES as n, maskUrlCredentials as nt, COCO_TO_MACRO as o, pickClusterStepModels as ot, DEFAULT_NATIVE_LEASE_SETTINGS as p, recordingExportCapability as pt, enumerateInferenceDevices as q, lazy as qt, AUDIO_MACRO_LABELS as r, motionDetectionCapability as rt, DEFAULT_AUDIO_ANALYZER_CONFIG as s, pickClusterStepSettings as st, APPLE_SA_TO_MACRO as t, mapAudioLabelToMacro as tt, DEFAULT_DETAIL_CROP_CONVENTION as u, pipelineExecutorCapability as ut, HF_BASE_URL as v, runtimeDevices as vt, RATE_CONTROL_TIGHT as w, AUDIO_PRESETS as wt, OpsLogEntrySchema as x, supportedRuntimes as xt, NativeLeaseAdmissionSchema as y, storageEvictableCapability as yt, defaultDeviceFor as z, parseJsonUnknown as zt };
39874
+ export { loadContributionCapability as $, string as $t, audioAnalysisCapability as A, BaseAddon as At, defineCustomActions as B, parseJsonUnknown as Bt, RATE_CONTROL_RELAXED as C, errMsg as Ct, RingBuffer as D, invocationFromEncodeProfile as Dt, RecordingConfigSchema as E, buildFfmpegArgs as Et, createHwAccelCache as F, hydrateSchema as Ft, egressTranscodeSharingKey as G, array as Gt, deriveDetailCropRect as H, selectAssignedProfileSlots as Ht, createLogChannelsProvider as I, isEvent as It, evaluateSensorEdge as J, lazy as Jt, egressTransportFromRequest as K, boolean as Kt, customAction as L, makeProfileBrokerId as Lt, audioKindId as M, DeviceFeature as Mt, batteryCapability as N, DeviceType as Nt, YAMNET_TO_MACRO as O, isSoftwareDecode as Ot, cameraStreamsCapability as P, createEvent as Pt, isFirstLevelMacroClass as Q, record as Qt, declareLogChannel as R, makeSourceBrokerId as Rt, PoolMemoryWatchdog as S, webrtcSessionCapability as St, RECORDING_EXPORT_MAX_READ_BYTES as T, Fmp4BoxSplitter as Tt, deriveRecordingMode as U, sleep as Ut, deriveBatteryPresence as V, parseProfileBrokerId as Vt, detectionPipelineCapability as W, _enum as Wt, hfModelUrl as X, number as Xt, evaluateZoneRules as Y, literal as Yt, inferModelProvider as Z, object as Zt, ExportRecordSchema as _, resolveRecordingProfiles as _t, COCO_80_LABELS as a, parseProcStatus as at, NativeLeaseSettingsSchema as b, streamBrokerCapability as bt, DEFAULT_CLUSTER_STEP_MODELS as c, pickDetailCropConvention as ct, DEFAULT_EVENTS_BAND_BUFFER_SEC as d, pipelineRunnerCapability as dt, union as en, logChannelsCapability as et, DEFAULT_FIRST_SIGHTING_FRESHNESS_MS as f, recordingCapability as ft, EncodeProfileSchema as g, resolvePoolMemoryPolicy as gt, EVENT_PAD_MS as h, resolveEgressDecodeHwAccel as ht, BatteryStatusSchema as i, overlayClusterStepSettings as it, audioAnalyzerCapability as j, CAM_PROFILE_ORDER as jt, addonWidgetsSourceCapability as k, BOOT_RECOVERY_BACKOFF_MS as kt, DEFAULT_CLUSTER_STEP_SETTINGS as l, pickNativeLeaseOverride as lt, DEVICE_BACKEND_TO_FORMAT as m, resolveClusterStepModelId as mt, AUDIO_BACKEND_CHOICES as n, maskUrlCredentials as nt, COCO_TO_MACRO as o, pickClusterStepModels as ot, DEFAULT_NATIVE_LEASE_SETTINGS as p, recordingExportCapability as pt, enumerateInferenceDevices as q, discriminatedUnion as qt, AUDIO_MACRO_LABELS as r, motionDetectionCapability as rt, DEFAULT_AUDIO_ANALYZER_CONFIG as s, pickClusterStepSettings as st, APPLE_SA_TO_MACRO as t, EventCategory as tn, mapAudioLabelToMacro as tt, DEFAULT_DETAIL_CROP_CONVENTION as u, pipelineExecutorCapability as ut, HF_BASE_URL as v, runtimeDevices as vt, RATE_CONTROL_TIGHT as w, AUDIO_PRESETS as wt, OpsLogEntrySchema as x, supportedRuntimes as xt, NativeLeaseAdmissionSchema as y, storageEvictableCapability as yt, defaultDeviceFor as z, nodePin as zt };
@@ -5955,6 +5955,40 @@ var BaseAddon = class {
5955
5955
  deviceSettingsSchema() {
5956
5956
  return null;
5957
5957
  }
5958
+ /**
5959
+ * INTEGRATION-LEVEL SETTINGS — declare which of this addon's global sections
5960
+ * ARE the configuration of its integration.
5961
+ *
5962
+ * Return the `ConfigSection.id`s, from {@link globalSettingsSchema}, that an
5963
+ * operator should find on the addon's integration page (System →
5964
+ * Integrations → <name>) rather than only in the cluster-wide list of every
5965
+ * addon. Empty (the default) means the addon has no integration-level
5966
+ * settings and no such surface is offered — this is opt-in, because whether
5967
+ * an addon's configuration IS its integration's configuration depends on the
5968
+ * nature of the integration.
5969
+ *
5970
+ * WHAT THIS IS NOT. It is not a scope. The selected sections keep living in
5971
+ * the ONE global schema, in the ONE addon store, written by the ONE
5972
+ * `updateGlobalSettings` path. There is deliberately no
5973
+ * `updateIntegrationSettings`: a second write path is how a surface acquires
5974
+ * a second store key, and this repo has shipped that twice (`btmPath@hub`,
5975
+ * D266). Selecting sections cannot introduce a key that selecting cannot.
5976
+ *
5977
+ * WHY IT IS A LIST OF SECTION IDS AND NOT A MARKER ON THE SECTION.
5978
+ * `ConfigFieldBase` used to carry `scope?: 'device' | 'global'` and it was
5979
+ * removed with the reason recorded at
5980
+ * `packages/types/src/interfaces/config-ui.ts:249` — *"a field's scope is
5981
+ * determined by WHICH schema it lives in, not by a field-level marker."* A
5982
+ * marker sprinkled across sections also has to borrow a field that already
5983
+ * means something else; borrowing `section.tab` put the literal word
5984
+ * "integration" into an operator-facing tab bar, because `tab` means "how to
5985
+ * GROUP this visually" and cannot also mean "where this lives" (D269
5986
+ * supersedes D268). One declaration, in one place, next to the schema whose
5987
+ * ids it names.
5988
+ */
5989
+ integrationSettingSections() {
5990
+ return [];
5991
+ }
5958
5992
  async getGlobalSettings(overlay, cap, nodeId) {
5959
5993
  const schema = this.globalSettingsSchema(cap);
5960
5994
  if (!schema) return { sections: [] };
@@ -5965,6 +5999,55 @@ var BaseAddon = class {
5965
5999
  } : projected);
5966
6000
  }
5967
6001
  /**
6002
+ * The integration-level view of this addon's settings: exactly the sections
6003
+ * named by {@link integrationSettingSections}, hydrated from the SAME store
6004
+ * `getGlobalSettings` reads, and narrowed to cluster-scoped fields.
6005
+ *
6006
+ * Returns `null` when the addon declared nothing — an addon that opts out has
6007
+ * no integration settings surface at all, rather than an empty one that reads
6008
+ * as a failed load.
6009
+ *
6010
+ * Three properties hold BY CONSTRUCTION, which is why they are here in core
6011
+ * and not in whichever UI happens to render this:
6012
+ *
6013
+ * 1. **One key.** The payload is a SUBSET of the global schema, so a field
6014
+ * shown here is the same field, with the same bare key, that the addon's
6015
+ * own page shows. There is no integration-specific writer — callers save
6016
+ * through `updateGlobalSettings` — so a second store key is unreachable,
6017
+ * not merely discouraged.
6018
+ * 2. **No node scope.** `perNode: true` fields are DROPPED. Their store key
6019
+ * is `<key>@<nodeId>` and an integration is not a node; whichever node
6020
+ * such a field silently picked would be a wrong answer for the operator
6021
+ * who opened the page (D266).
6022
+ * 3. **No silent typo.** A declared id that names no section throws. The
6023
+ * alternative — skip it — turns a rename into a surface that quietly
6024
+ * empties, which looks exactly like an addon with nothing to configure.
6025
+ */
6026
+ async getIntegrationSettings(nodeId) {
6027
+ const declared = this.integrationSettingSections();
6028
+ if (declared.length === 0) return null;
6029
+ const schema = this.globalSettingsSchema();
6030
+ if (!schema) throw new Error(`${this.constructor.name}: integrationSettingSections() names [${declared.join(", ")}] but globalSettingsSchema() returns null.`);
6031
+ const byId = new Map(schema.sections.map((section) => [section.id, section]));
6032
+ const sections = [];
6033
+ for (const id of declared) {
6034
+ const section = byId.get(id);
6035
+ if (!section) throw new Error(`${this.constructor.name}: integrationSettingSections() names unknown section "${id}". Known sections: [${[...byId.keys()].join(", ")}].`);
6036
+ const fields = dropPerNodeFields(section.fields);
6037
+ if (fields.length === 0) continue;
6038
+ sections.push({
6039
+ ...section,
6040
+ fields
6041
+ });
6042
+ }
6043
+ if (sections.length === 0) return null;
6044
+ const projected = await this.resolveGlobalStore(nodeId);
6045
+ return hydrateSchema({
6046
+ ...schema,
6047
+ sections
6048
+ }, projected);
6049
+ }
6050
+ /**
5968
6051
  * The raw addon store PROJECTED onto the target node's bare per-node keys:
5969
6052
  * every `perNode: true` field carries THAT node's scoped value on its bare
5970
6053
  * key (absent scoped key ⇒ key absent, so the schema `default` wins — no
@@ -6268,6 +6351,41 @@ var BaseAddon = class {
6268
6351
  * `hydrateSchema` does. Valueless structural fields (separator/info/…)
6269
6352
  * don't declare `perNode` and are excluded by the `in` narrowing.
6270
6353
  */
6354
+ /**
6355
+ * The same fields with every `perNode: true` one removed, recursing into layout
6356
+ * containers exactly as {@link collectPerNodeFieldKeys} does. A container left
6357
+ * with no child is dropped rather than rendered empty.
6358
+ *
6359
+ * Used by `getIntegrationSettings`: an integration is not a node, so a field
6360
+ * whose store key is `<key>@<nodeId>` has no node to belong to there.
6361
+ */
6362
+ function dropPerNodeFields(fields) {
6363
+ const kept = [];
6364
+ for (const field of fields) {
6365
+ if (field.type === "group") {
6366
+ const inner = dropPerNodeFields(field.fields);
6367
+ if (inner.length > 0) kept.push({
6368
+ ...field,
6369
+ fields: inner
6370
+ });
6371
+ continue;
6372
+ }
6373
+ if (field.type === "sub-tabs") {
6374
+ const tabs = field.tabs.map((tab) => ({
6375
+ ...tab,
6376
+ fields: dropPerNodeFields(tab.fields)
6377
+ })).filter((tab) => tab.fields.length > 0);
6378
+ if (tabs.length > 0) kept.push({
6379
+ ...field,
6380
+ tabs
6381
+ });
6382
+ continue;
6383
+ }
6384
+ if ("perNode" in field && field.perNode === true) continue;
6385
+ kept.push(field);
6386
+ }
6387
+ return kept;
6388
+ }
6271
6389
  function collectPerNodeFieldKeys(fields) {
6272
6390
  const collected = [];
6273
6391
  for (const field of fields) {
@@ -7196,6 +7314,40 @@ method(object({ deviceId: number() }), array(StreamSourceEntrySchema)), method(o
7196
7314
  input: unknown()
7197
7315
  }), unknown(), { kind: "mutation" }), method(object({ deviceId: number() }), _void(), { kind: "mutation" }), method(object({ deviceId: number() }), unknown().nullable()), method(object({ deviceId: number() }), RawStateResultSchema.nullable(), { auth: "protected" });
7198
7316
  /**
7317
+ * The ONE retry schedule for work that can fail simply because the node was
7318
+ * still booting.
7319
+ *
7320
+ * The measured failure (docs/design/boot-to-usable-camera.md, item E): a hub
7321
+ * boot that serialised its addon graph starved every driver dial on it. Fifteen
7322
+ * Hikvision devices logged `Initial probe did not complete — scheduling retry
7323
+ * chain {firstRetryMs=300000}` and then sat unprobed for five minutes, long
7324
+ * after the condition that caused the failure had gone. The Tailscale addon's
7325
+ * auto-rejoin carried a byte-identical schedule, armed from `initialize()`, for
7326
+ * the same reason.
7327
+ *
7328
+ * The shape that fixes it: **two cheap attempts first, then the long tail.**
7329
+ *
7330
+ * - 15 s / 60 s — the boot window. Anything that failed because the node was
7331
+ * busy is recovered here, before an operator can open the UI and see a
7332
+ * camera that is wrong about itself.
7333
+ * - 5 m → 90 m — the genuinely-offline case. A battery cam that is asleep, a
7334
+ * VLAN that is down, a peer that has not come back. Unchanged in intent from
7335
+ * the schedule this replaces (~3 h 30 m of coverage): here ~3 h 21 m, then
7336
+ * the chain ends and manual re-arm (reprobe / restart) is the only path.
7337
+ *
7338
+ * Bounded on purpose — see D6. A chain that never gives up is a busy loop with
7339
+ * extra steps.
7340
+ */
7341
+ var BOOT_RECOVERY_BACKOFF_MS = [
7342
+ 15e3,
7343
+ 6e4,
7344
+ 5 * 6e4,
7345
+ 15 * 6e4,
7346
+ 30 * 6e4,
7347
+ 60 * 6e4,
7348
+ 90 * 6e4
7349
+ ];
7350
+ /**
7199
7351
  * Type-safe JSON parsing helpers.
7200
7352
  *
7201
7353
  * `JSON.parse` is typed as `any` in lib.es5.d.ts, which triggers
@@ -10731,6 +10883,9 @@ method(object({
10731
10883
  kind: "mutation",
10732
10884
  auth: "admin"
10733
10885
  }), method(object({
10886
+ addonId: string(),
10887
+ nodeId: string().optional()
10888
+ }), SettingsSchemaWithValuesSchema.nullable()), method(object({
10734
10889
  addonId: string(),
10735
10890
  deviceId: number(),
10736
10891
  nodeId: string().optional()
@@ -11118,8 +11273,7 @@ var AUDIO_BACKEND_CHOICES = [
11118
11273
  ];
11119
11274
  var DEFAULT_AUDIO_ANALYZER_CONFIG = {
11120
11275
  audioBackend: "auto",
11121
- probedBestAudioBackend: "",
11122
- selectedAudioModel: ""
11276
+ probedBestAudioBackend: ""
11123
11277
  };
11124
11278
  var audioAnalyzerCapability = {
11125
11279
  name: "audio-analyzer",
@@ -30779,6 +30933,12 @@ Object.freeze({
30779
30933
  addonId: null,
30780
30934
  access: "view"
30781
30935
  },
30936
+ "addonSettings.getIntegrationSettings": {
30937
+ capName: "addon-settings",
30938
+ capScope: "system",
30939
+ addonId: null,
30940
+ access: "view"
30941
+ },
30782
30942
  "addonSettings.updateDeviceSettings": {
30783
30943
  capName: "addon-settings",
30784
30944
  capScope: "system",
@@ -39758,6 +39918,12 @@ Object.defineProperty(exports, "AUDIO_PRESETS", {
39758
39918
  return AUDIO_PRESETS;
39759
39919
  }
39760
39920
  });
39921
+ Object.defineProperty(exports, "BOOT_RECOVERY_BACKOFF_MS", {
39922
+ enumerable: true,
39923
+ get: function() {
39924
+ return BOOT_RECOVERY_BACKOFF_MS;
39925
+ }
39926
+ });
39761
39927
  Object.defineProperty(exports, "BaseAddon", {
39762
39928
  enumerable: true,
39763
39929
  get: function() {
@@ -1,4 +1,4 @@
1
- import { M as audioKindId, X as hfModelUrl, a as COCO_80_LABELS, o as COCO_TO_MACRO, r as AUDIO_MACRO_LABELS } from "./dist-g0Or5_pR.mjs";
1
+ import { M as audioKindId, X as hfModelUrl, a as COCO_80_LABELS, o as COCO_TO_MACRO, r as AUDIO_MACRO_LABELS } from "./dist-CTmOaCwF.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { PerformanceObserver } from "node:perf_hooks";
4
4
  //#region src/detection-pipeline/registry/model-catalogs.ts
@@ -1,4 +1,4 @@
1
- const require_dist = require("./dist-4EMKvxCp.js");
1
+ const require_dist = require("./dist-WUZH-lAM.js");
2
2
  let node_crypto = require("node:crypto");
3
3
  let node_perf_hooks = require("node:perf_hooks");
4
4
  //#region src/detection-pipeline/registry/model-catalogs.ts
@@ -1,4 +1,4 @@
1
- const require_dist = require("./dist-4EMKvxCp.js");
1
+ const require_dist = require("./dist-WUZH-lAM.js");
2
2
  let node_path = require("node:path");
3
3
  node_path = require_dist.__toESM(node_path);
4
4
  let node_url = require("node:url");
@@ -2,8 +2,8 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-4EMKvxCp.js");
6
- const require_lazy_sharp = require("../lazy-sharp-BlA9PxWk.js");
5
+ const require_dist = require("../dist-WUZH-lAM.js");
6
+ const require_lazy_sharp = require("../lazy-sharp-1WF7ZnxZ.js");
7
7
  let node_fs = require("node:fs");
8
8
  let node_path = require("node:path");
9
9
  //#region src/motion-wasm/wasm-motion-detector.ts
@@ -1,4 +1,4 @@
1
- import { Mt as DeviceType, Pt as hydrateSchema, Y as evaluateZoneRules, kt as BaseAddon, rt as motionDetectionCapability } from "../dist-g0Or5_pR.mjs";
1
+ import { At as BaseAddon, Ft as hydrateSchema, Nt as DeviceType, Y as evaluateZoneRules, rt as motionDetectionCapability } from "../dist-CTmOaCwF.mjs";
2
2
  import { t as getSharp } from "../lazy-sharp-DXsqwpph.mjs";
3
3
  import { readFileSync } from "node:fs";
4
4
  import { join } from "node:path";
@@ -1,4 +1,4 @@
1
- const require_dist = require("./dist-4EMKvxCp.js");
1
+ const require_dist = require("./dist-WUZH-lAM.js");
2
2
  let node_crypto = require("node:crypto");
3
3
  let node_fs = require("node:fs");
4
4
  node_fs = require_dist.__toESM(node_fs, 1);
@@ -1,4 +1,4 @@
1
- import { Ct as errMsg, Et as buildFfmpegArgs, Ot as isSoftwareDecode, Tt as Fmp4BoxSplitter } from "./dist-g0Or5_pR.mjs";
1
+ import { Ct as errMsg, Et as buildFfmpegArgs, Ot as isSoftwareDecode, Tt as Fmp4BoxSplitter } from "./dist-CTmOaCwF.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import "node:fs";
4
4
  import "node:path";
@@ -2,12 +2,12 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-4EMKvxCp.js");
6
- const require_node = require("../node-BOHn8gpk.js");
7
- const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-DDm07Ei6.js");
8
- const require_worker_protocol = require("../worker-protocol-BLfaD5uC.js");
5
+ const require_dist = require("../dist-WUZH-lAM.js");
6
+ const require_node = require("../node-BAo2ivVu.js");
7
+ const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-CoM21UjL.js");
8
+ const require_worker_protocol = require("../worker-protocol-C-0LYdSt.js");
9
9
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
10
- const require_lazy_sharp = require("../lazy-sharp-BlA9PxWk.js");
10
+ const require_lazy_sharp = require("../lazy-sharp-1WF7ZnxZ.js");
11
11
  const require_restream_intent = require("../restream-intent-Cv9x3jmu.js");
12
12
  const require_remote_restream = require("../remote-restream-CO36Sr30.js");
13
13
  let node_child_process = require("node:child_process");
@@ -891,10 +891,29 @@ var DEFAULT_FIRST_FRAME_TIMEOUT_MS = 2e4;
891
891
  *
892
892
  * The first decodable frame is not a fixed cost: it is one IDR interval plus
893
893
  * every GOP the decoder has to discard. Measured on device 618 (2560x1440
894
- * h264, 8.0 s IDR interval, `Invalid NAL unit` / `corrupt decoded frame` in its
895
- * bitstream), dial → first decoded frame over the broker's own muted restream
896
- * was 1.8 / 6.5 / 7.0 / 7.2 / 9.2 / 10.0 / 10.8 / 12.9 / 14.9 / 20.0 / 30.3 /
897
- * >40 s across 12 trials — against a healthy 587's 2.0 s ± 0.02.
894
+ * h264, `Invalid NAL unit` / `corrupt decoded frame` in its bitstream), dial →
895
+ * first decoded frame over the broker's own muted restream was 1.8 / 6.5 / 7.0
896
+ * / 7.2 / 9.2 / 10.0 / 10.8 / 12.9 / 14.9 / 20.0 / 30.3 / >40 s across 12
897
+ * trials — against a healthy 587's 2.0 s ± 0.02.
898
+ *
899
+ * **The "8.0 s IDR interval" this used to attribute 618's spread to was wrong,
900
+ * and is corrected here rather than deleted** (2026-08-28): a stated number
901
+ * nobody re-derived reads as verification, and this one was used to argue that
902
+ * 618 was an encoder outlier. The fleet runs 100-frame GOPs at 25 fps — a
903
+ * **4 000 ms** IDR interval — and two independent readings of 6 h of live logs
904
+ * agree with the operator rather than with this docblock. First, the join-burst
905
+ * span logged on `restream prime (lat-probe)` is the time since the last IDR,
906
+ * uniform in `[0, IDR]` and censored at `PRIME_MAX_SPAN_MS`: its median is
907
+ * ~1 000 ms on eleven cameras, which is what `uniform[0, 4000]` conditioned on
908
+ * `≤2000` predicts. Second, `detectionIntentServed` over detection dials puts
909
+ * the SERVED fraction at 44-61 % on the bulk of the fleet, i.e. the ~50 %
910
+ * `P(span > 2000)` a 4 000 ms interval implies. 618 measures 51 %, which is the
911
+ * fleet norm. The genuine long-GOP outlier is **637** at 30 % served (⇒ ~6.7 s).
912
+ *
913
+ * So the spread is not this camera's encoder. It is D57's fixed 2 000 ms bound
914
+ * withholding the join burst WHOLE on roughly half of every 4 s-GOP camera's
915
+ * dials — see `join-burst-bound.ts`, which states the arithmetic, and
916
+ * `docs/architecture/load-ledger.md` (2026-08-28) for the measurement.
898
917
  *
899
918
  * Abandoning at 20 s was therefore not a timeout, it was a coin flip, and the
900
919
  * recovery made it worse: ending the session RE-DIALS, which throws the
@@ -930,6 +949,7 @@ var SessionDecodeSession = class {
930
949
  firstFrameTimeoutMs;
931
950
  frameTimeoutMs;
932
951
  firstFrameMaxWaitMs;
952
+ onDialTiming;
933
953
  pendingPull = null;
934
954
  pendingToBuffers = /* @__PURE__ */ new Map();
935
955
  /** Outstanding `nativeCrop` requests, keyed by the monotonic `requestId`. */
@@ -943,13 +963,14 @@ var SessionDecodeSession = class {
943
963
  firstFrameReceived = false;
944
964
  /** `Date.now()` of the FIRST `pull` — the origin of the cold-start ceiling. */
945
965
  firstPullAt = 0;
946
- constructor(child, logger, deviceId, firstFrameTimeoutMs, frameTimeoutMs, firstFrameMaxWaitMs) {
966
+ constructor(child, logger, deviceId, firstFrameTimeoutMs, frameTimeoutMs, firstFrameMaxWaitMs, onDialTiming) {
947
967
  this.child = child;
948
968
  this.logger = logger;
949
969
  this.deviceId = deviceId;
950
970
  this.firstFrameTimeoutMs = firstFrameTimeoutMs;
951
971
  this.frameTimeoutMs = frameTimeoutMs;
952
972
  this.firstFrameMaxWaitMs = firstFrameMaxWaitMs;
973
+ this.onDialTiming = onDialTiming;
953
974
  this.child.on("message", (m) => this.handleMessage(m));
954
975
  this.child.on("exit", (m) => this.handleExit(toWorkerExitInfo(m)));
955
976
  }
@@ -1125,6 +1146,15 @@ var SessionDecodeSession = class {
1125
1146
  case "log":
1126
1147
  this.logger[raw.level](raw.message);
1127
1148
  return;
1149
+ case "dialTiming":
1150
+ this.onDialTiming?.({
1151
+ hwContextMs: raw.hwContextMs,
1152
+ demuxerOpenMs: raw.demuxerOpenMs,
1153
+ decoderCreateMs: raw.decoderCreateMs,
1154
+ firstDecodeMs: raw.firstDecodeMs,
1155
+ streamInfoProbed: raw.streamInfoProbed
1156
+ });
1157
+ return;
1128
1158
  }
1129
1159
  }
1130
1160
  handleExit(info) {
@@ -1349,7 +1379,7 @@ function createSessionDecodeCoordinator(deps) {
1349
1379
  const frameTimeoutMs = deps.frameTimeoutMs ?? DEFAULT_FRAME_TIMEOUT_MS;
1350
1380
  const firstFrameMaxWaitMs = Math.max(deps.firstFrameMaxWaitMs ?? DEFAULT_FIRST_FRAME_MAX_WAIT_MS, firstFrameTimeoutMs);
1351
1381
  return { async *generateVideoFrames(source, opts) {
1352
- const session = new SessionDecodeSession(deps.fork(source), deps.logger, source.deviceId, firstFrameTimeoutMs, frameTimeoutMs, firstFrameMaxWaitMs);
1382
+ const session = new SessionDecodeSession(deps.fork(source), deps.logger, source.deviceId, firstFrameTimeoutMs, frameTimeoutMs, firstFrameMaxWaitMs, deps.onDialTiming);
1353
1383
  session.start(source, opts);
1354
1384
  try {
1355
1385
  while (true) {
@@ -1403,9 +1433,13 @@ async function toDecodedFrame(frame, format, viewSpec) {
1403
1433
  */
1404
1434
  function startSessionDecodePump(deps) {
1405
1435
  const format = deps.opts.format ?? "rgb";
1436
+ let dialTiming = null;
1406
1437
  const iterator = createSessionDecodeCoordinator({
1407
1438
  fork: deps.fork,
1408
1439
  logger: deps.logger,
1440
+ onDialTiming: (timing) => {
1441
+ dialTiming = timing;
1442
+ },
1409
1443
  ...deps.firstFrameTimeoutMs !== void 0 ? { firstFrameTimeoutMs: deps.firstFrameTimeoutMs } : {},
1410
1444
  ...deps.frameTimeoutMs !== void 0 ? { frameTimeoutMs: deps.frameTimeoutMs } : {}
1411
1445
  }).generateVideoFrames(deps.source, deps.opts);
@@ -1436,9 +1470,11 @@ function startSessionDecodePump(deps) {
1436
1470
  if (!firstFrameSeen) {
1437
1471
  firstFrameSeen = true;
1438
1472
  releaseSpawnSlotOnce();
1473
+ const dial = dialTiming;
1439
1474
  deps.onFirstFrame?.({
1440
1475
  spawnWaitMs: spawnGrantedAt - pumpStartedAt,
1441
- forkToFrameMs: Date.now() - spawnGrantedAt
1476
+ forkToFrameMs: Date.now() - spawnGrantedAt,
1477
+ ...dial !== null ? { dial } : {}
1442
1478
  });
1443
1479
  }
1444
1480
  if (stopped) {
@@ -7346,6 +7382,13 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
7346
7382
  dialMs,
7347
7383
  spawnWaitMs: timing.spawnWaitMs,
7348
7384
  forkToFrameMs: timing.forkToFrameMs,
7385
+ ...timing.dial ? {
7386
+ hwContextMs: timing.dial.hwContextMs,
7387
+ demuxerOpenMs: timing.dial.demuxerOpenMs,
7388
+ decoderCreateMs: timing.dial.decoderCreateMs,
7389
+ firstDecodeMs: timing.dial.firstDecodeMs,
7390
+ streamInfoProbed: timing.dial.streamInfoProbed
7391
+ } : {},
7349
7392
  firstFrameMs: Date.now() - requestedAt,
7350
7393
  ...phaseActiveAt !== void 0 ? { sincePhaseActiveMs: Date.now() - phaseActiveAt } : {}
7351
7394
  }