@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.mjs CHANGED
@@ -983,7 +983,7 @@ var Rademacher = class {
983
983
  }
984
984
  };
985
985
  //#endregion
986
- //#region ../types/dist/event-category-BLcNejAE.mjs
986
+ //#region ../types/dist/event-category-Bz24uP1U.mjs
987
987
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
988
988
  EventCategory["SystemBoot"] = "system.boot";
989
989
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -1254,6 +1254,19 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
1254
1254
  */
1255
1255
  EventCategory["DeviceStateChanged"] = "device.state-changed";
1256
1256
  /**
1257
+ * Frame occupancy for a camera CHANGED — a tracked object was gained or
1258
+ * lost. Carries `{ deviceId, totalObjects, byClass, zones }`.
1259
+ *
1260
+ * Emitted only on a change, so a steady scene is silent. It exists so a
1261
+ * client can stop polling `zoneAnalytics.getCurrentSnapshot`: that was the
1262
+ * one live badge with no push signal at all, and it cost a request every
1263
+ * four seconds per visible camera.
1264
+ *
1265
+ * Like every event it is telemetry and may be dropped ([D8]) — a consumer
1266
+ * keeps a slow reconcile rather than trusting it alone.
1267
+ */
1268
+ EventCategory["ZoneAnalyticsOccupancyChanged"] = "zone-analytics.occupancy-changed";
1269
+ /**
1257
1270
  * Cap event fired by every device that registers the `battery`
1258
1271
  * capability. Mirrors the cap definition's `onStatusChanged`. Carries
1259
1272
  * `{ deviceId, status: BatteryStatus }`. Subscribers (alert center,
@@ -8197,35 +8210,22 @@ var ConvertResultSchema = object({
8197
8210
  */
8198
8211
  var RecordingWeekdaySchema = number().int().min(0).max(6);
8199
8212
  var HHMM = /^([01]\d|2[0-3]):[0-5]\d$/;
8200
- var RecordingScheduleSchema = discriminatedUnion("kind", [object({ kind: literal("always") }), object({
8201
- kind: literal("timeOfDay"),
8202
- start: string().regex(HHMM),
8203
- end: string().regex(HHMM),
8204
- /** Restrict to these weekdays; omit = every day. */
8205
- days: array(RecordingWeekdaySchema).optional()
8206
- })]);
8207
- var RecordingModeSchema = _enum([
8208
- "continuous",
8209
- "onMotion",
8210
- "onAudioThreshold"
8211
- ]);
8212
8213
  /**
8213
- * First-class, authoritative per-camera storage mode — the explicit choice the
8214
- * UI reads directly (never inferred from `rules`):
8215
- * - `off` — not recording.
8216
- * - `events` — record only around triggers (motion / audio threshold),
8217
- * with pre/post-buffer.
8218
- * - `continuous` — record 24/7 within the schedule.
8214
+ * DERIVED per-camera storage summary — the single field cheap consumers read
8215
+ * (the viewer's status dot, the camera list) instead of walking `bands`:
8216
+ * - `off` — no band covers the camera (or it is disabled).
8217
+ * - `events` — every band records around triggers only.
8218
+ * - `continuous` — at least one band records continuously.
8219
8219
  *
8220
- * `mode` compiles one-way to the internal `rules[]` consumed by the policy
8221
- * engine (see `compileRules`); `rules[]` is never authored directly anymore.
8220
+ * NEVER authored: the recorder stamps it from the authoritative `bands` on
8221
+ * every save (`activeModeForConfig`). Writing it has no effect.
8222
8222
  */
8223
8223
  var RecordingStorageModeSchema = _enum([
8224
8224
  "off",
8225
8225
  "events",
8226
8226
  "continuous"
8227
8227
  ]);
8228
- /** Which detectors trigger an `events`-mode recording. */
8228
+ /** Which detectors trigger an `events`-mode band. */
8229
8229
  var RecordingTriggersSchema = object({
8230
8230
  motion: boolean().optional(),
8231
8231
  audioThresholdDbfs: number().optional()
@@ -8261,18 +8261,6 @@ var RecordingBandSchema = object({
8261
8261
  preBufferSec: number().min(0).optional(),
8262
8262
  postBufferSec: number().min(0).optional()
8263
8263
  });
8264
- var RecordingRuleSchema = object({
8265
- schedule: RecordingScheduleSchema,
8266
- mode: RecordingModeSchema,
8267
- /** Seconds of footage to retain BEFORE a trigger (applied at keep/discard). */
8268
- preBufferSec: number().min(0).default(0),
8269
- /** Keep recording until this many seconds after the last trigger. */
8270
- postBufferSec: number().min(0).default(0),
8271
- /** Each new trigger restarts the post-buffer window. */
8272
- resetTimeoutOnNewEvent: boolean().default(true),
8273
- /** onAudioThreshold only — dBFS level that counts as a trigger. */
8274
- thresholdDbfs: number().optional()
8275
- });
8276
8264
  /**
8277
8265
  * Per-device retention overrides. Every field is optional; an unset or `0`
8278
8266
  * value inherits the node-wide recorder default. Only footage-lifetime limits
@@ -8306,40 +8294,28 @@ var ScrubThumbnailPresetSchema = _enum([
8306
8294
  /**
8307
8295
  * The full per-camera recording intent — the wire shape of a RecordingTarget.
8308
8296
  *
8309
- * `mode` is the authoritative storage choice; `schedule`/`triggers`/`pre`/`post`
8310
- * are its mode-specific parameters. `rules` is a DEPRECATED authoring input kept
8311
- * only for transition + migration (`migrateRulesToMode`); the policy engine
8312
- * consumes the compiled output of `compileRules(config)`, never `rules` directly.
8297
+ * `bands` is the ONLY authored recording intent: what to record, when, and on
8298
+ * which trigger. `mode` is a derived summary the recorder stamps on save; every
8299
+ * other field is a storage knob (profiles, segment length, retention, scrub).
8300
+ *
8301
+ * STRICT on purpose: the legacy authoring surface (`schedule`/`schedules`/
8302
+ * `triggers`/`preBufferSec`/`postBufferSec`/`rules`) was retired 2026-07-30.
8303
+ * A stale caller must fail loudly — silently stripping its legacy intent would
8304
+ * persist a band-less config, i.e. silently stop recording the camera.
8313
8305
  */
8314
8306
  var RecordingConfigSchema = object({
8315
8307
  enabled: boolean(),
8316
- /** Authoritative storage mode. Absent on legacy targets derived once via
8317
- * `migrateRulesToMode`, then persisted. */
8308
+ /** DERIVED summary of `bands`, stamped by the recorder on every save.
8309
+ * Authoring it has no effect — see {@link RecordingStorageModeSchema}. */
8318
8310
  mode: RecordingStorageModeSchema.optional(),
8319
8311
  profiles: array(CamProfileSchema).optional(),
8320
8312
  segmentSeconds: number().int().positive().optional(),
8321
- /** Shared recording time-bands for `events` & `continuous` — record only when
8322
- * the wall-clock falls inside one of these windows. Omit or empty = always.
8323
- * `continuous` compiles to one rule per band; `events` to band × trigger. */
8324
- schedules: array(RecordingScheduleSchema).optional(),
8325
- /** Legacy single-band predecessor of `schedules`. Read-compat only — it is
8326
- * normalized into `schedules` on read and never written going forward. (Not
8327
- * tagged `@deprecated`: the normalization paths must read it cast-free.) */
8328
- schedule: RecordingScheduleSchema.optional(),
8329
- /** `events`-mode only — which detectors trigger a recording. */
8330
- triggers: RecordingTriggersSchema.optional(),
8331
- /** `events`-mode only — seconds retained before / after a trigger. */
8332
- preBufferSec: number().min(0).optional(),
8333
- postBufferSec: number().min(0).optional(),
8334
- /** DEPRECATED authoring input; retained for migration/transition. */
8335
- rules: array(RecordingRuleSchema).optional(),
8336
8313
  /**
8337
- * AUTHORITATIVE mode-per-band recording model (recorder). When present it
8338
- * is the single source of truth; the legacy `mode`/`schedules`/`schedule`/
8339
- * `triggers`/`rules` fields above are kept for READ-COMPAT only and are
8340
- * derived into bands once via `migrateConfigToBands`.
8314
+ * AUTHORITATIVE mode-per-band recording model the single source of truth
8315
+ * the recorder's band engine consumes. An empty array = record nothing;
8316
+ * "off" is the absence of a covering band, never a band value.
8341
8317
  */
8342
- bands: array(RecordingBandSchema).optional(),
8318
+ bands: array(RecordingBandSchema).default([]),
8343
8319
  retention: RecordingRetentionSchema.optional(),
8344
8320
  /**
8345
8321
  * Per-camera scrub-thumbnail fidelity preset (resolution + JPEG quality for
@@ -8347,8 +8323,15 @@ var RecordingConfigSchema = object({
8347
8323
  * windows only — existing sheets are immutable, and each window's index
8348
8324
  * carries its own tile dims so mixed-preset history renders correctly.
8349
8325
  */
8350
- scrubThumbnails: ScrubThumbnailPresetSchema.optional()
8351
- });
8326
+ scrubThumbnails: ScrubThumbnailPresetSchema.optional(),
8327
+ /**
8328
+ * OPT-IN thumbnail-strip generation for this camera: every keyframe of the
8329
+ * low recording saved as a JPEG (the fast-drag scrub depth), a derived
8330
+ * cache that eviction reclaims with the footage. Absent/false = no strips
8331
+ * are written and scrub reads exact keyframes at every velocity.
8332
+ */
8333
+ stripsEnabled: boolean().optional()
8334
+ }).strict();
8352
8335
  /**
8353
8336
  * Ops-log — the durable, append-only operations audit shared by the
8354
8337
  * recordings and events management surfaces.
@@ -8367,7 +8350,8 @@ var OpsLogOpSchema = _enum([
8367
8350
  "prune",
8368
8351
  "manual-delete",
8369
8352
  "rescan",
8370
- "retention-run"
8353
+ "retention-run",
8354
+ "relocate"
8371
8355
  ]);
8372
8356
  /** Why the operation ran. */
8373
8357
  var OpsLogReasonSchema = _enum([
@@ -8406,6 +8390,55 @@ var OpsLogQueryInputSchema = object({
8406
8390
  limit: number().int().min(1).max(1e3).optional()
8407
8391
  });
8408
8392
  /**
8393
+ * Entity-relocation job state (storage entity-routing spec, Phase 4).
8394
+ *
8395
+ * One shape shared by the recorder's `relocateFootage` (segments + strips) and
8396
+ * pipeline-analytics' `relocateMedia` (event media blobs) so the admin Data
8397
+ * page renders both movers with one component. Jobs are in-RAM (a restart
8398
+ * forgets them — re-running is safe by construction: copy-if-absent, delete
8399
+ * after verify) and each completed/failed run also lands one durable ops-log
8400
+ * row on the owning addon's surface.
8401
+ */
8402
+ var RelocateJobStateSchema = _enum([
8403
+ "running",
8404
+ "done",
8405
+ "failed",
8406
+ "cancelled"
8407
+ ]);
8408
+ var RelocateJobSchema = object({
8409
+ jobId: string(),
8410
+ state: RelocateJobStateSchema,
8411
+ /** Source location — for media relocation this is informational ('*': rows
8412
+ * move from wherever they are to the target). */
8413
+ fromLocationId: string(),
8414
+ toLocationId: string(),
8415
+ /** Scoped device, or null = every device. */
8416
+ deviceId: number().nullable(),
8417
+ /** What the job moves (owner-addon specific: segments/strips or media). */
8418
+ entities: array(string()),
8419
+ filesMoved: number().int(),
8420
+ bytesMoved: number().int(),
8421
+ /** Total files discovered up front; null while (or when) unknown. */
8422
+ filesTotal: number().int().nullable(),
8423
+ startedAt: number(),
8424
+ finishedAt: number().nullable(),
8425
+ error: string().nullable()
8426
+ });
8427
+ var RelocateFootageInputSchema = object({
8428
+ deviceId: number().optional(),
8429
+ fromLocationId: string(),
8430
+ toLocationId: string(),
8431
+ entities: array(_enum(["segments", "strips"])).optional(),
8432
+ /** Copy throttle in MB/s (default 40) — the drain is a background chore,
8433
+ * never allowed to starve live writers. */
8434
+ throttleMbps: number().min(1).max(1e3).optional()
8435
+ });
8436
+ var RelocateMediaInputSchema = object({
8437
+ deviceId: number().optional(),
8438
+ toLocationId: string(),
8439
+ throttleMbps: number().min(1).max(1e3).optional()
8440
+ });
8441
+ /**
8409
8442
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
8410
8443
  * storage a location serves. Defined here (not in `capabilities/storage.cap.ts`)
8411
8444
  * so the persisted record schema and the consumer-facing cap can both consume it
@@ -8455,6 +8488,13 @@ var StorageLocationSchema = object({
8455
8488
  nodeId: string().optional(),
8456
8489
  isDefault: boolean().default(false),
8457
8490
  isSystem: boolean().default(false),
8491
+ /** COMPUTED at read time by the orchestrator (statfs of the backing volume
8492
+ * for node-local locations it can reach) — never persisted, absent when the
8493
+ * volume is remote/unreachable. The single capacity truth every UI reads. */
8494
+ capacity: object({
8495
+ totalBytes: number(),
8496
+ availableBytes: number()
8497
+ }).nullable().optional(),
8458
8498
  createdAt: number(),
8459
8499
  updatedAt: number()
8460
8500
  });
@@ -9222,7 +9262,8 @@ var NcTaxonomyEntrySchema = object({
9222
9262
  /** Macro/category parent for grouping ('car' → 'vehicle'); null for a top. */
9223
9263
  parentKind: string().nullable()
9224
9264
  });
9225
- object({
9265
+ /** The complete NC picker taxonomy — three grouped buckets. */
9266
+ var NcTaxonomySchema = object({
9226
9267
  videoClasses: array(NcTaxonomyEntrySchema),
9227
9268
  audioKinds: array(NcTaxonomyEntrySchema),
9228
9269
  labels: array(NcTaxonomyEntrySchema)
@@ -10125,6 +10166,7 @@ function shallowEqual(a, b) {
10125
10166
  for (const k of ak) if (a[k] !== b[k]) return false;
10126
10167
  return true;
10127
10168
  }
10169
+ new Set(["devices", "classes"]);
10128
10170
  /**
10129
10171
  * Shared geometry vocabulary for on-frame shape caps — privacy-mask,
10130
10172
  * motion-zones, and the detection zones/lines editor all speak this one
@@ -10283,6 +10325,29 @@ var NcOccupancyConditionSchema = object({
10283
10325
  count: number().int().min(0).default(1),
10284
10326
  sustainSeconds: number().int().min(0).max(3600).default(15)
10285
10327
  });
10328
+ /**
10329
+ * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
10330
+ *
10331
+ * The values are not symmetric, and deliberately so — the absent value has to
10332
+ * mean exactly what every rule authored before this condition existed already
10333
+ * does:
10334
+ * - `enter` — entries and every NON-crossing record (movement state,
10335
+ * package, sensor). Exits are rejected. **This is the absent behaviour**:
10336
+ * an operator who never asked for exits must not start receiving them.
10337
+ * - `exit` — ONLY an exit crossing. A record that is not a crossing at all
10338
+ * fails closed, because "the car left the drive" is a question about a
10339
+ * boundary, not about a detection.
10340
+ * - `any` — no direction filter; entries, exits and non-crossings alike.
10341
+ *
10342
+ * A rule asking for a direction should normally also scope `zones`, which the
10343
+ * engine evaluates against the crossed zone as well as the current membership
10344
+ * (an exit's membership no longer contains the zone it just left).
10345
+ */
10346
+ var NcCrossingSchema = _enum([
10347
+ "enter",
10348
+ "exit",
10349
+ "any"
10350
+ ]);
10286
10351
  /** Admin-zone membership condition (zone IDs as stamped by the ZoneEngine). */
10287
10352
  var NcZoneConditionSchema = object({
10288
10353
  ids: array(string().min(1)).min(1),
@@ -10307,6 +10372,13 @@ var NcConditionsSchema = object({
10307
10372
  /** Veto zones — any hit fails the rule. */
10308
10373
  zonesExclude: array(string().min(1)).optional(),
10309
10374
  /**
10375
+ * Zone-crossing direction. IMMEDIATE only — a crossing is a per-event fact
10376
+ * and a closed track carries none, so a `track-end` rule asking for one
10377
+ * fails closed (use `zones`, which tests `zonesVisited`). ABSENT = `enter`,
10378
+ * which is exactly today's behaviour. See {@link NcCrossingSchema}.
10379
+ */
10380
+ crossing: NcCrossingSchema.optional(),
10381
+ /**
10310
10382
  * Exact (case-insensitive) match on the record's collapsed `label`
10311
10383
  * (identity name / plate text / subclass).
10312
10384
  */
@@ -10441,17 +10513,85 @@ var NcRuleTargetSchema = object({
10441
10513
  * - `keyFrame` — the clean scene frame (no subject box).
10442
10514
  * - `none` — no attachment.
10443
10515
  */
10444
- var NcMediaPolicySchema = object({ attach: _enum([
10445
- "best",
10446
- "best-matching",
10447
- "keyFrame",
10448
- "none"
10449
- ]).default("best") });
10516
+ /**
10517
+ * WHAT THE PICTURE SHOWS — chosen explicitly, because the implicit ladders
10518
+ * conflate it with the selection strategy and betray the request: asking for
10519
+ * the clean scene frame on an object-event owner used to start at
10520
+ * `fullFrameBoxed`, i.e. the annotated frame (2026-07-30).
10521
+ * - `cropped` — the subject, tight. What "who is at the door" wants.
10522
+ * - `full` — the clean scene, no annotation. What "what is going on" wants.
10523
+ * - `boxed` — the scene WITH the detection boxes drawn, for verifying what
10524
+ * the pipeline actually saw.
10525
+ */
10526
+ var NcMediaFrameSchema = _enum([
10527
+ "cropped",
10528
+ "full",
10529
+ "boxed"
10530
+ ]);
10531
+ var NcMediaPolicySchema = object({
10532
+ attach: _enum([
10533
+ "best",
10534
+ "best-matching",
10535
+ "keyFrame",
10536
+ "none"
10537
+ ]).default("best"),
10538
+ /** What the still shows. Absent = `cropped` (the historical `best` shape). */
10539
+ frame: NcMediaFrameSchema.optional(),
10540
+ /** Crop the attached still to the rule's condition-zone bbox (padded) —
10541
+ * "show me the ZONE", not the whole scene or the subject crop. */
10542
+ zoneCrop: boolean().optional(),
10543
+ /**
10544
+ * Also attach a short GIF cut from the stream broker's clip ring around the
10545
+ * event — NOT from the recording, so the camera does not have to be
10546
+ * recording, and the window sits AROUND the moment instead of a segment
10547
+ * behind it. Fail-closed: a window the ring does not cover contributes no
10548
+ * gif, never a failed notification.
10549
+ */
10550
+ gif: boolean().optional(),
10551
+ /**
10552
+ * Also attach a short MP4 CLIP of the same cut. Same source and the same
10553
+ * fail-closed rule as `gif`. Prefer it where the backend takes video
10554
+ * (telegram, discord, zentik, webhook); backends that do not are handled by
10555
+ * the degrade engine, which drops the video and keeps the still.
10556
+ */
10557
+ clip: boolean().optional(),
10558
+ /** Seconds of footage BEFORE / AFTER the event instant. Defaults 3 / 7. */
10559
+ clipPreRollSec: number().int().min(0).max(30).optional(),
10560
+ clipPostRollSec: number().int().min(0).max(30).optional(),
10561
+ /**
10562
+ * Which stream profile the footage is cut from. Absent = the CHEAPEST
10563
+ * assigned profile: a notification is watched on a phone, so the 4K
10564
+ * rendition would burn CPU to produce a file the client downscales anyway.
10565
+ * A profile that is not assigned falls back to the cheapest, and the render
10566
+ * reports which one actually ran.
10567
+ */
10568
+ profile: CamProfileSchema.optional()
10569
+ });
10570
+ /**
10571
+ * Cooldown GRANULARITY over the subject's class — how much a fired
10572
+ * notification suppresses.
10573
+ * - `shared` (default, and the absent value) — one window for the whole
10574
+ * rule/scope: a cat silences the next dog for `cooldownSec`.
10575
+ * - `per-class` — an independent window per detected class, so cat→dog fires
10576
+ * at once and cat→cat still waits.
10577
+ *
10578
+ * AUDIO subjects are ALWAYS per-class regardless of this setting: a scream
10579
+ * must not be swallowed by a bark's window (the precedent this generalizes —
10580
+ * see `cooldownKey` in the rule engine).
10581
+ */
10582
+ var NcThrottleGranularitySchema = _enum(["shared", "per-class"]);
10450
10583
  /** Throttle — cooldown survives restarts (rebuilt from the outbox on boot). */
10451
10584
  var NcThrottleSchema = object({
10452
10585
  cooldownSec: number().int().min(0).max(86400).default(60),
10453
10586
  /** `rule` = one shared cooldown; `rule-device` = per-camera cooldown. */
10454
- scope: _enum(["rule", "rule-device"]).default("rule-device")
10587
+ scope: _enum(["rule", "rule-device"]).default("rule-device"),
10588
+ /**
10589
+ * Class granularity of the cooldown key. Optional rather than defaulted:
10590
+ * a Zod default does NOT run on the addon→addon cap path, so a persisted
10591
+ * rule authored before this field simply carries none — and the engine
10592
+ * reads absent as `shared`, the pre-existing behaviour.
10593
+ */
10594
+ granularity: NcThrottleGranularitySchema.optional()
10455
10595
  });
10456
10596
  /** Client-supplied rule fields (server stamps id/createdBy/createdAt/updatedAt). */
10457
10597
  var NcRuleInputSchema = object({
@@ -10460,7 +10600,19 @@ var NcRuleInputSchema = object({
10460
10600
  delivery: NcDeliverySchema,
10461
10601
  conditions: NcConditionsSchema.default({}),
10462
10602
  schedule: NcScheduleSchema.optional(),
10463
- targets: array(NcRuleTargetSchema).min(1),
10603
+ /** May be empty when `targetUsers` addresses at least one user — the
10604
+ * "at least one addressee" invariant is enforced by the provider, because
10605
+ * a cross-field refine here would break `NcRulePatchSchema.partial()`. */
10606
+ targets: array(NcRuleTargetSchema),
10607
+ /**
10608
+ * USERS this rule addresses in addition to `targets` (Phase 4). At fire
10609
+ * time each user fans out to the personal targets they own
10610
+ * (`config.ownerUserId`), filtered by that user's `allowedDevices` for the
10611
+ * firing camera — a user is never notified about a device they cannot open.
10612
+ * Per-user opt-outs (`disabledTargetIds`) still apply to the fanned-out
10613
+ * targets.
10614
+ */
10615
+ targetUsers: array(string()).optional(),
10464
10616
  media: NcMediaPolicySchema.default({ attach: "best" }),
10465
10617
  throttle: NcThrottleSchema.default({
10466
10618
  cooldownSec: 60,
@@ -10547,6 +10699,7 @@ var NcConditionDescriptorSchema = object({
10547
10699
  "schedule",
10548
10700
  "plateMatcher",
10549
10701
  "packagePhase",
10702
+ "crossingSelect",
10550
10703
  "polygonDraw",
10551
10704
  "occupancy"
10552
10705
  ]),
@@ -10673,7 +10826,10 @@ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), m
10673
10826
  }), object({ results: array(NcTestResultSchema) }), {
10674
10827
  kind: "mutation",
10675
10828
  auth: "admin"
10676
- }), method(object({}), object({ catalog: array(NcConditionDescriptorSchema) })), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" });
10829
+ }), method(object({}), object({
10830
+ catalog: array(NcConditionDescriptorSchema),
10831
+ taxonomy: NcTaxonomySchema.optional()
10832
+ })), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" });
10677
10833
  /**
10678
10834
  * TimelapseRule — the STANDALONE scheduled timelapse producer's rule model.
10679
10835
  *
@@ -11682,6 +11838,28 @@ method(object({
11682
11838
  }), object({ success: literal(true) }), {
11683
11839
  kind: "mutation",
11684
11840
  auth: "admin"
11841
+ }), method(object({
11842
+ deviceId: number(),
11843
+ /** Absent = the LOWEST assigned profile — a notification attachment is
11844
+ * watched on a phone, and the cheap rendition is the right default. */
11845
+ profile: CamProfileSchema.optional(),
11846
+ aroundMs: number(),
11847
+ preRollSec: number().min(0).max(20).default(3),
11848
+ postRollSec: number().min(0).max(20).default(5),
11849
+ format: _enum(["gif", "mp4"]).default("gif"),
11850
+ maxWidth: number().int().min(120).max(1920).default(480),
11851
+ /** GIF only — MP4 keeps the source cadence. */
11852
+ fps: number().int().min(1).max(15).default(5)
11853
+ }), object({
11854
+ base64: string(),
11855
+ mime: string(),
11856
+ bytes: number().int(),
11857
+ /** The profile actually rendered (what the default resolved to). */
11858
+ profile: CamProfileSchema,
11859
+ durationMs: number()
11860
+ }), {
11861
+ kind: "mutation",
11862
+ auth: "admin"
11685
11863
  }), method(_void(), array(CameraStreamSchema).readonly()), method(_void(), array(ProfileSlotSchema).readonly()), method(object({ brokerId: string() }), BrokerStatsSchema), method(object({ brokerId: string() }), object({
11686
11864
  probed: boolean(),
11687
11865
  summary: string()
@@ -19744,7 +19922,10 @@ method(object({
19744
19922
  }), method(object({
19745
19923
  deviceId: number(),
19746
19924
  caps: array(string()).readonly().optional()
19747
- }), record(string(), unknown().nullable()));
19925
+ }), record(string(), unknown().nullable())), method(object({
19926
+ deviceIds: array(number()).readonly(),
19927
+ caps: array(string()).readonly().optional()
19928
+ }), record(string(), record(string(), unknown().nullable())));
19748
19929
  method(object({ deviceId: number() }), record(string(), record(string(), unknown()))), method(object({
19749
19930
  deviceId: number(),
19750
19931
  capName: string()
@@ -20675,6 +20856,36 @@ var TargetKindSchema = object({
20675
20856
  icon: string(),
20676
20857
  /** Stamped by each provider so the concat-fanned catalog stays routable. */
20677
20858
  addonId: string(),
20859
+ /**
20860
+ * URL of the kind's bundled BRAND icon, served by the providing addon over
20861
+ * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
20862
+ * when the addon bundles no icon for that kind — the client then falls back
20863
+ * to a neutral glyph rather than rendering the raw `icon` NAME as text.
20864
+ *
20865
+ * Root-relative on purpose: it resolves against whatever origin serves a web
20866
+ * client, and a native client joins it onto its own hub base.
20867
+ *
20868
+ * DECLARED here deliberately. It used to travel as an undeclared passthrough
20869
+ * field that survived only because the runtime cap-router forwards provider
20870
+ * output verbatim — so every consumer had to re-declare it by hand to stop
20871
+ * its own Zod parse from stripping it, and the whole arrangement would have
20872
+ * broken silently the moment output validation was tightened anywhere.
20873
+ */
20874
+ iconUrl: string().optional(),
20875
+ /**
20876
+ * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
20877
+ *
20878
+ * The server knows this and therefore says it, because the client cannot
20879
+ * safely guess: a React-Native client renders SVG and raster through two
20880
+ * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
20881
+ * not decode SVG on iOS/Android), so without this it silently fell back to a
20882
+ * placeholder glyph for every vector icon while the web build looked fine.
20883
+ *
20884
+ * Absent when {@link iconUrl} is absent, or for a legacy provider that has
20885
+ * not been updated — a client that cannot determine the type should prefer
20886
+ * its raster path, which is the safe default for an unknown image.
20887
+ */
20888
+ iconMediaType: string().optional(),
20678
20889
  configSchema: ConfigSchemaPassthrough,
20679
20890
  supportsDiscovery: boolean(),
20680
20891
  caps: TargetKindCapsSchema
@@ -21122,6 +21333,29 @@ var MotionEventSchema = object({
21122
21333
  * Absent on legacy rows ⇒ treat as `pipeline`.
21123
21334
  */
21124
21335
  var DetectionSourceSchema = _enum(["pipeline", "onboard"]);
21336
+ /**
21337
+ * The confirmed zone crossing that produced an object event. Present ONLY on
21338
+ * an event emitted BY a crossing (`zone.enter` / `zone.exit`); a movement-state
21339
+ * event (`object.entering` / `leaving` / `stationary` / `loitering`) and an
21340
+ * appearance event carry none, so a rule asking for a direction fails closed
21341
+ * on them.
21342
+ *
21343
+ * Exactly ONE crossing per event: the emitter turns each confirmed crossing
21344
+ * into its own event, so a frame in which a track enters A while leaving B
21345
+ * produces two events with two directions — never one ambiguous row.
21346
+ *
21347
+ * `zoneId` is load-bearing for an EXIT: the event's `zones` list is the
21348
+ * membership the box has NOW, and by definition it no longer contains the zone
21349
+ * that was just left. Without the id here, a zone-scoped rule could never match
21350
+ * the exit it asked for.
21351
+ */
21352
+ var ZoneCrossingSchema = object({
21353
+ direction: _enum(["enter", "exit"]),
21354
+ /** Admin zone id crossed. */
21355
+ zoneId: string(),
21356
+ /** Zone display name at crossing time (falls back to the id). */
21357
+ zoneName: string().optional()
21358
+ });
21125
21359
  var ObjectEventSchema = object({
21126
21360
  ...BaseEventFields,
21127
21361
  kind: literal("object"),
@@ -21148,6 +21382,12 @@ var ObjectEventSchema = object({
21148
21382
  zones: array(string()).readonly().optional(),
21149
21383
  /** Omitted in slim projection. */
21150
21384
  state: TrackStateSchema.optional(),
21385
+ /**
21386
+ * The zone crossing this event IS, when it is one. Absent on every other
21387
+ * event kind (movement state, appearance, package) — see
21388
+ * {@link ZoneCrossingSchema}. Omitted in slim projection.
21389
+ */
21390
+ zoneCrossing: ZoneCrossingSchema.optional(),
21151
21391
  /** Detection-frame dimensions in pixels — let consumers normalize the
21152
21392
  * pixel-space `bbox` onto a displayed image. Omitted in slim projection. */
21153
21393
  frameWidth: number().optional(),
@@ -21406,6 +21646,15 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
21406
21646
  }), method(object({ deviceId: number() }), EventPruneCountsSchema, {
21407
21647
  kind: "mutation",
21408
21648
  auth: "admin"
21649
+ }), method(RelocateMediaInputSchema, object({ jobId: string() }), {
21650
+ kind: "mutation",
21651
+ auth: "admin"
21652
+ }), method(object({}), array(RelocateJobSchema).readonly(), {
21653
+ kind: "query",
21654
+ auth: "admin"
21655
+ }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
21656
+ kind: "mutation",
21657
+ auth: "admin"
21409
21658
  }), method(OpsLogQueryInputSchema, array(OpsLogEntrySchema).readonly(), {
21410
21659
  kind: "query",
21411
21660
  auth: "admin"
@@ -23885,6 +24134,17 @@ var GetConnectionEndpointsResultSchema = object({ endpoints: array(object({
23885
24134
  */
23886
24135
  priority: number()
23887
24136
  })).readonly() });
24137
+ /**
24138
+ * The chosen outbound endpoint for notification artifacts. `baseUrl: null` =
24139
+ * AUTO (resolved from the candidate ranking at send time); `resolved` reports
24140
+ * what AUTO currently picks, so the UI can show the effective value either way.
24141
+ */
24142
+ var NotificationEndpointSchema = object({
24143
+ /** The operator's explicit choice, or null for AUTO. */
24144
+ baseUrl: string().nullable(),
24145
+ /** What the ranking currently resolves to (null when nothing is reachable). */
24146
+ resolved: string().nullable()
24147
+ });
23888
24148
  var AllowedAddressesSchema = object({
23889
24149
  /**
23890
24150
  * Allowlist of interface addresses operators have explicitly opted
@@ -23907,7 +24167,7 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
23907
24167
  * to avoid mixed-content blocks in the browser. The public
23908
24168
  * tunnel always emits `https://` regardless. */
23909
24169
  scheme: _enum(["http", "https"]).optional()
23910
- }), GetConnectionEndpointsResultSchema), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" });
24170
+ }), 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" });
23911
24171
  /**
23912
24172
  * mesh-network — collection cap for mesh-VPN providers.
23913
24173
  *
@@ -24706,7 +24966,12 @@ var RecordingDeviceUsageSchema = object({
24706
24966
  var RecordingLocationUsageSchema = object({
24707
24967
  /** StorageLocation id; null for the legacy/degraded single-root fallback. */
24708
24968
  locationId: string().nullable(),
24709
- /** Bytes of recordings stored on this location. */
24969
+ /** Every location id sharing this row's PHYSICAL volume (aliases). One row
24970
+ * is emitted per physical disk (2026-07-29): two locations on one root
24971
+ * previously rendered as two identical "disks" with a nonsensical used
24972
+ * split — hydrate attribution across aliases is arbitrary by nature. */
24973
+ locationIds: array(string()).optional(),
24974
+ /** Bytes of recordings stored on this PHYSICAL volume (all aliases). */
24710
24975
  usedBytes: number(),
24711
24976
  /** Free bytes on the location's volume; null when capacity is unknown (remote). */
24712
24977
  availableBytes: number().nullable(),
@@ -24818,6 +25083,44 @@ method(object({
24818
25083
  }), method(OpsLogQueryInputSchema, array(OpsLogEntrySchema).readonly(), {
24819
25084
  kind: "query",
24820
25085
  auth: "admin"
25086
+ }), method(object({
25087
+ deviceId: number(),
25088
+ aroundMs: number(),
25089
+ preRollSec: number().min(0).max(30).default(2),
25090
+ postRollSec: number().min(0).max(30).default(5),
25091
+ maxWidth: number().int().min(120).max(1280).default(480),
25092
+ fps: number().int().min(1).max(15).default(5)
25093
+ }), object({
25094
+ gifBase64: string(),
25095
+ fromMs: number(),
25096
+ toMs: number()
25097
+ }), {
25098
+ kind: "mutation",
25099
+ auth: "admin"
25100
+ }), method(object({
25101
+ deviceId: number(),
25102
+ aroundMs: number(),
25103
+ preRollSec: number().min(0).max(30).default(3),
25104
+ postRollSec: number().min(0).max(30).default(7),
25105
+ maxWidth: number().int().min(160).max(1920).default(640)
25106
+ }), object({
25107
+ clipBase64: string(),
25108
+ mime: string(),
25109
+ fromMs: number(),
25110
+ toMs: number(),
25111
+ bytes: number().int()
25112
+ }), {
25113
+ kind: "mutation",
25114
+ auth: "admin"
25115
+ }), method(RelocateFootageInputSchema, object({ jobId: string() }), {
25116
+ kind: "mutation",
25117
+ auth: "admin"
25118
+ }), method(object({}), array(RelocateJobSchema).readonly(), {
25119
+ kind: "query",
25120
+ auth: "admin"
25121
+ }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
25122
+ kind: "mutation",
25123
+ auth: "admin"
24821
25124
  });
24822
25125
  /**
24823
25126
  * `recordingExport` cap — render a footage time range into a single downloadable
@@ -26409,6 +26712,12 @@ Object.freeze({
26409
26712
  addonId: null,
26410
26713
  access: "view"
26411
26714
  },
26715
+ "deviceManager.getDeviceStatusAggregateBatch": {
26716
+ capName: "device-manager",
26717
+ capScope: "system",
26718
+ addonId: null,
26719
+ access: "view"
26720
+ },
26412
26721
  "deviceManager.getLinkedDevices": {
26413
26722
  capName: "device-manager",
26414
26723
  capScope: "system",
@@ -27303,6 +27612,12 @@ Object.freeze({
27303
27612
  addonId: null,
27304
27613
  access: "view"
27305
27614
  },
27615
+ "localNetwork.getNotificationEndpoint": {
27616
+ capName: "local-network",
27617
+ capScope: "system",
27618
+ addonId: null,
27619
+ access: "view"
27620
+ },
27306
27621
  "localNetwork.getPreferred": {
27307
27622
  capName: "local-network",
27308
27623
  capScope: "system",
@@ -27327,6 +27642,12 @@ Object.freeze({
27327
27642
  addonId: null,
27328
27643
  access: "create"
27329
27644
  },
27645
+ "localNetwork.setNotificationEndpoint": {
27646
+ capName: "local-network",
27647
+ capScope: "system",
27648
+ addonId: null,
27649
+ access: "create"
27650
+ },
27330
27651
  "lockControl.lock": {
27331
27652
  capName: "lock-control",
27332
27653
  capScope: "device",
@@ -27969,6 +28290,12 @@ Object.freeze({
27969
28290
  addonId: null,
27970
28291
  access: "create"
27971
28292
  },
28293
+ "pipelineAnalytics.cancelMediaRelocate": {
28294
+ capName: "pipeline-analytics",
28295
+ capScope: "device",
28296
+ addonId: null,
28297
+ access: "create"
28298
+ },
27972
28299
  "pipelineAnalytics.clearTracks": {
27973
28300
  capName: "pipeline-analytics",
27974
28301
  capScope: "device",
@@ -28023,6 +28350,12 @@ Object.freeze({
28023
28350
  addonId: null,
28024
28351
  access: "view"
28025
28352
  },
28353
+ "pipelineAnalytics.getMediaRelocateStatus": {
28354
+ capName: "pipeline-analytics",
28355
+ capScope: "device",
28356
+ addonId: null,
28357
+ access: "view"
28358
+ },
28026
28359
  "pipelineAnalytics.getMotionEvents": {
28027
28360
  capName: "pipeline-analytics",
28028
28361
  capScope: "device",
@@ -28095,6 +28428,12 @@ Object.freeze({
28095
28428
  addonId: null,
28096
28429
  access: "create"
28097
28430
  },
28431
+ "pipelineAnalytics.relocateMedia": {
28432
+ capName: "pipeline-analytics",
28433
+ capScope: "device",
28434
+ addonId: null,
28435
+ access: "create"
28436
+ },
28098
28437
  "pipelineAnalytics.searchObjectEvents": {
28099
28438
  capName: "pipeline-analytics",
28100
28439
  capScope: "device",
@@ -28857,6 +29196,12 @@ Object.freeze({
28857
29196
  addonId: null,
28858
29197
  access: "create"
28859
29198
  },
29199
+ "recording.cancelRelocate": {
29200
+ capName: "recording",
29201
+ capScope: "system",
29202
+ addonId: null,
29203
+ access: "create"
29204
+ },
28860
29205
  "recording.deleteFootprint": {
28861
29206
  capName: "recording",
28862
29207
  capScope: "system",
@@ -28887,6 +29232,12 @@ Object.freeze({
28887
29232
  addonId: null,
28888
29233
  access: "view"
28889
29234
  },
29235
+ "recording.getRelocateStatus": {
29236
+ capName: "recording",
29237
+ capScope: "system",
29238
+ addonId: null,
29239
+ access: "view"
29240
+ },
28890
29241
  "recording.getStorageUsage": {
28891
29242
  capName: "recording",
28892
29243
  capScope: "system",
@@ -28917,6 +29268,24 @@ Object.freeze({
28917
29268
  addonId: null,
28918
29269
  access: "view"
28919
29270
  },
29271
+ "recording.relocateFootage": {
29272
+ capName: "recording",
29273
+ capScope: "system",
29274
+ addonId: null,
29275
+ access: "create"
29276
+ },
29277
+ "recording.renderClip": {
29278
+ capName: "recording",
29279
+ capScope: "system",
29280
+ addonId: null,
29281
+ access: "create"
29282
+ },
29283
+ "recording.renderGif": {
29284
+ capName: "recording",
29285
+ capScope: "system",
29286
+ addonId: null,
29287
+ access: "create"
29288
+ },
28920
29289
  "recording.rescanStorage": {
28921
29290
  capName: "recording",
28922
29291
  capScope: "system",
@@ -29511,6 +29880,12 @@ Object.freeze({
29511
29880
  addonId: null,
29512
29881
  access: "create"
29513
29882
  },
29883
+ "streamBroker.renderPreBufferClip": {
29884
+ capName: "stream-broker",
29885
+ capScope: "system",
29886
+ addonId: null,
29887
+ access: "create"
29888
+ },
29514
29889
  "streamBroker.restartProfile": {
29515
29890
  capName: "stream-broker",
29516
29891
  capScope: "system",