@camstack/addon-provider-rademacher 0.2.6 → 0.2.7

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 +451 -76
  2. package/dist/addon.mjs +451 -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()
@@ -19745,7 +19923,10 @@ method(object({
19745
19923
  }), method(object({
19746
19924
  deviceId: number(),
19747
19925
  caps: array(string()).readonly().optional()
19748
- }), record(string(), unknown().nullable()));
19926
+ }), record(string(), unknown().nullable())), method(object({
19927
+ deviceIds: array(number()).readonly(),
19928
+ caps: array(string()).readonly().optional()
19929
+ }), record(string(), record(string(), unknown().nullable())));
19749
19930
  method(object({ deviceId: number() }), record(string(), record(string(), unknown()))), method(object({
19750
19931
  deviceId: number(),
19751
19932
  capName: string()
@@ -20676,6 +20857,36 @@ var TargetKindSchema = object({
20676
20857
  icon: string(),
20677
20858
  /** Stamped by each provider so the concat-fanned catalog stays routable. */
20678
20859
  addonId: string(),
20860
+ /**
20861
+ * URL of the kind's bundled BRAND icon, served by the providing addon over
20862
+ * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
20863
+ * when the addon bundles no icon for that kind — the client then falls back
20864
+ * to a neutral glyph rather than rendering the raw `icon` NAME as text.
20865
+ *
20866
+ * Root-relative on purpose: it resolves against whatever origin serves a web
20867
+ * client, and a native client joins it onto its own hub base.
20868
+ *
20869
+ * DECLARED here deliberately. It used to travel as an undeclared passthrough
20870
+ * field that survived only because the runtime cap-router forwards provider
20871
+ * output verbatim — so every consumer had to re-declare it by hand to stop
20872
+ * its own Zod parse from stripping it, and the whole arrangement would have
20873
+ * broken silently the moment output validation was tightened anywhere.
20874
+ */
20875
+ iconUrl: string().optional(),
20876
+ /**
20877
+ * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
20878
+ *
20879
+ * The server knows this and therefore says it, because the client cannot
20880
+ * safely guess: a React-Native client renders SVG and raster through two
20881
+ * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
20882
+ * not decode SVG on iOS/Android), so without this it silently fell back to a
20883
+ * placeholder glyph for every vector icon while the web build looked fine.
20884
+ *
20885
+ * Absent when {@link iconUrl} is absent, or for a legacy provider that has
20886
+ * not been updated — a client that cannot determine the type should prefer
20887
+ * its raster path, which is the safe default for an unknown image.
20888
+ */
20889
+ iconMediaType: string().optional(),
20679
20890
  configSchema: ConfigSchemaPassthrough,
20680
20891
  supportsDiscovery: boolean(),
20681
20892
  caps: TargetKindCapsSchema
@@ -21123,6 +21334,29 @@ var MotionEventSchema = object({
21123
21334
  * Absent on legacy rows ⇒ treat as `pipeline`.
21124
21335
  */
21125
21336
  var DetectionSourceSchema = _enum(["pipeline", "onboard"]);
21337
+ /**
21338
+ * The confirmed zone crossing that produced an object event. Present ONLY on
21339
+ * an event emitted BY a crossing (`zone.enter` / `zone.exit`); a movement-state
21340
+ * event (`object.entering` / `leaving` / `stationary` / `loitering`) and an
21341
+ * appearance event carry none, so a rule asking for a direction fails closed
21342
+ * on them.
21343
+ *
21344
+ * Exactly ONE crossing per event: the emitter turns each confirmed crossing
21345
+ * into its own event, so a frame in which a track enters A while leaving B
21346
+ * produces two events with two directions — never one ambiguous row.
21347
+ *
21348
+ * `zoneId` is load-bearing for an EXIT: the event's `zones` list is the
21349
+ * membership the box has NOW, and by definition it no longer contains the zone
21350
+ * that was just left. Without the id here, a zone-scoped rule could never match
21351
+ * the exit it asked for.
21352
+ */
21353
+ var ZoneCrossingSchema = object({
21354
+ direction: _enum(["enter", "exit"]),
21355
+ /** Admin zone id crossed. */
21356
+ zoneId: string(),
21357
+ /** Zone display name at crossing time (falls back to the id). */
21358
+ zoneName: string().optional()
21359
+ });
21126
21360
  var ObjectEventSchema = object({
21127
21361
  ...BaseEventFields,
21128
21362
  kind: literal("object"),
@@ -21149,6 +21383,12 @@ var ObjectEventSchema = object({
21149
21383
  zones: array(string()).readonly().optional(),
21150
21384
  /** Omitted in slim projection. */
21151
21385
  state: TrackStateSchema.optional(),
21386
+ /**
21387
+ * The zone crossing this event IS, when it is one. Absent on every other
21388
+ * event kind (movement state, appearance, package) — see
21389
+ * {@link ZoneCrossingSchema}. Omitted in slim projection.
21390
+ */
21391
+ zoneCrossing: ZoneCrossingSchema.optional(),
21152
21392
  /** Detection-frame dimensions in pixels — let consumers normalize the
21153
21393
  * pixel-space `bbox` onto a displayed image. Omitted in slim projection. */
21154
21394
  frameWidth: number().optional(),
@@ -21407,6 +21647,15 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
21407
21647
  }), method(object({ deviceId: number() }), EventPruneCountsSchema, {
21408
21648
  kind: "mutation",
21409
21649
  auth: "admin"
21650
+ }), method(RelocateMediaInputSchema, object({ jobId: string() }), {
21651
+ kind: "mutation",
21652
+ auth: "admin"
21653
+ }), method(object({}), array(RelocateJobSchema).readonly(), {
21654
+ kind: "query",
21655
+ auth: "admin"
21656
+ }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
21657
+ kind: "mutation",
21658
+ auth: "admin"
21410
21659
  }), method(OpsLogQueryInputSchema, array(OpsLogEntrySchema).readonly(), {
21411
21660
  kind: "query",
21412
21661
  auth: "admin"
@@ -23886,6 +24135,17 @@ var GetConnectionEndpointsResultSchema = object({ endpoints: array(object({
23886
24135
  */
23887
24136
  priority: number()
23888
24137
  })).readonly() });
24138
+ /**
24139
+ * The chosen outbound endpoint for notification artifacts. `baseUrl: null` =
24140
+ * AUTO (resolved from the candidate ranking at send time); `resolved` reports
24141
+ * what AUTO currently picks, so the UI can show the effective value either way.
24142
+ */
24143
+ var NotificationEndpointSchema = object({
24144
+ /** The operator's explicit choice, or null for AUTO. */
24145
+ baseUrl: string().nullable(),
24146
+ /** What the ranking currently resolves to (null when nothing is reachable). */
24147
+ resolved: string().nullable()
24148
+ });
23889
24149
  var AllowedAddressesSchema = object({
23890
24150
  /**
23891
24151
  * Allowlist of interface addresses operators have explicitly opted
@@ -23908,7 +24168,7 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
23908
24168
  * to avoid mixed-content blocks in the browser. The public
23909
24169
  * tunnel always emits `https://` regardless. */
23910
24170
  scheme: _enum(["http", "https"]).optional()
23911
- }), GetConnectionEndpointsResultSchema), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" });
24171
+ }), 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
24172
  /**
23913
24173
  * mesh-network — collection cap for mesh-VPN providers.
23914
24174
  *
@@ -24707,7 +24967,12 @@ var RecordingDeviceUsageSchema = object({
24707
24967
  var RecordingLocationUsageSchema = object({
24708
24968
  /** StorageLocation id; null for the legacy/degraded single-root fallback. */
24709
24969
  locationId: string().nullable(),
24710
- /** Bytes of recordings stored on this location. */
24970
+ /** Every location id sharing this row's PHYSICAL volume (aliases). One row
24971
+ * is emitted per physical disk (2026-07-29): two locations on one root
24972
+ * previously rendered as two identical "disks" with a nonsensical used
24973
+ * split — hydrate attribution across aliases is arbitrary by nature. */
24974
+ locationIds: array(string()).optional(),
24975
+ /** Bytes of recordings stored on this PHYSICAL volume (all aliases). */
24711
24976
  usedBytes: number(),
24712
24977
  /** Free bytes on the location's volume; null when capacity is unknown (remote). */
24713
24978
  availableBytes: number().nullable(),
@@ -24819,6 +25084,44 @@ method(object({
24819
25084
  }), method(OpsLogQueryInputSchema, array(OpsLogEntrySchema).readonly(), {
24820
25085
  kind: "query",
24821
25086
  auth: "admin"
25087
+ }), method(object({
25088
+ deviceId: number(),
25089
+ aroundMs: number(),
25090
+ preRollSec: number().min(0).max(30).default(2),
25091
+ postRollSec: number().min(0).max(30).default(5),
25092
+ maxWidth: number().int().min(120).max(1280).default(480),
25093
+ fps: number().int().min(1).max(15).default(5)
25094
+ }), object({
25095
+ gifBase64: string(),
25096
+ fromMs: number(),
25097
+ toMs: number()
25098
+ }), {
25099
+ kind: "mutation",
25100
+ auth: "admin"
25101
+ }), method(object({
25102
+ deviceId: number(),
25103
+ aroundMs: number(),
25104
+ preRollSec: number().min(0).max(30).default(3),
25105
+ postRollSec: number().min(0).max(30).default(7),
25106
+ maxWidth: number().int().min(160).max(1920).default(640)
25107
+ }), object({
25108
+ clipBase64: string(),
25109
+ mime: string(),
25110
+ fromMs: number(),
25111
+ toMs: number(),
25112
+ bytes: number().int()
25113
+ }), {
25114
+ kind: "mutation",
25115
+ auth: "admin"
25116
+ }), method(RelocateFootageInputSchema, object({ jobId: string() }), {
25117
+ kind: "mutation",
25118
+ auth: "admin"
25119
+ }), method(object({}), array(RelocateJobSchema).readonly(), {
25120
+ kind: "query",
25121
+ auth: "admin"
25122
+ }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
25123
+ kind: "mutation",
25124
+ auth: "admin"
24822
25125
  });
24823
25126
  /**
24824
25127
  * `recordingExport` cap — render a footage time range into a single downloadable
@@ -26410,6 +26713,12 @@ Object.freeze({
26410
26713
  addonId: null,
26411
26714
  access: "view"
26412
26715
  },
26716
+ "deviceManager.getDeviceStatusAggregateBatch": {
26717
+ capName: "device-manager",
26718
+ capScope: "system",
26719
+ addonId: null,
26720
+ access: "view"
26721
+ },
26413
26722
  "deviceManager.getLinkedDevices": {
26414
26723
  capName: "device-manager",
26415
26724
  capScope: "system",
@@ -27304,6 +27613,12 @@ Object.freeze({
27304
27613
  addonId: null,
27305
27614
  access: "view"
27306
27615
  },
27616
+ "localNetwork.getNotificationEndpoint": {
27617
+ capName: "local-network",
27618
+ capScope: "system",
27619
+ addonId: null,
27620
+ access: "view"
27621
+ },
27307
27622
  "localNetwork.getPreferred": {
27308
27623
  capName: "local-network",
27309
27624
  capScope: "system",
@@ -27328,6 +27643,12 @@ Object.freeze({
27328
27643
  addonId: null,
27329
27644
  access: "create"
27330
27645
  },
27646
+ "localNetwork.setNotificationEndpoint": {
27647
+ capName: "local-network",
27648
+ capScope: "system",
27649
+ addonId: null,
27650
+ access: "create"
27651
+ },
27331
27652
  "lockControl.lock": {
27332
27653
  capName: "lock-control",
27333
27654
  capScope: "device",
@@ -27970,6 +28291,12 @@ Object.freeze({
27970
28291
  addonId: null,
27971
28292
  access: "create"
27972
28293
  },
28294
+ "pipelineAnalytics.cancelMediaRelocate": {
28295
+ capName: "pipeline-analytics",
28296
+ capScope: "device",
28297
+ addonId: null,
28298
+ access: "create"
28299
+ },
27973
28300
  "pipelineAnalytics.clearTracks": {
27974
28301
  capName: "pipeline-analytics",
27975
28302
  capScope: "device",
@@ -28024,6 +28351,12 @@ Object.freeze({
28024
28351
  addonId: null,
28025
28352
  access: "view"
28026
28353
  },
28354
+ "pipelineAnalytics.getMediaRelocateStatus": {
28355
+ capName: "pipeline-analytics",
28356
+ capScope: "device",
28357
+ addonId: null,
28358
+ access: "view"
28359
+ },
28027
28360
  "pipelineAnalytics.getMotionEvents": {
28028
28361
  capName: "pipeline-analytics",
28029
28362
  capScope: "device",
@@ -28096,6 +28429,12 @@ Object.freeze({
28096
28429
  addonId: null,
28097
28430
  access: "create"
28098
28431
  },
28432
+ "pipelineAnalytics.relocateMedia": {
28433
+ capName: "pipeline-analytics",
28434
+ capScope: "device",
28435
+ addonId: null,
28436
+ access: "create"
28437
+ },
28099
28438
  "pipelineAnalytics.searchObjectEvents": {
28100
28439
  capName: "pipeline-analytics",
28101
28440
  capScope: "device",
@@ -28858,6 +29197,12 @@ Object.freeze({
28858
29197
  addonId: null,
28859
29198
  access: "create"
28860
29199
  },
29200
+ "recording.cancelRelocate": {
29201
+ capName: "recording",
29202
+ capScope: "system",
29203
+ addonId: null,
29204
+ access: "create"
29205
+ },
28861
29206
  "recording.deleteFootprint": {
28862
29207
  capName: "recording",
28863
29208
  capScope: "system",
@@ -28888,6 +29233,12 @@ Object.freeze({
28888
29233
  addonId: null,
28889
29234
  access: "view"
28890
29235
  },
29236
+ "recording.getRelocateStatus": {
29237
+ capName: "recording",
29238
+ capScope: "system",
29239
+ addonId: null,
29240
+ access: "view"
29241
+ },
28891
29242
  "recording.getStorageUsage": {
28892
29243
  capName: "recording",
28893
29244
  capScope: "system",
@@ -28918,6 +29269,24 @@ Object.freeze({
28918
29269
  addonId: null,
28919
29270
  access: "view"
28920
29271
  },
29272
+ "recording.relocateFootage": {
29273
+ capName: "recording",
29274
+ capScope: "system",
29275
+ addonId: null,
29276
+ access: "create"
29277
+ },
29278
+ "recording.renderClip": {
29279
+ capName: "recording",
29280
+ capScope: "system",
29281
+ addonId: null,
29282
+ access: "create"
29283
+ },
29284
+ "recording.renderGif": {
29285
+ capName: "recording",
29286
+ capScope: "system",
29287
+ addonId: null,
29288
+ access: "create"
29289
+ },
28921
29290
  "recording.rescanStorage": {
28922
29291
  capName: "recording",
28923
29292
  capScope: "system",
@@ -29512,6 +29881,12 @@ Object.freeze({
29512
29881
  addonId: null,
29513
29882
  access: "create"
29514
29883
  },
29884
+ "streamBroker.renderPreBufferClip": {
29885
+ capName: "stream-broker",
29886
+ capScope: "system",
29887
+ addonId: null,
29888
+ access: "create"
29889
+ },
29515
29890
  "streamBroker.restartProfile": {
29516
29891
  capName: "stream-broker",
29517
29892
  capScope: "system",