@camstack/addon-post-analysis 1.2.47 → 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.
@@ -7090,6 +7090,30 @@ var DEVICE_SCOPED_CAPS = new Set([
7090
7090
  function isDeviceScopedCap(capName) {
7091
7091
  return DEVICE_SCOPED_CAPS.has(capName);
7092
7092
  }
7093
+ /**
7094
+ * Promise-based timer helpers — used everywhere the codebase needs to
7095
+ * wait, back off, or schedule a retry. Before these helpers landed, each
7096
+ * call site re-implemented `new Promise(r => setTimeout(r, ms))` inline,
7097
+ * with subtle variations (some swallowing cancellation, some not). Two
7098
+ * shapes cover every observed use case:
7099
+ *
7100
+ * - {@link sleep} for a plain, uncancellable wait — the default choice.
7101
+ * - {@link sleepCancellable} for a wait that wakes early when an
7102
+ * abort signal trips, used by long-running pollers whose teardown
7103
+ * must stop a pending backoff promptly.
7104
+ */
7105
+ /**
7106
+ * Resolve after `ms` milliseconds. Never rejects, never cancels. The
7107
+ * sleep cannot be interrupted; for a wakeable variant use
7108
+ * {@link sleepCancellable}.
7109
+ *
7110
+ * `ms <= 0` resolves on the next microtask via `setTimeout(0)`, which
7111
+ * still gives the event loop a chance to drain — useful for breaking
7112
+ * up tight async loops without changing call-site semantics.
7113
+ */
7114
+ function sleep(ms) {
7115
+ return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
7116
+ }
7093
7117
  //#endregion
7094
7118
  //#region ../types/dist/err-msg-IQTHeDzc.mjs
7095
7119
  /**
@@ -7114,6 +7138,14 @@ var EncodeProfileSchema = object({
7114
7138
  "main",
7115
7139
  "high"
7116
7140
  ]).optional(),
7141
+ /**
7142
+ * `-level`, e.g. `'3.1'`. A consumer that ADVERTISES a level in its SDP
7143
+ * (`profile-level-id=42e01f` is Baseline 3.1) must constrain the encoder to
7144
+ * it, or it ships a stream that does not match its own advertisement — the
7145
+ * defect class that kept HomeKit black for a year and that Alexa carried
7146
+ * silently. Optional because a browser negotiates the level itself.
7147
+ */
7148
+ level: string().optional(),
7117
7149
  width: number().int().positive().optional(),
7118
7150
  height: number().int().positive().optional(),
7119
7151
  fps: number().positive().optional(),
@@ -7161,6 +7193,29 @@ var EncodeProfileSchema = object({
7161
7193
  outputArgs: array(string()).optional()
7162
7194
  });
7163
7195
  /**
7196
+ * The shape every live egress starts from: H.264 Baseline 3.1 at 720p25.
7197
+ * Baseline because it is the one profile every consumer in this repo decodes
7198
+ * (Echo, iOS, an old browser); 3.1 because that is what the SDPs advertise.
7199
+ */
7200
+ var BASE_LIVE_EGRESS_PROFILE = {
7201
+ video: {
7202
+ codec: "h264",
7203
+ profile: "baseline",
7204
+ level: "3.1",
7205
+ width: 1280,
7206
+ height: 720,
7207
+ fps: 25,
7208
+ bitrateKbps: 2500,
7209
+ gopFrames: 25,
7210
+ bf: 0,
7211
+ preset: "veryfast",
7212
+ tune: "zerolatency"
7213
+ },
7214
+ audio: "passthrough"
7215
+ };
7216
+ ({ ...BASE_LIVE_EGRESS_PROFILE }), { ...BASE_LIVE_EGRESS_PROFILE.video };
7217
+ ({ ...BASE_LIVE_EGRESS_PROFILE });
7218
+ /**
7164
7219
  * Deep wiring healthcheck — snapshot of active reachability probes across
7165
7220
  * every declared capability + widget of every installed plugin, on every
7166
7221
  * node. Produced by the backend `WiringHealthService` and surfaced via
@@ -7210,6 +7265,105 @@ object({
7210
7265
  })
7211
7266
  });
7212
7267
  /**
7268
+ * Per-camera FUNCTION SWITCHES — the one coherent on/off surface over the
7269
+ * pipeline functions an operator thinks in terms of.
7270
+ *
7271
+ * ## This file adds no state
7272
+ *
7273
+ * Every switch here is a VIEW onto an authority that already existed
7274
+ * ([D61](../../../../docs/decisions/adr-0062.md)). The whole point of the
7275
+ * group is that there is exactly one place each function is turned off, and
7276
+ * the group routes to it:
7277
+ *
7278
+ * | Switch | Authority | Proven "off stops the work" gate |
7279
+ * | --- | --- | --- |
7280
+ * | `stream-broker` | `deviceManager.setDisabled` | `StreamBrokerManager.reconcileAllCatalogs` releases the brokers; `ensureBroker` refuses re-creation |
7281
+ * | `object-detection` | `deviceManager.setWrapperActive('detection-pipeline')` | `PipelineSettingsStore.resolvePipelineForDevice` returns `{ steps: [], audio: null }` |
7282
+ * | `audio-analysis` | `deviceManager.setWrapperActive('audio-analysis')` | `AudioSubscriptionController.subscribeAudioStream` returns `null` before opening the stream |
7283
+ * | `recording` | `recording.setDeviceConfig` → `RecordingConfig.enabled` | `band-decision.shouldRecord` returns false; the controller detaches the device |
7284
+ * | `notifications` | `notificationRules.setDeviceMuted` | `NotificationCenter.evaluateAndEnqueue` returns before any rule is evaluated |
7285
+ *
7286
+ * The wrapper-binding pair is not a new idea: `legacy-migrations.ts` already
7287
+ * migrated the legacy `audioEnabled` / `pipelineEnabled` /
7288
+ * `motionDetectionEnabled` booleans ONTO `setWrapperActive`. The group is the
7289
+ * surface that decision never got.
7290
+ *
7291
+ * ## Two rules that are load-bearing
7292
+ *
7293
+ * - **Recording's switch is `enabled`, never the bands.** `bands` is the only
7294
+ * authored intent and `mode` is derived from it (`deriveRecordingMode`).
7295
+ * Expressing "off" by clearing bands destroys the operator's schedule and
7296
+ * turning the camera back on would then silently record nothing.
7297
+ * - **A switch that is off must be reported as off**, not merely produce
7298
+ * nothing. {@link CameraSwitch.enabled} is what a status surface renders as
7299
+ * "disabled by an operator" instead of "broken" — see
7300
+ * `CameraStatus.switchedOff`.
7301
+ */
7302
+ /**
7303
+ * The five functions the operator named (2026-08-05). Deliberately NOT one id
7304
+ * per pipeline step: face recognition and plate/LPR are per-step toggles on
7305
+ * `pipelineOrchestrator.setCameraStepToggle` and belong in the pipeline
7306
+ * editor, not in a five-button safety group.
7307
+ */
7308
+ var CameraSwitchIdSchema = _enum([
7309
+ "stream-broker",
7310
+ "object-detection",
7311
+ "audio-analysis",
7312
+ "recording",
7313
+ "notifications"
7314
+ ]);
7315
+ /**
7316
+ * WHERE the switch's state actually lives. A discriminated union rather than a
7317
+ * string so both the writer (the orchestrator's `setCameraSwitch`) and any
7318
+ * reader can exhaustively narrow — and so "the group added a parallel map" is
7319
+ * a compile error rather than a review comment.
7320
+ */
7321
+ var CameraSwitchAuthoritySchema = discriminatedUnion("kind", [
7322
+ object({ kind: literal("device-disabled") }),
7323
+ object({
7324
+ kind: literal("wrapper-binding"),
7325
+ capName: string()
7326
+ }),
7327
+ object({ kind: literal("recording-config") }),
7328
+ object({ kind: literal("notification-mute") })
7329
+ ]);
7330
+ /**
7331
+ * Why a switch is not offered for this camera. Rendered instead of the
7332
+ * control, never as a dead control — an absent function and a broken one must
7333
+ * not look the same.
7334
+ */
7335
+ var CameraSwitchUnavailableReasonSchema = _enum(["no-provider", "source-unreachable"]);
7336
+ /**
7337
+ * One switch, resolved for one camera.
7338
+ *
7339
+ * `label` and `costWhenOff` travel ON THE WIRE rather than being looked up
7340
+ * client-side: the viewer is a separate repository that does not import
7341
+ * `@camstack/types`, and a cost line duplicated in two clients is a cost line
7342
+ * that will disagree with itself. Five rows per camera is nothing.
7343
+ */
7344
+ var CameraSwitchSchema = object({
7345
+ id: CameraSwitchIdSchema,
7346
+ label: string(),
7347
+ /**
7348
+ * What the operator LOSES while this is off, in one sentence. Required, not
7349
+ * optional: a switch that cannot say what it costs should not ship.
7350
+ */
7351
+ costWhenOff: string(),
7352
+ /** False = do not render a control. `unavailableReason` says why. */
7353
+ available: boolean(),
7354
+ unavailableReason: CameraSwitchUnavailableReasonSchema.optional(),
7355
+ /** Current state. Meaningless when `available` is false — read it as `true`. */
7356
+ enabled: boolean(),
7357
+ authority: CameraSwitchAuthoritySchema
7358
+ });
7359
+ /** The whole group for one camera. */
7360
+ var CameraSwitchGroupSchema = object({
7361
+ deviceId: number().int(),
7362
+ switches: array(CameraSwitchSchema).readonly(),
7363
+ /** Unix ms when the group was composed server-side. */
7364
+ fetchedAt: number()
7365
+ });
7366
+ /**
7213
7367
  * Ops-log — the durable, append-only operations audit shared by the
7214
7368
  * recordings and events management surfaces.
7215
7369
  *
@@ -7228,14 +7382,16 @@ var OpsLogOpSchema = _enum([
7228
7382
  "manual-delete",
7229
7383
  "rescan",
7230
7384
  "retention-run",
7231
- "relocate"
7385
+ "relocate",
7386
+ "orphan-audit"
7232
7387
  ]);
7233
7388
  /** Why the operation ran. */
7234
7389
  var OpsLogReasonSchema = _enum([
7235
7390
  "retention",
7236
7391
  "quota",
7237
7392
  "manual",
7238
- "operator"
7393
+ "operator",
7394
+ "maintenance"
7239
7395
  ]);
7240
7396
  /** One audit row, shared verbatim by both domains. */
7241
7397
  var OpsLogEntrySchema = object({
@@ -9121,6 +9277,100 @@ var RtpSourceSchema = object({
9121
9277
  encoder: string(),
9122
9278
  pipelineKey: string()
9123
9279
  });
9280
+ /**
9281
+ * The encode request — **structured and serialisable, with NO raw-flag escape
9282
+ * hatch.** This is deliberate and it is the one lesson taken from
9283
+ * `getStreamWithCodec`: that method's `outputArgs: string[]` is simultaneously
9284
+ * its extensibility mechanism AND part of `pipelineKeyFor`'s sharing key, so
9285
+ * adding a flag silently forks the shared child, and two consumers that mean
9286
+ * the same thing but spell it differently never share. Here every knob is a
9287
+ * NAMED field: a new requirement becomes a schema field (and a codegen run),
9288
+ * never an opaque array.
9289
+ *
9290
+ * `inputArgs` / `outputArgs` are omitted from the profile for the same reason.
9291
+ * The operator-facing derived-stream transform editor still has them — that is
9292
+ * a different surface (`publishCameraStream({ kind: 'derived' })`) with a
9293
+ * different purpose (reshaping a badly-behaved SOURCE), and it is unchanged.
9294
+ */
9295
+ var EgressEncodeSchema = EncodeProfileSchema.omit({
9296
+ inputArgs: true,
9297
+ outputArgs: true
9298
+ });
9299
+ /**
9300
+ * How the encoder is bounded. `'tight'` is a one-second VBV window for a
9301
+ * consumer whose budget is enforced per second (HomeKit); `'relaxed'` is two
9302
+ * seconds, letting a keyframe spike borrow from the next second (a browser,
9303
+ * an Echo). Named rather than numeric so the INTENT survives.
9304
+ */
9305
+ var EgressRateControlSchema = _enum(["tight", "relaxed"]);
9306
+ var EgressTranscodeRequestSchema = object({
9307
+ deviceId: number().int().nonnegative(),
9308
+ /** Which published stream to read. */
9309
+ source: discriminatedUnion("kind", [object({
9310
+ kind: literal("profile"),
9311
+ profile: CamProfileSchema
9312
+ }), object({
9313
+ kind: literal("cam-stream"),
9314
+ camStreamId: string().min(1)
9315
+ })]),
9316
+ encode: EgressEncodeSchema,
9317
+ rateControl: EgressRateControlSchema.optional(),
9318
+ /**
9319
+ * `-bsf:v`. A consumer that negotiates its OWN SDP (HomeKit) cannot carry
9320
+ * out-of-band extradata and needs `dump_extra` on both the copy and encode
9321
+ * branches. Enumerated, not free text.
9322
+ */
9323
+ bitstreamFilter: _enum([
9324
+ "dump_extra",
9325
+ "h264_mp4toannexb",
9326
+ "hevc_mp4toannexb"
9327
+ ]).optional(),
9328
+ pixelFormat: _enum(["yuv420p", "nv12"]).optional(),
9329
+ /**
9330
+ * Operator/consumer override for decode hardware. ABSENT is the normal case
9331
+ * and the one that matters: the broker then resolves the backend from the
9332
+ * DECODER ADDON's per-node `probedBestHwaccel` (see
9333
+ * `@camstack/types` `ffmpeg/hwaccel.ts`), which is the ranking known to work
9334
+ * on this hardware — never the raw kernel resolver's qsv-first order.
9335
+ */
9336
+ decodeHwAccel: _enum([
9337
+ "auto",
9338
+ "none",
9339
+ "videotoolbox",
9340
+ "vaapi",
9341
+ "qsv",
9342
+ "cuda"
9343
+ ]).optional(),
9344
+ /**
9345
+ * Host to embed in the returned restream `url`. The broker mints hub-local
9346
+ * `127.0.0.1` URLs; a consumer on another node passes a cluster-resolvable
9347
+ * host (`NodeTopologyService.reachableHostByNode`) so the returned URL is
9348
+ * dialable from there. Same contract as `getStreamWithCodec.hostname` —
9349
+ * `substituteRtspHost` rewrites only the dial address, never the restreamer.
9350
+ */
9351
+ hostname: string().optional(),
9352
+ /** Attribution for the broker panel. Never part of the sharing key. */
9353
+ tag: string().optional()
9354
+ });
9355
+ var EgressTranscodeSchema = object({
9356
+ /** Dial-able RTSP url (host-substituted when `hostname` was supplied). */
9357
+ url: string(),
9358
+ /** Release handle. Refcounted — the child dies when the last holder releases. */
9359
+ pipelineKey: string(),
9360
+ videoCodec: _enum(["H264", "H265"]),
9361
+ resolution: object({
9362
+ width: number().int().positive(),
9363
+ height: number().int().positive()
9364
+ }),
9365
+ transcoded: boolean(),
9366
+ encoder: string(),
9367
+ /**
9368
+ * The decode backend the child ACTUALLY ran with — `null` for software.
9369
+ * Returned rather than assumed: a consumer that asked for hardware and got
9370
+ * software needs to be able to see that without reading the broker's logs.
9371
+ */
9372
+ decodeHwAccel: string().nullable()
9373
+ });
9124
9374
  method(object({
9125
9375
  deviceId: number().int().nonnegative(),
9126
9376
  camStreamId: string().min(1),
@@ -9230,6 +9480,15 @@ method(object({
9230
9480
  }), {
9231
9481
  kind: "mutation",
9232
9482
  auth: "admin"
9483
+ }), method(EgressTranscodeRequestSchema, EgressTranscodeSchema, {
9484
+ kind: "mutation",
9485
+ auth: "admin"
9486
+ }), method(object({ pipelineKey: string() }), object({
9487
+ released: boolean(),
9488
+ refcount: number().int().nonnegative()
9489
+ }), {
9490
+ kind: "mutation",
9491
+ auth: "admin"
9233
9492
  }), method(SubscribeAudioChunksInputSchema, SubscribeAudioChunksResultSchema, { kind: "mutation" }), method(object({
9234
9493
  subscriptionId: string(),
9235
9494
  maxCount: number().int().positive().default(8)
@@ -13842,12 +14101,13 @@ var NcConditionsSchema = object({
13842
14101
  * source; otherwise the subject's source must equal it. Legacy records
13843
14102
  * with no stamped source are treated as `pipeline`. The union spans both
13844
14103
  * record kinds — object events carry `pipeline` | `onboard`, synthetic
13845
- * tracks carry `sensor`.
14104
+ * tracks carry `sensor` (a linked device) or `audio` (a D62 audio marker).
13846
14105
  */
13847
14106
  source: _enum([
13848
14107
  "pipeline",
13849
14108
  "onboard",
13850
14109
  "sensor",
14110
+ "audio",
13851
14111
  "any"
13852
14112
  ]).optional(),
13853
14113
  /**
@@ -14401,6 +14661,10 @@ var NC_CONDITION_CATALOG = [
14401
14661
  {
14402
14662
  value: "sensor",
14403
14663
  label: "Sensor"
14664
+ },
14665
+ {
14666
+ value: "audio",
14667
+ label: "Audio marker"
14404
14668
  }
14405
14669
  ],
14406
14670
  operator: "in",
@@ -14411,7 +14675,7 @@ var NC_CONDITION_CATALOG = [
14411
14675
  "package-event"
14412
14676
  ],
14413
14677
  phase: "P1",
14414
- description: "pipeline / onboard / sensor; a record with no stamped source counts as pipeline."
14678
+ description: "pipeline / onboard / sensor / audio; a record with no stamped source counts as pipeline."
14415
14679
  },
14416
14680
  {
14417
14681
  id: "deviceState",
@@ -14757,6 +15021,35 @@ var notificationRulesCapability = {
14757
15021
  auth: "admin"
14758
15022
  }),
14759
15023
  /**
15024
+ * PERMANENT per-camera mute — the notifications half of the per-camera
15025
+ * function switch group ([D61](../../../../docs/decisions/adr-0067.md)).
15026
+ *
15027
+ * Deliberately NOT a snooze. A snooze is bounded at
15028
+ * {@link NC_SNOOZE_MAX_MINUTES} on purpose — "a snooze that could not
15029
+ * expire would be an outage the operator asked for once and forgot" — and
15030
+ * widening it to express "this camera never notifies" would destroy that
15031
+ * property for every snooze. A mute is the other thing: an explicit,
15032
+ * indefinite, admin-only decision, visible in the switch group next to the
15033
+ * other four, and reported on `CameraStatus.switchedOff` so a silent
15034
+ * camera never reads as a working one.
15035
+ *
15036
+ * Returned as ONE list rather than a per-camera query: the group's reader
15037
+ * needs every camera's state, and a per-camera fan-out over the viewer's
15038
+ * single WebSocket is N frames serialised on one socket.
15039
+ */
15040
+ listDeviceMutes: method(object({}), object({ mutedDeviceIds: array(number().int()).readonly() }), { auth: "admin" }),
15041
+ /**
15042
+ * Mute or unmute one camera. Idempotent; an unmute of a camera that was
15043
+ * never muted succeeds.
15044
+ */
15045
+ setDeviceMuted: method(object({
15046
+ deviceId: number().int(),
15047
+ muted: boolean()
15048
+ }), object({ success: literal(true) }), {
15049
+ kind: "mutation",
15050
+ auth: "admin"
15051
+ }),
15052
+ /**
14760
15053
  * Dry-run a rule against recently persisted records (object events for
14761
15054
  * `immediate`, closed tracks for `track-end`). Mutation kind only to
14762
15055
  * carry the full rule object safely; no side effects.
@@ -15162,12 +15455,60 @@ var TrackAudioLabelSchema = object({
15162
15455
  });
15163
15456
  /**
15164
15457
  * How a track was produced. `pipeline` (default / absent) = the spatial
15165
- * detection+tracking pipeline. `sensor` = a SYNTHETIC track projected from a
15166
- * linked sensor/control state change (no positions; carries a snapshot). The
15167
- * spatial subsystems (tracker association, occupancy count, re-id/embedding,
15168
- * resurrection) MUST skip `sensor` tracks — they have no bbox trajectory.
15458
+ * detection+tracking pipeline. Every OTHER value is a SYNTHETIC projection —
15459
+ * no positions, a single snapshot, and no bbox trajectory at all:
15460
+ *
15461
+ * - `sensor` — a linked sensor/control device state change.
15462
+ * - `audio` — an audio event on the camera itself that was anomalous for
15463
+ * THAT camera, loud, and heard while nothing visual was happening (D62).
15464
+ *
15465
+ * The spatial subsystems (tracker association, occupancy count, re-id /
15466
+ * embedding, resurrection) MUST skip every synthetic source. Test for that
15467
+ * with `isSpatialTrack`, which allow-lists `pipeline` — a `!== 'sensor'`
15468
+ * check silently readmits every source added after it was written.
15169
15469
  */
15170
- var TrackSourceSchema = _enum(["pipeline", "sensor"]);
15470
+ var TrackSourceSchema = _enum([
15471
+ "pipeline",
15472
+ "sensor",
15473
+ "audio"
15474
+ ]);
15475
+ /**
15476
+ * Per-track OPERATOR flags — set by hand from the admin UI or the viewer, never
15477
+ * by the pipeline. Spread into `TrackSchema` and `KeyEventSchema` from one place
15478
+ * so the two surfaces cannot drift.
15479
+ *
15480
+ * **Absent ≠ false.** A track that has never been touched omits the field; an
15481
+ * explicitly un-flagged track carries `false`. Legacy rows written before the
15482
+ * columns existed read as absent, and a consumer that needs a boolean should say
15483
+ * `flag === true`, not `flag !== false`.
15484
+ *
15485
+ * What the flags DO is deliberately UNDEFINED at the time of writing: they are
15486
+ * operator curation, and the behaviour they drive will be specified separately.
15487
+ * In particular a `markForTrain` track is NOT pinned against retention — see
15488
+ * `docs/decisions/adr-0059.md` for why that is a store-level change, not a flag.
15489
+ */
15490
+ var TrackFlagFields = {
15491
+ /** Operator marked this track as training material. */
15492
+ markForTrain: boolean().optional(),
15493
+ /** Operator marked this track for diagnostic attention. */
15494
+ debug: boolean().optional()
15495
+ };
15496
+ /**
15497
+ * The write half: a PARTIAL patch. An omitted key is left untouched, so setting
15498
+ * one flag can never clear the other — the toggles are independent and are
15499
+ * driven from three surfaces that do not know about each other.
15500
+ */
15501
+ var TrackFlagsPatchSchema = object(TrackFlagFields);
15502
+ /**
15503
+ * The resolved flag state after a write. Both fields are REQUIRED here (absent
15504
+ * collapses to `false`) so a caller can drive a toggle's checked state off the
15505
+ * mutation result without a re-fetch.
15506
+ */
15507
+ var TrackFlagsSchema = object({
15508
+ trackId: string(),
15509
+ markForTrain: boolean(),
15510
+ debug: boolean()
15511
+ });
15171
15512
  var TrackSchema = object({
15172
15513
  trackId: string(),
15173
15514
  deviceId: number(),
@@ -15210,7 +15551,8 @@ var TrackSchema = object({
15210
15551
  /** Normalized 0..1 trajectory envelope (see {@link TrackEnvelopeSchema}).
15211
15552
  * Populated from the persisted envelope columns on historical reads;
15212
15553
  * absent on legacy rows, dims-less tracks and active (in-RAM) tracks. */
15213
- envelope: TrackEnvelopeSchema.optional()
15554
+ envelope: TrackEnvelopeSchema.optional(),
15555
+ ...TrackFlagFields
15214
15556
  });
15215
15557
  var BaseEventFields = {
15216
15558
  id: string(),
@@ -15423,7 +15765,8 @@ var KeyEventSchema = object({
15423
15765
  /** Highest-confidence ObjectEvent id for the track (empty when none). */
15424
15766
  bestEventId: string(),
15425
15767
  /** Track lifetime in ms (lastSeen - firstSeen). */
15426
- windowMs: number().optional()
15768
+ windowMs: number().optional(),
15769
+ ...TrackFlagFields
15427
15770
  });
15428
15771
  object({
15429
15772
  trackId: string(),
@@ -15509,7 +15852,31 @@ var RebuildObjectEmbeddingsInput = object({
15509
15852
  since: number().optional(),
15510
15853
  until: number().optional(),
15511
15854
  /** Stop after this many tracks; the result reports whether more remain. */
15512
- maxTracks: number().int().positive().optional()
15855
+ maxTracks: number().int().positive().optional(),
15856
+ /**
15857
+ * Run every embedding on THIS node instead of round-robining the fleet.
15858
+ *
15859
+ * Named `executeOnNodeId` and not `nodeId` on purpose: an inline `nodeId`
15860
+ * field in cap args is read by `parent-unowned-call.ts` as a ROUTING PIN, so
15861
+ * calling it that would pin the rebuild REQUEST itself to that node — the
15862
+ * rebuild orchestration lives on the hub, and only the per-track step runs
15863
+ * remotely. This field is data; the per-track pin is applied inside.
15864
+ *
15865
+ * Absent ⇒ round-robin over every online node whose runner can serve the
15866
+ * pinned model.
15867
+ */
15868
+ executeOnNodeId: string().optional(),
15869
+ /**
15870
+ * Milliseconds to wait between tracks; omit for the built-in default, `0` to
15871
+ * run flat out.
15872
+ *
15873
+ * A rebuild is bulk maintenance on hub-main's single thread. Measured
15874
+ * 2026-08-06, an unpaced pass held that thread busy 82.2 s out of 120 and
15875
+ * pushed `nodes.topology` from 0.25 s to 26 s for 43 minutes. The value in
15876
+ * force is logged at start and finish so a deliberately slow pass reads
15877
+ * differently from a stalled one.
15878
+ */
15879
+ pacingMs: number().int().nonnegative().optional()
15513
15880
  });
15514
15881
  /**
15515
15882
  * Result of emptying the CLIP index.
@@ -15543,13 +15910,23 @@ var RebuildStatusSchema = object({
15543
15910
  /** Tracks with no usable detection box. */
15544
15911
  missingBbox: number(),
15545
15912
  /**
15546
- * Tracks the pipeline REFUSED rather than broke on: the camera is not
15547
- * attached, or `clip-embedding` is not enabled in its step tree. Separate
15548
- * from `failed` because the remedy is a configuration change, not an engine
15549
- * investigation — and because a pass over decommissioned cameras would
15550
- * otherwise read as a total engine outage.
15913
+ * Tracks an executing node REFUSED rather than broke on — an unreadable key
15914
+ * frame, a step that threw. Separate from `failed` because the remedy is
15915
+ * different, and because a whole camera silently contributing zero vectors
15916
+ * is the shape of failure a rebuild must never hide.
15551
15917
  */
15552
15918
  notRunnable: number(),
15919
+ /**
15920
+ * The pass stopped because NO node could serve the pinned model.
15921
+ *
15922
+ * Distinct from `notRunnable` on purpose: that one says "this track was
15923
+ * refused", this one says "the cluster cannot do this work at all" — every
15924
+ * candidate node either lacks the `clip-embedding` step, lacks a build of the
15925
+ * pinned model for its engine format, or dropped out. The remedy is a model /
15926
+ * engine change, not a per-camera one. Non-zero here always comes with
15927
+ * `complete: false`.
15928
+ */
15929
+ noCapableNode: number(),
15553
15930
  failed: number(),
15554
15931
  /** Set once a pass ends: true only when EVERYTHING was covered. */
15555
15932
  complete: boolean().nullable(),
@@ -15739,6 +16116,31 @@ var pipelineAnalyticsCapability = {
15739
16116
  auth: "admin"
15740
16117
  }),
15741
16118
  /**
16119
+ * Set the per-track operator flags (`markForTrain`, `debug`) on ONE track.
16120
+ * The patch is PARTIAL — an omitted key is left untouched — because the
16121
+ * three surfaces that write it (admin Events grid, viewer track detail,
16122
+ * viewer cluster detail) each own one toggle and must not clobber the other.
16123
+ *
16124
+ * Writes the track ROW: `markForTrain`/`debug` are per-TRACK state, so they
16125
+ * live where `label` and `importance` live, not in any per-device settings
16126
+ * store. Updates the in-RAM active track too, so a flag set on a live track
16127
+ * survives its expiry-time persist.
16128
+ *
16129
+ * `auth: 'protected'` (the default), NOT `admin`: the viewer is an
16130
+ * authenticated non-admin surface and two of the three call sites are
16131
+ * there. Revisit if a flag ever gains an effect that costs storage —
16132
+ * `deleteTracks` next door is admin for exactly that reason.
16133
+ *
16134
+ * Returns the RESOLVED state of both flags (absent → `false`) so a caller
16135
+ * can drive its toggle without a re-fetch. Rejects an unknown track.
16136
+ */
16137
+ setTrackFlags: method(object({
16138
+ /** Log/audit scope only — the trackId is globally unique on its own. */
16139
+ deviceId: number(),
16140
+ trackId: string(),
16141
+ flags: TrackFlagsPatchSchema
16142
+ }), TrackFlagsSchema, { kind: "mutation" }),
16143
+ /**
15742
16144
  * Durable event-store footprint for the management UI: event rows
15743
16145
  * (motion + object + audio) counted per camera + total, plus the
15744
16146
  * event-owned media bytes on disk per camera + total. Stat/count-based,
@@ -16465,6 +16867,53 @@ var DetailResultSchema = object({
16465
16867
  nativeFaceShortSidePx: number().optional()
16466
16868
  });
16467
16869
  /**
16870
+ * Why an executing node REFUSED a stateless step run (`runStatelessStep`).
16871
+ *
16872
+ * A refusal is a first-class answer, not an error, because the caller's next
16873
+ * move depends on WHICH one it is — and because "the pass produced nothing"
16874
+ * must never be reachable without a named, counted cause. The two tiers:
16875
+ *
16876
+ * - **node-level** (`unknown-step`, `model-not-servable`) — this node can
16877
+ * never serve this (step, model) pair. The caller drops it from its rotation
16878
+ * and retries the same work elsewhere; nothing about the work changes.
16879
+ * - **work-level** (`unreadable-frame`, `execution-failed`) — this node is
16880
+ * fine, this one request is not. Retrying it on another node would only
16881
+ * spread the same failure.
16882
+ */
16883
+ var StatelessStepRefusalSchema = _enum([
16884
+ "unknown-step",
16885
+ "model-not-servable",
16886
+ "unreadable-frame",
16887
+ "execution-failed"
16888
+ ]);
16889
+ /**
16890
+ * Answer to `runStatelessStep` — a discriminated union rather than a nullable
16891
+ * result, because `null` is exactly what made the camera-bound detail path
16892
+ * unable to tell "refused" from "never asked".
16893
+ */
16894
+ var RunStatelessStepResultSchema = discriminatedUnion("kind", [object({
16895
+ kind: literal("ran"),
16896
+ /** The node that actually executed it — the pin, echoed back for the log. */
16897
+ nodeId: string(),
16898
+ /**
16899
+ * The model the step ran with.
16900
+ *
16901
+ * The node verified this exact id has a build for the format it dispatched
16902
+ * on BEFORE running, so the executor's format resolution returns it
16903
+ * unchanged. A caller that pinned a model must compare this field and
16904
+ * treat a mismatch as a refusal — the whole point of the pin is that a
16905
+ * pass writes one feature space.
16906
+ */
16907
+ modelId: string(),
16908
+ details: array(DetailResultSchema)
16909
+ }), object({
16910
+ kind: literal("refused"),
16911
+ nodeId: string(),
16912
+ reason: StatelessStepRefusalSchema,
16913
+ /** Human-readable specifics — the format tried, the formats shipped, etc. */
16914
+ detail: string()
16915
+ })]);
16916
+ /**
16468
16917
  * Per-camera tunable ranges + defaults. Single source of truth used
16469
16918
  * by both the Zod data schema (validation + default fallback) and
16470
16919
  * the device settings UI (slider min/max/step). Touch one place and
@@ -16814,7 +17263,32 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
16814
17263
  cropJpeg: string().optional(),
16815
17264
  parent: DetailParentSchema,
16816
17265
  steps: array(string()).optional()
16817
- }), object({ details: array(DetailResultSchema) }).nullable(), { kind: "mutation" });
17266
+ }), object({ details: array(DetailResultSchema) }).nullable(), { kind: "mutation" }), method(object({
17267
+ /** Catalog step id, e.g. `clip-embedding`. */
17268
+ stepId: string(),
17269
+ /**
17270
+ * REQUIRED model pin. The node runs this exact model or refuses with
17271
+ * `model-not-servable` — it never substitutes a format default, because
17272
+ * a fleet pass that round-robins across nodes would then fill one index
17273
+ * from several encoders.
17274
+ */
17275
+ modelId: string(),
17276
+ /** FULL FRAME, base64 JPEG. The runner cuts — do NOT pre-crop. */
17277
+ frameJpeg: string(),
17278
+ /**
17279
+ * The subject box, NORMALISED [0,1] against `frameJpeg`. Normalised on
17280
+ * purpose: the caller stores boxes against a downscaled analysis frame
17281
+ * while the stored key frame is native-resolution, and the only side
17282
+ * that reliably knows the image's pixel dimensions is the side that
17283
+ * decodes it. Denormalising here removes a second reader of the
17284
+ * dimensions and the class of mismatch that comes with it.
17285
+ */
17286
+ bbox: NativeCropBboxSchema,
17287
+ /** Parent class of the subject (`person`, `vehicle`, …) — carried into the result. */
17288
+ className: string(),
17289
+ /** Camera the pixels came from. Diagnostics + log tags ONLY — never routing. */
17290
+ sourceDeviceId: number()
17291
+ }), RunStatelessStepResultSchema, { kind: "mutation" });
16818
17292
  var CameraPipelineConfigSchema = object({
16819
17293
  engine: PipelineEngineChoiceSchema.optional(),
16820
17294
  steps: array(PipelineStepInputSchema).readonly(),
@@ -17112,6 +17586,20 @@ var CameraStatusSchema = object({
17112
17586
  detection: CameraDetectionStatusSchema.nullable(),
17113
17587
  audio: CameraAudioStatusSchema.nullable(),
17114
17588
  recording: CameraRecordingStatusSchema.nullable(),
17589
+ /**
17590
+ * Per-camera function switches an OPERATOR has turned off
17591
+ * ([D61](../../../../docs/decisions/adr-0067.md)).
17592
+ *
17593
+ * This is the difference between DISABLED and BROKEN. A camera whose
17594
+ * `detection` block reports zero fps and whose `switchedOff` contains
17595
+ * `'object-detection'` was switched off by a person; the same camera with an
17596
+ * empty list is failing. Every status surface must render the two
17597
+ * differently — a quiet camera that looks identical to a dead one is the
17598
+ * silence-reads-as-never-happened trap this repo keeps paying for.
17599
+ *
17600
+ * Empty when nothing is off. Never contains a switch no provider offers.
17601
+ */
17602
+ switchedOff: array(CameraSwitchIdSchema).readonly(),
17115
17603
  /** Unix timestamp (ms) when this snapshot was composed server-side. */
17116
17604
  fetchedAt: number()
17117
17605
  });
@@ -17280,7 +17768,14 @@ method(object({
17280
17768
  }), method(object({
17281
17769
  deviceId: number(),
17282
17770
  agentNodeId: string().optional()
17283
- }), CameraPipelineConfigSchema), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
17771
+ }), CameraPipelineConfigSchema), method(object({ deviceId: number() }), CameraSwitchGroupSchema), method(object({
17772
+ deviceId: number(),
17773
+ switchId: CameraSwitchIdSchema,
17774
+ enabled: boolean()
17775
+ }), CameraSwitchGroupSchema, {
17776
+ kind: "mutation",
17777
+ auth: "admin"
17778
+ }), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
17284
17779
  name: string(),
17285
17780
  description: string().optional(),
17286
17781
  config: CameraPipelineConfigSchema
@@ -28854,6 +29349,12 @@ Object.freeze({
28854
29349
  addonId: null,
28855
29350
  access: "view"
28856
29351
  },
29352
+ "notificationRules.listDeviceMutes": {
29353
+ capName: "notification-rules",
29354
+ capScope: "system",
29355
+ addonId: null,
29356
+ access: "view"
29357
+ },
28857
29358
  "notificationRules.listRules": {
28858
29359
  capName: "notification-rules",
28859
29360
  capScope: "system",
@@ -28872,6 +29373,12 @@ Object.freeze({
28872
29373
  addonId: null,
28873
29374
  access: "create"
28874
29375
  },
29376
+ "notificationRules.setDeviceMuted": {
29377
+ capName: "notification-rules",
29378
+ capScope: "system",
29379
+ addonId: null,
29380
+ access: "create"
29381
+ },
28875
29382
  "notificationRules.setRuleEnabled": {
28876
29383
  capName: "notification-rules",
28877
29384
  capScope: "system",
@@ -29148,6 +29655,12 @@ Object.freeze({
29148
29655
  addonId: null,
29149
29656
  access: "view"
29150
29657
  },
29658
+ "pipelineAnalytics.setTrackFlags": {
29659
+ capName: "pipeline-analytics",
29660
+ capScope: "device",
29661
+ addonId: null,
29662
+ access: "create"
29663
+ },
29151
29664
  "pipelineAnalytics.wipeAllAnalytics": {
29152
29665
  capName: "pipeline-analytics",
29153
29666
  capScope: "device",
@@ -29454,6 +29967,12 @@ Object.freeze({
29454
29967
  addonId: null,
29455
29968
  access: "view"
29456
29969
  },
29970
+ "pipelineOrchestrator.getCameraSwitches": {
29971
+ capName: "pipeline-orchestrator",
29972
+ capScope: "system",
29973
+ addonId: null,
29974
+ access: "view"
29975
+ },
29457
29976
  "pipelineOrchestrator.getCapabilityBindings": {
29458
29977
  capName: "pipeline-orchestrator",
29459
29978
  capScope: "system",
@@ -29586,6 +30105,12 @@ Object.freeze({
29586
30105
  addonId: null,
29587
30106
  access: "create"
29588
30107
  },
30108
+ "pipelineOrchestrator.setCameraSwitch": {
30109
+ capName: "pipeline-orchestrator",
30110
+ capScope: "system",
30111
+ addonId: null,
30112
+ access: "create"
30113
+ },
29589
30114
  "pipelineOrchestrator.setCapabilityBinding": {
29590
30115
  capName: "pipeline-orchestrator",
29591
30116
  capScope: "system",
@@ -29676,6 +30201,12 @@ Object.freeze({
29676
30201
  addonId: null,
29677
30202
  access: "create"
29678
30203
  },
30204
+ "pipelineRunner.runStatelessStep": {
30205
+ capName: "pipeline-runner",
30206
+ capScope: "system",
30207
+ addonId: null,
30208
+ access: "create"
30209
+ },
29679
30210
  "plateGallery.assignPlate": {
29680
30211
  capName: "plate-gallery",
29681
30212
  capScope: "system",
@@ -30492,6 +31023,12 @@ Object.freeze({
30492
31023
  addonId: null,
30493
31024
  access: "create"
30494
31025
  },
31026
+ "streamBroker.acquireEgressTranscode": {
31027
+ capName: "stream-broker",
31028
+ capScope: "system",
31029
+ addonId: null,
31030
+ access: "create"
31031
+ },
30495
31032
  "streamBroker.assignProfile": {
30496
31033
  capName: "stream-broker",
30497
31034
  capScope: "system",
@@ -30600,6 +31137,12 @@ Object.freeze({
30600
31137
  addonId: null,
30601
31138
  access: "create"
30602
31139
  },
31140
+ "streamBroker.releaseEgressTranscode": {
31141
+ capName: "stream-broker",
31142
+ capScope: "system",
31143
+ addonId: null,
31144
+ access: "create"
31145
+ },
30603
31146
  "streamBroker.releaseStreamWithCodec": {
30604
31147
  capName: "stream-broker",
30605
31148
  capScope: "system",
@@ -31557,6 +32100,12 @@ Object.defineProperty(exports, "TimelapseRuleSchema", {
31557
32100
  return TimelapseRuleSchema;
31558
32101
  }
31559
32102
  });
32103
+ Object.defineProperty(exports, "TrackSourceSchema", {
32104
+ enumerable: true,
32105
+ get: function() {
32106
+ return TrackSourceSchema;
32107
+ }
32108
+ });
31560
32109
  Object.defineProperty(exports, "__toESM", {
31561
32110
  enumerable: true,
31562
32111
  get: function() {
@@ -31731,6 +32280,12 @@ Object.defineProperty(exports, "record", {
31731
32280
  return record;
31732
32281
  }
31733
32282
  });
32283
+ Object.defineProperty(exports, "sleep", {
32284
+ enumerable: true,
32285
+ get: function() {
32286
+ return sleep;
32287
+ }
32288
+ });
31734
32289
  Object.defineProperty(exports, "string", {
31735
32290
  enumerable: true,
31736
32291
  get: function() {