@camstack/addon-pipeline 1.2.19 → 1.2.28

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 (35) hide show
  1. package/dist/audio-analyzer/index.js +1 -1
  2. package/dist/audio-analyzer/index.mjs +3 -2
  3. package/dist/chunk-DnnnRqeS.mjs +26 -0
  4. package/dist/detection-pipeline/index.js +2 -2
  5. package/dist/detection-pipeline/index.mjs +4 -3
  6. package/dist/{dist-DZBePRjP.mjs → dist-CsKaXmP8.mjs} +415 -126
  7. package/dist/{dist-DKqOqH5j.js → dist-DUHlZcWd.js} +420 -131
  8. package/dist/{model-download-service-Cp9f4dk6-WO-foD7W.mjs → model-download-service-Cp9f4dk6-CwUb5v3Y.mjs} +1 -26
  9. package/dist/motion-wasm/index.js +1 -1
  10. package/dist/motion-wasm/index.mjs +1 -1
  11. package/dist/pipeline-runner/index.js +230 -28
  12. package/dist/pipeline-runner/index.mjs +230 -28
  13. package/dist/recorder/index.js +2604 -458
  14. package/dist/recorder/index.mjs +2599 -459
  15. package/dist/{step-definitions-zDq8cP8M.js → step-definitions-Bo1hJQQw.js} +1 -1
  16. package/dist/{step-definitions-BwB6TBMB.mjs → step-definitions-C8A78dBi.mjs} +1 -1
  17. package/dist/stream-broker/_stub.js +2 -2
  18. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Bwuv7iK_.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-tHahN-lK.mjs} +2 -2
  19. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-D1zJmtUe.mjs +26 -0
  20. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DfnRAN5T.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CSWUWZ8c.mjs} +1 -1
  21. package/dist/stream-broker/{hostInit-DC0aArn1.mjs → hostInit-CIwilJUZ.mjs} +2 -2
  22. package/dist/stream-broker/index.js +7374 -250
  23. package/dist/stream-broker/index.mjs +7371 -249
  24. package/dist/stream-broker/remoteEntry.js +1 -1
  25. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-C2hbIwzB.js → MaskShapeCanvas-DI4BY7W2-B7VdNJlO.js} +1 -1
  26. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-BpJ-foJW.js → MotionZonesSettings-NcxxQN8r-YyB4uLdH.js} +1 -1
  27. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-BjLs9Z3w.js → PrivacyMaskSettings-APgPLF7p-DfqixEad.js} +1 -1
  28. package/embed-dist/assets/{index-Dv5H-32C.css → index-CAe6Dg-X.css} +1 -1
  29. package/embed-dist/assets/index-zVLLVdIS.js +343 -0
  30. package/embed-dist/index.html +2 -2
  31. package/package.json +5 -1
  32. package/dist/fmp4-keyframes-5s8T9Mng.js +0 -6711
  33. package/dist/fmp4-keyframes-CV0X1mGy.mjs +0 -6694
  34. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BDeEvhV5.mjs +0 -26
  35. package/embed-dist/assets/index-_BRQTmdE.js +0 -116
@@ -7371,35 +7371,22 @@ var EVENT_PAD_MS = {
7371
7371
  */
7372
7372
  var RecordingWeekdaySchema = number().int().min(0).max(6);
7373
7373
  var HHMM = /^([01]\d|2[0-3]):[0-5]\d$/;
7374
- var RecordingScheduleSchema = discriminatedUnion("kind", [object({ kind: literal("always") }), object({
7375
- kind: literal("timeOfDay"),
7376
- start: string().regex(HHMM),
7377
- end: string().regex(HHMM),
7378
- /** Restrict to these weekdays; omit = every day. */
7379
- days: array(RecordingWeekdaySchema).optional()
7380
- })]);
7381
- var RecordingModeSchema = _enum([
7382
- "continuous",
7383
- "onMotion",
7384
- "onAudioThreshold"
7385
- ]);
7386
7374
  /**
7387
- * First-class, authoritative per-camera storage mode — the explicit choice the
7388
- * UI reads directly (never inferred from `rules`):
7389
- * - `off` — not recording.
7390
- * - `events` — record only around triggers (motion / audio threshold),
7391
- * with pre/post-buffer.
7392
- * - `continuous` — record 24/7 within the schedule.
7375
+ * DERIVED per-camera storage summary — the single field cheap consumers read
7376
+ * (the viewer's status dot, the camera list) instead of walking `bands`:
7377
+ * - `off` — no band covers the camera (or it is disabled).
7378
+ * - `events` — every band records around triggers only.
7379
+ * - `continuous` — at least one band records continuously.
7393
7380
  *
7394
- * `mode` compiles one-way to the internal `rules[]` consumed by the policy
7395
- * engine (see `compileRules`); `rules[]` is never authored directly anymore.
7381
+ * NEVER authored: the recorder stamps it from the authoritative `bands` on
7382
+ * every save (`activeModeForConfig`). Writing it has no effect.
7396
7383
  */
7397
7384
  var RecordingStorageModeSchema = _enum([
7398
7385
  "off",
7399
7386
  "events",
7400
7387
  "continuous"
7401
7388
  ]);
7402
- /** Which detectors trigger an `events`-mode recording. */
7389
+ /** Which detectors trigger an `events`-mode band. */
7403
7390
  var RecordingTriggersSchema = object({
7404
7391
  motion: boolean().optional(),
7405
7392
  audioThresholdDbfs: number().optional()
@@ -7435,18 +7422,6 @@ var RecordingBandSchema = object({
7435
7422
  preBufferSec: number().min(0).optional(),
7436
7423
  postBufferSec: number().min(0).optional()
7437
7424
  });
7438
- var RecordingRuleSchema = object({
7439
- schedule: RecordingScheduleSchema,
7440
- mode: RecordingModeSchema,
7441
- /** Seconds of footage to retain BEFORE a trigger (applied at keep/discard). */
7442
- preBufferSec: number().min(0).default(0),
7443
- /** Keep recording until this many seconds after the last trigger. */
7444
- postBufferSec: number().min(0).default(0),
7445
- /** Each new trigger restarts the post-buffer window. */
7446
- resetTimeoutOnNewEvent: boolean().default(true),
7447
- /** onAudioThreshold only — dBFS level that counts as a trigger. */
7448
- thresholdDbfs: number().optional()
7449
- });
7450
7425
  /**
7451
7426
  * Per-device retention overrides. Every field is optional; an unset or `0`
7452
7427
  * value inherits the node-wide recorder default. Only footage-lifetime limits
@@ -7480,40 +7455,28 @@ var ScrubThumbnailPresetSchema = _enum([
7480
7455
  /**
7481
7456
  * The full per-camera recording intent — the wire shape of a RecordingTarget.
7482
7457
  *
7483
- * `mode` is the authoritative storage choice; `schedule`/`triggers`/`pre`/`post`
7484
- * are its mode-specific parameters. `rules` is a DEPRECATED authoring input kept
7485
- * only for transition + migration (`migrateRulesToMode`); the policy engine
7486
- * consumes the compiled output of `compileRules(config)`, never `rules` directly.
7458
+ * `bands` is the ONLY authored recording intent: what to record, when, and on
7459
+ * which trigger. `mode` is a derived summary the recorder stamps on save; every
7460
+ * other field is a storage knob (profiles, segment length, retention, scrub).
7461
+ *
7462
+ * STRICT on purpose: the legacy authoring surface (`schedule`/`schedules`/
7463
+ * `triggers`/`preBufferSec`/`postBufferSec`/`rules`) was retired 2026-07-30.
7464
+ * A stale caller must fail loudly — silently stripping its legacy intent would
7465
+ * persist a band-less config, i.e. silently stop recording the camera.
7487
7466
  */
7488
7467
  var RecordingConfigSchema = object({
7489
7468
  enabled: boolean(),
7490
- /** Authoritative storage mode. Absent on legacy targets derived once via
7491
- * `migrateRulesToMode`, then persisted. */
7469
+ /** DERIVED summary of `bands`, stamped by the recorder on every save.
7470
+ * Authoring it has no effect — see {@link RecordingStorageModeSchema}. */
7492
7471
  mode: RecordingStorageModeSchema.optional(),
7493
7472
  profiles: array(CamProfileSchema).optional(),
7494
7473
  segmentSeconds: number().int().positive().optional(),
7495
- /** Shared recording time-bands for `events` & `continuous` — record only when
7496
- * the wall-clock falls inside one of these windows. Omit or empty = always.
7497
- * `continuous` compiles to one rule per band; `events` to band × trigger. */
7498
- schedules: array(RecordingScheduleSchema).optional(),
7499
- /** Legacy single-band predecessor of `schedules`. Read-compat only — it is
7500
- * normalized into `schedules` on read and never written going forward. (Not
7501
- * tagged `@deprecated`: the normalization paths must read it cast-free.) */
7502
- schedule: RecordingScheduleSchema.optional(),
7503
- /** `events`-mode only — which detectors trigger a recording. */
7504
- triggers: RecordingTriggersSchema.optional(),
7505
- /** `events`-mode only — seconds retained before / after a trigger. */
7506
- preBufferSec: number().min(0).optional(),
7507
- postBufferSec: number().min(0).optional(),
7508
- /** DEPRECATED authoring input; retained for migration/transition. */
7509
- rules: array(RecordingRuleSchema).optional(),
7510
7474
  /**
7511
- * AUTHORITATIVE mode-per-band recording model (recorder). When present it
7512
- * is the single source of truth; the legacy `mode`/`schedules`/`schedule`/
7513
- * `triggers`/`rules` fields above are kept for READ-COMPAT only and are
7514
- * derived into bands once via `migrateConfigToBands`.
7475
+ * AUTHORITATIVE mode-per-band recording model the single source of truth
7476
+ * the recorder's band engine consumes. An empty array = record nothing;
7477
+ * "off" is the absence of a covering band, never a band value.
7515
7478
  */
7516
- bands: array(RecordingBandSchema).optional(),
7479
+ bands: array(RecordingBandSchema).default([]),
7517
7480
  retention: RecordingRetentionSchema.optional(),
7518
7481
  /**
7519
7482
  * Per-camera scrub-thumbnail fidelity preset (resolution + JPEG quality for
@@ -7521,60 +7484,30 @@ var RecordingConfigSchema = object({
7521
7484
  * windows only — existing sheets are immutable, and each window's index
7522
7485
  * carries its own tile dims so mixed-preset history renders correctly.
7523
7486
  */
7524
- scrubThumbnails: ScrubThumbnailPresetSchema.optional()
7525
- });
7526
- /** All seven weekdays (0 = Sunday 6 = Saturday), used when a schedule omits a
7527
- * weekday filter. */
7528
- var ALL_DAYS = [
7529
- 0,
7530
- 1,
7531
- 2,
7532
- 3,
7533
- 4,
7534
- 5,
7535
- 6
7536
- ];
7537
- /** Full-day span used to represent an `always` schedule as a band. The `HHMM`
7538
- * regex maxes out at 23:59, so a 00:00–23:59 same-day span all-day (the band
7539
- * engine treats `end > start` as a normal, non-wrapping span). */
7540
- var ALL_DAY_START = "00:00";
7541
- var ALL_DAY_END = "23:59";
7542
- function bandFromSchedule(schedule, mode, config) {
7543
- const days = schedule.kind === "timeOfDay" && schedule.days ? schedule.days : [...ALL_DAYS];
7544
- const start = schedule.kind === "timeOfDay" ? schedule.start : ALL_DAY_START;
7545
- const end = schedule.kind === "timeOfDay" ? schedule.end : ALL_DAY_END;
7546
- if (mode === "continuous") return {
7547
- days,
7548
- start,
7549
- end,
7550
- mode
7551
- };
7552
- return {
7553
- days,
7554
- start,
7555
- end,
7556
- mode,
7557
- ...config.triggers ? { triggers: config.triggers } : {},
7558
- ...config.preBufferSec !== void 0 ? { preBufferSec: config.preBufferSec } : {},
7559
- ...config.postBufferSec !== void 0 ? { postBufferSec: config.postBufferSec } : {}
7560
- };
7561
- }
7562
- /**
7563
- * One-way migration from the legacy recording-config shape to the authoritative
7564
- * mode-per-band model. Pure (no clock access).
7565
- *
7566
- * - If `config.bands` is present, it is authoritative and returned as-is.
7567
- * - Otherwise the result is derived from the legacy fields: a disabled config or
7568
- * `mode: 'off'` yields no bands; every legacy schedule maps to one band whose
7569
- * mode is `continuous` when `config.mode === 'continuous'`, else `events`
7570
- * (carrying `triggers`/`preBufferSec`/`postBufferSec`).
7571
- */
7572
- function migrateConfigToBands(config) {
7573
- if (config.bands) return config.bands;
7574
- if (!config.enabled || config.mode === "off") return [];
7575
- const schedules = config.schedules ?? (config.schedule ? [config.schedule] : [{ kind: "always" }]);
7576
- const bandMode = config.mode === "continuous" ? "continuous" : "events";
7577
- return schedules.map((schedule) => bandFromSchedule(schedule, bandMode, config));
7487
+ scrubThumbnails: ScrubThumbnailPresetSchema.optional(),
7488
+ /**
7489
+ * OPT-IN thumbnail-strip generation for this camera: every keyframe of the
7490
+ * low recording saved as a JPEG (the fast-drag scrub depth), a derived
7491
+ * cache that eviction reclaims with the footage. Absent/false = no strips
7492
+ * are written and scrub reads exact keyframes at every velocity.
7493
+ */
7494
+ stripsEnabled: boolean().optional()
7495
+ }).strict();
7496
+ /**
7497
+ * Derive the {@link RecordingStorageModeSchema} summary from the authoritative
7498
+ * bands: `continuous` when any band records continuously, `events` when a band
7499
+ * exists at all, else `off`. Pure.
7500
+ *
7501
+ * THE one definition: the recorder stamps `config.mode` with it on save and
7502
+ * reports it as the cap's `activeMode`, and the settings UI reads it back to
7503
+ * decide which authoring tab a config opens in. Two copies would drift, and the
7504
+ * symptom (a status dot disagreeing with what is being recorded) is exactly the
7505
+ * bug this field was added to fix.
7506
+ */
7507
+ function deriveRecordingMode(config) {
7508
+ if (!config.enabled || config.bands.length === 0) return "off";
7509
+ if (config.bands.some((band) => band.mode === "continuous")) return "continuous";
7510
+ return "events";
7578
7511
  }
7579
7512
  /**
7580
7513
  * Ops-log — the durable, append-only operations audit shared by the
@@ -7594,7 +7527,8 @@ var OpsLogOpSchema = _enum([
7594
7527
  "prune",
7595
7528
  "manual-delete",
7596
7529
  "rescan",
7597
- "retention-run"
7530
+ "retention-run",
7531
+ "relocate"
7598
7532
  ]);
7599
7533
  /** Why the operation ran. */
7600
7534
  var OpsLogReasonSchema = _enum([
@@ -7633,6 +7567,55 @@ var OpsLogQueryInputSchema = object({
7633
7567
  limit: number().int().min(1).max(1e3).optional()
7634
7568
  });
7635
7569
  /**
7570
+ * Entity-relocation job state (storage entity-routing spec, Phase 4).
7571
+ *
7572
+ * One shape shared by the recorder's `relocateFootage` (segments + strips) and
7573
+ * pipeline-analytics' `relocateMedia` (event media blobs) so the admin Data
7574
+ * page renders both movers with one component. Jobs are in-RAM (a restart
7575
+ * forgets them — re-running is safe by construction: copy-if-absent, delete
7576
+ * after verify) and each completed/failed run also lands one durable ops-log
7577
+ * row on the owning addon's surface.
7578
+ */
7579
+ var RelocateJobStateSchema = _enum([
7580
+ "running",
7581
+ "done",
7582
+ "failed",
7583
+ "cancelled"
7584
+ ]);
7585
+ var RelocateJobSchema = object({
7586
+ jobId: string(),
7587
+ state: RelocateJobStateSchema,
7588
+ /** Source location — for media relocation this is informational ('*': rows
7589
+ * move from wherever they are to the target). */
7590
+ fromLocationId: string(),
7591
+ toLocationId: string(),
7592
+ /** Scoped device, or null = every device. */
7593
+ deviceId: number().nullable(),
7594
+ /** What the job moves (owner-addon specific: segments/strips or media). */
7595
+ entities: array(string()),
7596
+ filesMoved: number().int(),
7597
+ bytesMoved: number().int(),
7598
+ /** Total files discovered up front; null while (or when) unknown. */
7599
+ filesTotal: number().int().nullable(),
7600
+ startedAt: number(),
7601
+ finishedAt: number().nullable(),
7602
+ error: string().nullable()
7603
+ });
7604
+ var RelocateFootageInputSchema = object({
7605
+ deviceId: number().optional(),
7606
+ fromLocationId: string(),
7607
+ toLocationId: string(),
7608
+ entities: array(_enum(["segments", "strips"])).optional(),
7609
+ /** Copy throttle in MB/s (default 40) — the drain is a background chore,
7610
+ * never allowed to starve live writers. */
7611
+ throttleMbps: number().min(1).max(1e3).optional()
7612
+ });
7613
+ var RelocateMediaInputSchema = object({
7614
+ deviceId: number().optional(),
7615
+ toLocationId: string(),
7616
+ throttleMbps: number().min(1).max(1e3).optional()
7617
+ });
7618
+ /**
7636
7619
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
7637
7620
  * storage a location serves. Defined here (not in `capabilities/storage.cap.ts`)
7638
7621
  * so the persisted record schema and the consumer-facing cap can both consume it
@@ -7682,6 +7665,13 @@ var StorageLocationSchema = object({
7682
7665
  nodeId: string().optional(),
7683
7666
  isDefault: boolean().default(false),
7684
7667
  isSystem: boolean().default(false),
7668
+ /** COMPUTED at read time by the orchestrator (statfs of the backing volume
7669
+ * for node-local locations it can reach) — never persisted, absent when the
7670
+ * volume is remote/unreachable. The single capacity truth every UI reads. */
7671
+ capacity: object({
7672
+ totalBytes: number(),
7673
+ availableBytes: number()
7674
+ }).nullable().optional(),
7685
7675
  createdAt: number(),
7686
7676
  updatedAt: number()
7687
7677
  });
@@ -8797,7 +8787,8 @@ var NcTaxonomyEntrySchema = object({
8797
8787
  /** Macro/category parent for grouping ('car' → 'vehicle'); null for a top. */
8798
8788
  parentKind: string().nullable()
8799
8789
  });
8800
- object({
8790
+ /** The complete NC picker taxonomy — three grouped buckets. */
8791
+ var NcTaxonomySchema = object({
8801
8792
  videoClasses: array(NcTaxonomyEntrySchema),
8802
8793
  audioKinds: array(NcTaxonomyEntrySchema),
8803
8794
  labels: array(NcTaxonomyEntrySchema)
@@ -9457,6 +9448,7 @@ var AccessoryKind = {
9457
9448
  };
9458
9449
  AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
9459
9450
  DeviceFeature.BatteryOperated;
9451
+ new Set(["devices", "classes"]);
9460
9452
  /**
9461
9453
  * Shared geometry vocabulary for on-frame shape caps — privacy-mask,
9462
9454
  * motion-zones, and the detection zones/lines editor all speak this one
@@ -9773,12 +9765,60 @@ var NcRuleTargetSchema = object({
9773
9765
  * - `keyFrame` — the clean scene frame (no subject box).
9774
9766
  * - `none` — no attachment.
9775
9767
  */
9776
- var NcMediaPolicySchema = object({ attach: _enum([
9777
- "best",
9778
- "best-matching",
9779
- "keyFrame",
9780
- "none"
9781
- ]).default("best") });
9768
+ /**
9769
+ * WHAT THE PICTURE SHOWS — chosen explicitly, because the implicit ladders
9770
+ * conflate it with the selection strategy and betray the request: asking for
9771
+ * the clean scene frame on an object-event owner used to start at
9772
+ * `fullFrameBoxed`, i.e. the annotated frame (2026-07-30).
9773
+ * - `cropped` — the subject, tight. What "who is at the door" wants.
9774
+ * - `full` — the clean scene, no annotation. What "what is going on" wants.
9775
+ * - `boxed` — the scene WITH the detection boxes drawn, for verifying what
9776
+ * the pipeline actually saw.
9777
+ */
9778
+ var NcMediaFrameSchema = _enum([
9779
+ "cropped",
9780
+ "full",
9781
+ "boxed"
9782
+ ]);
9783
+ var NcMediaPolicySchema = object({
9784
+ attach: _enum([
9785
+ "best",
9786
+ "best-matching",
9787
+ "keyFrame",
9788
+ "none"
9789
+ ]).default("best"),
9790
+ /** What the still shows. Absent = `cropped` (the historical `best` shape). */
9791
+ frame: NcMediaFrameSchema.optional(),
9792
+ /** Crop the attached still to the rule's condition-zone bbox (padded) —
9793
+ * "show me the ZONE", not the whole scene or the subject crop. */
9794
+ zoneCrop: boolean().optional(),
9795
+ /**
9796
+ * Also attach a short GIF cut from the stream broker's clip ring around the
9797
+ * event — NOT from the recording, so the camera does not have to be
9798
+ * recording, and the window sits AROUND the moment instead of a segment
9799
+ * behind it. Fail-closed: a window the ring does not cover contributes no
9800
+ * gif, never a failed notification.
9801
+ */
9802
+ gif: boolean().optional(),
9803
+ /**
9804
+ * Also attach a short MP4 CLIP of the same cut. Same source and the same
9805
+ * fail-closed rule as `gif`. Prefer it where the backend takes video
9806
+ * (telegram, discord, zentik, webhook); backends that do not are handled by
9807
+ * the degrade engine, which drops the video and keeps the still.
9808
+ */
9809
+ clip: boolean().optional(),
9810
+ /** Seconds of footage BEFORE / AFTER the event instant. Defaults 3 / 7. */
9811
+ clipPreRollSec: number().int().min(0).max(30).optional(),
9812
+ clipPostRollSec: number().int().min(0).max(30).optional(),
9813
+ /**
9814
+ * Which stream profile the footage is cut from. Absent = the CHEAPEST
9815
+ * assigned profile: a notification is watched on a phone, so the 4K
9816
+ * rendition would burn CPU to produce a file the client downscales anyway.
9817
+ * A profile that is not assigned falls back to the cheapest, and the render
9818
+ * reports which one actually ran.
9819
+ */
9820
+ profile: CamProfileSchema.optional()
9821
+ });
9782
9822
  /** Throttle — cooldown survives restarts (rebuilt from the outbox on boot). */
9783
9823
  var NcThrottleSchema = object({
9784
9824
  cooldownSec: number().int().min(0).max(86400).default(60),
@@ -9792,7 +9832,19 @@ var NcRuleInputSchema = object({
9792
9832
  delivery: NcDeliverySchema,
9793
9833
  conditions: NcConditionsSchema.default({}),
9794
9834
  schedule: NcScheduleSchema.optional(),
9795
- targets: array(NcRuleTargetSchema).min(1),
9835
+ /** May be empty when `targetUsers` addresses at least one user — the
9836
+ * "at least one addressee" invariant is enforced by the provider, because
9837
+ * a cross-field refine here would break `NcRulePatchSchema.partial()`. */
9838
+ targets: array(NcRuleTargetSchema),
9839
+ /**
9840
+ * USERS this rule addresses in addition to `targets` (Phase 4). At fire
9841
+ * time each user fans out to the personal targets they own
9842
+ * (`config.ownerUserId`), filtered by that user's `allowedDevices` for the
9843
+ * firing camera — a user is never notified about a device they cannot open.
9844
+ * Per-user opt-outs (`disabledTargetIds`) still apply to the fanned-out
9845
+ * targets.
9846
+ */
9847
+ targetUsers: array(string()).optional(),
9796
9848
  media: NcMediaPolicySchema.default({ attach: "best" }),
9797
9849
  throttle: NcThrottleSchema.default({
9798
9850
  cooldownSec: 60,
@@ -10005,7 +10057,10 @@ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), m
10005
10057
  }), object({ results: array(NcTestResultSchema) }), {
10006
10058
  kind: "mutation",
10007
10059
  auth: "admin"
10008
- }), method(object({}), object({ catalog: array(NcConditionDescriptorSchema) })), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" });
10060
+ }), method(object({}), object({
10061
+ catalog: array(NcConditionDescriptorSchema),
10062
+ taxonomy: NcTaxonomySchema.optional()
10063
+ })), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" });
10009
10064
  /**
10010
10065
  * TimelapseRule — the STANDALONE scheduled timelapse producer's rule model.
10011
10066
  *
@@ -10752,6 +10807,48 @@ var streamBrokerCapability = {
10752
10807
  kind: "mutation",
10753
10808
  auth: "admin"
10754
10809
  }),
10810
+ /**
10811
+ * Render a short GIF or MP4 from the broker's PRE-BUFFER around an instant.
10812
+ *
10813
+ * The pre-buffer is the only source that already holds the seconds BEFORE
10814
+ * an event (it is what it exists for — "when an addon needs the last N
10815
+ * seconds of video for clip creation"). Cutting from recorded segments
10816
+ * cannot: only finalized segments are addressable and the one covering the
10817
+ * moment is still being written, so a segment-sourced clip is either
10818
+ * delayed or shows the approach instead of the event (operator,
10819
+ * 2026-07-30). It also means a camera does not have to be RECORDING to
10820
+ * produce a notification clip.
10821
+ *
10822
+ * Returns muxed BYTES, never packets: the frame plane never crosses a
10823
+ * process boundary at frame rate (D9/D18) — this is one compressed,
10824
+ * on-demand payload per notification.
10825
+ *
10826
+ * Waits up to `postRollSec` for the tail to accumulate, so the window is
10827
+ * CENTRED on `aroundMs`.
10828
+ */
10829
+ renderPreBufferClip: method(object({
10830
+ deviceId: number(),
10831
+ /** Absent = the LOWEST assigned profile — a notification attachment is
10832
+ * watched on a phone, and the cheap rendition is the right default. */
10833
+ profile: CamProfileSchema.optional(),
10834
+ aroundMs: number(),
10835
+ preRollSec: number().min(0).max(20).default(3),
10836
+ postRollSec: number().min(0).max(20).default(5),
10837
+ format: _enum(["gif", "mp4"]).default("gif"),
10838
+ maxWidth: number().int().min(120).max(1920).default(480),
10839
+ /** GIF only — MP4 keeps the source cadence. */
10840
+ fps: number().int().min(1).max(15).default(5)
10841
+ }), object({
10842
+ base64: string(),
10843
+ mime: string(),
10844
+ bytes: number().int(),
10845
+ /** The profile actually rendered (what the default resolved to). */
10846
+ profile: CamProfileSchema,
10847
+ durationMs: number()
10848
+ }), {
10849
+ kind: "mutation",
10850
+ auth: "admin"
10851
+ }),
10755
10852
  listAllCameraStreams: method(_void(), array(CameraStreamSchema).readonly()),
10756
10853
  listAllProfileSlots: method(_void(), array(ProfileSlotSchema).readonly()),
10757
10854
  getBrokerStats: method(object({ brokerId: string() }), BrokerStatsSchema),
@@ -17926,6 +18023,36 @@ var TargetKindSchema = object({
17926
18023
  icon: string(),
17927
18024
  /** Stamped by each provider so the concat-fanned catalog stays routable. */
17928
18025
  addonId: string(),
18026
+ /**
18027
+ * URL of the kind's bundled BRAND icon, served by the providing addon over
18028
+ * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
18029
+ * when the addon bundles no icon for that kind — the client then falls back
18030
+ * to a neutral glyph rather than rendering the raw `icon` NAME as text.
18031
+ *
18032
+ * Root-relative on purpose: it resolves against whatever origin serves a web
18033
+ * client, and a native client joins it onto its own hub base.
18034
+ *
18035
+ * DECLARED here deliberately. It used to travel as an undeclared passthrough
18036
+ * field that survived only because the runtime cap-router forwards provider
18037
+ * output verbatim — so every consumer had to re-declare it by hand to stop
18038
+ * its own Zod parse from stripping it, and the whole arrangement would have
18039
+ * broken silently the moment output validation was tightened anywhere.
18040
+ */
18041
+ iconUrl: string().optional(),
18042
+ /**
18043
+ * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
18044
+ *
18045
+ * The server knows this and therefore says it, because the client cannot
18046
+ * safely guess: a React-Native client renders SVG and raster through two
18047
+ * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
18048
+ * not decode SVG on iOS/Android), so without this it silently fell back to a
18049
+ * placeholder glyph for every vector icon while the web build looked fine.
18050
+ *
18051
+ * Absent when {@link iconUrl} is absent, or for a legacy provider that has
18052
+ * not been updated — a client that cannot determine the type should prefer
18053
+ * its raster path, which is the safe default for an unknown image.
18054
+ */
18055
+ iconMediaType: string().optional(),
17929
18056
  configSchema: ConfigSchemaPassthrough,
17930
18057
  supportsDiscovery: boolean(),
17931
18058
  caps: TargetKindCapsSchema
@@ -18657,6 +18784,15 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
18657
18784
  }), method(object({ deviceId: number() }), EventPruneCountsSchema, {
18658
18785
  kind: "mutation",
18659
18786
  auth: "admin"
18787
+ }), method(RelocateMediaInputSchema, object({ jobId: string() }), {
18788
+ kind: "mutation",
18789
+ auth: "admin"
18790
+ }), method(object({}), array(RelocateJobSchema).readonly(), {
18791
+ kind: "query",
18792
+ auth: "admin"
18793
+ }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
18794
+ kind: "mutation",
18795
+ auth: "admin"
18660
18796
  }), method(OpsLogQueryInputSchema, array(OpsLogEntrySchema).readonly(), {
18661
18797
  kind: "query",
18662
18798
  auth: "admin"
@@ -21231,6 +21367,17 @@ var GetConnectionEndpointsResultSchema = object({ endpoints: array(object({
21231
21367
  */
21232
21368
  priority: number()
21233
21369
  })).readonly() });
21370
+ /**
21371
+ * The chosen outbound endpoint for notification artifacts. `baseUrl: null` =
21372
+ * AUTO (resolved from the candidate ranking at send time); `resolved` reports
21373
+ * what AUTO currently picks, so the UI can show the effective value either way.
21374
+ */
21375
+ var NotificationEndpointSchema = object({
21376
+ /** The operator's explicit choice, or null for AUTO. */
21377
+ baseUrl: string().nullable(),
21378
+ /** What the ranking currently resolves to (null when nothing is reachable). */
21379
+ resolved: string().nullable()
21380
+ });
21234
21381
  var AllowedAddressesSchema = object({
21235
21382
  /**
21236
21383
  * Allowlist of interface addresses operators have explicitly opted
@@ -21253,7 +21400,7 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
21253
21400
  * to avoid mixed-content blocks in the browser. The public
21254
21401
  * tunnel always emits `https://` regardless. */
21255
21402
  scheme: _enum(["http", "https"]).optional()
21256
- }), GetConnectionEndpointsResultSchema), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" });
21403
+ }), 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" });
21257
21404
  /**
21258
21405
  * mesh-network — collection cap for mesh-VPN providers.
21259
21406
  *
@@ -22052,7 +22199,12 @@ var RecordingDeviceUsageSchema = object({
22052
22199
  var RecordingLocationUsageSchema = object({
22053
22200
  /** StorageLocation id; null for the legacy/degraded single-root fallback. */
22054
22201
  locationId: string().nullable(),
22055
- /** Bytes of recordings stored on this location. */
22202
+ /** Every location id sharing this row's PHYSICAL volume (aliases). One row
22203
+ * is emitted per physical disk (2026-07-29): two locations on one root
22204
+ * previously rendered as two identical "disks" with a nonsensical used
22205
+ * split — hydrate attribution across aliases is arbitrary by nature. */
22206
+ locationIds: array(string()).optional(),
22207
+ /** Bytes of recordings stored on this PHYSICAL volume (all aliases). */
22056
22208
  usedBytes: number(),
22057
22209
  /** Free bytes on the location's volume; null when capacity is unknown (remote). */
22058
22210
  availableBytes: number().nullable(),
@@ -22208,6 +22360,77 @@ var recordingCapability = {
22208
22360
  listOpsLog: method(OpsLogQueryInputSchema, array(OpsLogEntrySchema).readonly(), {
22209
22361
  kind: "query",
22210
22362
  auth: "admin"
22363
+ }),
22364
+ /**
22365
+ * Move footage (segments and/or strips) from one recordings location to
22366
+ * another — the drain workflow's mover (entity-routing spec Phase 4).
22367
+ * Throttled copy → size-verify → delete source → index refresh; resumable
22368
+ * by construction (copy-if-absent). Single-flight: one job at a time.
22369
+ */
22370
+ /**
22371
+ * Render a short GIF from recorded footage around `aroundMs` (low profile,
22372
+ * palette-free ffmpeg gif). Synchronous — the window is a few seconds of
22373
+ * `low`, rendering in ~1-2s. Built for notification attachments: the
22374
+ * caller gets bytes, nothing persists. Fails when no footage covers the
22375
+ * window (camera not recording → caller skips the attachment).
22376
+ */
22377
+ renderGif: method(object({
22378
+ deviceId: number(),
22379
+ aroundMs: number(),
22380
+ preRollSec: number().min(0).max(30).default(2),
22381
+ postRollSec: number().min(0).max(30).default(5),
22382
+ maxWidth: number().int().min(120).max(1280).default(480),
22383
+ fps: number().int().min(1).max(15).default(5)
22384
+ }), object({
22385
+ gifBase64: string(),
22386
+ fromMs: number(),
22387
+ toMs: number()
22388
+ }), {
22389
+ kind: "mutation",
22390
+ auth: "admin"
22391
+ }),
22392
+ /**
22393
+ * Render a short MP4 clip from recorded footage around `aroundMs` — the
22394
+ * "send a clip, not a gif" notification attachment. Same window, source
22395
+ * and fail-closed contract as {@link renderGif}: no footage covering the
22396
+ * window ⇒ it throws and the caller ships no attachment. Re-encoded (never
22397
+ * stream-copied) so a concat across segments with different encoder
22398
+ * parameters still plays. Silent by contract.
22399
+ *
22400
+ * The returned window is SEGMENT-GRANULAR: the source playlist is built
22401
+ * from whole segments, so the clip starts at or before `fromMs` and ends
22402
+ * at or after `toMs` — overshoot bounded by one segment per side (an 8 s
22403
+ * ask over 4 s segments measured 12 s). `fromMs`/`toMs` report the window
22404
+ * ASKED for, not the encoded duration.
22405
+ */
22406
+ renderClip: method(object({
22407
+ deviceId: number(),
22408
+ aroundMs: number(),
22409
+ preRollSec: number().min(0).max(30).default(3),
22410
+ postRollSec: number().min(0).max(30).default(7),
22411
+ maxWidth: number().int().min(160).max(1920).default(640)
22412
+ }), object({
22413
+ clipBase64: string(),
22414
+ mime: string(),
22415
+ fromMs: number(),
22416
+ toMs: number(),
22417
+ bytes: number().int()
22418
+ }), {
22419
+ kind: "mutation",
22420
+ auth: "admin"
22421
+ }),
22422
+ relocateFootage: method(RelocateFootageInputSchema, object({ jobId: string() }), {
22423
+ kind: "mutation",
22424
+ auth: "admin"
22425
+ }),
22426
+ /** Every known relocate job (in-RAM, newest-first). */
22427
+ getRelocateStatus: method(object({}), array(RelocateJobSchema).readonly(), {
22428
+ kind: "query",
22429
+ auth: "admin"
22430
+ }),
22431
+ cancelRelocate: method(object({ jobId: string() }), object({ cancelled: boolean() }), {
22432
+ kind: "mutation",
22433
+ auth: "admin"
22211
22434
  })
22212
22435
  }
22213
22436
  };
@@ -24712,6 +24935,12 @@ Object.freeze({
24712
24935
  addonId: null,
24713
24936
  access: "view"
24714
24937
  },
24938
+ "localNetwork.getNotificationEndpoint": {
24939
+ capName: "local-network",
24940
+ capScope: "system",
24941
+ addonId: null,
24942
+ access: "view"
24943
+ },
24715
24944
  "localNetwork.getPreferred": {
24716
24945
  capName: "local-network",
24717
24946
  capScope: "system",
@@ -24736,6 +24965,12 @@ Object.freeze({
24736
24965
  addonId: null,
24737
24966
  access: "create"
24738
24967
  },
24968
+ "localNetwork.setNotificationEndpoint": {
24969
+ capName: "local-network",
24970
+ capScope: "system",
24971
+ addonId: null,
24972
+ access: "create"
24973
+ },
24739
24974
  "lockControl.lock": {
24740
24975
  capName: "lock-control",
24741
24976
  capScope: "device",
@@ -25378,6 +25613,12 @@ Object.freeze({
25378
25613
  addonId: null,
25379
25614
  access: "create"
25380
25615
  },
25616
+ "pipelineAnalytics.cancelMediaRelocate": {
25617
+ capName: "pipeline-analytics",
25618
+ capScope: "device",
25619
+ addonId: null,
25620
+ access: "create"
25621
+ },
25381
25622
  "pipelineAnalytics.clearTracks": {
25382
25623
  capName: "pipeline-analytics",
25383
25624
  capScope: "device",
@@ -25432,6 +25673,12 @@ Object.freeze({
25432
25673
  addonId: null,
25433
25674
  access: "view"
25434
25675
  },
25676
+ "pipelineAnalytics.getMediaRelocateStatus": {
25677
+ capName: "pipeline-analytics",
25678
+ capScope: "device",
25679
+ addonId: null,
25680
+ access: "view"
25681
+ },
25435
25682
  "pipelineAnalytics.getMotionEvents": {
25436
25683
  capName: "pipeline-analytics",
25437
25684
  capScope: "device",
@@ -25504,6 +25751,12 @@ Object.freeze({
25504
25751
  addonId: null,
25505
25752
  access: "create"
25506
25753
  },
25754
+ "pipelineAnalytics.relocateMedia": {
25755
+ capName: "pipeline-analytics",
25756
+ capScope: "device",
25757
+ addonId: null,
25758
+ access: "create"
25759
+ },
25507
25760
  "pipelineAnalytics.searchObjectEvents": {
25508
25761
  capName: "pipeline-analytics",
25509
25762
  capScope: "device",
@@ -26266,6 +26519,12 @@ Object.freeze({
26266
26519
  addonId: null,
26267
26520
  access: "create"
26268
26521
  },
26522
+ "recording.cancelRelocate": {
26523
+ capName: "recording",
26524
+ capScope: "system",
26525
+ addonId: null,
26526
+ access: "create"
26527
+ },
26269
26528
  "recording.deleteFootprint": {
26270
26529
  capName: "recording",
26271
26530
  capScope: "system",
@@ -26296,6 +26555,12 @@ Object.freeze({
26296
26555
  addonId: null,
26297
26556
  access: "view"
26298
26557
  },
26558
+ "recording.getRelocateStatus": {
26559
+ capName: "recording",
26560
+ capScope: "system",
26561
+ addonId: null,
26562
+ access: "view"
26563
+ },
26299
26564
  "recording.getStorageUsage": {
26300
26565
  capName: "recording",
26301
26566
  capScope: "system",
@@ -26326,6 +26591,24 @@ Object.freeze({
26326
26591
  addonId: null,
26327
26592
  access: "view"
26328
26593
  },
26594
+ "recording.relocateFootage": {
26595
+ capName: "recording",
26596
+ capScope: "system",
26597
+ addonId: null,
26598
+ access: "create"
26599
+ },
26600
+ "recording.renderClip": {
26601
+ capName: "recording",
26602
+ capScope: "system",
26603
+ addonId: null,
26604
+ access: "create"
26605
+ },
26606
+ "recording.renderGif": {
26607
+ capName: "recording",
26608
+ capScope: "system",
26609
+ addonId: null,
26610
+ access: "create"
26611
+ },
26329
26612
  "recording.rescanStorage": {
26330
26613
  capName: "recording",
26331
26614
  capScope: "system",
@@ -26920,6 +27203,12 @@ Object.freeze({
26920
27203
  addonId: null,
26921
27204
  access: "create"
26922
27205
  },
27206
+ "streamBroker.renderPreBufferClip": {
27207
+ capName: "stream-broker",
27208
+ capScope: "system",
27209
+ addonId: null,
27210
+ access: "create"
27211
+ },
26923
27212
  "streamBroker.restartProfile": {
26924
27213
  capName: "stream-broker",
26925
27214
  capScope: "system",
@@ -27852,4 +28141,4 @@ function enumerateInferenceDevices(hw) {
27852
28141
  return out;
27853
28142
  }
27854
28143
  //#endregion
27855
- export { selectAssignedProfileSlots as $, motionDetectionCapability as A, errMsg as B, detectionPipelineCapability as C, mapAudioLabelToMacro as D, hfModelUrl as E, runtimeDevices as F, createEvent as G, CAM_PROFILE_ORDER as H, storageEvictableCapability as I, makeProfileBrokerId as J, hydrateSchema as K, streamBrokerCapability as L, pipelineRunnerCapability as M, recordingCapability as N, maskUrlCredentials as O, recordingExportCapability as P, parseProfileBrokerId as Q, supportedRuntimes as R, defineCustomActions as S, evaluateZoneRules as T, DeviceFeature as U, BaseAddon as V, DeviceType as W, nodePin as X, makeSourceBrokerId as Y, parseJsonUnknown as Z, audioAnalysisCapability as _, COCO_TO_MACRO as a, lazy as at, customAction as b, EVENT_PAD_MS as c, object as ct, HF_BASE_URL as d, union as dt, sleep as et, OpsLogEntrySchema as f, EventCategory as ft, addonWidgetsSourceCapability as g, YAMNET_TO_MACRO as h, COCO_80_LABELS as i, discriminatedUnion as it, pipelineExecutorCapability as j, migrateConfigToBands as k, EncodeProfileSchema as l, record as lt, RingBuffer as m, AUDIO_BACKEND_CHOICES as n, array as nt, DEFAULT_AUDIO_ANALYZER_CONFIG as o, literal as ot, RecordingConfigSchema as p, isEvent as q, AUDIO_MACRO_LABELS as r, boolean as rt, DEVICE_BACKEND_TO_FORMAT as s, number as st, APPLE_SA_TO_MACRO as t, _enum as tt, ExportRecordSchema as u, string as ut, audioAnalyzerCapability as v, enumerateInferenceDevices as w, defaultDeviceFor as x, cameraStreamsCapability as y, webrtcSessionCapability as z };
28144
+ export { selectAssignedProfileSlots as $, motionDetectionCapability as A, errMsg as B, deriveRecordingMode as C, hfModelUrl as D, evaluateZoneRules as E, runtimeDevices as F, createEvent as G, CAM_PROFILE_ORDER as H, storageEvictableCapability as I, makeProfileBrokerId as J, hydrateSchema as K, streamBrokerCapability as L, pipelineRunnerCapability as M, recordingCapability as N, mapAudioLabelToMacro as O, recordingExportCapability as P, parseProfileBrokerId as Q, supportedRuntimes as R, defineCustomActions as S, enumerateInferenceDevices as T, DeviceFeature as U, BaseAddon as V, DeviceType as W, nodePin as X, makeSourceBrokerId as Y, parseJsonUnknown as Z, audioAnalysisCapability as _, COCO_TO_MACRO as a, lazy as at, customAction as b, EVENT_PAD_MS as c, object as ct, HF_BASE_URL as d, union as dt, sleep as et, OpsLogEntrySchema as f, EventCategory as ft, addonWidgetsSourceCapability as g, YAMNET_TO_MACRO as h, COCO_80_LABELS as i, discriminatedUnion as it, pipelineExecutorCapability as j, maskUrlCredentials as k, EncodeProfileSchema as l, record as lt, RingBuffer as m, AUDIO_BACKEND_CHOICES as n, array as nt, DEFAULT_AUDIO_ANALYZER_CONFIG as o, literal as ot, RecordingConfigSchema as p, isEvent as q, AUDIO_MACRO_LABELS as r, boolean as rt, DEVICE_BACKEND_TO_FORMAT as s, number as st, APPLE_SA_TO_MACRO as t, _enum as tt, ExportRecordSchema as u, string as ut, audioAnalyzerCapability as v, detectionPipelineCapability as w, defaultDeviceFor as x, cameraStreamsCapability as y, webrtcSessionCapability as z };