@camstack/addon-provider-rademacher 0.2.6 → 0.2.8

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 (3) hide show
  1. package/dist/addon.js +463 -76
  2. package/dist/addon.mjs +463 -76
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -984,7 +984,7 @@ var Rademacher = class {
984
984
  }
985
985
  };
986
986
  //#endregion
987
- //#region ../types/dist/event-category-BLcNejAE.mjs
987
+ //#region ../types/dist/event-category-Bz24uP1U.mjs
988
988
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
989
989
  EventCategory["SystemBoot"] = "system.boot";
990
990
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -1255,6 +1255,19 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
1255
1255
  */
1256
1256
  EventCategory["DeviceStateChanged"] = "device.state-changed";
1257
1257
  /**
1258
+ * Frame occupancy for a camera CHANGED — a tracked object was gained or
1259
+ * lost. Carries `{ deviceId, totalObjects, byClass, zones }`.
1260
+ *
1261
+ * Emitted only on a change, so a steady scene is silent. It exists so a
1262
+ * client can stop polling `zoneAnalytics.getCurrentSnapshot`: that was the
1263
+ * one live badge with no push signal at all, and it cost a request every
1264
+ * four seconds per visible camera.
1265
+ *
1266
+ * Like every event it is telemetry and may be dropped ([D8]) — a consumer
1267
+ * keeps a slow reconcile rather than trusting it alone.
1268
+ */
1269
+ EventCategory["ZoneAnalyticsOccupancyChanged"] = "zone-analytics.occupancy-changed";
1270
+ /**
1258
1271
  * Cap event fired by every device that registers the `battery`
1259
1272
  * capability. Mirrors the cap definition's `onStatusChanged`. Carries
1260
1273
  * `{ deviceId, status: BatteryStatus }`. Subscribers (alert center,
@@ -8198,35 +8211,22 @@ var ConvertResultSchema = object({
8198
8211
  */
8199
8212
  var RecordingWeekdaySchema = number().int().min(0).max(6);
8200
8213
  var HHMM = /^([01]\d|2[0-3]):[0-5]\d$/;
8201
- var RecordingScheduleSchema = discriminatedUnion("kind", [object({ kind: literal("always") }), object({
8202
- kind: literal("timeOfDay"),
8203
- start: string().regex(HHMM),
8204
- end: string().regex(HHMM),
8205
- /** Restrict to these weekdays; omit = every day. */
8206
- days: array(RecordingWeekdaySchema).optional()
8207
- })]);
8208
- var RecordingModeSchema = _enum([
8209
- "continuous",
8210
- "onMotion",
8211
- "onAudioThreshold"
8212
- ]);
8213
8214
  /**
8214
- * First-class, authoritative per-camera storage mode — the explicit choice the
8215
- * UI reads directly (never inferred from `rules`):
8216
- * - `off` — not recording.
8217
- * - `events` — record only around triggers (motion / audio threshold),
8218
- * with pre/post-buffer.
8219
- * - `continuous` — record 24/7 within the schedule.
8215
+ * DERIVED per-camera storage summary — the single field cheap consumers read
8216
+ * (the viewer's status dot, the camera list) instead of walking `bands`:
8217
+ * - `off` — no band covers the camera (or it is disabled).
8218
+ * - `events` — every band records around triggers only.
8219
+ * - `continuous` — at least one band records continuously.
8220
8220
  *
8221
- * `mode` compiles one-way to the internal `rules[]` consumed by the policy
8222
- * engine (see `compileRules`); `rules[]` is never authored directly anymore.
8221
+ * NEVER authored: the recorder stamps it from the authoritative `bands` on
8222
+ * every save (`activeModeForConfig`). Writing it has no effect.
8223
8223
  */
8224
8224
  var RecordingStorageModeSchema = _enum([
8225
8225
  "off",
8226
8226
  "events",
8227
8227
  "continuous"
8228
8228
  ]);
8229
- /** Which detectors trigger an `events`-mode recording. */
8229
+ /** Which detectors trigger an `events`-mode band. */
8230
8230
  var RecordingTriggersSchema = object({
8231
8231
  motion: boolean().optional(),
8232
8232
  audioThresholdDbfs: number().optional()
@@ -8262,18 +8262,6 @@ var RecordingBandSchema = object({
8262
8262
  preBufferSec: number().min(0).optional(),
8263
8263
  postBufferSec: number().min(0).optional()
8264
8264
  });
8265
- var RecordingRuleSchema = object({
8266
- schedule: RecordingScheduleSchema,
8267
- mode: RecordingModeSchema,
8268
- /** Seconds of footage to retain BEFORE a trigger (applied at keep/discard). */
8269
- preBufferSec: number().min(0).default(0),
8270
- /** Keep recording until this many seconds after the last trigger. */
8271
- postBufferSec: number().min(0).default(0),
8272
- /** Each new trigger restarts the post-buffer window. */
8273
- resetTimeoutOnNewEvent: boolean().default(true),
8274
- /** onAudioThreshold only — dBFS level that counts as a trigger. */
8275
- thresholdDbfs: number().optional()
8276
- });
8277
8265
  /**
8278
8266
  * Per-device retention overrides. Every field is optional; an unset or `0`
8279
8267
  * value inherits the node-wide recorder default. Only footage-lifetime limits
@@ -8307,40 +8295,28 @@ var ScrubThumbnailPresetSchema = _enum([
8307
8295
  /**
8308
8296
  * The full per-camera recording intent — the wire shape of a RecordingTarget.
8309
8297
  *
8310
- * `mode` is the authoritative storage choice; `schedule`/`triggers`/`pre`/`post`
8311
- * are its mode-specific parameters. `rules` is a DEPRECATED authoring input kept
8312
- * only for transition + migration (`migrateRulesToMode`); the policy engine
8313
- * consumes the compiled output of `compileRules(config)`, never `rules` directly.
8298
+ * `bands` is the ONLY authored recording intent: what to record, when, and on
8299
+ * which trigger. `mode` is a derived summary the recorder stamps on save; every
8300
+ * other field is a storage knob (profiles, segment length, retention, scrub).
8301
+ *
8302
+ * STRICT on purpose: the legacy authoring surface (`schedule`/`schedules`/
8303
+ * `triggers`/`preBufferSec`/`postBufferSec`/`rules`) was retired 2026-07-30.
8304
+ * A stale caller must fail loudly — silently stripping its legacy intent would
8305
+ * persist a band-less config, i.e. silently stop recording the camera.
8314
8306
  */
8315
8307
  var RecordingConfigSchema = object({
8316
8308
  enabled: boolean(),
8317
- /** Authoritative storage mode. Absent on legacy targets derived once via
8318
- * `migrateRulesToMode`, then persisted. */
8309
+ /** DERIVED summary of `bands`, stamped by the recorder on every save.
8310
+ * Authoring it has no effect — see {@link RecordingStorageModeSchema}. */
8319
8311
  mode: RecordingStorageModeSchema.optional(),
8320
8312
  profiles: array(CamProfileSchema).optional(),
8321
8313
  segmentSeconds: number().int().positive().optional(),
8322
- /** Shared recording time-bands for `events` & `continuous` — record only when
8323
- * the wall-clock falls inside one of these windows. Omit or empty = always.
8324
- * `continuous` compiles to one rule per band; `events` to band × trigger. */
8325
- schedules: array(RecordingScheduleSchema).optional(),
8326
- /** Legacy single-band predecessor of `schedules`. Read-compat only — it is
8327
- * normalized into `schedules` on read and never written going forward. (Not
8328
- * tagged `@deprecated`: the normalization paths must read it cast-free.) */
8329
- schedule: RecordingScheduleSchema.optional(),
8330
- /** `events`-mode only — which detectors trigger a recording. */
8331
- triggers: RecordingTriggersSchema.optional(),
8332
- /** `events`-mode only — seconds retained before / after a trigger. */
8333
- preBufferSec: number().min(0).optional(),
8334
- postBufferSec: number().min(0).optional(),
8335
- /** DEPRECATED authoring input; retained for migration/transition. */
8336
- rules: array(RecordingRuleSchema).optional(),
8337
8314
  /**
8338
- * AUTHORITATIVE mode-per-band recording model (recorder). When present it
8339
- * is the single source of truth; the legacy `mode`/`schedules`/`schedule`/
8340
- * `triggers`/`rules` fields above are kept for READ-COMPAT only and are
8341
- * derived into bands once via `migrateConfigToBands`.
8315
+ * AUTHORITATIVE mode-per-band recording model the single source of truth
8316
+ * the recorder's band engine consumes. An empty array = record nothing;
8317
+ * "off" is the absence of a covering band, never a band value.
8342
8318
  */
8343
- bands: array(RecordingBandSchema).optional(),
8319
+ bands: array(RecordingBandSchema).default([]),
8344
8320
  retention: RecordingRetentionSchema.optional(),
8345
8321
  /**
8346
8322
  * Per-camera scrub-thumbnail fidelity preset (resolution + JPEG quality for
@@ -8348,8 +8324,15 @@ var RecordingConfigSchema = object({
8348
8324
  * windows only — existing sheets are immutable, and each window's index
8349
8325
  * carries its own tile dims so mixed-preset history renders correctly.
8350
8326
  */
8351
- scrubThumbnails: ScrubThumbnailPresetSchema.optional()
8352
- });
8327
+ scrubThumbnails: ScrubThumbnailPresetSchema.optional(),
8328
+ /**
8329
+ * OPT-IN thumbnail-strip generation for this camera: every keyframe of the
8330
+ * low recording saved as a JPEG (the fast-drag scrub depth), a derived
8331
+ * cache that eviction reclaims with the footage. Absent/false = no strips
8332
+ * are written and scrub reads exact keyframes at every velocity.
8333
+ */
8334
+ stripsEnabled: boolean().optional()
8335
+ }).strict();
8353
8336
  /**
8354
8337
  * Ops-log — the durable, append-only operations audit shared by the
8355
8338
  * recordings and events management surfaces.
@@ -8368,7 +8351,8 @@ var OpsLogOpSchema = _enum([
8368
8351
  "prune",
8369
8352
  "manual-delete",
8370
8353
  "rescan",
8371
- "retention-run"
8354
+ "retention-run",
8355
+ "relocate"
8372
8356
  ]);
8373
8357
  /** Why the operation ran. */
8374
8358
  var OpsLogReasonSchema = _enum([
@@ -8407,6 +8391,55 @@ var OpsLogQueryInputSchema = object({
8407
8391
  limit: number().int().min(1).max(1e3).optional()
8408
8392
  });
8409
8393
  /**
8394
+ * Entity-relocation job state (storage entity-routing spec, Phase 4).
8395
+ *
8396
+ * One shape shared by the recorder's `relocateFootage` (segments + strips) and
8397
+ * pipeline-analytics' `relocateMedia` (event media blobs) so the admin Data
8398
+ * page renders both movers with one component. Jobs are in-RAM (a restart
8399
+ * forgets them — re-running is safe by construction: copy-if-absent, delete
8400
+ * after verify) and each completed/failed run also lands one durable ops-log
8401
+ * row on the owning addon's surface.
8402
+ */
8403
+ var RelocateJobStateSchema = _enum([
8404
+ "running",
8405
+ "done",
8406
+ "failed",
8407
+ "cancelled"
8408
+ ]);
8409
+ var RelocateJobSchema = object({
8410
+ jobId: string(),
8411
+ state: RelocateJobStateSchema,
8412
+ /** Source location — for media relocation this is informational ('*': rows
8413
+ * move from wherever they are to the target). */
8414
+ fromLocationId: string(),
8415
+ toLocationId: string(),
8416
+ /** Scoped device, or null = every device. */
8417
+ deviceId: number().nullable(),
8418
+ /** What the job moves (owner-addon specific: segments/strips or media). */
8419
+ entities: array(string()),
8420
+ filesMoved: number().int(),
8421
+ bytesMoved: number().int(),
8422
+ /** Total files discovered up front; null while (or when) unknown. */
8423
+ filesTotal: number().int().nullable(),
8424
+ startedAt: number(),
8425
+ finishedAt: number().nullable(),
8426
+ error: string().nullable()
8427
+ });
8428
+ var RelocateFootageInputSchema = object({
8429
+ deviceId: number().optional(),
8430
+ fromLocationId: string(),
8431
+ toLocationId: string(),
8432
+ entities: array(_enum(["segments", "strips"])).optional(),
8433
+ /** Copy throttle in MB/s (default 40) — the drain is a background chore,
8434
+ * never allowed to starve live writers. */
8435
+ throttleMbps: number().min(1).max(1e3).optional()
8436
+ });
8437
+ var RelocateMediaInputSchema = object({
8438
+ deviceId: number().optional(),
8439
+ toLocationId: string(),
8440
+ throttleMbps: number().min(1).max(1e3).optional()
8441
+ });
8442
+ /**
8410
8443
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
8411
8444
  * storage a location serves. Defined here (not in `capabilities/storage.cap.ts`)
8412
8445
  * so the persisted record schema and the consumer-facing cap can both consume it
@@ -8456,6 +8489,13 @@ var StorageLocationSchema = object({
8456
8489
  nodeId: string().optional(),
8457
8490
  isDefault: boolean().default(false),
8458
8491
  isSystem: boolean().default(false),
8492
+ /** COMPUTED at read time by the orchestrator (statfs of the backing volume
8493
+ * for node-local locations it can reach) — never persisted, absent when the
8494
+ * volume is remote/unreachable. The single capacity truth every UI reads. */
8495
+ capacity: object({
8496
+ totalBytes: number(),
8497
+ availableBytes: number()
8498
+ }).nullable().optional(),
8459
8499
  createdAt: number(),
8460
8500
  updatedAt: number()
8461
8501
  });
@@ -9223,7 +9263,8 @@ var NcTaxonomyEntrySchema = object({
9223
9263
  /** Macro/category parent for grouping ('car' → 'vehicle'); null for a top. */
9224
9264
  parentKind: string().nullable()
9225
9265
  });
9226
- object({
9266
+ /** The complete NC picker taxonomy — three grouped buckets. */
9267
+ var NcTaxonomySchema = object({
9227
9268
  videoClasses: array(NcTaxonomyEntrySchema),
9228
9269
  audioKinds: array(NcTaxonomyEntrySchema),
9229
9270
  labels: array(NcTaxonomyEntrySchema)
@@ -10126,6 +10167,7 @@ function shallowEqual(a, b) {
10126
10167
  for (const k of ak) if (a[k] !== b[k]) return false;
10127
10168
  return true;
10128
10169
  }
10170
+ new Set(["devices", "classes"]);
10129
10171
  /**
10130
10172
  * Shared geometry vocabulary for on-frame shape caps — privacy-mask,
10131
10173
  * motion-zones, and the detection zones/lines editor all speak this one
@@ -10284,6 +10326,29 @@ var NcOccupancyConditionSchema = object({
10284
10326
  count: number().int().min(0).default(1),
10285
10327
  sustainSeconds: number().int().min(0).max(3600).default(15)
10286
10328
  });
10329
+ /**
10330
+ * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
10331
+ *
10332
+ * The values are not symmetric, and deliberately so — the absent value has to
10333
+ * mean exactly what every rule authored before this condition existed already
10334
+ * does:
10335
+ * - `enter` — entries and every NON-crossing record (movement state,
10336
+ * package, sensor). Exits are rejected. **This is the absent behaviour**:
10337
+ * an operator who never asked for exits must not start receiving them.
10338
+ * - `exit` — ONLY an exit crossing. A record that is not a crossing at all
10339
+ * fails closed, because "the car left the drive" is a question about a
10340
+ * boundary, not about a detection.
10341
+ * - `any` — no direction filter; entries, exits and non-crossings alike.
10342
+ *
10343
+ * A rule asking for a direction should normally also scope `zones`, which the
10344
+ * engine evaluates against the crossed zone as well as the current membership
10345
+ * (an exit's membership no longer contains the zone it just left).
10346
+ */
10347
+ var NcCrossingSchema = _enum([
10348
+ "enter",
10349
+ "exit",
10350
+ "any"
10351
+ ]);
10287
10352
  /** Admin-zone membership condition (zone IDs as stamped by the ZoneEngine). */
10288
10353
  var NcZoneConditionSchema = object({
10289
10354
  ids: array(string().min(1)).min(1),
@@ -10308,6 +10373,13 @@ var NcConditionsSchema = object({
10308
10373
  /** Veto zones — any hit fails the rule. */
10309
10374
  zonesExclude: array(string().min(1)).optional(),
10310
10375
  /**
10376
+ * Zone-crossing direction. IMMEDIATE only — a crossing is a per-event fact
10377
+ * and a closed track carries none, so a `track-end` rule asking for one
10378
+ * fails closed (use `zones`, which tests `zonesVisited`). ABSENT = `enter`,
10379
+ * which is exactly today's behaviour. See {@link NcCrossingSchema}.
10380
+ */
10381
+ crossing: NcCrossingSchema.optional(),
10382
+ /**
10311
10383
  * Exact (case-insensitive) match on the record's collapsed `label`
10312
10384
  * (identity name / plate text / subclass).
10313
10385
  */
@@ -10442,17 +10514,85 @@ var NcRuleTargetSchema = object({
10442
10514
  * - `keyFrame` — the clean scene frame (no subject box).
10443
10515
  * - `none` — no attachment.
10444
10516
  */
10445
- var NcMediaPolicySchema = object({ attach: _enum([
10446
- "best",
10447
- "best-matching",
10448
- "keyFrame",
10449
- "none"
10450
- ]).default("best") });
10517
+ /**
10518
+ * WHAT THE PICTURE SHOWS — chosen explicitly, because the implicit ladders
10519
+ * conflate it with the selection strategy and betray the request: asking for
10520
+ * the clean scene frame on an object-event owner used to start at
10521
+ * `fullFrameBoxed`, i.e. the annotated frame (2026-07-30).
10522
+ * - `cropped` — the subject, tight. What "who is at the door" wants.
10523
+ * - `full` — the clean scene, no annotation. What "what is going on" wants.
10524
+ * - `boxed` — the scene WITH the detection boxes drawn, for verifying what
10525
+ * the pipeline actually saw.
10526
+ */
10527
+ var NcMediaFrameSchema = _enum([
10528
+ "cropped",
10529
+ "full",
10530
+ "boxed"
10531
+ ]);
10532
+ var NcMediaPolicySchema = object({
10533
+ attach: _enum([
10534
+ "best",
10535
+ "best-matching",
10536
+ "keyFrame",
10537
+ "none"
10538
+ ]).default("best"),
10539
+ /** What the still shows. Absent = `cropped` (the historical `best` shape). */
10540
+ frame: NcMediaFrameSchema.optional(),
10541
+ /** Crop the attached still to the rule's condition-zone bbox (padded) —
10542
+ * "show me the ZONE", not the whole scene or the subject crop. */
10543
+ zoneCrop: boolean().optional(),
10544
+ /**
10545
+ * Also attach a short GIF cut from the stream broker's clip ring around the
10546
+ * event — NOT from the recording, so the camera does not have to be
10547
+ * recording, and the window sits AROUND the moment instead of a segment
10548
+ * behind it. Fail-closed: a window the ring does not cover contributes no
10549
+ * gif, never a failed notification.
10550
+ */
10551
+ gif: boolean().optional(),
10552
+ /**
10553
+ * Also attach a short MP4 CLIP of the same cut. Same source and the same
10554
+ * fail-closed rule as `gif`. Prefer it where the backend takes video
10555
+ * (telegram, discord, zentik, webhook); backends that do not are handled by
10556
+ * the degrade engine, which drops the video and keeps the still.
10557
+ */
10558
+ clip: boolean().optional(),
10559
+ /** Seconds of footage BEFORE / AFTER the event instant. Defaults 3 / 7. */
10560
+ clipPreRollSec: number().int().min(0).max(30).optional(),
10561
+ clipPostRollSec: number().int().min(0).max(30).optional(),
10562
+ /**
10563
+ * Which stream profile the footage is cut from. Absent = the CHEAPEST
10564
+ * assigned profile: a notification is watched on a phone, so the 4K
10565
+ * rendition would burn CPU to produce a file the client downscales anyway.
10566
+ * A profile that is not assigned falls back to the cheapest, and the render
10567
+ * reports which one actually ran.
10568
+ */
10569
+ profile: CamProfileSchema.optional()
10570
+ });
10571
+ /**
10572
+ * Cooldown GRANULARITY over the subject's class — how much a fired
10573
+ * notification suppresses.
10574
+ * - `shared` (default, and the absent value) — one window for the whole
10575
+ * rule/scope: a cat silences the next dog for `cooldownSec`.
10576
+ * - `per-class` — an independent window per detected class, so cat→dog fires
10577
+ * at once and cat→cat still waits.
10578
+ *
10579
+ * AUDIO subjects are ALWAYS per-class regardless of this setting: a scream
10580
+ * must not be swallowed by a bark's window (the precedent this generalizes —
10581
+ * see `cooldownKey` in the rule engine).
10582
+ */
10583
+ var NcThrottleGranularitySchema = _enum(["shared", "per-class"]);
10451
10584
  /** Throttle — cooldown survives restarts (rebuilt from the outbox on boot). */
10452
10585
  var NcThrottleSchema = object({
10453
10586
  cooldownSec: number().int().min(0).max(86400).default(60),
10454
10587
  /** `rule` = one shared cooldown; `rule-device` = per-camera cooldown. */
10455
- scope: _enum(["rule", "rule-device"]).default("rule-device")
10588
+ scope: _enum(["rule", "rule-device"]).default("rule-device"),
10589
+ /**
10590
+ * Class granularity of the cooldown key. Optional rather than defaulted:
10591
+ * a Zod default does NOT run on the addon→addon cap path, so a persisted
10592
+ * rule authored before this field simply carries none — and the engine
10593
+ * reads absent as `shared`, the pre-existing behaviour.
10594
+ */
10595
+ granularity: NcThrottleGranularitySchema.optional()
10456
10596
  });
10457
10597
  /** Client-supplied rule fields (server stamps id/createdBy/createdAt/updatedAt). */
10458
10598
  var NcRuleInputSchema = object({
@@ -10461,7 +10601,19 @@ var NcRuleInputSchema = object({
10461
10601
  delivery: NcDeliverySchema,
10462
10602
  conditions: NcConditionsSchema.default({}),
10463
10603
  schedule: NcScheduleSchema.optional(),
10464
- targets: array(NcRuleTargetSchema).min(1),
10604
+ /** May be empty when `targetUsers` addresses at least one user — the
10605
+ * "at least one addressee" invariant is enforced by the provider, because
10606
+ * a cross-field refine here would break `NcRulePatchSchema.partial()`. */
10607
+ targets: array(NcRuleTargetSchema),
10608
+ /**
10609
+ * USERS this rule addresses in addition to `targets` (Phase 4). At fire
10610
+ * time each user fans out to the personal targets they own
10611
+ * (`config.ownerUserId`), filtered by that user's `allowedDevices` for the
10612
+ * firing camera — a user is never notified about a device they cannot open.
10613
+ * Per-user opt-outs (`disabledTargetIds`) still apply to the fanned-out
10614
+ * targets.
10615
+ */
10616
+ targetUsers: array(string()).optional(),
10465
10617
  media: NcMediaPolicySchema.default({ attach: "best" }),
10466
10618
  throttle: NcThrottleSchema.default({
10467
10619
  cooldownSec: 60,
@@ -10548,6 +10700,7 @@ var NcConditionDescriptorSchema = object({
10548
10700
  "schedule",
10549
10701
  "plateMatcher",
10550
10702
  "packagePhase",
10703
+ "crossingSelect",
10551
10704
  "polygonDraw",
10552
10705
  "occupancy"
10553
10706
  ]),
@@ -10674,7 +10827,10 @@ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), m
10674
10827
  }), object({ results: array(NcTestResultSchema) }), {
10675
10828
  kind: "mutation",
10676
10829
  auth: "admin"
10677
- }), method(object({}), object({ catalog: array(NcConditionDescriptorSchema) })), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" });
10830
+ }), method(object({}), object({
10831
+ catalog: array(NcConditionDescriptorSchema),
10832
+ taxonomy: NcTaxonomySchema.optional()
10833
+ })), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" });
10678
10834
  /**
10679
10835
  * TimelapseRule — the STANDALONE scheduled timelapse producer's rule model.
10680
10836
  *
@@ -11683,6 +11839,28 @@ method(object({
11683
11839
  }), object({ success: literal(true) }), {
11684
11840
  kind: "mutation",
11685
11841
  auth: "admin"
11842
+ }), method(object({
11843
+ deviceId: number(),
11844
+ /** Absent = the LOWEST assigned profile — a notification attachment is
11845
+ * watched on a phone, and the cheap rendition is the right default. */
11846
+ profile: CamProfileSchema.optional(),
11847
+ aroundMs: number(),
11848
+ preRollSec: number().min(0).max(20).default(3),
11849
+ postRollSec: number().min(0).max(20).default(5),
11850
+ format: _enum(["gif", "mp4"]).default("gif"),
11851
+ maxWidth: number().int().min(120).max(1920).default(480),
11852
+ /** GIF only — MP4 keeps the source cadence. */
11853
+ fps: number().int().min(1).max(15).default(5)
11854
+ }), object({
11855
+ base64: string(),
11856
+ mime: string(),
11857
+ bytes: number().int(),
11858
+ /** The profile actually rendered (what the default resolved to). */
11859
+ profile: CamProfileSchema,
11860
+ durationMs: number()
11861
+ }), {
11862
+ kind: "mutation",
11863
+ auth: "admin"
11686
11864
  }), method(_void(), array(CameraStreamSchema).readonly()), method(_void(), array(ProfileSlotSchema).readonly()), method(object({ brokerId: string() }), BrokerStatsSchema), method(object({ brokerId: string() }), object({
11687
11865
  probed: boolean(),
11688
11866
  summary: string()
@@ -14813,6 +14991,18 @@ var motionCapability = {
14813
14991
  name: "motion",
14814
14992
  scope: "device",
14815
14993
  mode: "singleton",
14994
+ /**
14995
+ * Providers register per-device natives via `ctx.registerNativeCap`
14996
+ * (Hikvision/Reolink/Amcrest/Wyze/HA/Homematic/Alexa/Matter) — there is
14997
+ * NO system singleton provider. Without this flag `resolveCapMount`
14998
+ * derived `{ kind: 'singleton' }`, so `motion.getStatus`/`isDetected`
14999
+ * resolved via `registry.getSingleton('motion')` (always null) and every
15000
+ * call 412'd "provider not available" while bindings listed a live
15001
+ * `motion` native (2026-08-02). The flag routes the router through
15002
+ * `requireDeviceScoped` → `getProviderForDevice`, like `motion-trigger`,
15003
+ * `snapshot` and every other per-device native cap.
15004
+ */
15005
+ deviceNative: true,
14816
15006
  deviceTypes: [DeviceType.Camera, DeviceType.Sensor],
14817
15007
  methods: {
14818
15008
  /**
@@ -19745,7 +19935,10 @@ method(object({
19745
19935
  }), method(object({
19746
19936
  deviceId: number(),
19747
19937
  caps: array(string()).readonly().optional()
19748
- }), record(string(), unknown().nullable()));
19938
+ }), record(string(), unknown().nullable())), method(object({
19939
+ deviceIds: array(number()).readonly(),
19940
+ caps: array(string()).readonly().optional()
19941
+ }), record(string(), record(string(), unknown().nullable())));
19749
19942
  method(object({ deviceId: number() }), record(string(), record(string(), unknown()))), method(object({
19750
19943
  deviceId: number(),
19751
19944
  capName: string()
@@ -20676,6 +20869,36 @@ var TargetKindSchema = object({
20676
20869
  icon: string(),
20677
20870
  /** Stamped by each provider so the concat-fanned catalog stays routable. */
20678
20871
  addonId: string(),
20872
+ /**
20873
+ * URL of the kind's bundled BRAND icon, served by the providing addon over
20874
+ * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
20875
+ * when the addon bundles no icon for that kind — the client then falls back
20876
+ * to a neutral glyph rather than rendering the raw `icon` NAME as text.
20877
+ *
20878
+ * Root-relative on purpose: it resolves against whatever origin serves a web
20879
+ * client, and a native client joins it onto its own hub base.
20880
+ *
20881
+ * DECLARED here deliberately. It used to travel as an undeclared passthrough
20882
+ * field that survived only because the runtime cap-router forwards provider
20883
+ * output verbatim — so every consumer had to re-declare it by hand to stop
20884
+ * its own Zod parse from stripping it, and the whole arrangement would have
20885
+ * broken silently the moment output validation was tightened anywhere.
20886
+ */
20887
+ iconUrl: string().optional(),
20888
+ /**
20889
+ * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
20890
+ *
20891
+ * The server knows this and therefore says it, because the client cannot
20892
+ * safely guess: a React-Native client renders SVG and raster through two
20893
+ * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
20894
+ * not decode SVG on iOS/Android), so without this it silently fell back to a
20895
+ * placeholder glyph for every vector icon while the web build looked fine.
20896
+ *
20897
+ * Absent when {@link iconUrl} is absent, or for a legacy provider that has
20898
+ * not been updated — a client that cannot determine the type should prefer
20899
+ * its raster path, which is the safe default for an unknown image.
20900
+ */
20901
+ iconMediaType: string().optional(),
20679
20902
  configSchema: ConfigSchemaPassthrough,
20680
20903
  supportsDiscovery: boolean(),
20681
20904
  caps: TargetKindCapsSchema
@@ -21123,6 +21346,29 @@ var MotionEventSchema = object({
21123
21346
  * Absent on legacy rows ⇒ treat as `pipeline`.
21124
21347
  */
21125
21348
  var DetectionSourceSchema = _enum(["pipeline", "onboard"]);
21349
+ /**
21350
+ * The confirmed zone crossing that produced an object event. Present ONLY on
21351
+ * an event emitted BY a crossing (`zone.enter` / `zone.exit`); a movement-state
21352
+ * event (`object.entering` / `leaving` / `stationary` / `loitering`) and an
21353
+ * appearance event carry none, so a rule asking for a direction fails closed
21354
+ * on them.
21355
+ *
21356
+ * Exactly ONE crossing per event: the emitter turns each confirmed crossing
21357
+ * into its own event, so a frame in which a track enters A while leaving B
21358
+ * produces two events with two directions — never one ambiguous row.
21359
+ *
21360
+ * `zoneId` is load-bearing for an EXIT: the event's `zones` list is the
21361
+ * membership the box has NOW, and by definition it no longer contains the zone
21362
+ * that was just left. Without the id here, a zone-scoped rule could never match
21363
+ * the exit it asked for.
21364
+ */
21365
+ var ZoneCrossingSchema = object({
21366
+ direction: _enum(["enter", "exit"]),
21367
+ /** Admin zone id crossed. */
21368
+ zoneId: string(),
21369
+ /** Zone display name at crossing time (falls back to the id). */
21370
+ zoneName: string().optional()
21371
+ });
21126
21372
  var ObjectEventSchema = object({
21127
21373
  ...BaseEventFields,
21128
21374
  kind: literal("object"),
@@ -21149,6 +21395,12 @@ var ObjectEventSchema = object({
21149
21395
  zones: array(string()).readonly().optional(),
21150
21396
  /** Omitted in slim projection. */
21151
21397
  state: TrackStateSchema.optional(),
21398
+ /**
21399
+ * The zone crossing this event IS, when it is one. Absent on every other
21400
+ * event kind (movement state, appearance, package) — see
21401
+ * {@link ZoneCrossingSchema}. Omitted in slim projection.
21402
+ */
21403
+ zoneCrossing: ZoneCrossingSchema.optional(),
21152
21404
  /** Detection-frame dimensions in pixels — let consumers normalize the
21153
21405
  * pixel-space `bbox` onto a displayed image. Omitted in slim projection. */
21154
21406
  frameWidth: number().optional(),
@@ -21407,6 +21659,15 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
21407
21659
  }), method(object({ deviceId: number() }), EventPruneCountsSchema, {
21408
21660
  kind: "mutation",
21409
21661
  auth: "admin"
21662
+ }), method(RelocateMediaInputSchema, object({ jobId: string() }), {
21663
+ kind: "mutation",
21664
+ auth: "admin"
21665
+ }), method(object({}), array(RelocateJobSchema).readonly(), {
21666
+ kind: "query",
21667
+ auth: "admin"
21668
+ }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
21669
+ kind: "mutation",
21670
+ auth: "admin"
21410
21671
  }), method(OpsLogQueryInputSchema, array(OpsLogEntrySchema).readonly(), {
21411
21672
  kind: "query",
21412
21673
  auth: "admin"
@@ -23886,6 +24147,17 @@ var GetConnectionEndpointsResultSchema = object({ endpoints: array(object({
23886
24147
  */
23887
24148
  priority: number()
23888
24149
  })).readonly() });
24150
+ /**
24151
+ * The chosen outbound endpoint for notification artifacts. `baseUrl: null` =
24152
+ * AUTO (resolved from the candidate ranking at send time); `resolved` reports
24153
+ * what AUTO currently picks, so the UI can show the effective value either way.
24154
+ */
24155
+ var NotificationEndpointSchema = object({
24156
+ /** The operator's explicit choice, or null for AUTO. */
24157
+ baseUrl: string().nullable(),
24158
+ /** What the ranking currently resolves to (null when nothing is reachable). */
24159
+ resolved: string().nullable()
24160
+ });
23889
24161
  var AllowedAddressesSchema = object({
23890
24162
  /**
23891
24163
  * Allowlist of interface addresses operators have explicitly opted
@@ -23908,7 +24180,7 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
23908
24180
  * to avoid mixed-content blocks in the browser. The public
23909
24181
  * tunnel always emits `https://` regardless. */
23910
24182
  scheme: _enum(["http", "https"]).optional()
23911
- }), GetConnectionEndpointsResultSchema), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" });
24183
+ }), GetConnectionEndpointsResultSchema), method(_void(), NotificationEndpointSchema), method(object({ baseUrl: string().nullable() }), NotificationEndpointSchema, { kind: "mutation" }), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" });
23912
24184
  /**
23913
24185
  * mesh-network — collection cap for mesh-VPN providers.
23914
24186
  *
@@ -24707,7 +24979,12 @@ var RecordingDeviceUsageSchema = object({
24707
24979
  var RecordingLocationUsageSchema = object({
24708
24980
  /** StorageLocation id; null for the legacy/degraded single-root fallback. */
24709
24981
  locationId: string().nullable(),
24710
- /** Bytes of recordings stored on this location. */
24982
+ /** Every location id sharing this row's PHYSICAL volume (aliases). One row
24983
+ * is emitted per physical disk (2026-07-29): two locations on one root
24984
+ * previously rendered as two identical "disks" with a nonsensical used
24985
+ * split — hydrate attribution across aliases is arbitrary by nature. */
24986
+ locationIds: array(string()).optional(),
24987
+ /** Bytes of recordings stored on this PHYSICAL volume (all aliases). */
24711
24988
  usedBytes: number(),
24712
24989
  /** Free bytes on the location's volume; null when capacity is unknown (remote). */
24713
24990
  availableBytes: number().nullable(),
@@ -24819,6 +25096,44 @@ method(object({
24819
25096
  }), method(OpsLogQueryInputSchema, array(OpsLogEntrySchema).readonly(), {
24820
25097
  kind: "query",
24821
25098
  auth: "admin"
25099
+ }), method(object({
25100
+ deviceId: number(),
25101
+ aroundMs: number(),
25102
+ preRollSec: number().min(0).max(30).default(2),
25103
+ postRollSec: number().min(0).max(30).default(5),
25104
+ maxWidth: number().int().min(120).max(1280).default(480),
25105
+ fps: number().int().min(1).max(15).default(5)
25106
+ }), object({
25107
+ gifBase64: string(),
25108
+ fromMs: number(),
25109
+ toMs: number()
25110
+ }), {
25111
+ kind: "mutation",
25112
+ auth: "admin"
25113
+ }), method(object({
25114
+ deviceId: number(),
25115
+ aroundMs: number(),
25116
+ preRollSec: number().min(0).max(30).default(3),
25117
+ postRollSec: number().min(0).max(30).default(7),
25118
+ maxWidth: number().int().min(160).max(1920).default(640)
25119
+ }), object({
25120
+ clipBase64: string(),
25121
+ mime: string(),
25122
+ fromMs: number(),
25123
+ toMs: number(),
25124
+ bytes: number().int()
25125
+ }), {
25126
+ kind: "mutation",
25127
+ auth: "admin"
25128
+ }), method(RelocateFootageInputSchema, object({ jobId: string() }), {
25129
+ kind: "mutation",
25130
+ auth: "admin"
25131
+ }), method(object({}), array(RelocateJobSchema).readonly(), {
25132
+ kind: "query",
25133
+ auth: "admin"
25134
+ }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
25135
+ kind: "mutation",
25136
+ auth: "admin"
24822
25137
  });
24823
25138
  /**
24824
25139
  * `recordingExport` cap — render a footage time range into a single downloadable
@@ -26410,6 +26725,12 @@ Object.freeze({
26410
26725
  addonId: null,
26411
26726
  access: "view"
26412
26727
  },
26728
+ "deviceManager.getDeviceStatusAggregateBatch": {
26729
+ capName: "device-manager",
26730
+ capScope: "system",
26731
+ addonId: null,
26732
+ access: "view"
26733
+ },
26413
26734
  "deviceManager.getLinkedDevices": {
26414
26735
  capName: "device-manager",
26415
26736
  capScope: "system",
@@ -27304,6 +27625,12 @@ Object.freeze({
27304
27625
  addonId: null,
27305
27626
  access: "view"
27306
27627
  },
27628
+ "localNetwork.getNotificationEndpoint": {
27629
+ capName: "local-network",
27630
+ capScope: "system",
27631
+ addonId: null,
27632
+ access: "view"
27633
+ },
27307
27634
  "localNetwork.getPreferred": {
27308
27635
  capName: "local-network",
27309
27636
  capScope: "system",
@@ -27328,6 +27655,12 @@ Object.freeze({
27328
27655
  addonId: null,
27329
27656
  access: "create"
27330
27657
  },
27658
+ "localNetwork.setNotificationEndpoint": {
27659
+ capName: "local-network",
27660
+ capScope: "system",
27661
+ addonId: null,
27662
+ access: "create"
27663
+ },
27331
27664
  "lockControl.lock": {
27332
27665
  capName: "lock-control",
27333
27666
  capScope: "device",
@@ -27970,6 +28303,12 @@ Object.freeze({
27970
28303
  addonId: null,
27971
28304
  access: "create"
27972
28305
  },
28306
+ "pipelineAnalytics.cancelMediaRelocate": {
28307
+ capName: "pipeline-analytics",
28308
+ capScope: "device",
28309
+ addonId: null,
28310
+ access: "create"
28311
+ },
27973
28312
  "pipelineAnalytics.clearTracks": {
27974
28313
  capName: "pipeline-analytics",
27975
28314
  capScope: "device",
@@ -28024,6 +28363,12 @@ Object.freeze({
28024
28363
  addonId: null,
28025
28364
  access: "view"
28026
28365
  },
28366
+ "pipelineAnalytics.getMediaRelocateStatus": {
28367
+ capName: "pipeline-analytics",
28368
+ capScope: "device",
28369
+ addonId: null,
28370
+ access: "view"
28371
+ },
28027
28372
  "pipelineAnalytics.getMotionEvents": {
28028
28373
  capName: "pipeline-analytics",
28029
28374
  capScope: "device",
@@ -28096,6 +28441,12 @@ Object.freeze({
28096
28441
  addonId: null,
28097
28442
  access: "create"
28098
28443
  },
28444
+ "pipelineAnalytics.relocateMedia": {
28445
+ capName: "pipeline-analytics",
28446
+ capScope: "device",
28447
+ addonId: null,
28448
+ access: "create"
28449
+ },
28099
28450
  "pipelineAnalytics.searchObjectEvents": {
28100
28451
  capName: "pipeline-analytics",
28101
28452
  capScope: "device",
@@ -28858,6 +29209,12 @@ Object.freeze({
28858
29209
  addonId: null,
28859
29210
  access: "create"
28860
29211
  },
29212
+ "recording.cancelRelocate": {
29213
+ capName: "recording",
29214
+ capScope: "system",
29215
+ addonId: null,
29216
+ access: "create"
29217
+ },
28861
29218
  "recording.deleteFootprint": {
28862
29219
  capName: "recording",
28863
29220
  capScope: "system",
@@ -28888,6 +29245,12 @@ Object.freeze({
28888
29245
  addonId: null,
28889
29246
  access: "view"
28890
29247
  },
29248
+ "recording.getRelocateStatus": {
29249
+ capName: "recording",
29250
+ capScope: "system",
29251
+ addonId: null,
29252
+ access: "view"
29253
+ },
28891
29254
  "recording.getStorageUsage": {
28892
29255
  capName: "recording",
28893
29256
  capScope: "system",
@@ -28918,6 +29281,24 @@ Object.freeze({
28918
29281
  addonId: null,
28919
29282
  access: "view"
28920
29283
  },
29284
+ "recording.relocateFootage": {
29285
+ capName: "recording",
29286
+ capScope: "system",
29287
+ addonId: null,
29288
+ access: "create"
29289
+ },
29290
+ "recording.renderClip": {
29291
+ capName: "recording",
29292
+ capScope: "system",
29293
+ addonId: null,
29294
+ access: "create"
29295
+ },
29296
+ "recording.renderGif": {
29297
+ capName: "recording",
29298
+ capScope: "system",
29299
+ addonId: null,
29300
+ access: "create"
29301
+ },
28921
29302
  "recording.rescanStorage": {
28922
29303
  capName: "recording",
28923
29304
  capScope: "system",
@@ -29512,6 +29893,12 @@ Object.freeze({
29512
29893
  addonId: null,
29513
29894
  access: "create"
29514
29895
  },
29896
+ "streamBroker.renderPreBufferClip": {
29897
+ capName: "stream-broker",
29898
+ capScope: "system",
29899
+ addonId: null,
29900
+ access: "create"
29901
+ },
29515
29902
  "streamBroker.restartProfile": {
29516
29903
  capName: "stream-broker",
29517
29904
  capScope: "system",