@camstack/addon-osd-manager 0.1.19 → 0.1.21

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 (18) hide show
  1. package/dist/{MotionZonesSettings-2bARhPh6.mjs → MotionZonesSettings-C1tuU9vA.mjs} +2 -2
  2. package/dist/{PrivacyMaskSettings-Cdqm3uIC.mjs → PrivacyMaskSettings-BfYwmnx-.mjs} +4 -4
  3. package/dist/{SceneMonitorEditor-C58VvxWO.mjs → SceneMonitorEditor-Fui6aPeX.mjs} +3 -3
  4. package/dist/_stub.js +12 -12
  5. package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_osd_manager_page-SPTPw18n.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_osd_manager_page-CUYUIYix.mjs} +4 -4
  6. package/dist/_virtual_mf___mfe_internal__addon_osd_manager_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BhABwRxy.mjs +26 -0
  7. package/dist/{hostInit-CaN1IxGo.mjs → hostInit-wPjKQ8mT.mjs} +3 -3
  8. package/dist/index.js +506 -57
  9. package/dist/index.mjs +506 -57
  10. package/dist/{player-overlays-CQmBy0Zm.mjs → player-overlays-B_C4YyEi.mjs} +1 -1
  11. package/dist/remoteEntry.js +1 -1
  12. package/dist/{responsive-jU_1DnOd.mjs → responsive-BzwsIYSh.mjs} +1 -1
  13. package/dist/{square-BewXHDNm.mjs → square-X_zDrz0l.mjs} +1 -1
  14. package/dist/{trash-2-h2ZuBBnj.mjs → trash-2-BldQMEFN.mjs} +1 -1
  15. package/dist/{use-device-snapshot--wXDROMR.mjs → use-device-snapshot-Bxo1wH26.mjs} +1 -1
  16. package/dist/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_osd_manager_page__remoteEntry_js-BqFfMnBU.mjs → virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_osd_manager_page__remoteEntry_js-DZH4YPzH.mjs} +1 -1
  17. package/package.json +1 -1
  18. package/dist/_virtual_mf___mfe_internal__addon_osd_manager_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-KlN_tI2R.mjs +0 -26
package/dist/index.js CHANGED
@@ -5804,6 +5804,13 @@ var BaseAddon = class {
5804
5804
  _readinessGeneration = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2, 14);
5805
5805
  /** Capability names this addon registered at init — used to emit matching `down` events on shutdown. */
5806
5806
  _registeredCapNames = [];
5807
+ /**
5808
+ * True only after `readAddonStore` actually answered. Constructor
5809
+ * defaults look like stored config when the store is down — a forked
5810
+ * addon that auto-starts from those defaults (cloudflare-tunnel quick
5811
+ * mode, 2026-08-25) is not "the operator chose this".
5812
+ */
5813
+ settingsStoreReady = false;
5807
5814
  /** Default config values. Provided via constructor. */
5808
5815
  defaults;
5809
5816
  constructor(defaults) {
@@ -6204,7 +6211,9 @@ var BaseAddon = class {
6204
6211
  ];
6205
6212
  let lastErr;
6206
6213
  for (let attempt = 0; attempt <= delaysMs.length; attempt++) try {
6207
- return await settings.readAddonStore() ?? {};
6214
+ const stored = await settings.readAddonStore() ?? {};
6215
+ this.settingsStoreReady = true;
6216
+ return stored;
6208
6217
  } catch (err) {
6209
6218
  lastErr = err;
6210
6219
  const msg = err instanceof Error ? err.message : String(err);
@@ -6212,6 +6221,7 @@ var BaseAddon = class {
6212
6221
  if (attempt === delaysMs.length) break;
6213
6222
  await new Promise((r) => setTimeout(r, delaysMs[attempt]));
6214
6223
  }
6224
+ this.settingsStoreReady = false;
6215
6225
  this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
6216
6226
  return {};
6217
6227
  }
@@ -8095,6 +8105,15 @@ var LabelDefinitionSchema = object({
8095
8105
  description: string().optional(),
8096
8106
  icon: string().optional()
8097
8107
  });
8108
+ var ClassMapDefinitionSchema = object({
8109
+ mapping: record(string(), _enum([
8110
+ "person",
8111
+ "vehicle",
8112
+ "animal",
8113
+ "package"
8114
+ ])),
8115
+ preserveOriginal: boolean()
8116
+ });
8098
8117
  var MODEL_FORMATS = [
8099
8118
  "onnx",
8100
8119
  "coreml",
@@ -8178,6 +8197,12 @@ var ModelVariantGroupSchema = object({
8178
8197
  */
8179
8198
  resolution: number().int().positive().optional()
8180
8199
  });
8200
+ var ModelProviderIdSchema = _enum([
8201
+ "camstack",
8202
+ "frigate",
8203
+ "scrypted",
8204
+ "custom"
8205
+ ]);
8181
8206
  var ModelCatalogEntrySchema = object({
8182
8207
  id: string(),
8183
8208
  name: string(),
@@ -8273,7 +8298,19 @@ var ModelCatalogEntrySchema = object({
8273
8298
  * `id` stays the source of truth for resolution/download/persistence; grouping
8274
8299
  * is a presentation overlay resolved back to an `id`.
8275
8300
  */
8276
- group: ModelVariantGroupSchema.optional()
8301
+ group: ModelVariantGroupSchema.optional(),
8302
+ /**
8303
+ * Catalog source for the pipeline stepper's provider-first picker. Absent on
8304
+ * built-in CamStack entries (treated as `camstack`) and on registry rows
8305
+ * persisted before this field existed (`inferModelProvider` fills those).
8306
+ */
8307
+ provider: ModelProviderIdSchema.optional(),
8308
+ /**
8309
+ * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8310
+ * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8311
+ * labels already ARE the CamStack macros (Scrypted identity map).
8312
+ */
8313
+ classMap: ClassMapDefinitionSchema.optional()
8277
8314
  });
8278
8315
  var ConvertTargetSchema = discriminatedUnion("format", [object({
8279
8316
  format: literal("openvino"),
@@ -8302,7 +8339,8 @@ var ModelConvertMetadataSchema = object({
8302
8339
  "ocr",
8303
8340
  "segmentation"
8304
8341
  ]),
8305
- faceAlignment: boolean().optional()
8342
+ faceAlignment: boolean().optional(),
8343
+ classMap: ClassMapDefinitionSchema.optional()
8306
8344
  });
8307
8345
  var ConvertResultSchema = object({
8308
8346
  entry: ModelCatalogEntrySchema,
@@ -12868,6 +12906,27 @@ var LinkedDeviceSchema = object({
12868
12906
  features: array(string()),
12869
12907
  producesTrackedEvents: boolean().optional()
12870
12908
  });
12909
+ /** One camera's resolved linked set, tagged with the camera it belongs to.
12910
+ * The batch answer needs the tag; the single-device answer already has it
12911
+ * from the input, which is why `getLinkedDevices` keeps the untagged shape. */
12912
+ var LinkedDevicesForDeviceSchema = object({
12913
+ deviceId: number(),
12914
+ mode: LinkedDevicesModeSchema,
12915
+ devices: array(LinkedDeviceSchema)
12916
+ });
12917
+ /** One device's binding map — the shape `getBindings`, `getBindingsBatch` and
12918
+ * `getAllBindings` all answer in. Declared once: three copies of the same
12919
+ * object literal is exactly how the three drift apart. */
12920
+ var DeviceBindingsForDeviceSchema = object({
12921
+ deviceId: number(),
12922
+ entries: array(object({
12923
+ capName: string(),
12924
+ kind: _enum(["native", "wrapped"]),
12925
+ providerAddonId: string(),
12926
+ providerNodeId: string(),
12927
+ nativeAddonId: string()
12928
+ }))
12929
+ });
12871
12930
  var SavedDeviceRowSchema = object({
12872
12931
  /** Numeric id reserved at allocateDeviceId time. */
12873
12932
  id: number(),
@@ -13230,7 +13289,21 @@ var deviceManagerCapability = {
13230
13289
  projection: _enum(["full", "slim"]).optional(),
13231
13290
  /** Return only camera devices. Filtering server-side instead of
13232
13291
  * shipping 293 rows to find 12. */
13233
- isCamera: boolean().optional()
13292
+ isCamera: boolean().optional(),
13293
+ /**
13294
+ * Return only these device ids. For the caller that already KNOWS the
13295
+ * handful it wants and needs a field the id-bearing answer does not
13296
+ * carry — the viewer's linked-devices panel joins `type` and `online`
13297
+ * onto ~8 linked ids and, unfiltered, dragged the fleet across to do
13298
+ * it: 433 KB slim / 958 KB full for 967 devices, on a query that
13299
+ * refetches on the reconcile interval, on a phone.
13300
+ *
13301
+ * Safe to send at a hub that predates it: Zod STRIPS unknown input
13302
+ * keys rather than rejecting them (verified against the live hub
13303
+ * 2026-08-25 — 967 rows came back), so an old hub answers exactly what
13304
+ * it answers today and the caller filters as it already does.
13305
+ */
13306
+ deviceIds: array(number()).optional()
13234
13307
  }), array(DeviceInfoSchema)),
13235
13308
  /** Get a single device by numeric deviceId. */
13236
13309
  getDevice: method(object({ deviceId: number() }), DeviceInfoSchema.nullable()),
@@ -13249,6 +13322,23 @@ var deviceManagerCapability = {
13249
13322
  mode: LinkedDevicesModeSchema,
13250
13323
  devices: array(LinkedDeviceSchema)
13251
13324
  })),
13325
+ /**
13326
+ * `getLinkedDevices` for MANY cameras, in one call.
13327
+ *
13328
+ * Not a convenience wrapper — a cost fix. Resolving one camera's linked set
13329
+ * needs the whole fleet as candidates (children ∪ same-location), so the
13330
+ * single-device path runs a full `listAll` per call. Asked per camera that
13331
+ * is N full-fleet sweeps of device-manager's event loop, and they do not
13332
+ * overlap: measured on the live hub 2026-08-25, 30 concurrent
13333
+ * `getLinkedDevices` took 4110 ms wall (median 4060 ms each) to return
13334
+ * 7260 bytes in total. The batch takes ONE sweep and one settings read per
13335
+ * camera.
13336
+ *
13337
+ * A device id that resolves to nothing still gets a row (`devices: []`) —
13338
+ * a caller that asked for thirty and got twenty-eight cannot tell which two
13339
+ * are missing, or that any are.
13340
+ */
13341
+ getLinkedDevicesBatch: method(object({ deviceIds: array(number()) }), array(LinkedDevicesForDeviceSchema)),
13252
13342
  /** Get stream sources for a camera device. */
13253
13343
  getStreamSources: method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)),
13254
13344
  /** Get config entries (key + value + description) for a device. */
@@ -13306,16 +13396,22 @@ var deviceManagerCapability = {
13306
13396
  * currently-active provider (native or wrapper) + the underlying native
13307
13397
  * addon id, so consumers can decide routing without re-running discovery.
13308
13398
  */
13309
- getBindings: method(object({ deviceId: number() }), object({
13310
- deviceId: number(),
13311
- entries: array(object({
13312
- capName: string(),
13313
- kind: _enum(["native", "wrapped"]),
13314
- providerAddonId: string(),
13315
- providerNodeId: string(),
13316
- nativeAddonId: string()
13317
- }))
13318
- })),
13399
+ getBindings: method(object({ deviceId: number() }), DeviceBindingsForDeviceSchema),
13400
+ /**
13401
+ * `getBindings` for a NAMED set of devices, in one call.
13402
+ *
13403
+ * Between `getBindings` (one device) and `getAllBindings` (all 988) there
13404
+ * was nothing, so a caller that needs seventy-five either pays
13405
+ * seventy-five round-trips or drags the whole fleet across. Measured on
13406
+ * the live hub 2026-08-25: 75 concurrent `getBindings` = 1211 ms / 118 KB,
13407
+ * `getAllBindings({})` = 511 ms / 679 KB. Neither is the right answer to
13408
+ * "these seventy-five".
13409
+ *
13410
+ * Same resolver, same routing rules, same per-device shape as
13411
+ * `getBindings`; ids are deduped and a device with no bindings answers
13412
+ * `entries: []` rather than dropping out.
13413
+ */
13414
+ getBindingsBatch: method(object({ deviceIds: array(number()) }), array(DeviceBindingsForDeviceSchema)),
13319
13415
  /**
13320
13416
  * Return the binding map for every device known to the hub. Used by
13321
13417
  * `SystemManager` warm-boot: a single round-trip resolves the
@@ -13324,16 +13420,7 @@ var deviceManagerCapability = {
13324
13420
  * device add/remove) — clients invalidate via the
13325
13421
  * `capability.binding-changed` event.
13326
13422
  */
13327
- getAllBindings: method(object({}), array(object({
13328
- deviceId: number(),
13329
- entries: array(object({
13330
- capName: string(),
13331
- kind: _enum(["native", "wrapped"]),
13332
- providerAddonId: string(),
13333
- providerNodeId: string(),
13334
- nativeAddonId: string()
13335
- }))
13336
- }))),
13423
+ getAllBindings: method(object({}), array(DeviceBindingsForDeviceSchema)),
13337
13424
  /**
13338
13425
  * Activate (or deactivate) a wrapper addon for a (device, cap) pair.
13339
13426
  * Persists the binding via ctx.settings. active=false clears the wrapper
@@ -16221,12 +16308,15 @@ var NcOccupancyConditionSchema = object({
16221
16308
  * there is no second switch that can disagree with the first and every rule
16222
16309
  * authored before the decision migrates for free (`audioModeOf`):
16223
16310
  *
16224
- * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
16225
- * classifier labels with one of them. No window, no percentage:
16226
- * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
16227
- * `throttle` cooldown is the only brake. The per-label confidence floor is
16228
- * the analyzer's (`classificationMinScore`, per device) a label only
16229
- * reaches this condition if the classifier was already confident enough.
16311
+ * - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
16312
+ * labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
16313
+ * `hitPercent` and `samplingSeconds` are still ignored a percentage of
16314
+ * frames is the wrong question for a classifier that labels 1–3 frames
16315
+ * per episode. The count window is the brake that drops a single-frame
16316
+ * false positive; the rule's own `throttle` cooldown is the other. The
16317
+ * per-label confidence floor is the analyzer's (`classificationMinScore`,
16318
+ * per device) — a label only reaches this condition if the classifier was
16319
+ * already confident enough. `confirmHits: 1` restores first-frame fire.
16230
16320
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
16231
16321
  * the condition: at least `hitPercent`% of the samples over
16232
16322
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -16253,14 +16343,22 @@ var NcOccupancyConditionSchema = object({
16253
16343
  * an operator who typed `dog` mean the same thing.
16254
16344
  */
16255
16345
  var NcAudioConditionSchema = object({
16256
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
16346
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
16257
16347
  labels: array(string().min(1)).min(1).optional(),
16258
16348
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
16259
16349
  dbThreshold: number().min(-96).max(0).optional(),
16260
16350
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
16261
16351
  hitPercent: number().int().min(1).max(100).default(60),
16262
16352
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
16263
- samplingSeconds: number().int().min(1).max(300).default(10)
16353
+ samplingSeconds: number().int().min(1).max(300).default(10),
16354
+ /**
16355
+ * LABEL MODE: how many labelled frames must land inside
16356
+ * {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
16357
+ * Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
16358
+ */
16359
+ confirmHits: number().int().min(1).max(20).optional(),
16360
+ /** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
16361
+ confirmWindowSec: number().int().min(1).max(60).optional()
16264
16362
  });
16265
16363
  /**
16266
16364
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -19185,6 +19283,46 @@ var RecentTracksPageSchema = object({
19185
19283
  /** Cursor for the next page, or null when this page is the last. */
19186
19284
  nextCursor: string().nullable()
19187
19285
  });
19286
+ var LIST_GROUPS_DEFAULT_LIMIT = 40;
19287
+ var LIST_GROUPS_MAX_LIMIT = 100;
19288
+ var AnalyticsGroupRecordSchema = object({
19289
+ id: string(),
19290
+ deviceId: number().int(),
19291
+ openedAt: number().int(),
19292
+ closedAt: number().int(),
19293
+ timestamp: number().int(),
19294
+ memberCount: number().int(),
19295
+ memberTrackIds: array(string()).readonly(),
19296
+ className: string(),
19297
+ classes: array(string()).readonly(),
19298
+ /** Relative event-media path, or null when the group has no picture yet. */
19299
+ mediaUrl: string().nullable(),
19300
+ singleton: boolean()
19301
+ });
19302
+ var AnalyticsGroupMemberSchema = object({
19303
+ trackId: string(),
19304
+ deviceId: number().int(),
19305
+ className: string(),
19306
+ firstSeen: number().int(),
19307
+ lastSeen: number().int(),
19308
+ mediaUrl: string().nullable()
19309
+ });
19310
+ var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
19311
+ var ListGroupsQueryInput = object({
19312
+ /** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
19313
+ deviceIds: array(number()),
19314
+ /** Window lower bound on `closedAt` (inclusive). */
19315
+ since: number().optional(),
19316
+ /** Window upper bound on `openedAt` (inclusive). */
19317
+ until: number().optional(),
19318
+ limit: number().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
19319
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
19320
+ cursor: string().optional()
19321
+ });
19322
+ var ListGroupsPageSchema = object({
19323
+ groups: array(AnalyticsGroupRecordSchema).readonly(),
19324
+ nextCursor: string().nullable()
19325
+ });
19188
19326
  var KeyEventQueryInput = object({
19189
19327
  deviceId: number(),
19190
19328
  /** Window lower bound (track firstSeen ≥ since). */
@@ -19260,7 +19398,9 @@ var TrackCascadeCountsSchema = object({
19260
19398
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
19261
19399
  plates: number().int(),
19262
19400
  /** Per-track CLIP search vectors removed (best-effort). */
19263
- embeddings: number().int()
19401
+ embeddings: number().int(),
19402
+ /** Group membership + group rows removed with their last member (best-effort). */
19403
+ groups: number().int()
19264
19404
  });
19265
19405
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
19266
19406
  var DiskReconcileCountsSchema = object({
@@ -19432,6 +19572,16 @@ var pipelineAnalyticsCapability = {
19432
19572
  * are not included (same contract as `listTracks`).
19433
19573
  */
19434
19574
  listRecentTracks: method(RecentTracksQueryInput, RecentTracksPageSchema),
19575
+ /**
19576
+ * Batched co-moving group listing — the Groups feed. Same merge/cursor
19577
+ * contract as {@link listRecentTracks}. A group is a sealed partition of
19578
+ * one session; `getGroup` is the detail with members.
19579
+ */
19580
+ listGroups: method(ListGroupsQueryInput, ListGroupsPageSchema),
19581
+ getGroup: method(object({
19582
+ deviceId: number(),
19583
+ groupId: string().min(1)
19584
+ }), AnalyticsGroupDetailSchema.nullable()),
19435
19585
  clearTracks: method(object({ deviceId: number() }), _void(), {
19436
19586
  kind: "mutation",
19437
19587
  auth: "admin"
@@ -20025,6 +20175,33 @@ var NativeCropRefSchema = object({
20025
20175
  h: number()
20026
20176
  })
20027
20177
  });
20178
+ object({
20179
+ crop: object({
20180
+ left: number(),
20181
+ top: number(),
20182
+ width: number().positive(),
20183
+ height: number().positive()
20184
+ }).optional(),
20185
+ content: object({
20186
+ width: number().int().positive(),
20187
+ height: number().int().positive()
20188
+ }),
20189
+ fit: _enum(["stretch", "contain"]),
20190
+ format: _enum([
20191
+ "rgb",
20192
+ "gray",
20193
+ "jpeg"
20194
+ ])
20195
+ });
20196
+ var FrameRefSchema = object({
20197
+ registryId: string().min(1),
20198
+ id: string().min(1),
20199
+ width: number().int().positive(),
20200
+ height: number().int().positive(),
20201
+ format: _enum(["rgb", "gray"]),
20202
+ timestamp: number(),
20203
+ capturedAt: number().optional()
20204
+ });
20028
20205
  var ModelFormatSchema$1 = _enum([
20029
20206
  "onnx",
20030
20207
  "coreml",
@@ -20090,7 +20267,8 @@ var PipelineModelOptionSchema = object({
20090
20267
  sizeMB: number()
20091
20268
  })),
20092
20269
  group: ModelVariantGroupSchema.optional(),
20093
- legacy: boolean().optional()
20270
+ legacy: boolean().optional(),
20271
+ provider: ModelProviderIdSchema.optional()
20094
20272
  });
20095
20273
  var ConfigFieldBridge = custom();
20096
20274
  var PipelineAddonSchemaSchema = object({
@@ -20345,7 +20523,7 @@ var pipelineExecutorCapability = {
20345
20523
  * legacy call shape used by existing benchmark code; once all
20346
20524
  * callers pass it explicitly we make it required.
20347
20525
  *
20348
- * Exactly one of `frame`, `frameHandle`, `imageBase64`,
20526
+ * Exactly one of `frame`, `frameRef`, `frameHandle`, `imageBase64`,
20349
20527
  * `referenceImage` must be provided:
20350
20528
  * - `frame`: runtime dispatch path (runner → decoded broker frame).
20351
20529
  * Carries the raw buffer, dimensions, and format; the executor
@@ -20367,6 +20545,12 @@ var pipelineExecutorCapability = {
20367
20545
  steps: array(PipelineStepInputSchema).min(1),
20368
20546
  frame: FrameInputSchema.optional(),
20369
20547
  /**
20548
+ * Process-local lazy frame. Valid only when caller and provider resolve
20549
+ * in the same execution-group process; split/cross-node callers use
20550
+ * `frame`/`image` inline compatibility instead.
20551
+ */
20552
+ frameRef: FrameRefSchema.optional(),
20553
+ /**
20370
20554
  * CB5 shm passthrough — a `FrameHandle` naming the same ring slot
20371
20555
  * the decoded pixels live in. One more member of the one-of
20372
20556
  * frame/frameHandle/image/imageBase64/referenceImage group.
@@ -20716,7 +20900,10 @@ var NativeCropResultSchema = object({
20716
20900
  * Which source served this crop, so a quality-sensitive consumer (the native
20717
20901
  * `keyFrame`) can reject a degraded fallback:
20718
20902
  * - `native` — cut from the decode worker's retained NATIVE surface (the
20719
- * quality path).
20903
+ * quality path). A subject-tile serve is also native-resolution and stays
20904
+ * `native` here: the public enum cannot name `tile` without a breaking cap
20905
+ * change. Runner telemetry distinguishes lease vs tile via `source` on the
20906
+ * internal crop result (`nativeHits` vs `tileHits`).
20720
20907
  * - `ram-fullframe` — the native surface MISSED but the request was full-frame,
20721
20908
  * so the ≤640 RAM `RetainedFrameStore` served it (honest lower-res; legit for
20722
20909
  * the blank-frame guard / 640-snapshot resolve, NOT for the clean keyFrame).
@@ -21207,12 +21394,41 @@ var RunnerLocalLoadSchema = object({
21207
21394
  * legacy `OrchestratorMetricsSchema` shape so existing dashboards keep
21208
21395
  * working unchanged when they switch to reading from the runner cap.
21209
21396
  */
21397
+ var FrameLazyCountersSchema = object({
21398
+ framesDecoded: number(),
21399
+ framesAdmitted: number(),
21400
+ framesDroppedPixelFree: number(),
21401
+ viewsMaterialized: number(),
21402
+ viewsSkipped: number(),
21403
+ workerToRunnerBytes: number(),
21404
+ runnerToPoolRawBytes: number(),
21405
+ runnerToPoolJpegBytes: number(),
21406
+ onDemandFullFrameRequests: number(),
21407
+ onDemandCropRequests: number(),
21408
+ nativeHits: number(),
21409
+ nativeMisses: number(),
21410
+ tileHits: number(),
21411
+ tileMisses: number(),
21412
+ fallbackHits: number(),
21413
+ fallbackMisses: number(),
21414
+ retainedWritesAvoided: number(),
21415
+ residentRefs: number(),
21416
+ residentBytes: number(),
21417
+ releases: number(),
21418
+ evictions: number(),
21419
+ staleMisses: number()
21420
+ });
21421
+ var FrameLazyMetricsSchema = object({
21422
+ node: FrameLazyCountersSchema,
21423
+ cameras: array(FrameLazyCountersSchema.extend({ deviceId: number() }))
21424
+ });
21210
21425
  var RunnerLocalMetricsSchema = object({
21211
21426
  nodeId: string(),
21212
21427
  activeCameras: number(),
21213
21428
  throttledCameras: number(),
21214
21429
  avgInferenceTimeMs: number(),
21215
- queueDepth: number()
21430
+ queueDepth: number(),
21431
+ frameLazy: FrameLazyMetricsSchema.optional()
21216
21432
  });
21217
21433
  /**
21218
21434
  * Pipeline Runner capability — runtime detection workhorse.
@@ -23244,6 +23460,9 @@ var storageProviderCapability = {
23244
23460
  endDownload: method(EndDownloadInputSchema, _void(), { kind: "mutation" })
23245
23461
  }
23246
23462
  };
23463
+ /** Profile-exported FormBuilder schema. Shape is ConfigUISchema at the UI. */
23464
+ var ProfileSettingsSchemaBridge = unknown().nullable();
23465
+ var ProfileSettingsBagSchema = record(string(), unknown());
23247
23466
  /**
23248
23467
  * A live terminal session hosted by the provider addon. Output and input do
23249
23468
  * NOT flow through the capability — they use the addon data plane
@@ -23273,7 +23492,14 @@ var TerminalSessionInfoSchema = object({
23273
23492
  var TerminalProfileInfoSchema = object({
23274
23493
  profileId: string(),
23275
23494
  label: string(),
23276
- description: string().optional()
23495
+ description: string().optional(),
23496
+ /** Spawn defaults the instance form copies on create. */
23497
+ executable: string().optional(),
23498
+ args: array(string()).readonly().optional(),
23499
+ cwd: string().optional(),
23500
+ environment: array(string()).readonly().optional(),
23501
+ /** ConfigUISchema for instance knobs, or null when the profile has none. */
23502
+ settingsSchema: ProfileSettingsSchemaBridge.optional()
23277
23503
  });
23278
23504
  /**
23279
23505
  * A durable operator-created Terminal instance. Profiles are templates; only
@@ -23286,7 +23512,12 @@ var TerminalInstanceInfoSchema = object({
23286
23512
  profileId: string(),
23287
23513
  profileLabel: string(),
23288
23514
  name: string(),
23289
- enabled: boolean()
23515
+ enabled: boolean(),
23516
+ executable: string(),
23517
+ args: array(string()).readonly(),
23518
+ cwd: string(),
23519
+ environment: array(string()).readonly(),
23520
+ profileSettings: ProfileSettingsBagSchema
23290
23521
  });
23291
23522
  var TerminalLegacyCameraSchema = object({
23292
23523
  stableId: string(),
@@ -23332,7 +23563,24 @@ var terminalSessionCapability = {
23332
23563
  createInstance: method(object({
23333
23564
  targetNodeId: string().min(1),
23334
23565
  profileId: string().min(1),
23335
- name: string().trim().min(1).max(160).optional()
23566
+ name: string().trim().min(1).max(160).optional(),
23567
+ executable: string().max(1024).optional(),
23568
+ args: array(string().max(2048)).max(64).optional(),
23569
+ cwd: string().max(1024).optional(),
23570
+ environment: array(string().max(4096)).max(64).optional(),
23571
+ profileSettings: ProfileSettingsBagSchema.optional()
23572
+ }), TerminalInstanceInfoSchema, {
23573
+ kind: "mutation",
23574
+ auth: "admin"
23575
+ }),
23576
+ updateInstance: method(object({
23577
+ instanceId: string().min(1),
23578
+ name: string().trim().min(1).max(160).optional(),
23579
+ executable: string().max(1024).optional(),
23580
+ args: array(string().max(2048)).max(64).optional(),
23581
+ cwd: string().max(1024).optional(),
23582
+ environment: array(string().max(4096)).max(64).optional(),
23583
+ profileSettings: ProfileSettingsBagSchema.optional()
23336
23584
  }), TerminalInstanceInfoSchema, {
23337
23585
  kind: "mutation",
23338
23586
  auth: "admin"
@@ -23367,7 +23615,11 @@ var terminalSessionCapability = {
23367
23615
  openSession: method(object({
23368
23616
  profileId: string(),
23369
23617
  cols: number().int().positive(),
23370
- rows: number().int().positive()
23618
+ rows: number().int().positive(),
23619
+ executable: string().max(1024).optional(),
23620
+ args: array(string().max(2048)).max(64).optional(),
23621
+ cwd: string().max(1024).optional(),
23622
+ environment: array(string().max(4096)).max(64).optional()
23371
23623
  }), TerminalSessionInfoSchema, {
23372
23624
  kind: "mutation",
23373
23625
  auth: "admin"
@@ -27745,10 +27997,10 @@ var lawnMowerControlCapability = {
27745
27997
  *
27746
27998
  * • The SDK (mobile / web client) consumes `getConnectionEndpoints()`
27747
27999
  * to receive an ordered list of candidate base URLs it should race
27748
- * on connect — LAN IPv4 first (lowest latency when on same network),
27749
- * then public hostname (if a tunnel is up), then IPv6. The SDK can
27750
- * race them with short timeouts and stick with the winner for the
27751
- * session.
28000
+ * on connect — LAN IPv4 and stable LAN IPv6 first (lowest latency
28001
+ * when on the same network), then public hostname (if a tunnel is
28002
+ * up). The SDK can race them with short timeouts and stick with the
28003
+ * winner for the session.
27752
28004
  *
27753
28005
  * Why hub-only: agents are not directly addressable by the operator's
27754
28006
  * clients — they reverse-connect to the hub. Exposing their interfaces
@@ -27903,6 +28155,17 @@ var NotificationEndpointSchema = object({
27903
28155
  /** What the ranking currently resolves to (null when nothing is reachable). */
27904
28156
  resolved: string().nullable()
27905
28157
  });
28158
+ /**
28159
+ * The URLs the SDK / viewer should race for API access. `baseUrls` empty =
28160
+ * AUTO (every LAN IPv4 + the public tunnel). `resolved` is what that choice
28161
+ * currently expands to, so the UI can show the effective set either way.
28162
+ */
28163
+ var ViewerEndpointsSchema = object({
28164
+ /** The operator's explicit race set, or empty for AUTO. */
28165
+ baseUrls: array(string()).readonly(),
28166
+ /** What the ranking currently resolves to (may be empty if nothing is up). */
28167
+ resolved: array(string()).readonly()
28168
+ });
27906
28169
  var AllowedAddressesSchema = object({
27907
28170
  /**
27908
28171
  * Allowlist of interface addresses operators have explicitly opted
@@ -27911,6 +28174,20 @@ var AllowedAddressesSchema = object({
27911
28174
  * Network Addresses admin page and persisted by the addon.
27912
28175
  */
27913
28176
  addresses: array(string()).readonly() });
28177
+ var TlsStatusSchema = object({
28178
+ mode: _enum([
28179
+ "generated",
28180
+ "uploaded",
28181
+ "disabled"
28182
+ ]),
28183
+ leafFingerprintSha256: string().nullable(),
28184
+ caFingerprintSha256: string().nullable(),
28185
+ validTo: string().nullable(),
28186
+ sans: array(string()),
28187
+ caCertPem: string().nullable(),
28188
+ reissueError: string().nullable(),
28189
+ restartRequired: boolean()
28190
+ });
27914
28191
  var localNetworkCapability = {
27915
28192
  name: "local-network",
27916
28193
  scope: "system",
@@ -27930,13 +28207,13 @@ var localNetworkCapability = {
27930
28207
  */
27931
28208
  getPreferred: method(_void(), PreferredSchema),
27932
28209
  /**
27933
- * Ordered candidate base URLs the SDK should try on connect.
27934
- * Includes LAN IPs (one per non-internal interface), the public
27935
- * tunnel hostname (when active), and loopback as a last-resort
27936
- * fallback. Filterable by `includeLoopback` / `ipv4Only`.
27937
- * Honours `getAllowedAddresses()` when set addresses outside
27938
- * the allowlist are dropped (the public tunnel + loopback are
27939
- * always included as escape hatches).
28210
+ * Ordered candidate base URLs (the palette the Network tab shows).
28211
+ * Includes LAN IPv4, stable LAN IPv6, the public tunnel, and mesh when
28212
+ * joined. Loopback is off by default. The SDK races the subset from
28213
+ * `getViewerEndpoints`, not this full list — IPv6 stays here because
28214
+ * WebRTC ICE gathers dual-stack regardless of the HTTP race. Honours
28215
+ * `getAllowedAddresses()` when set addresses outside the allowlist
28216
+ * are dropped (the public tunnel is still included as an escape hatch).
27940
28217
  *
27941
28218
  * **The port is the hub's, not the caller's** (D62 — a function's fact
27942
28219
  * belongs to whoever already owns it). This method used to take a `port`
@@ -27967,10 +28244,11 @@ var localNetworkCapability = {
27967
28244
  */
27968
28245
  port: number().int().min(1).max(65535).optional(),
27969
28246
  /** Include `http(s)://127.0.0.1:<port>` as the lowest-priority
27970
- * candidate. Default `true`. */
28247
+ * candidate. Default `false` — loopback is not a client route. */
27971
28248
  includeLoopback: boolean().optional(),
27972
- /** Skip IPv6 entries. Some legacy clients can't parse them.
27973
- * Default `false`. */
28249
+ /** Skip IPv6 entries. Default `false` the palette includes stable
28250
+ * LAN IPv6 (ICE/WebRTC uses dual-stack regardless). Pass `true` to
28251
+ * hide them. The viewer HTTP/WS race is `getViewerEndpoints`. */
27974
28252
  ipv4Only: boolean().optional(),
27975
28253
  /** Scheme to emit for LAN/loopback URLs. Default `'http'`.
27976
28254
  * Pass `'https'` when the caller is itself loaded over HTTPS
@@ -27999,6 +28277,19 @@ var localNetworkCapability = {
27999
28277
  */
28000
28278
  setNotificationEndpoint: method(object({ baseUrl: string().nullable() }), NotificationEndpointSchema, { kind: "mutation" }),
28001
28279
  /**
28280
+ * The endpoints the SDK / viewer races for API access. Empty `baseUrls`
28281
+ * means AUTO: every LAN IPv4 address plus the public tunnel, never IPv6,
28282
+ * never mesh, never loopback. `resolved` is that set (or the operator's
28283
+ * explicit subset) as it stands right now.
28284
+ */
28285
+ getViewerEndpoints: method(_void(), ViewerEndpointsSchema),
28286
+ /**
28287
+ * Replace the viewer race set. Empty `baseUrls` restores AUTO. Stored
28288
+ * verbatim (not indices) so a temporarily-down tunnel is not silently
28289
+ * dropped from the operator's choice.
28290
+ */
28291
+ setViewerEndpoints: method(object({ baseUrls: array(string()).readonly() }), ViewerEndpointsSchema, { kind: "mutation" }),
28292
+ /**
28002
28293
  * Read the operator's allowlist. Empty = "auto" (no filter). Used
28003
28294
  * by the admin UI's address selector to seed its checkbox state.
28004
28295
  */
@@ -28016,7 +28307,34 @@ var localNetworkCapability = {
28016
28307
  * when the operator wants to wipe their manual edits and start
28017
28308
  * over from the auto-detected best matches.
28018
28309
  */
28019
- resetAllowlistToBestMatch: method(_void(), AllowedAddressesSchema, { kind: "mutation" })
28310
+ resetAllowlistToBestMatch: method(_void(), AllowedAddressesSchema, { kind: "mutation" }),
28311
+ /**
28312
+ * Live TLS material for the Network → Local access certificate card.
28313
+ * LAN HTTP / hostname are addon settings (`globalSettingsSchema`), not
28314
+ * a second store — this query is status, not configuration.
28315
+ */
28316
+ getTlsStatus: method(_void(), TlsStatusSchema),
28317
+ /** Issue a new leaf under the existing local CA. Disabled in uploaded mode. */
28318
+ regenerateCertificate: method(object({ reason: string().optional() }), TlsStatusSchema, {
28319
+ kind: "mutation",
28320
+ auth: "admin"
28321
+ }),
28322
+ /** Replace the served material with operator-supplied PEMs. */
28323
+ uploadCertificate: method(object({
28324
+ certPem: string().min(1),
28325
+ keyPem: string().min(1),
28326
+ caPem: string().optional()
28327
+ }), TlsStatusSchema, {
28328
+ kind: "mutation",
28329
+ auth: "admin"
28330
+ }),
28331
+ /** The local CA PEM, or empty when there is none to download. */
28332
+ downloadCa: method(_void(), object({ pem: string() })),
28333
+ /** Drop uploaded material and return to the generated local CA. */
28334
+ revertToGeneratedCertificate: method(_void(), TlsStatusSchema, {
28335
+ kind: "mutation",
28336
+ auth: "admin"
28337
+ })
28020
28338
  },
28021
28339
  /** BIG PLAN 2: declarative mount hint — read by `@camstack/system` `buildCapRouters`. */
28022
28340
  mount: { kind: "hub-only" }
@@ -29788,7 +30106,12 @@ var PlateInfoSchema = object({
29788
30106
  plateBbox: BoundingBoxSchema.optional(),
29789
30107
  /** keyFrame parity: MediaStore key of the track's native-resolution key frame. */
29790
30108
  keyFrameMediaKey: string().optional(),
29791
- base64: string().optional()
30109
+ base64: string().optional(),
30110
+ /**
30111
+ * Same crop as a data-plane URL. `getPlateByTrack` returns this and
30112
+ * never inlines JPEG; `listPlates` still inlines for the admin-ui.
30113
+ */
30114
+ cropUrl: string().optional()
29792
30115
  });
29793
30116
  var MediaFileLiteSchema = object({
29794
30117
  key: string(),
@@ -34818,6 +35141,12 @@ Object.freeze({
34818
35141
  addonId: null,
34819
35142
  access: "view"
34820
35143
  },
35144
+ "deviceManager.getBindingsBatch": {
35145
+ capName: "device-manager",
35146
+ capScope: "system",
35147
+ addonId: null,
35148
+ access: "view"
35149
+ },
34821
35150
  "deviceManager.getChildren": {
34822
35151
  capName: "device-manager",
34823
35152
  capScope: "system",
@@ -34878,6 +35207,12 @@ Object.freeze({
34878
35207
  addonId: null,
34879
35208
  access: "view"
34880
35209
  },
35210
+ "deviceManager.getLinkedDevicesBatch": {
35211
+ capName: "device-manager",
35212
+ capScope: "system",
35213
+ addonId: null,
35214
+ access: "view"
35215
+ },
34881
35216
  "deviceManager.getRoleDisplayDefaults": {
34882
35217
  capName: "device-manager",
34883
35218
  capScope: "system",
@@ -35760,6 +36095,12 @@ Object.freeze({
35760
36095
  addonId: null,
35761
36096
  access: "create"
35762
36097
  },
36098
+ "localNetwork.downloadCa": {
36099
+ capName: "local-network",
36100
+ capScope: "system",
36101
+ addonId: null,
36102
+ access: "view"
36103
+ },
35763
36104
  "localNetwork.getAllowedAddresses": {
35764
36105
  capName: "local-network",
35765
36106
  capScope: "system",
@@ -35784,18 +36125,42 @@ Object.freeze({
35784
36125
  addonId: null,
35785
36126
  access: "view"
35786
36127
  },
36128
+ "localNetwork.getTlsStatus": {
36129
+ capName: "local-network",
36130
+ capScope: "system",
36131
+ addonId: null,
36132
+ access: "view"
36133
+ },
36134
+ "localNetwork.getViewerEndpoints": {
36135
+ capName: "local-network",
36136
+ capScope: "system",
36137
+ addonId: null,
36138
+ access: "view"
36139
+ },
35787
36140
  "localNetwork.list": {
35788
36141
  capName: "local-network",
35789
36142
  capScope: "system",
35790
36143
  addonId: null,
35791
36144
  access: "view"
35792
36145
  },
36146
+ "localNetwork.regenerateCertificate": {
36147
+ capName: "local-network",
36148
+ capScope: "system",
36149
+ addonId: null,
36150
+ access: "create"
36151
+ },
35793
36152
  "localNetwork.resetAllowlistToBestMatch": {
35794
36153
  capName: "local-network",
35795
36154
  capScope: "system",
35796
36155
  addonId: null,
35797
36156
  access: "delete"
35798
36157
  },
36158
+ "localNetwork.revertToGeneratedCertificate": {
36159
+ capName: "local-network",
36160
+ capScope: "system",
36161
+ addonId: null,
36162
+ access: "create"
36163
+ },
35799
36164
  "localNetwork.setAllowedAddresses": {
35800
36165
  capName: "local-network",
35801
36166
  capScope: "system",
@@ -35808,6 +36173,18 @@ Object.freeze({
35808
36173
  addonId: null,
35809
36174
  access: "create"
35810
36175
  },
36176
+ "localNetwork.setViewerEndpoints": {
36177
+ capName: "local-network",
36178
+ capScope: "system",
36179
+ addonId: null,
36180
+ access: "create"
36181
+ },
36182
+ "localNetwork.uploadCertificate": {
36183
+ capName: "local-network",
36184
+ capScope: "system",
36185
+ addonId: null,
36186
+ access: "create"
36187
+ },
35811
36188
  "lockControl.lock": {
35812
36189
  capName: "lock-control",
35813
36190
  capScope: "device",
@@ -36606,6 +36983,12 @@ Object.freeze({
36606
36983
  addonId: null,
36607
36984
  access: "view"
36608
36985
  },
36986
+ "pipelineAnalytics.getGroup": {
36987
+ capName: "pipeline-analytics",
36988
+ capScope: "device",
36989
+ addonId: null,
36990
+ access: "view"
36991
+ },
36609
36992
  "pipelineAnalytics.getKeyEvents": {
36610
36993
  capName: "pipeline-analytics",
36611
36994
  capScope: "device",
@@ -36690,6 +37073,12 @@ Object.freeze({
36690
37073
  addonId: null,
36691
37074
  access: "view"
36692
37075
  },
37076
+ "pipelineAnalytics.listGroups": {
37077
+ capName: "pipeline-analytics",
37078
+ capScope: "device",
37079
+ addonId: null,
37080
+ access: "view"
37081
+ },
36693
37082
  "pipelineAnalytics.listOpsLog": {
36694
37083
  capName: "pipeline-analytics",
36695
37084
  capScope: "device",
@@ -38688,6 +39077,12 @@ Object.freeze({
38688
39077
  addonId: null,
38689
39078
  access: "create"
38690
39079
  },
39080
+ "terminalSession.updateInstance": {
39081
+ capName: "terminal-session",
39082
+ capScope: "system",
39083
+ addonId: null,
39084
+ access: "create"
39085
+ },
38691
39086
  "terminalSession.writeInput": {
38692
39087
  capName: "terminal-session",
38693
39088
  capScope: "system",
@@ -39465,6 +39860,11 @@ Object.freeze({
39465
39860
  form: "single",
39466
39861
  optional: false
39467
39862
  }],
39863
+ "deviceManager.getBindingsBatch": [{
39864
+ name: "deviceIds",
39865
+ form: "array",
39866
+ optional: false
39867
+ }],
39468
39868
  "deviceManager.getChildren": [{
39469
39869
  name: "parentDeviceId",
39470
39870
  form: "single",
@@ -39510,6 +39910,11 @@ Object.freeze({
39510
39910
  form: "single",
39511
39911
  optional: false
39512
39912
  }],
39913
+ "deviceManager.getLinkedDevicesBatch": [{
39914
+ name: "deviceIds",
39915
+ form: "array",
39916
+ optional: false
39917
+ }],
39513
39918
  "deviceManager.getSettingsSchema": [{
39514
39919
  name: "deviceId",
39515
39920
  form: "single",
@@ -39530,6 +39935,11 @@ Object.freeze({
39530
39935
  form: "single",
39531
39936
  optional: false
39532
39937
  }],
39938
+ "deviceManager.listAll": [{
39939
+ name: "deviceIds",
39940
+ form: "array",
39941
+ optional: true
39942
+ }],
39533
39943
  "deviceManager.loadConfig": [{
39534
39944
  name: "deviceId",
39535
39945
  form: "single",
@@ -40103,6 +40513,11 @@ Object.freeze({
40103
40513
  form: "single",
40104
40514
  optional: false
40105
40515
  }],
40516
+ "pipelineAnalytics.getGroup": [{
40517
+ name: "deviceId",
40518
+ form: "single",
40519
+ optional: false
40520
+ }],
40106
40521
  "pipelineAnalytics.getKeyEvents": [{
40107
40522
  name: "deviceId",
40108
40523
  form: "single",
@@ -40158,6 +40573,11 @@ Object.freeze({
40158
40573
  form: "array",
40159
40574
  optional: false
40160
40575
  }],
40576
+ "pipelineAnalytics.listGroups": [{
40577
+ name: "deviceIds",
40578
+ form: "array",
40579
+ optional: false
40580
+ }],
40161
40581
  "pipelineAnalytics.listOpsLog": [{
40162
40582
  name: "deviceId",
40163
40583
  form: "single",
@@ -41235,6 +41655,35 @@ Object.freeze(Object.fromEntries([{
41235
41655
  }]
41236
41656
  }].map((s) => [s.stepId, s.defaultModelId])));
41237
41657
  string().min(1);
41658
+ var CLUSTER_STEP_SETTING_FIELDS = [{
41659
+ stepId: "face-embedding",
41660
+ key: "minLandmarkFaceSize",
41661
+ label: "Min face size for recognition (detection px)",
41662
+ description: "Refuse to embed a face smaller than this IN THE DETECTION FRAME. Applies to every node — the enrolled gallery is one index, and two admission floors would fill it from two policies. 0 disables the gate.",
41663
+ type: "slider",
41664
+ min: 0,
41665
+ max: 64,
41666
+ step: 2,
41667
+ default: 24
41668
+ }];
41669
+ function clusterStepSettingKey(stepId, fieldKey) {
41670
+ return `clusterStepSetting:${stepId}:${fieldKey}`;
41671
+ }
41672
+ var ClusterSettingNumberSchema = number().finite();
41673
+ function readClusterStepSettings(config) {
41674
+ const out = {};
41675
+ for (const field of CLUSTER_STEP_SETTING_FIELDS) {
41676
+ const parsed = ClusterSettingNumberSchema.safeParse(config[clusterStepSettingKey(field.stepId, field.key)]);
41677
+ const value = parsed.success ? parsed.data : field.default;
41678
+ const existing = out[field.stepId] ?? {};
41679
+ out[field.stepId] = {
41680
+ ...existing,
41681
+ [field.key]: value
41682
+ };
41683
+ }
41684
+ return out;
41685
+ }
41686
+ readClusterStepSettings({});
41238
41687
  object({
41239
41688
  /**
41240
41689
  * Fraction of the box's own size added on EACH side before cutting.