@camstack/addon-post-analysis 1.2.46 → 1.2.48

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.
@@ -7068,6 +7068,30 @@ var DEVICE_SCOPED_CAPS = new Set([
7068
7068
  function isDeviceScopedCap(capName) {
7069
7069
  return DEVICE_SCOPED_CAPS.has(capName);
7070
7070
  }
7071
+ /**
7072
+ * Promise-based timer helpers — used everywhere the codebase needs to
7073
+ * wait, back off, or schedule a retry. Before these helpers landed, each
7074
+ * call site re-implemented `new Promise(r => setTimeout(r, ms))` inline,
7075
+ * with subtle variations (some swallowing cancellation, some not). Two
7076
+ * shapes cover every observed use case:
7077
+ *
7078
+ * - {@link sleep} for a plain, uncancellable wait — the default choice.
7079
+ * - {@link sleepCancellable} for a wait that wakes early when an
7080
+ * abort signal trips, used by long-running pollers whose teardown
7081
+ * must stop a pending backoff promptly.
7082
+ */
7083
+ /**
7084
+ * Resolve after `ms` milliseconds. Never rejects, never cancels. The
7085
+ * sleep cannot be interrupted; for a wakeable variant use
7086
+ * {@link sleepCancellable}.
7087
+ *
7088
+ * `ms <= 0` resolves on the next microtask via `setTimeout(0)`, which
7089
+ * still gives the event loop a chance to drain — useful for breaking
7090
+ * up tight async loops without changing call-site semantics.
7091
+ */
7092
+ function sleep(ms) {
7093
+ return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
7094
+ }
7071
7095
  //#endregion
7072
7096
  //#region ../types/dist/err-msg-IQTHeDzc.mjs
7073
7097
  /**
@@ -7092,6 +7116,14 @@ var EncodeProfileSchema = object({
7092
7116
  "main",
7093
7117
  "high"
7094
7118
  ]).optional(),
7119
+ /**
7120
+ * `-level`, e.g. `'3.1'`. A consumer that ADVERTISES a level in its SDP
7121
+ * (`profile-level-id=42e01f` is Baseline 3.1) must constrain the encoder to
7122
+ * it, or it ships a stream that does not match its own advertisement — the
7123
+ * defect class that kept HomeKit black for a year and that Alexa carried
7124
+ * silently. Optional because a browser negotiates the level itself.
7125
+ */
7126
+ level: string().optional(),
7095
7127
  width: number().int().positive().optional(),
7096
7128
  height: number().int().positive().optional(),
7097
7129
  fps: number().positive().optional(),
@@ -7139,6 +7171,29 @@ var EncodeProfileSchema = object({
7139
7171
  outputArgs: array(string()).optional()
7140
7172
  });
7141
7173
  /**
7174
+ * The shape every live egress starts from: H.264 Baseline 3.1 at 720p25.
7175
+ * Baseline because it is the one profile every consumer in this repo decodes
7176
+ * (Echo, iOS, an old browser); 3.1 because that is what the SDPs advertise.
7177
+ */
7178
+ var BASE_LIVE_EGRESS_PROFILE = {
7179
+ video: {
7180
+ codec: "h264",
7181
+ profile: "baseline",
7182
+ level: "3.1",
7183
+ width: 1280,
7184
+ height: 720,
7185
+ fps: 25,
7186
+ bitrateKbps: 2500,
7187
+ gopFrames: 25,
7188
+ bf: 0,
7189
+ preset: "veryfast",
7190
+ tune: "zerolatency"
7191
+ },
7192
+ audio: "passthrough"
7193
+ };
7194
+ ({ ...BASE_LIVE_EGRESS_PROFILE }), { ...BASE_LIVE_EGRESS_PROFILE.video };
7195
+ ({ ...BASE_LIVE_EGRESS_PROFILE });
7196
+ /**
7142
7197
  * Deep wiring healthcheck — snapshot of active reachability probes across
7143
7198
  * every declared capability + widget of every installed plugin, on every
7144
7199
  * node. Produced by the backend `WiringHealthService` and surfaced via
@@ -7188,6 +7243,105 @@ object({
7188
7243
  })
7189
7244
  });
7190
7245
  /**
7246
+ * Per-camera FUNCTION SWITCHES — the one coherent on/off surface over the
7247
+ * pipeline functions an operator thinks in terms of.
7248
+ *
7249
+ * ## This file adds no state
7250
+ *
7251
+ * Every switch here is a VIEW onto an authority that already existed
7252
+ * ([D61](../../../../docs/decisions/adr-0062.md)). The whole point of the
7253
+ * group is that there is exactly one place each function is turned off, and
7254
+ * the group routes to it:
7255
+ *
7256
+ * | Switch | Authority | Proven "off stops the work" gate |
7257
+ * | --- | --- | --- |
7258
+ * | `stream-broker` | `deviceManager.setDisabled` | `StreamBrokerManager.reconcileAllCatalogs` releases the brokers; `ensureBroker` refuses re-creation |
7259
+ * | `object-detection` | `deviceManager.setWrapperActive('detection-pipeline')` | `PipelineSettingsStore.resolvePipelineForDevice` returns `{ steps: [], audio: null }` |
7260
+ * | `audio-analysis` | `deviceManager.setWrapperActive('audio-analysis')` | `AudioSubscriptionController.subscribeAudioStream` returns `null` before opening the stream |
7261
+ * | `recording` | `recording.setDeviceConfig` → `RecordingConfig.enabled` | `band-decision.shouldRecord` returns false; the controller detaches the device |
7262
+ * | `notifications` | `notificationRules.setDeviceMuted` | `NotificationCenter.evaluateAndEnqueue` returns before any rule is evaluated |
7263
+ *
7264
+ * The wrapper-binding pair is not a new idea: `legacy-migrations.ts` already
7265
+ * migrated the legacy `audioEnabled` / `pipelineEnabled` /
7266
+ * `motionDetectionEnabled` booleans ONTO `setWrapperActive`. The group is the
7267
+ * surface that decision never got.
7268
+ *
7269
+ * ## Two rules that are load-bearing
7270
+ *
7271
+ * - **Recording's switch is `enabled`, never the bands.** `bands` is the only
7272
+ * authored intent and `mode` is derived from it (`deriveRecordingMode`).
7273
+ * Expressing "off" by clearing bands destroys the operator's schedule and
7274
+ * turning the camera back on would then silently record nothing.
7275
+ * - **A switch that is off must be reported as off**, not merely produce
7276
+ * nothing. {@link CameraSwitch.enabled} is what a status surface renders as
7277
+ * "disabled by an operator" instead of "broken" — see
7278
+ * `CameraStatus.switchedOff`.
7279
+ */
7280
+ /**
7281
+ * The five functions the operator named (2026-08-05). Deliberately NOT one id
7282
+ * per pipeline step: face recognition and plate/LPR are per-step toggles on
7283
+ * `pipelineOrchestrator.setCameraStepToggle` and belong in the pipeline
7284
+ * editor, not in a five-button safety group.
7285
+ */
7286
+ var CameraSwitchIdSchema = _enum([
7287
+ "stream-broker",
7288
+ "object-detection",
7289
+ "audio-analysis",
7290
+ "recording",
7291
+ "notifications"
7292
+ ]);
7293
+ /**
7294
+ * WHERE the switch's state actually lives. A discriminated union rather than a
7295
+ * string so both the writer (the orchestrator's `setCameraSwitch`) and any
7296
+ * reader can exhaustively narrow — and so "the group added a parallel map" is
7297
+ * a compile error rather than a review comment.
7298
+ */
7299
+ var CameraSwitchAuthoritySchema = discriminatedUnion("kind", [
7300
+ object({ kind: literal("device-disabled") }),
7301
+ object({
7302
+ kind: literal("wrapper-binding"),
7303
+ capName: string()
7304
+ }),
7305
+ object({ kind: literal("recording-config") }),
7306
+ object({ kind: literal("notification-mute") })
7307
+ ]);
7308
+ /**
7309
+ * Why a switch is not offered for this camera. Rendered instead of the
7310
+ * control, never as a dead control — an absent function and a broken one must
7311
+ * not look the same.
7312
+ */
7313
+ var CameraSwitchUnavailableReasonSchema = _enum(["no-provider", "source-unreachable"]);
7314
+ /**
7315
+ * One switch, resolved for one camera.
7316
+ *
7317
+ * `label` and `costWhenOff` travel ON THE WIRE rather than being looked up
7318
+ * client-side: the viewer is a separate repository that does not import
7319
+ * `@camstack/types`, and a cost line duplicated in two clients is a cost line
7320
+ * that will disagree with itself. Five rows per camera is nothing.
7321
+ */
7322
+ var CameraSwitchSchema = object({
7323
+ id: CameraSwitchIdSchema,
7324
+ label: string(),
7325
+ /**
7326
+ * What the operator LOSES while this is off, in one sentence. Required, not
7327
+ * optional: a switch that cannot say what it costs should not ship.
7328
+ */
7329
+ costWhenOff: string(),
7330
+ /** False = do not render a control. `unavailableReason` says why. */
7331
+ available: boolean(),
7332
+ unavailableReason: CameraSwitchUnavailableReasonSchema.optional(),
7333
+ /** Current state. Meaningless when `available` is false — read it as `true`. */
7334
+ enabled: boolean(),
7335
+ authority: CameraSwitchAuthoritySchema
7336
+ });
7337
+ /** The whole group for one camera. */
7338
+ var CameraSwitchGroupSchema = object({
7339
+ deviceId: number().int(),
7340
+ switches: array(CameraSwitchSchema).readonly(),
7341
+ /** Unix ms when the group was composed server-side. */
7342
+ fetchedAt: number()
7343
+ });
7344
+ /**
7191
7345
  * Ops-log — the durable, append-only operations audit shared by the
7192
7346
  * recordings and events management surfaces.
7193
7347
  *
@@ -7206,14 +7360,16 @@ var OpsLogOpSchema = _enum([
7206
7360
  "manual-delete",
7207
7361
  "rescan",
7208
7362
  "retention-run",
7209
- "relocate"
7363
+ "relocate",
7364
+ "orphan-audit"
7210
7365
  ]);
7211
7366
  /** Why the operation ran. */
7212
7367
  var OpsLogReasonSchema = _enum([
7213
7368
  "retention",
7214
7369
  "quota",
7215
7370
  "manual",
7216
- "operator"
7371
+ "operator",
7372
+ "maintenance"
7217
7373
  ]);
7218
7374
  /** One audit row, shared verbatim by both domains. */
7219
7375
  var OpsLogEntrySchema = object({
@@ -9099,6 +9255,100 @@ var RtpSourceSchema = object({
9099
9255
  encoder: string(),
9100
9256
  pipelineKey: string()
9101
9257
  });
9258
+ /**
9259
+ * The encode request — **structured and serialisable, with NO raw-flag escape
9260
+ * hatch.** This is deliberate and it is the one lesson taken from
9261
+ * `getStreamWithCodec`: that method's `outputArgs: string[]` is simultaneously
9262
+ * its extensibility mechanism AND part of `pipelineKeyFor`'s sharing key, so
9263
+ * adding a flag silently forks the shared child, and two consumers that mean
9264
+ * the same thing but spell it differently never share. Here every knob is a
9265
+ * NAMED field: a new requirement becomes a schema field (and a codegen run),
9266
+ * never an opaque array.
9267
+ *
9268
+ * `inputArgs` / `outputArgs` are omitted from the profile for the same reason.
9269
+ * The operator-facing derived-stream transform editor still has them — that is
9270
+ * a different surface (`publishCameraStream({ kind: 'derived' })`) with a
9271
+ * different purpose (reshaping a badly-behaved SOURCE), and it is unchanged.
9272
+ */
9273
+ var EgressEncodeSchema = EncodeProfileSchema.omit({
9274
+ inputArgs: true,
9275
+ outputArgs: true
9276
+ });
9277
+ /**
9278
+ * How the encoder is bounded. `'tight'` is a one-second VBV window for a
9279
+ * consumer whose budget is enforced per second (HomeKit); `'relaxed'` is two
9280
+ * seconds, letting a keyframe spike borrow from the next second (a browser,
9281
+ * an Echo). Named rather than numeric so the INTENT survives.
9282
+ */
9283
+ var EgressRateControlSchema = _enum(["tight", "relaxed"]);
9284
+ var EgressTranscodeRequestSchema = object({
9285
+ deviceId: number().int().nonnegative(),
9286
+ /** Which published stream to read. */
9287
+ source: discriminatedUnion("kind", [object({
9288
+ kind: literal("profile"),
9289
+ profile: CamProfileSchema
9290
+ }), object({
9291
+ kind: literal("cam-stream"),
9292
+ camStreamId: string().min(1)
9293
+ })]),
9294
+ encode: EgressEncodeSchema,
9295
+ rateControl: EgressRateControlSchema.optional(),
9296
+ /**
9297
+ * `-bsf:v`. A consumer that negotiates its OWN SDP (HomeKit) cannot carry
9298
+ * out-of-band extradata and needs `dump_extra` on both the copy and encode
9299
+ * branches. Enumerated, not free text.
9300
+ */
9301
+ bitstreamFilter: _enum([
9302
+ "dump_extra",
9303
+ "h264_mp4toannexb",
9304
+ "hevc_mp4toannexb"
9305
+ ]).optional(),
9306
+ pixelFormat: _enum(["yuv420p", "nv12"]).optional(),
9307
+ /**
9308
+ * Operator/consumer override for decode hardware. ABSENT is the normal case
9309
+ * and the one that matters: the broker then resolves the backend from the
9310
+ * DECODER ADDON's per-node `probedBestHwaccel` (see
9311
+ * `@camstack/types` `ffmpeg/hwaccel.ts`), which is the ranking known to work
9312
+ * on this hardware — never the raw kernel resolver's qsv-first order.
9313
+ */
9314
+ decodeHwAccel: _enum([
9315
+ "auto",
9316
+ "none",
9317
+ "videotoolbox",
9318
+ "vaapi",
9319
+ "qsv",
9320
+ "cuda"
9321
+ ]).optional(),
9322
+ /**
9323
+ * Host to embed in the returned restream `url`. The broker mints hub-local
9324
+ * `127.0.0.1` URLs; a consumer on another node passes a cluster-resolvable
9325
+ * host (`NodeTopologyService.reachableHostByNode`) so the returned URL is
9326
+ * dialable from there. Same contract as `getStreamWithCodec.hostname` —
9327
+ * `substituteRtspHost` rewrites only the dial address, never the restreamer.
9328
+ */
9329
+ hostname: string().optional(),
9330
+ /** Attribution for the broker panel. Never part of the sharing key. */
9331
+ tag: string().optional()
9332
+ });
9333
+ var EgressTranscodeSchema = object({
9334
+ /** Dial-able RTSP url (host-substituted when `hostname` was supplied). */
9335
+ url: string(),
9336
+ /** Release handle. Refcounted — the child dies when the last holder releases. */
9337
+ pipelineKey: string(),
9338
+ videoCodec: _enum(["H264", "H265"]),
9339
+ resolution: object({
9340
+ width: number().int().positive(),
9341
+ height: number().int().positive()
9342
+ }),
9343
+ transcoded: boolean(),
9344
+ encoder: string(),
9345
+ /**
9346
+ * The decode backend the child ACTUALLY ran with — `null` for software.
9347
+ * Returned rather than assumed: a consumer that asked for hardware and got
9348
+ * software needs to be able to see that without reading the broker's logs.
9349
+ */
9350
+ decodeHwAccel: string().nullable()
9351
+ });
9102
9352
  method(object({
9103
9353
  deviceId: number().int().nonnegative(),
9104
9354
  camStreamId: string().min(1),
@@ -9208,6 +9458,15 @@ method(object({
9208
9458
  }), {
9209
9459
  kind: "mutation",
9210
9460
  auth: "admin"
9461
+ }), method(EgressTranscodeRequestSchema, EgressTranscodeSchema, {
9462
+ kind: "mutation",
9463
+ auth: "admin"
9464
+ }), method(object({ pipelineKey: string() }), object({
9465
+ released: boolean(),
9466
+ refcount: number().int().nonnegative()
9467
+ }), {
9468
+ kind: "mutation",
9469
+ auth: "admin"
9211
9470
  }), method(SubscribeAudioChunksInputSchema, SubscribeAudioChunksResultSchema, { kind: "mutation" }), method(object({
9212
9471
  subscriptionId: string(),
9213
9472
  maxCount: number().int().positive().default(8)
@@ -13820,12 +14079,13 @@ var NcConditionsSchema = object({
13820
14079
  * source; otherwise the subject's source must equal it. Legacy records
13821
14080
  * with no stamped source are treated as `pipeline`. The union spans both
13822
14081
  * record kinds — object events carry `pipeline` | `onboard`, synthetic
13823
- * tracks carry `sensor`.
14082
+ * tracks carry `sensor` (a linked device) or `audio` (a D62 audio marker).
13824
14083
  */
13825
14084
  source: _enum([
13826
14085
  "pipeline",
13827
14086
  "onboard",
13828
14087
  "sensor",
14088
+ "audio",
13829
14089
  "any"
13830
14090
  ]).optional(),
13831
14091
  /**
@@ -14379,6 +14639,10 @@ var NC_CONDITION_CATALOG = [
14379
14639
  {
14380
14640
  value: "sensor",
14381
14641
  label: "Sensor"
14642
+ },
14643
+ {
14644
+ value: "audio",
14645
+ label: "Audio marker"
14382
14646
  }
14383
14647
  ],
14384
14648
  operator: "in",
@@ -14389,7 +14653,7 @@ var NC_CONDITION_CATALOG = [
14389
14653
  "package-event"
14390
14654
  ],
14391
14655
  phase: "P1",
14392
- description: "pipeline / onboard / sensor; a record with no stamped source counts as pipeline."
14656
+ description: "pipeline / onboard / sensor / audio; a record with no stamped source counts as pipeline."
14393
14657
  },
14394
14658
  {
14395
14659
  id: "deviceState",
@@ -14735,6 +14999,35 @@ var notificationRulesCapability = {
14735
14999
  auth: "admin"
14736
15000
  }),
14737
15001
  /**
15002
+ * PERMANENT per-camera mute — the notifications half of the per-camera
15003
+ * function switch group ([D61](../../../../docs/decisions/adr-0067.md)).
15004
+ *
15005
+ * Deliberately NOT a snooze. A snooze is bounded at
15006
+ * {@link NC_SNOOZE_MAX_MINUTES} on purpose — "a snooze that could not
15007
+ * expire would be an outage the operator asked for once and forgot" — and
15008
+ * widening it to express "this camera never notifies" would destroy that
15009
+ * property for every snooze. A mute is the other thing: an explicit,
15010
+ * indefinite, admin-only decision, visible in the switch group next to the
15011
+ * other four, and reported on `CameraStatus.switchedOff` so a silent
15012
+ * camera never reads as a working one.
15013
+ *
15014
+ * Returned as ONE list rather than a per-camera query: the group's reader
15015
+ * needs every camera's state, and a per-camera fan-out over the viewer's
15016
+ * single WebSocket is N frames serialised on one socket.
15017
+ */
15018
+ listDeviceMutes: method(object({}), object({ mutedDeviceIds: array(number().int()).readonly() }), { auth: "admin" }),
15019
+ /**
15020
+ * Mute or unmute one camera. Idempotent; an unmute of a camera that was
15021
+ * never muted succeeds.
15022
+ */
15023
+ setDeviceMuted: method(object({
15024
+ deviceId: number().int(),
15025
+ muted: boolean()
15026
+ }), object({ success: literal(true) }), {
15027
+ kind: "mutation",
15028
+ auth: "admin"
15029
+ }),
15030
+ /**
14738
15031
  * Dry-run a rule against recently persisted records (object events for
14739
15032
  * `immediate`, closed tracks for `track-end`). Mutation kind only to
14740
15033
  * carry the full rule object safely; no side effects.
@@ -15140,12 +15433,60 @@ var TrackAudioLabelSchema = object({
15140
15433
  });
15141
15434
  /**
15142
15435
  * How a track was produced. `pipeline` (default / absent) = the spatial
15143
- * detection+tracking pipeline. `sensor` = a SYNTHETIC track projected from a
15144
- * linked sensor/control state change (no positions; carries a snapshot). The
15145
- * spatial subsystems (tracker association, occupancy count, re-id/embedding,
15146
- * resurrection) MUST skip `sensor` tracks — they have no bbox trajectory.
15436
+ * detection+tracking pipeline. Every OTHER value is a SYNTHETIC projection —
15437
+ * no positions, a single snapshot, and no bbox trajectory at all:
15438
+ *
15439
+ * - `sensor` — a linked sensor/control device state change.
15440
+ * - `audio` — an audio event on the camera itself that was anomalous for
15441
+ * THAT camera, loud, and heard while nothing visual was happening (D62).
15442
+ *
15443
+ * The spatial subsystems (tracker association, occupancy count, re-id /
15444
+ * embedding, resurrection) MUST skip every synthetic source. Test for that
15445
+ * with `isSpatialTrack`, which allow-lists `pipeline` — a `!== 'sensor'`
15446
+ * check silently readmits every source added after it was written.
15447
+ */
15448
+ var TrackSourceSchema = _enum([
15449
+ "pipeline",
15450
+ "sensor",
15451
+ "audio"
15452
+ ]);
15453
+ /**
15454
+ * Per-track OPERATOR flags — set by hand from the admin UI or the viewer, never
15455
+ * by the pipeline. Spread into `TrackSchema` and `KeyEventSchema` from one place
15456
+ * so the two surfaces cannot drift.
15457
+ *
15458
+ * **Absent ≠ false.** A track that has never been touched omits the field; an
15459
+ * explicitly un-flagged track carries `false`. Legacy rows written before the
15460
+ * columns existed read as absent, and a consumer that needs a boolean should say
15461
+ * `flag === true`, not `flag !== false`.
15462
+ *
15463
+ * What the flags DO is deliberately UNDEFINED at the time of writing: they are
15464
+ * operator curation, and the behaviour they drive will be specified separately.
15465
+ * In particular a `markForTrain` track is NOT pinned against retention — see
15466
+ * `docs/decisions/adr-0059.md` for why that is a store-level change, not a flag.
15467
+ */
15468
+ var TrackFlagFields = {
15469
+ /** Operator marked this track as training material. */
15470
+ markForTrain: boolean().optional(),
15471
+ /** Operator marked this track for diagnostic attention. */
15472
+ debug: boolean().optional()
15473
+ };
15474
+ /**
15475
+ * The write half: a PARTIAL patch. An omitted key is left untouched, so setting
15476
+ * one flag can never clear the other — the toggles are independent and are
15477
+ * driven from three surfaces that do not know about each other.
15478
+ */
15479
+ var TrackFlagsPatchSchema = object(TrackFlagFields);
15480
+ /**
15481
+ * The resolved flag state after a write. Both fields are REQUIRED here (absent
15482
+ * collapses to `false`) so a caller can drive a toggle's checked state off the
15483
+ * mutation result without a re-fetch.
15147
15484
  */
15148
- var TrackSourceSchema = _enum(["pipeline", "sensor"]);
15485
+ var TrackFlagsSchema = object({
15486
+ trackId: string(),
15487
+ markForTrain: boolean(),
15488
+ debug: boolean()
15489
+ });
15149
15490
  var TrackSchema = object({
15150
15491
  trackId: string(),
15151
15492
  deviceId: number(),
@@ -15188,7 +15529,8 @@ var TrackSchema = object({
15188
15529
  /** Normalized 0..1 trajectory envelope (see {@link TrackEnvelopeSchema}).
15189
15530
  * Populated from the persisted envelope columns on historical reads;
15190
15531
  * absent on legacy rows, dims-less tracks and active (in-RAM) tracks. */
15191
- envelope: TrackEnvelopeSchema.optional()
15532
+ envelope: TrackEnvelopeSchema.optional(),
15533
+ ...TrackFlagFields
15192
15534
  });
15193
15535
  var BaseEventFields = {
15194
15536
  id: string(),
@@ -15401,7 +15743,8 @@ var KeyEventSchema = object({
15401
15743
  /** Highest-confidence ObjectEvent id for the track (empty when none). */
15402
15744
  bestEventId: string(),
15403
15745
  /** Track lifetime in ms (lastSeen - firstSeen). */
15404
- windowMs: number().optional()
15746
+ windowMs: number().optional(),
15747
+ ...TrackFlagFields
15405
15748
  });
15406
15749
  object({
15407
15750
  trackId: string(),
@@ -15486,16 +15829,32 @@ var RebuildObjectEmbeddingsInput = object({
15486
15829
  deviceId: number().optional(),
15487
15830
  since: number().optional(),
15488
15831
  until: number().optional(),
15832
+ /** Stop after this many tracks; the result reports whether more remain. */
15833
+ maxTracks: number().int().positive().optional(),
15489
15834
  /**
15490
- * Fraction added on EACH side of the detection box before cropping.
15491
- * ~0.2 (a 1.4x window) gives CLIP the surroundings it is trained on; 0 is
15492
- * the pixel-tight crop the first implementation used.
15835
+ * Run every embedding on THIS node instead of round-robining the fleet.
15836
+ *
15837
+ * Named `executeOnNodeId` and not `nodeId` on purpose: an inline `nodeId`
15838
+ * field in cap args is read by `parent-unowned-call.ts` as a ROUTING PIN, so
15839
+ * calling it that would pin the rebuild REQUEST itself to that node — the
15840
+ * rebuild orchestration lives on the hub, and only the per-track step runs
15841
+ * remotely. This field is data; the per-track pin is applied inside.
15842
+ *
15843
+ * Absent ⇒ round-robin over every online node whose runner can serve the
15844
+ * pinned model.
15493
15845
  */
15494
- cropMargin: number().min(0).max(4).optional(),
15495
- /** Square the window before extracting, so a tall subject is not squashed. */
15496
- square: boolean().optional(),
15497
- /** Stop after this many tracks; the result reports whether more remain. */
15498
- maxTracks: number().int().positive().optional()
15846
+ executeOnNodeId: string().optional(),
15847
+ /**
15848
+ * Milliseconds to wait between tracks; omit for the built-in default, `0` to
15849
+ * run flat out.
15850
+ *
15851
+ * A rebuild is bulk maintenance on hub-main's single thread. Measured
15852
+ * 2026-08-06, an unpaced pass held that thread busy 82.2 s out of 120 and
15853
+ * pushed `nodes.topology` from 0.25 s to 26 s for 43 minutes. The value in
15854
+ * force is logged at start and finish so a deliberately slow pass reads
15855
+ * differently from a stalled one.
15856
+ */
15857
+ pacingMs: number().int().nonnegative().optional()
15499
15858
  });
15500
15859
  /**
15501
15860
  * Result of emptying the CLIP index.
@@ -15528,6 +15887,24 @@ var RebuildStatusSchema = object({
15528
15887
  missingKeyFrame: number(),
15529
15888
  /** Tracks with no usable detection box. */
15530
15889
  missingBbox: number(),
15890
+ /**
15891
+ * Tracks an executing node REFUSED rather than broke on — an unreadable key
15892
+ * frame, a step that threw. Separate from `failed` because the remedy is
15893
+ * different, and because a whole camera silently contributing zero vectors
15894
+ * is the shape of failure a rebuild must never hide.
15895
+ */
15896
+ notRunnable: number(),
15897
+ /**
15898
+ * The pass stopped because NO node could serve the pinned model.
15899
+ *
15900
+ * Distinct from `notRunnable` on purpose: that one says "this track was
15901
+ * refused", this one says "the cluster cannot do this work at all" — every
15902
+ * candidate node either lacks the `clip-embedding` step, lacks a build of the
15903
+ * pinned model for its engine format, or dropped out. The remedy is a model /
15904
+ * engine change, not a per-camera one. Non-zero here always comes with
15905
+ * `complete: false`.
15906
+ */
15907
+ noCapableNode: number(),
15531
15908
  failed: number(),
15532
15909
  /** Set once a pass ends: true only when EVERYTHING was covered. */
15533
15910
  complete: boolean().nullable(),
@@ -15717,6 +16094,31 @@ var pipelineAnalyticsCapability = {
15717
16094
  auth: "admin"
15718
16095
  }),
15719
16096
  /**
16097
+ * Set the per-track operator flags (`markForTrain`, `debug`) on ONE track.
16098
+ * The patch is PARTIAL — an omitted key is left untouched — because the
16099
+ * three surfaces that write it (admin Events grid, viewer track detail,
16100
+ * viewer cluster detail) each own one toggle and must not clobber the other.
16101
+ *
16102
+ * Writes the track ROW: `markForTrain`/`debug` are per-TRACK state, so they
16103
+ * live where `label` and `importance` live, not in any per-device settings
16104
+ * store. Updates the in-RAM active track too, so a flag set on a live track
16105
+ * survives its expiry-time persist.
16106
+ *
16107
+ * `auth: 'protected'` (the default), NOT `admin`: the viewer is an
16108
+ * authenticated non-admin surface and two of the three call sites are
16109
+ * there. Revisit if a flag ever gains an effect that costs storage —
16110
+ * `deleteTracks` next door is admin for exactly that reason.
16111
+ *
16112
+ * Returns the RESOLVED state of both flags (absent → `false`) so a caller
16113
+ * can drive its toggle without a re-fetch. Rejects an unknown track.
16114
+ */
16115
+ setTrackFlags: method(object({
16116
+ /** Log/audit scope only — the trackId is globally unique on its own. */
16117
+ deviceId: number(),
16118
+ trackId: string(),
16119
+ flags: TrackFlagsPatchSchema
16120
+ }), TrackFlagsSchema, { kind: "mutation" }),
16121
+ /**
15720
16122
  * Durable event-store footprint for the management UI: event rows
15721
16123
  * (motion + object + audio) counted per camera + total, plus the
15722
16124
  * event-owned media bytes on disk per camera + total. Stat/count-based,
@@ -16443,6 +16845,53 @@ var DetailResultSchema = object({
16443
16845
  nativeFaceShortSidePx: number().optional()
16444
16846
  });
16445
16847
  /**
16848
+ * Why an executing node REFUSED a stateless step run (`runStatelessStep`).
16849
+ *
16850
+ * A refusal is a first-class answer, not an error, because the caller's next
16851
+ * move depends on WHICH one it is — and because "the pass produced nothing"
16852
+ * must never be reachable without a named, counted cause. The two tiers:
16853
+ *
16854
+ * - **node-level** (`unknown-step`, `model-not-servable`) — this node can
16855
+ * never serve this (step, model) pair. The caller drops it from its rotation
16856
+ * and retries the same work elsewhere; nothing about the work changes.
16857
+ * - **work-level** (`unreadable-frame`, `execution-failed`) — this node is
16858
+ * fine, this one request is not. Retrying it on another node would only
16859
+ * spread the same failure.
16860
+ */
16861
+ var StatelessStepRefusalSchema = _enum([
16862
+ "unknown-step",
16863
+ "model-not-servable",
16864
+ "unreadable-frame",
16865
+ "execution-failed"
16866
+ ]);
16867
+ /**
16868
+ * Answer to `runStatelessStep` — a discriminated union rather than a nullable
16869
+ * result, because `null` is exactly what made the camera-bound detail path
16870
+ * unable to tell "refused" from "never asked".
16871
+ */
16872
+ var RunStatelessStepResultSchema = discriminatedUnion("kind", [object({
16873
+ kind: literal("ran"),
16874
+ /** The node that actually executed it — the pin, echoed back for the log. */
16875
+ nodeId: string(),
16876
+ /**
16877
+ * The model the step ran with.
16878
+ *
16879
+ * The node verified this exact id has a build for the format it dispatched
16880
+ * on BEFORE running, so the executor's format resolution returns it
16881
+ * unchanged. A caller that pinned a model must compare this field and
16882
+ * treat a mismatch as a refusal — the whole point of the pin is that a
16883
+ * pass writes one feature space.
16884
+ */
16885
+ modelId: string(),
16886
+ details: array(DetailResultSchema)
16887
+ }), object({
16888
+ kind: literal("refused"),
16889
+ nodeId: string(),
16890
+ reason: StatelessStepRefusalSchema,
16891
+ /** Human-readable specifics — the format tried, the formats shipped, etc. */
16892
+ detail: string()
16893
+ })]);
16894
+ /**
16446
16895
  * Per-camera tunable ranges + defaults. Single source of truth used
16447
16896
  * by both the Zod data schema (validation + default fallback) and
16448
16897
  * the device settings UI (slider min/max/step). Touch one place and
@@ -16778,10 +17227,46 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
16778
17227
  }), NativeCropResultSchema.nullable()), method(object({
16779
17228
  deviceId: number(),
16780
17229
  frameHandle: FrameHandleSchema.optional(),
17230
+ /**
17231
+ * FULL FRAME (base64 JPEG). The runner derives the crop rectangle from
17232
+ * `parent.bbox` with the cluster crop convention and cuts it itself —
17233
+ * do NOT pre-crop for this field, that is what `cropJpeg` is.
17234
+ */
17235
+ frameJpeg: string().optional(),
17236
+ /**
17237
+ * PRE-CUT tile (base64 JPEG), used verbatim — NO padding is applied.
17238
+ * The fallback when the lease/session backing the frame is gone and the
17239
+ * caller already holds a crop.
17240
+ */
16781
17241
  cropJpeg: string().optional(),
16782
17242
  parent: DetailParentSchema,
16783
17243
  steps: array(string()).optional()
16784
- }), object({ details: array(DetailResultSchema) }).nullable(), { kind: "mutation" });
17244
+ }), object({ details: array(DetailResultSchema) }).nullable(), { kind: "mutation" }), method(object({
17245
+ /** Catalog step id, e.g. `clip-embedding`. */
17246
+ stepId: string(),
17247
+ /**
17248
+ * REQUIRED model pin. The node runs this exact model or refuses with
17249
+ * `model-not-servable` — it never substitutes a format default, because
17250
+ * a fleet pass that round-robins across nodes would then fill one index
17251
+ * from several encoders.
17252
+ */
17253
+ modelId: string(),
17254
+ /** FULL FRAME, base64 JPEG. The runner cuts — do NOT pre-crop. */
17255
+ frameJpeg: string(),
17256
+ /**
17257
+ * The subject box, NORMALISED [0,1] against `frameJpeg`. Normalised on
17258
+ * purpose: the caller stores boxes against a downscaled analysis frame
17259
+ * while the stored key frame is native-resolution, and the only side
17260
+ * that reliably knows the image's pixel dimensions is the side that
17261
+ * decodes it. Denormalising here removes a second reader of the
17262
+ * dimensions and the class of mismatch that comes with it.
17263
+ */
17264
+ bbox: NativeCropBboxSchema,
17265
+ /** Parent class of the subject (`person`, `vehicle`, …) — carried into the result. */
17266
+ className: string(),
17267
+ /** Camera the pixels came from. Diagnostics + log tags ONLY — never routing. */
17268
+ sourceDeviceId: number()
17269
+ }), RunStatelessStepResultSchema, { kind: "mutation" });
16785
17270
  var CameraPipelineConfigSchema = object({
16786
17271
  engine: PipelineEngineChoiceSchema.optional(),
16787
17272
  steps: array(PipelineStepInputSchema).readonly(),
@@ -17079,6 +17564,20 @@ var CameraStatusSchema = object({
17079
17564
  detection: CameraDetectionStatusSchema.nullable(),
17080
17565
  audio: CameraAudioStatusSchema.nullable(),
17081
17566
  recording: CameraRecordingStatusSchema.nullable(),
17567
+ /**
17568
+ * Per-camera function switches an OPERATOR has turned off
17569
+ * ([D61](../../../../docs/decisions/adr-0067.md)).
17570
+ *
17571
+ * This is the difference between DISABLED and BROKEN. A camera whose
17572
+ * `detection` block reports zero fps and whose `switchedOff` contains
17573
+ * `'object-detection'` was switched off by a person; the same camera with an
17574
+ * empty list is failing. Every status surface must render the two
17575
+ * differently — a quiet camera that looks identical to a dead one is the
17576
+ * silence-reads-as-never-happened trap this repo keeps paying for.
17577
+ *
17578
+ * Empty when nothing is off. Never contains a switch no provider offers.
17579
+ */
17580
+ switchedOff: array(CameraSwitchIdSchema).readonly(),
17082
17581
  /** Unix timestamp (ms) when this snapshot was composed server-side. */
17083
17582
  fetchedAt: number()
17084
17583
  });
@@ -17247,7 +17746,14 @@ method(object({
17247
17746
  }), method(object({
17248
17747
  deviceId: number(),
17249
17748
  agentNodeId: string().optional()
17250
- }), CameraPipelineConfigSchema), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
17749
+ }), CameraPipelineConfigSchema), method(object({ deviceId: number() }), CameraSwitchGroupSchema), method(object({
17750
+ deviceId: number(),
17751
+ switchId: CameraSwitchIdSchema,
17752
+ enabled: boolean()
17753
+ }), CameraSwitchGroupSchema, {
17754
+ kind: "mutation",
17755
+ auth: "admin"
17756
+ }), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
17251
17757
  name: string(),
17252
17758
  description: string().optional(),
17253
17759
  config: CameraPipelineConfigSchema
@@ -28821,6 +29327,12 @@ Object.freeze({
28821
29327
  addonId: null,
28822
29328
  access: "view"
28823
29329
  },
29330
+ "notificationRules.listDeviceMutes": {
29331
+ capName: "notification-rules",
29332
+ capScope: "system",
29333
+ addonId: null,
29334
+ access: "view"
29335
+ },
28824
29336
  "notificationRules.listRules": {
28825
29337
  capName: "notification-rules",
28826
29338
  capScope: "system",
@@ -28839,6 +29351,12 @@ Object.freeze({
28839
29351
  addonId: null,
28840
29352
  access: "create"
28841
29353
  },
29354
+ "notificationRules.setDeviceMuted": {
29355
+ capName: "notification-rules",
29356
+ capScope: "system",
29357
+ addonId: null,
29358
+ access: "create"
29359
+ },
28842
29360
  "notificationRules.setRuleEnabled": {
28843
29361
  capName: "notification-rules",
28844
29362
  capScope: "system",
@@ -29115,6 +29633,12 @@ Object.freeze({
29115
29633
  addonId: null,
29116
29634
  access: "view"
29117
29635
  },
29636
+ "pipelineAnalytics.setTrackFlags": {
29637
+ capName: "pipeline-analytics",
29638
+ capScope: "device",
29639
+ addonId: null,
29640
+ access: "create"
29641
+ },
29118
29642
  "pipelineAnalytics.wipeAllAnalytics": {
29119
29643
  capName: "pipeline-analytics",
29120
29644
  capScope: "device",
@@ -29421,6 +29945,12 @@ Object.freeze({
29421
29945
  addonId: null,
29422
29946
  access: "view"
29423
29947
  },
29948
+ "pipelineOrchestrator.getCameraSwitches": {
29949
+ capName: "pipeline-orchestrator",
29950
+ capScope: "system",
29951
+ addonId: null,
29952
+ access: "view"
29953
+ },
29424
29954
  "pipelineOrchestrator.getCapabilityBindings": {
29425
29955
  capName: "pipeline-orchestrator",
29426
29956
  capScope: "system",
@@ -29553,6 +30083,12 @@ Object.freeze({
29553
30083
  addonId: null,
29554
30084
  access: "create"
29555
30085
  },
30086
+ "pipelineOrchestrator.setCameraSwitch": {
30087
+ capName: "pipeline-orchestrator",
30088
+ capScope: "system",
30089
+ addonId: null,
30090
+ access: "create"
30091
+ },
29556
30092
  "pipelineOrchestrator.setCapabilityBinding": {
29557
30093
  capName: "pipeline-orchestrator",
29558
30094
  capScope: "system",
@@ -29643,6 +30179,12 @@ Object.freeze({
29643
30179
  addonId: null,
29644
30180
  access: "create"
29645
30181
  },
30182
+ "pipelineRunner.runStatelessStep": {
30183
+ capName: "pipeline-runner",
30184
+ capScope: "system",
30185
+ addonId: null,
30186
+ access: "create"
30187
+ },
29646
30188
  "plateGallery.assignPlate": {
29647
30189
  capName: "plate-gallery",
29648
30190
  capScope: "system",
@@ -30459,6 +31001,12 @@ Object.freeze({
30459
31001
  addonId: null,
30460
31002
  access: "create"
30461
31003
  },
31004
+ "streamBroker.acquireEgressTranscode": {
31005
+ capName: "stream-broker",
31006
+ capScope: "system",
31007
+ addonId: null,
31008
+ access: "create"
31009
+ },
30462
31010
  "streamBroker.assignProfile": {
30463
31011
  capName: "stream-broker",
30464
31012
  capScope: "system",
@@ -30567,6 +31115,12 @@ Object.freeze({
30567
31115
  addonId: null,
30568
31116
  access: "create"
30569
31117
  },
31118
+ "streamBroker.releaseEgressTranscode": {
31119
+ capName: "stream-broker",
31120
+ capScope: "system",
31121
+ addonId: null,
31122
+ access: "create"
31123
+ },
30570
31124
  "streamBroker.releaseStreamWithCodec": {
30571
31125
  capName: "stream-broker",
30572
31126
  capScope: "system",
@@ -31369,5 +31923,33 @@ function encodeVectorBase64(vector) {
31369
31923
  for (let i = 0; i < floats.length; i += 1) view.setFloat32(i * 4, floats[i] ?? 0, true);
31370
31924
  return Buffer.from(bytes).toString("base64");
31371
31925
  }
31926
+ /** Vector length implied by a base64 payload, without decoding it. */
31927
+ function vectorDimFromBase64(encoded) {
31928
+ return Math.floor(Buffer.from(encoded, "base64").byteLength / 4);
31929
+ }
31930
+ object({
31931
+ /**
31932
+ * Fraction of the box's own size added on EACH side before cutting.
31933
+ *
31934
+ * CLIP is trained on natural images WITH surroundings; a pixel-tight crop
31935
+ * removes exactly the context it is strongest on (a dog cut to its outline
31936
+ * is a dark blob). The right value is an empirical question, which is why it
31937
+ * is a setting: 0 / 0.15 / 0.2 / 0.5 are the interesting points.
31938
+ */
31939
+ paddingRatio: number().min(0).max(4),
31940
+ /**
31941
+ * Square the window (in PIXELS) before cutting.
31942
+ *
31943
+ * CLIP's input is square, so a tall bbox resized straight to NxN is squashed
31944
+ * — a standing person becomes a shape the model never saw. Squaring costs
31945
+ * extra background, which is context the model wants anyway. Off by default
31946
+ * because the live path has never squared and the stored index reflects that.
31947
+ */
31948
+ square: boolean()
31949
+ });
31950
+ ({
31951
+ paddingRatio: .15,
31952
+ square: false
31953
+ }).paddingRatio;
31372
31954
  //#endregion
31373
- export { unknown as $, kebabToCamel as A, DeviceType as B, cosineSimilarity as C, encodeVectorBase64 as D, embeddingEncoderCapability as E, subKindsOf as F, _enum as G, hydrateSchema as H, videoclipsCapability as I, literal as J, array as K, zoneAnalyticsCapability as L, pipelineAnalyticsCapability as M, plateGalleryCapability as N, faceGalleryCapability as O, readDeviceStateFrom as P, string as Q, errMsg as R, buildEventKindDescriptor as S, defineCustomActions as T, isDeviceScopedCap as U, createEvent as V, nodePin as W, object as X, number as Y, record as Z, TimelapseRuleInputSchema as _, MACRO_LABELS as a, alarmPanelCapability as b, NcConditionDescriptorSchema as c, NcRuleSchema as d, EventCategory as et, NcSnoozeInputSchema as f, OpsLogEntrySchema as g, NcTaxonomySchema as h, EVENT_PAD_MS as i, notificationRulesCapability as j, hfModelUrl as k, NcRuleInputSchema as l, NcSnoozeSuppressedSchema as m, DEFAULT_EVENT_COLOR as n, NC_CONDITION_CATALOG as o, NcSnoozeSchema as p, boolean as q, EVENT_KIND_BY_CAP as r, NC_TAXONOMY as s, BaseDevice as t, NcRulePatchSchema as u, TimelapseRuleSchema as v, customAction as w, audioMetricsCapability as x, addonWidgetsSourceCapability as y, BaseAddon as z };
31955
+ export { object as $, hfModelUrl as A, errMsg as B, buildEventKindDescriptor as C, embeddingEncoderCapability as D, defineCustomActions as E, readDeviceStateFrom as F, isDeviceScopedCap as G, DeviceType as H, subKindsOf as I, _enum as J, nodePin as K, vectorDimFromBase64 as L, notificationRulesCapability as M, pipelineAnalyticsCapability as N, encodeVectorBase64 as O, plateGalleryCapability as P, number as Q, videoclipsCapability as R, audioMetricsCapability as S, customAction as T, createEvent as U, BaseAddon as V, hydrateSchema as W, boolean as X, array as Y, literal as Z, TimelapseRuleInputSchema as _, MACRO_LABELS as a, addonWidgetsSourceCapability as b, NcConditionDescriptorSchema as c, NcRuleSchema as d, record as et, NcSnoozeInputSchema as f, OpsLogEntrySchema as g, NcTaxonomySchema as h, EVENT_PAD_MS as i, kebabToCamel as j, faceGalleryCapability as k, NcRuleInputSchema as l, NcSnoozeSuppressedSchema as m, DEFAULT_EVENT_COLOR as n, unknown as nt, NC_CONDITION_CATALOG as o, NcSnoozeSchema as p, sleep as q, EVENT_KIND_BY_CAP as r, EventCategory as rt, NC_TAXONOMY as s, BaseDevice as t, string as tt, NcRulePatchSchema as u, TimelapseRuleSchema as v, cosineSimilarity as w, alarmPanelCapability as x, TrackSourceSchema as y, zoneAnalyticsCapability as z };