@camstack/addon-provider-reolink 1.2.13 → 1.2.14

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 +346 -73
  2. package/dist/addon.mjs +346 -73
  3. package/package.json +3 -2
package/dist/addon.js CHANGED
@@ -7413,35 +7413,22 @@ function findTimezone(id) {
7413
7413
  */
7414
7414
  var RecordingWeekdaySchema = number().int().min(0).max(6);
7415
7415
  var HHMM = /^([01]\d|2[0-3]):[0-5]\d$/;
7416
- var RecordingScheduleSchema = discriminatedUnion("kind", [object({ kind: literal("always") }), object({
7417
- kind: literal("timeOfDay"),
7418
- start: string().regex(HHMM),
7419
- end: string().regex(HHMM),
7420
- /** Restrict to these weekdays; omit = every day. */
7421
- days: array(RecordingWeekdaySchema).optional()
7422
- })]);
7423
- var RecordingModeSchema = _enum([
7424
- "continuous",
7425
- "onMotion",
7426
- "onAudioThreshold"
7427
- ]);
7428
7416
  /**
7429
- * First-class, authoritative per-camera storage mode — the explicit choice the
7430
- * UI reads directly (never inferred from `rules`):
7431
- * - `off` — not recording.
7432
- * - `events` — record only around triggers (motion / audio threshold),
7433
- * with pre/post-buffer.
7434
- * - `continuous` — record 24/7 within the schedule.
7417
+ * DERIVED per-camera storage summary — the single field cheap consumers read
7418
+ * (the viewer's status dot, the camera list) instead of walking `bands`:
7419
+ * - `off` — no band covers the camera (or it is disabled).
7420
+ * - `events` — every band records around triggers only.
7421
+ * - `continuous` — at least one band records continuously.
7435
7422
  *
7436
- * `mode` compiles one-way to the internal `rules[]` consumed by the policy
7437
- * engine (see `compileRules`); `rules[]` is never authored directly anymore.
7423
+ * NEVER authored: the recorder stamps it from the authoritative `bands` on
7424
+ * every save (`activeModeForConfig`). Writing it has no effect.
7438
7425
  */
7439
7426
  var RecordingStorageModeSchema = _enum([
7440
7427
  "off",
7441
7428
  "events",
7442
7429
  "continuous"
7443
7430
  ]);
7444
- /** Which detectors trigger an `events`-mode recording. */
7431
+ /** Which detectors trigger an `events`-mode band. */
7445
7432
  var RecordingTriggersSchema = object({
7446
7433
  motion: boolean().optional(),
7447
7434
  audioThresholdDbfs: number().optional()
@@ -7477,18 +7464,6 @@ var RecordingBandSchema = object({
7477
7464
  preBufferSec: number().min(0).optional(),
7478
7465
  postBufferSec: number().min(0).optional()
7479
7466
  });
7480
- var RecordingRuleSchema = object({
7481
- schedule: RecordingScheduleSchema,
7482
- mode: RecordingModeSchema,
7483
- /** Seconds of footage to retain BEFORE a trigger (applied at keep/discard). */
7484
- preBufferSec: number().min(0).default(0),
7485
- /** Keep recording until this many seconds after the last trigger. */
7486
- postBufferSec: number().min(0).default(0),
7487
- /** Each new trigger restarts the post-buffer window. */
7488
- resetTimeoutOnNewEvent: boolean().default(true),
7489
- /** onAudioThreshold only — dBFS level that counts as a trigger. */
7490
- thresholdDbfs: number().optional()
7491
- });
7492
7467
  /**
7493
7468
  * Per-device retention overrides. Every field is optional; an unset or `0`
7494
7469
  * value inherits the node-wide recorder default. Only footage-lifetime limits
@@ -7522,40 +7497,28 @@ var ScrubThumbnailPresetSchema = _enum([
7522
7497
  /**
7523
7498
  * The full per-camera recording intent — the wire shape of a RecordingTarget.
7524
7499
  *
7525
- * `mode` is the authoritative storage choice; `schedule`/`triggers`/`pre`/`post`
7526
- * are its mode-specific parameters. `rules` is a DEPRECATED authoring input kept
7527
- * only for transition + migration (`migrateRulesToMode`); the policy engine
7528
- * consumes the compiled output of `compileRules(config)`, never `rules` directly.
7500
+ * `bands` is the ONLY authored recording intent: what to record, when, and on
7501
+ * which trigger. `mode` is a derived summary the recorder stamps on save; every
7502
+ * other field is a storage knob (profiles, segment length, retention, scrub).
7503
+ *
7504
+ * STRICT on purpose: the legacy authoring surface (`schedule`/`schedules`/
7505
+ * `triggers`/`preBufferSec`/`postBufferSec`/`rules`) was retired 2026-07-30.
7506
+ * A stale caller must fail loudly — silently stripping its legacy intent would
7507
+ * persist a band-less config, i.e. silently stop recording the camera.
7529
7508
  */
7530
7509
  var RecordingConfigSchema = object({
7531
7510
  enabled: boolean(),
7532
- /** Authoritative storage mode. Absent on legacy targets derived once via
7533
- * `migrateRulesToMode`, then persisted. */
7511
+ /** DERIVED summary of `bands`, stamped by the recorder on every save.
7512
+ * Authoring it has no effect — see {@link RecordingStorageModeSchema}. */
7534
7513
  mode: RecordingStorageModeSchema.optional(),
7535
7514
  profiles: array(CamProfileSchema).optional(),
7536
7515
  segmentSeconds: number().int().positive().optional(),
7537
- /** Shared recording time-bands for `events` & `continuous` — record only when
7538
- * the wall-clock falls inside one of these windows. Omit or empty = always.
7539
- * `continuous` compiles to one rule per band; `events` to band × trigger. */
7540
- schedules: array(RecordingScheduleSchema).optional(),
7541
- /** Legacy single-band predecessor of `schedules`. Read-compat only — it is
7542
- * normalized into `schedules` on read and never written going forward. (Not
7543
- * tagged `@deprecated`: the normalization paths must read it cast-free.) */
7544
- schedule: RecordingScheduleSchema.optional(),
7545
- /** `events`-mode only — which detectors trigger a recording. */
7546
- triggers: RecordingTriggersSchema.optional(),
7547
- /** `events`-mode only — seconds retained before / after a trigger. */
7548
- preBufferSec: number().min(0).optional(),
7549
- postBufferSec: number().min(0).optional(),
7550
- /** DEPRECATED authoring input; retained for migration/transition. */
7551
- rules: array(RecordingRuleSchema).optional(),
7552
7516
  /**
7553
- * AUTHORITATIVE mode-per-band recording model (recorder). When present it
7554
- * is the single source of truth; the legacy `mode`/`schedules`/`schedule`/
7555
- * `triggers`/`rules` fields above are kept for READ-COMPAT only and are
7556
- * derived into bands once via `migrateConfigToBands`.
7517
+ * AUTHORITATIVE mode-per-band recording model the single source of truth
7518
+ * the recorder's band engine consumes. An empty array = record nothing;
7519
+ * "off" is the absence of a covering band, never a band value.
7557
7520
  */
7558
- bands: array(RecordingBandSchema).optional(),
7521
+ bands: array(RecordingBandSchema).default([]),
7559
7522
  retention: RecordingRetentionSchema.optional(),
7560
7523
  /**
7561
7524
  * Per-camera scrub-thumbnail fidelity preset (resolution + JPEG quality for
@@ -7563,8 +7526,15 @@ var RecordingConfigSchema = object({
7563
7526
  * windows only — existing sheets are immutable, and each window's index
7564
7527
  * carries its own tile dims so mixed-preset history renders correctly.
7565
7528
  */
7566
- scrubThumbnails: ScrubThumbnailPresetSchema.optional()
7567
- });
7529
+ scrubThumbnails: ScrubThumbnailPresetSchema.optional(),
7530
+ /**
7531
+ * OPT-IN thumbnail-strip generation for this camera: every keyframe of the
7532
+ * low recording saved as a JPEG (the fast-drag scrub depth), a derived
7533
+ * cache that eviction reclaims with the footage. Absent/false = no strips
7534
+ * are written and scrub reads exact keyframes at every velocity.
7535
+ */
7536
+ stripsEnabled: boolean().optional()
7537
+ }).strict();
7568
7538
  /**
7569
7539
  * Ops-log — the durable, append-only operations audit shared by the
7570
7540
  * recordings and events management surfaces.
@@ -7583,7 +7553,8 @@ var OpsLogOpSchema = _enum([
7583
7553
  "prune",
7584
7554
  "manual-delete",
7585
7555
  "rescan",
7586
- "retention-run"
7556
+ "retention-run",
7557
+ "relocate"
7587
7558
  ]);
7588
7559
  /** Why the operation ran. */
7589
7560
  var OpsLogReasonSchema = _enum([
@@ -7622,6 +7593,55 @@ var OpsLogQueryInputSchema = object({
7622
7593
  limit: number().int().min(1).max(1e3).optional()
7623
7594
  });
7624
7595
  /**
7596
+ * Entity-relocation job state (storage entity-routing spec, Phase 4).
7597
+ *
7598
+ * One shape shared by the recorder's `relocateFootage` (segments + strips) and
7599
+ * pipeline-analytics' `relocateMedia` (event media blobs) so the admin Data
7600
+ * page renders both movers with one component. Jobs are in-RAM (a restart
7601
+ * forgets them — re-running is safe by construction: copy-if-absent, delete
7602
+ * after verify) and each completed/failed run also lands one durable ops-log
7603
+ * row on the owning addon's surface.
7604
+ */
7605
+ var RelocateJobStateSchema = _enum([
7606
+ "running",
7607
+ "done",
7608
+ "failed",
7609
+ "cancelled"
7610
+ ]);
7611
+ var RelocateJobSchema = object({
7612
+ jobId: string(),
7613
+ state: RelocateJobStateSchema,
7614
+ /** Source location — for media relocation this is informational ('*': rows
7615
+ * move from wherever they are to the target). */
7616
+ fromLocationId: string(),
7617
+ toLocationId: string(),
7618
+ /** Scoped device, or null = every device. */
7619
+ deviceId: number().nullable(),
7620
+ /** What the job moves (owner-addon specific: segments/strips or media). */
7621
+ entities: array(string()),
7622
+ filesMoved: number().int(),
7623
+ bytesMoved: number().int(),
7624
+ /** Total files discovered up front; null while (or when) unknown. */
7625
+ filesTotal: number().int().nullable(),
7626
+ startedAt: number(),
7627
+ finishedAt: number().nullable(),
7628
+ error: string().nullable()
7629
+ });
7630
+ var RelocateFootageInputSchema = object({
7631
+ deviceId: number().optional(),
7632
+ fromLocationId: string(),
7633
+ toLocationId: string(),
7634
+ entities: array(_enum(["segments", "strips"])).optional(),
7635
+ /** Copy throttle in MB/s (default 40) — the drain is a background chore,
7636
+ * never allowed to starve live writers. */
7637
+ throttleMbps: number().min(1).max(1e3).optional()
7638
+ });
7639
+ var RelocateMediaInputSchema = object({
7640
+ deviceId: number().optional(),
7641
+ toLocationId: string(),
7642
+ throttleMbps: number().min(1).max(1e3).optional()
7643
+ });
7644
+ /**
7625
7645
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
7626
7646
  * storage a location serves. Defined here (not in `capabilities/storage.cap.ts`)
7627
7647
  * so the persisted record schema and the consumer-facing cap can both consume it
@@ -7671,6 +7691,13 @@ var StorageLocationSchema = object({
7671
7691
  nodeId: string().optional(),
7672
7692
  isDefault: boolean().default(false),
7673
7693
  isSystem: boolean().default(false),
7694
+ /** COMPUTED at read time by the orchestrator (statfs of the backing volume
7695
+ * for node-local locations it can reach) — never persisted, absent when the
7696
+ * volume is remote/unreachable. The single capacity truth every UI reads. */
7697
+ capacity: object({
7698
+ totalBytes: number(),
7699
+ availableBytes: number()
7700
+ }).nullable().optional(),
7674
7701
  createdAt: number(),
7675
7702
  updatedAt: number()
7676
7703
  });
@@ -8438,7 +8465,8 @@ var NcTaxonomyEntrySchema = object({
8438
8465
  /** Macro/category parent for grouping ('car' → 'vehicle'); null for a top. */
8439
8466
  parentKind: string().nullable()
8440
8467
  });
8441
- object({
8468
+ /** The complete NC picker taxonomy — three grouped buckets. */
8469
+ var NcTaxonomySchema = object({
8442
8470
  videoClasses: array(NcTaxonomyEntrySchema),
8443
8471
  audioKinds: array(NcTaxonomyEntrySchema),
8444
8472
  labels: array(NcTaxonomyEntrySchema)
@@ -9380,6 +9408,7 @@ function createRuntimeStateBridge(params) {
9380
9408
  getStatus
9381
9409
  };
9382
9410
  }
9411
+ new Set(["devices", "classes"]);
9383
9412
  /**
9384
9413
  * Shared geometry vocabulary for on-frame shape caps — privacy-mask,
9385
9414
  * motion-zones, and the detection zones/lines editor all speak this one
@@ -9696,12 +9725,60 @@ var NcRuleTargetSchema = object({
9696
9725
  * - `keyFrame` — the clean scene frame (no subject box).
9697
9726
  * - `none` — no attachment.
9698
9727
  */
9699
- var NcMediaPolicySchema = object({ attach: _enum([
9700
- "best",
9701
- "best-matching",
9702
- "keyFrame",
9703
- "none"
9704
- ]).default("best") });
9728
+ /**
9729
+ * WHAT THE PICTURE SHOWS — chosen explicitly, because the implicit ladders
9730
+ * conflate it with the selection strategy and betray the request: asking for
9731
+ * the clean scene frame on an object-event owner used to start at
9732
+ * `fullFrameBoxed`, i.e. the annotated frame (2026-07-30).
9733
+ * - `cropped` — the subject, tight. What "who is at the door" wants.
9734
+ * - `full` — the clean scene, no annotation. What "what is going on" wants.
9735
+ * - `boxed` — the scene WITH the detection boxes drawn, for verifying what
9736
+ * the pipeline actually saw.
9737
+ */
9738
+ var NcMediaFrameSchema = _enum([
9739
+ "cropped",
9740
+ "full",
9741
+ "boxed"
9742
+ ]);
9743
+ var NcMediaPolicySchema = object({
9744
+ attach: _enum([
9745
+ "best",
9746
+ "best-matching",
9747
+ "keyFrame",
9748
+ "none"
9749
+ ]).default("best"),
9750
+ /** What the still shows. Absent = `cropped` (the historical `best` shape). */
9751
+ frame: NcMediaFrameSchema.optional(),
9752
+ /** Crop the attached still to the rule's condition-zone bbox (padded) —
9753
+ * "show me the ZONE", not the whole scene or the subject crop. */
9754
+ zoneCrop: boolean().optional(),
9755
+ /**
9756
+ * Also attach a short GIF cut from the stream broker's clip ring around the
9757
+ * event — NOT from the recording, so the camera does not have to be
9758
+ * recording, and the window sits AROUND the moment instead of a segment
9759
+ * behind it. Fail-closed: a window the ring does not cover contributes no
9760
+ * gif, never a failed notification.
9761
+ */
9762
+ gif: boolean().optional(),
9763
+ /**
9764
+ * Also attach a short MP4 CLIP of the same cut. Same source and the same
9765
+ * fail-closed rule as `gif`. Prefer it where the backend takes video
9766
+ * (telegram, discord, zentik, webhook); backends that do not are handled by
9767
+ * the degrade engine, which drops the video and keeps the still.
9768
+ */
9769
+ clip: boolean().optional(),
9770
+ /** Seconds of footage BEFORE / AFTER the event instant. Defaults 3 / 7. */
9771
+ clipPreRollSec: number().int().min(0).max(30).optional(),
9772
+ clipPostRollSec: number().int().min(0).max(30).optional(),
9773
+ /**
9774
+ * Which stream profile the footage is cut from. Absent = the CHEAPEST
9775
+ * assigned profile: a notification is watched on a phone, so the 4K
9776
+ * rendition would burn CPU to produce a file the client downscales anyway.
9777
+ * A profile that is not assigned falls back to the cheapest, and the render
9778
+ * reports which one actually ran.
9779
+ */
9780
+ profile: CamProfileSchema.optional()
9781
+ });
9705
9782
  /** Throttle — cooldown survives restarts (rebuilt from the outbox on boot). */
9706
9783
  var NcThrottleSchema = object({
9707
9784
  cooldownSec: number().int().min(0).max(86400).default(60),
@@ -9715,7 +9792,19 @@ var NcRuleInputSchema = object({
9715
9792
  delivery: NcDeliverySchema,
9716
9793
  conditions: NcConditionsSchema.default({}),
9717
9794
  schedule: NcScheduleSchema.optional(),
9718
- targets: array(NcRuleTargetSchema).min(1),
9795
+ /** May be empty when `targetUsers` addresses at least one user — the
9796
+ * "at least one addressee" invariant is enforced by the provider, because
9797
+ * a cross-field refine here would break `NcRulePatchSchema.partial()`. */
9798
+ targets: array(NcRuleTargetSchema),
9799
+ /**
9800
+ * USERS this rule addresses in addition to `targets` (Phase 4). At fire
9801
+ * time each user fans out to the personal targets they own
9802
+ * (`config.ownerUserId`), filtered by that user's `allowedDevices` for the
9803
+ * firing camera — a user is never notified about a device they cannot open.
9804
+ * Per-user opt-outs (`disabledTargetIds`) still apply to the fanned-out
9805
+ * targets.
9806
+ */
9807
+ targetUsers: array(string()).optional(),
9719
9808
  media: NcMediaPolicySchema.default({ attach: "best" }),
9720
9809
  throttle: NcThrottleSchema.default({
9721
9810
  cooldownSec: 60,
@@ -9928,7 +10017,10 @@ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), m
9928
10017
  }), object({ results: array(NcTestResultSchema) }), {
9929
10018
  kind: "mutation",
9930
10019
  auth: "admin"
9931
- }), method(object({}), object({ catalog: array(NcConditionDescriptorSchema) })), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" });
10020
+ }), method(object({}), object({
10021
+ catalog: array(NcConditionDescriptorSchema),
10022
+ taxonomy: NcTaxonomySchema.optional()
10023
+ })), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" });
9932
10024
  /**
9933
10025
  * TimelapseRule — the STANDALONE scheduled timelapse producer's rule model.
9934
10026
  *
@@ -10937,6 +11029,28 @@ method(object({
10937
11029
  }), object({ success: literal(true) }), {
10938
11030
  kind: "mutation",
10939
11031
  auth: "admin"
11032
+ }), method(object({
11033
+ deviceId: number(),
11034
+ /** Absent = the LOWEST assigned profile — a notification attachment is
11035
+ * watched on a phone, and the cheap rendition is the right default. */
11036
+ profile: CamProfileSchema.optional(),
11037
+ aroundMs: number(),
11038
+ preRollSec: number().min(0).max(20).default(3),
11039
+ postRollSec: number().min(0).max(20).default(5),
11040
+ format: _enum(["gif", "mp4"]).default("gif"),
11041
+ maxWidth: number().int().min(120).max(1920).default(480),
11042
+ /** GIF only — MP4 keeps the source cadence. */
11043
+ fps: number().int().min(1).max(15).default(5)
11044
+ }), object({
11045
+ base64: string(),
11046
+ mime: string(),
11047
+ bytes: number().int(),
11048
+ /** The profile actually rendered (what the default resolved to). */
11049
+ profile: CamProfileSchema,
11050
+ durationMs: number()
11051
+ }), {
11052
+ kind: "mutation",
11053
+ auth: "admin"
10940
11054
  }), method(_void(), array(CameraStreamSchema).readonly()), method(_void(), array(ProfileSlotSchema).readonly()), method(object({ brokerId: string() }), BrokerStatsSchema), method(object({ brokerId: string() }), object({
10941
11055
  probed: boolean(),
10942
11056
  summary: string()
@@ -19942,6 +20056,36 @@ var TargetKindSchema = object({
19942
20056
  icon: string(),
19943
20057
  /** Stamped by each provider so the concat-fanned catalog stays routable. */
19944
20058
  addonId: string(),
20059
+ /**
20060
+ * URL of the kind's bundled BRAND icon, served by the providing addon over
20061
+ * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
20062
+ * when the addon bundles no icon for that kind — the client then falls back
20063
+ * to a neutral glyph rather than rendering the raw `icon` NAME as text.
20064
+ *
20065
+ * Root-relative on purpose: it resolves against whatever origin serves a web
20066
+ * client, and a native client joins it onto its own hub base.
20067
+ *
20068
+ * DECLARED here deliberately. It used to travel as an undeclared passthrough
20069
+ * field that survived only because the runtime cap-router forwards provider
20070
+ * output verbatim — so every consumer had to re-declare it by hand to stop
20071
+ * its own Zod parse from stripping it, and the whole arrangement would have
20072
+ * broken silently the moment output validation was tightened anywhere.
20073
+ */
20074
+ iconUrl: string().optional(),
20075
+ /**
20076
+ * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
20077
+ *
20078
+ * The server knows this and therefore says it, because the client cannot
20079
+ * safely guess: a React-Native client renders SVG and raster through two
20080
+ * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
20081
+ * not decode SVG on iOS/Android), so without this it silently fell back to a
20082
+ * placeholder glyph for every vector icon while the web build looked fine.
20083
+ *
20084
+ * Absent when {@link iconUrl} is absent, or for a legacy provider that has
20085
+ * not been updated — a client that cannot determine the type should prefer
20086
+ * its raster path, which is the safe default for an unknown image.
20087
+ */
20088
+ iconMediaType: string().optional(),
19945
20089
  configSchema: ConfigSchemaPassthrough,
19946
20090
  supportsDiscovery: boolean(),
19947
20091
  caps: TargetKindCapsSchema
@@ -20673,6 +20817,15 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
20673
20817
  }), method(object({ deviceId: number() }), EventPruneCountsSchema, {
20674
20818
  kind: "mutation",
20675
20819
  auth: "admin"
20820
+ }), method(RelocateMediaInputSchema, object({ jobId: string() }), {
20821
+ kind: "mutation",
20822
+ auth: "admin"
20823
+ }), method(object({}), array(RelocateJobSchema).readonly(), {
20824
+ kind: "query",
20825
+ auth: "admin"
20826
+ }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
20827
+ kind: "mutation",
20828
+ auth: "admin"
20676
20829
  }), method(OpsLogQueryInputSchema, array(OpsLogEntrySchema).readonly(), {
20677
20830
  kind: "query",
20678
20831
  auth: "admin"
@@ -23254,6 +23407,17 @@ var GetConnectionEndpointsResultSchema = object({ endpoints: array(object({
23254
23407
  */
23255
23408
  priority: number()
23256
23409
  })).readonly() });
23410
+ /**
23411
+ * The chosen outbound endpoint for notification artifacts. `baseUrl: null` =
23412
+ * AUTO (resolved from the candidate ranking at send time); `resolved` reports
23413
+ * what AUTO currently picks, so the UI can show the effective value either way.
23414
+ */
23415
+ var NotificationEndpointSchema = object({
23416
+ /** The operator's explicit choice, or null for AUTO. */
23417
+ baseUrl: string().nullable(),
23418
+ /** What the ranking currently resolves to (null when nothing is reachable). */
23419
+ resolved: string().nullable()
23420
+ });
23257
23421
  var AllowedAddressesSchema = object({
23258
23422
  /**
23259
23423
  * Allowlist of interface addresses operators have explicitly opted
@@ -23276,7 +23440,7 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
23276
23440
  * to avoid mixed-content blocks in the browser. The public
23277
23441
  * tunnel always emits `https://` regardless. */
23278
23442
  scheme: _enum(["http", "https"]).optional()
23279
- }), GetConnectionEndpointsResultSchema), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" });
23443
+ }), 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" });
23280
23444
  /**
23281
23445
  * mesh-network — collection cap for mesh-VPN providers.
23282
23446
  *
@@ -24142,7 +24306,12 @@ var RecordingDeviceUsageSchema = object({
24142
24306
  var RecordingLocationUsageSchema = object({
24143
24307
  /** StorageLocation id; null for the legacy/degraded single-root fallback. */
24144
24308
  locationId: string().nullable(),
24145
- /** Bytes of recordings stored on this location. */
24309
+ /** Every location id sharing this row's PHYSICAL volume (aliases). One row
24310
+ * is emitted per physical disk (2026-07-29): two locations on one root
24311
+ * previously rendered as two identical "disks" with a nonsensical used
24312
+ * split — hydrate attribution across aliases is arbitrary by nature. */
24313
+ locationIds: array(string()).optional(),
24314
+ /** Bytes of recordings stored on this PHYSICAL volume (all aliases). */
24146
24315
  usedBytes: number(),
24147
24316
  /** Free bytes on the location's volume; null when capacity is unknown (remote). */
24148
24317
  availableBytes: number().nullable(),
@@ -24254,6 +24423,44 @@ method(object({
24254
24423
  }), method(OpsLogQueryInputSchema, array(OpsLogEntrySchema).readonly(), {
24255
24424
  kind: "query",
24256
24425
  auth: "admin"
24426
+ }), method(object({
24427
+ deviceId: number(),
24428
+ aroundMs: number(),
24429
+ preRollSec: number().min(0).max(30).default(2),
24430
+ postRollSec: number().min(0).max(30).default(5),
24431
+ maxWidth: number().int().min(120).max(1280).default(480),
24432
+ fps: number().int().min(1).max(15).default(5)
24433
+ }), object({
24434
+ gifBase64: string(),
24435
+ fromMs: number(),
24436
+ toMs: number()
24437
+ }), {
24438
+ kind: "mutation",
24439
+ auth: "admin"
24440
+ }), method(object({
24441
+ deviceId: number(),
24442
+ aroundMs: number(),
24443
+ preRollSec: number().min(0).max(30).default(3),
24444
+ postRollSec: number().min(0).max(30).default(7),
24445
+ maxWidth: number().int().min(160).max(1920).default(640)
24446
+ }), object({
24447
+ clipBase64: string(),
24448
+ mime: string(),
24449
+ fromMs: number(),
24450
+ toMs: number(),
24451
+ bytes: number().int()
24452
+ }), {
24453
+ kind: "mutation",
24454
+ auth: "admin"
24455
+ }), method(RelocateFootageInputSchema, object({ jobId: string() }), {
24456
+ kind: "mutation",
24457
+ auth: "admin"
24458
+ }), method(object({}), array(RelocateJobSchema).readonly(), {
24459
+ kind: "query",
24460
+ auth: "admin"
24461
+ }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
24462
+ kind: "mutation",
24463
+ auth: "admin"
24257
24464
  });
24258
24465
  /**
24259
24466
  * `recordingExport` cap — render a footage time range into a single downloadable
@@ -26950,6 +27157,12 @@ Object.freeze({
26950
27157
  addonId: null,
26951
27158
  access: "view"
26952
27159
  },
27160
+ "localNetwork.getNotificationEndpoint": {
27161
+ capName: "local-network",
27162
+ capScope: "system",
27163
+ addonId: null,
27164
+ access: "view"
27165
+ },
26953
27166
  "localNetwork.getPreferred": {
26954
27167
  capName: "local-network",
26955
27168
  capScope: "system",
@@ -26974,6 +27187,12 @@ Object.freeze({
26974
27187
  addonId: null,
26975
27188
  access: "create"
26976
27189
  },
27190
+ "localNetwork.setNotificationEndpoint": {
27191
+ capName: "local-network",
27192
+ capScope: "system",
27193
+ addonId: null,
27194
+ access: "create"
27195
+ },
26977
27196
  "lockControl.lock": {
26978
27197
  capName: "lock-control",
26979
27198
  capScope: "device",
@@ -27616,6 +27835,12 @@ Object.freeze({
27616
27835
  addonId: null,
27617
27836
  access: "create"
27618
27837
  },
27838
+ "pipelineAnalytics.cancelMediaRelocate": {
27839
+ capName: "pipeline-analytics",
27840
+ capScope: "device",
27841
+ addonId: null,
27842
+ access: "create"
27843
+ },
27619
27844
  "pipelineAnalytics.clearTracks": {
27620
27845
  capName: "pipeline-analytics",
27621
27846
  capScope: "device",
@@ -27670,6 +27895,12 @@ Object.freeze({
27670
27895
  addonId: null,
27671
27896
  access: "view"
27672
27897
  },
27898
+ "pipelineAnalytics.getMediaRelocateStatus": {
27899
+ capName: "pipeline-analytics",
27900
+ capScope: "device",
27901
+ addonId: null,
27902
+ access: "view"
27903
+ },
27673
27904
  "pipelineAnalytics.getMotionEvents": {
27674
27905
  capName: "pipeline-analytics",
27675
27906
  capScope: "device",
@@ -27742,6 +27973,12 @@ Object.freeze({
27742
27973
  addonId: null,
27743
27974
  access: "create"
27744
27975
  },
27976
+ "pipelineAnalytics.relocateMedia": {
27977
+ capName: "pipeline-analytics",
27978
+ capScope: "device",
27979
+ addonId: null,
27980
+ access: "create"
27981
+ },
27745
27982
  "pipelineAnalytics.searchObjectEvents": {
27746
27983
  capName: "pipeline-analytics",
27747
27984
  capScope: "device",
@@ -28504,6 +28741,12 @@ Object.freeze({
28504
28741
  addonId: null,
28505
28742
  access: "create"
28506
28743
  },
28744
+ "recording.cancelRelocate": {
28745
+ capName: "recording",
28746
+ capScope: "system",
28747
+ addonId: null,
28748
+ access: "create"
28749
+ },
28507
28750
  "recording.deleteFootprint": {
28508
28751
  capName: "recording",
28509
28752
  capScope: "system",
@@ -28534,6 +28777,12 @@ Object.freeze({
28534
28777
  addonId: null,
28535
28778
  access: "view"
28536
28779
  },
28780
+ "recording.getRelocateStatus": {
28781
+ capName: "recording",
28782
+ capScope: "system",
28783
+ addonId: null,
28784
+ access: "view"
28785
+ },
28537
28786
  "recording.getStorageUsage": {
28538
28787
  capName: "recording",
28539
28788
  capScope: "system",
@@ -28564,6 +28813,24 @@ Object.freeze({
28564
28813
  addonId: null,
28565
28814
  access: "view"
28566
28815
  },
28816
+ "recording.relocateFootage": {
28817
+ capName: "recording",
28818
+ capScope: "system",
28819
+ addonId: null,
28820
+ access: "create"
28821
+ },
28822
+ "recording.renderClip": {
28823
+ capName: "recording",
28824
+ capScope: "system",
28825
+ addonId: null,
28826
+ access: "create"
28827
+ },
28828
+ "recording.renderGif": {
28829
+ capName: "recording",
28830
+ capScope: "system",
28831
+ addonId: null,
28832
+ access: "create"
28833
+ },
28567
28834
  "recording.rescanStorage": {
28568
28835
  capName: "recording",
28569
28836
  capScope: "system",
@@ -29158,6 +29425,12 @@ Object.freeze({
29158
29425
  addonId: null,
29159
29426
  access: "create"
29160
29427
  },
29428
+ "streamBroker.renderPreBufferClip": {
29429
+ capName: "stream-broker",
29430
+ capScope: "system",
29431
+ addonId: null,
29432
+ access: "create"
29433
+ },
29161
29434
  "streamBroker.restartProfile": {
29162
29435
  capName: "stream-broker",
29163
29436
  capScope: "system",
package/dist/addon.mjs CHANGED
@@ -7408,35 +7408,22 @@ function findTimezone(id) {
7408
7408
  */
7409
7409
  var RecordingWeekdaySchema = number().int().min(0).max(6);
7410
7410
  var HHMM = /^([01]\d|2[0-3]):[0-5]\d$/;
7411
- var RecordingScheduleSchema = discriminatedUnion("kind", [object({ kind: literal("always") }), object({
7412
- kind: literal("timeOfDay"),
7413
- start: string().regex(HHMM),
7414
- end: string().regex(HHMM),
7415
- /** Restrict to these weekdays; omit = every day. */
7416
- days: array(RecordingWeekdaySchema).optional()
7417
- })]);
7418
- var RecordingModeSchema = _enum([
7419
- "continuous",
7420
- "onMotion",
7421
- "onAudioThreshold"
7422
- ]);
7423
7411
  /**
7424
- * First-class, authoritative per-camera storage mode — the explicit choice the
7425
- * UI reads directly (never inferred from `rules`):
7426
- * - `off` — not recording.
7427
- * - `events` — record only around triggers (motion / audio threshold),
7428
- * with pre/post-buffer.
7429
- * - `continuous` — record 24/7 within the schedule.
7412
+ * DERIVED per-camera storage summary — the single field cheap consumers read
7413
+ * (the viewer's status dot, the camera list) instead of walking `bands`:
7414
+ * - `off` — no band covers the camera (or it is disabled).
7415
+ * - `events` — every band records around triggers only.
7416
+ * - `continuous` — at least one band records continuously.
7430
7417
  *
7431
- * `mode` compiles one-way to the internal `rules[]` consumed by the policy
7432
- * engine (see `compileRules`); `rules[]` is never authored directly anymore.
7418
+ * NEVER authored: the recorder stamps it from the authoritative `bands` on
7419
+ * every save (`activeModeForConfig`). Writing it has no effect.
7433
7420
  */
7434
7421
  var RecordingStorageModeSchema = _enum([
7435
7422
  "off",
7436
7423
  "events",
7437
7424
  "continuous"
7438
7425
  ]);
7439
- /** Which detectors trigger an `events`-mode recording. */
7426
+ /** Which detectors trigger an `events`-mode band. */
7440
7427
  var RecordingTriggersSchema = object({
7441
7428
  motion: boolean().optional(),
7442
7429
  audioThresholdDbfs: number().optional()
@@ -7472,18 +7459,6 @@ var RecordingBandSchema = object({
7472
7459
  preBufferSec: number().min(0).optional(),
7473
7460
  postBufferSec: number().min(0).optional()
7474
7461
  });
7475
- var RecordingRuleSchema = object({
7476
- schedule: RecordingScheduleSchema,
7477
- mode: RecordingModeSchema,
7478
- /** Seconds of footage to retain BEFORE a trigger (applied at keep/discard). */
7479
- preBufferSec: number().min(0).default(0),
7480
- /** Keep recording until this many seconds after the last trigger. */
7481
- postBufferSec: number().min(0).default(0),
7482
- /** Each new trigger restarts the post-buffer window. */
7483
- resetTimeoutOnNewEvent: boolean().default(true),
7484
- /** onAudioThreshold only — dBFS level that counts as a trigger. */
7485
- thresholdDbfs: number().optional()
7486
- });
7487
7462
  /**
7488
7463
  * Per-device retention overrides. Every field is optional; an unset or `0`
7489
7464
  * value inherits the node-wide recorder default. Only footage-lifetime limits
@@ -7517,40 +7492,28 @@ var ScrubThumbnailPresetSchema = _enum([
7517
7492
  /**
7518
7493
  * The full per-camera recording intent — the wire shape of a RecordingTarget.
7519
7494
  *
7520
- * `mode` is the authoritative storage choice; `schedule`/`triggers`/`pre`/`post`
7521
- * are its mode-specific parameters. `rules` is a DEPRECATED authoring input kept
7522
- * only for transition + migration (`migrateRulesToMode`); the policy engine
7523
- * consumes the compiled output of `compileRules(config)`, never `rules` directly.
7495
+ * `bands` is the ONLY authored recording intent: what to record, when, and on
7496
+ * which trigger. `mode` is a derived summary the recorder stamps on save; every
7497
+ * other field is a storage knob (profiles, segment length, retention, scrub).
7498
+ *
7499
+ * STRICT on purpose: the legacy authoring surface (`schedule`/`schedules`/
7500
+ * `triggers`/`preBufferSec`/`postBufferSec`/`rules`) was retired 2026-07-30.
7501
+ * A stale caller must fail loudly — silently stripping its legacy intent would
7502
+ * persist a band-less config, i.e. silently stop recording the camera.
7524
7503
  */
7525
7504
  var RecordingConfigSchema = object({
7526
7505
  enabled: boolean(),
7527
- /** Authoritative storage mode. Absent on legacy targets derived once via
7528
- * `migrateRulesToMode`, then persisted. */
7506
+ /** DERIVED summary of `bands`, stamped by the recorder on every save.
7507
+ * Authoring it has no effect — see {@link RecordingStorageModeSchema}. */
7529
7508
  mode: RecordingStorageModeSchema.optional(),
7530
7509
  profiles: array(CamProfileSchema).optional(),
7531
7510
  segmentSeconds: number().int().positive().optional(),
7532
- /** Shared recording time-bands for `events` & `continuous` — record only when
7533
- * the wall-clock falls inside one of these windows. Omit or empty = always.
7534
- * `continuous` compiles to one rule per band; `events` to band × trigger. */
7535
- schedules: array(RecordingScheduleSchema).optional(),
7536
- /** Legacy single-band predecessor of `schedules`. Read-compat only — it is
7537
- * normalized into `schedules` on read and never written going forward. (Not
7538
- * tagged `@deprecated`: the normalization paths must read it cast-free.) */
7539
- schedule: RecordingScheduleSchema.optional(),
7540
- /** `events`-mode only — which detectors trigger a recording. */
7541
- triggers: RecordingTriggersSchema.optional(),
7542
- /** `events`-mode only — seconds retained before / after a trigger. */
7543
- preBufferSec: number().min(0).optional(),
7544
- postBufferSec: number().min(0).optional(),
7545
- /** DEPRECATED authoring input; retained for migration/transition. */
7546
- rules: array(RecordingRuleSchema).optional(),
7547
7511
  /**
7548
- * AUTHORITATIVE mode-per-band recording model (recorder). When present it
7549
- * is the single source of truth; the legacy `mode`/`schedules`/`schedule`/
7550
- * `triggers`/`rules` fields above are kept for READ-COMPAT only and are
7551
- * derived into bands once via `migrateConfigToBands`.
7512
+ * AUTHORITATIVE mode-per-band recording model the single source of truth
7513
+ * the recorder's band engine consumes. An empty array = record nothing;
7514
+ * "off" is the absence of a covering band, never a band value.
7552
7515
  */
7553
- bands: array(RecordingBandSchema).optional(),
7516
+ bands: array(RecordingBandSchema).default([]),
7554
7517
  retention: RecordingRetentionSchema.optional(),
7555
7518
  /**
7556
7519
  * Per-camera scrub-thumbnail fidelity preset (resolution + JPEG quality for
@@ -7558,8 +7521,15 @@ var RecordingConfigSchema = object({
7558
7521
  * windows only — existing sheets are immutable, and each window's index
7559
7522
  * carries its own tile dims so mixed-preset history renders correctly.
7560
7523
  */
7561
- scrubThumbnails: ScrubThumbnailPresetSchema.optional()
7562
- });
7524
+ scrubThumbnails: ScrubThumbnailPresetSchema.optional(),
7525
+ /**
7526
+ * OPT-IN thumbnail-strip generation for this camera: every keyframe of the
7527
+ * low recording saved as a JPEG (the fast-drag scrub depth), a derived
7528
+ * cache that eviction reclaims with the footage. Absent/false = no strips
7529
+ * are written and scrub reads exact keyframes at every velocity.
7530
+ */
7531
+ stripsEnabled: boolean().optional()
7532
+ }).strict();
7563
7533
  /**
7564
7534
  * Ops-log — the durable, append-only operations audit shared by the
7565
7535
  * recordings and events management surfaces.
@@ -7578,7 +7548,8 @@ var OpsLogOpSchema = _enum([
7578
7548
  "prune",
7579
7549
  "manual-delete",
7580
7550
  "rescan",
7581
- "retention-run"
7551
+ "retention-run",
7552
+ "relocate"
7582
7553
  ]);
7583
7554
  /** Why the operation ran. */
7584
7555
  var OpsLogReasonSchema = _enum([
@@ -7617,6 +7588,55 @@ var OpsLogQueryInputSchema = object({
7617
7588
  limit: number().int().min(1).max(1e3).optional()
7618
7589
  });
7619
7590
  /**
7591
+ * Entity-relocation job state (storage entity-routing spec, Phase 4).
7592
+ *
7593
+ * One shape shared by the recorder's `relocateFootage` (segments + strips) and
7594
+ * pipeline-analytics' `relocateMedia` (event media blobs) so the admin Data
7595
+ * page renders both movers with one component. Jobs are in-RAM (a restart
7596
+ * forgets them — re-running is safe by construction: copy-if-absent, delete
7597
+ * after verify) and each completed/failed run also lands one durable ops-log
7598
+ * row on the owning addon's surface.
7599
+ */
7600
+ var RelocateJobStateSchema = _enum([
7601
+ "running",
7602
+ "done",
7603
+ "failed",
7604
+ "cancelled"
7605
+ ]);
7606
+ var RelocateJobSchema = object({
7607
+ jobId: string(),
7608
+ state: RelocateJobStateSchema,
7609
+ /** Source location — for media relocation this is informational ('*': rows
7610
+ * move from wherever they are to the target). */
7611
+ fromLocationId: string(),
7612
+ toLocationId: string(),
7613
+ /** Scoped device, or null = every device. */
7614
+ deviceId: number().nullable(),
7615
+ /** What the job moves (owner-addon specific: segments/strips or media). */
7616
+ entities: array(string()),
7617
+ filesMoved: number().int(),
7618
+ bytesMoved: number().int(),
7619
+ /** Total files discovered up front; null while (or when) unknown. */
7620
+ filesTotal: number().int().nullable(),
7621
+ startedAt: number(),
7622
+ finishedAt: number().nullable(),
7623
+ error: string().nullable()
7624
+ });
7625
+ var RelocateFootageInputSchema = object({
7626
+ deviceId: number().optional(),
7627
+ fromLocationId: string(),
7628
+ toLocationId: string(),
7629
+ entities: array(_enum(["segments", "strips"])).optional(),
7630
+ /** Copy throttle in MB/s (default 40) — the drain is a background chore,
7631
+ * never allowed to starve live writers. */
7632
+ throttleMbps: number().min(1).max(1e3).optional()
7633
+ });
7634
+ var RelocateMediaInputSchema = object({
7635
+ deviceId: number().optional(),
7636
+ toLocationId: string(),
7637
+ throttleMbps: number().min(1).max(1e3).optional()
7638
+ });
7639
+ /**
7620
7640
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
7621
7641
  * storage a location serves. Defined here (not in `capabilities/storage.cap.ts`)
7622
7642
  * so the persisted record schema and the consumer-facing cap can both consume it
@@ -7666,6 +7686,13 @@ var StorageLocationSchema = object({
7666
7686
  nodeId: string().optional(),
7667
7687
  isDefault: boolean().default(false),
7668
7688
  isSystem: boolean().default(false),
7689
+ /** COMPUTED at read time by the orchestrator (statfs of the backing volume
7690
+ * for node-local locations it can reach) — never persisted, absent when the
7691
+ * volume is remote/unreachable. The single capacity truth every UI reads. */
7692
+ capacity: object({
7693
+ totalBytes: number(),
7694
+ availableBytes: number()
7695
+ }).nullable().optional(),
7669
7696
  createdAt: number(),
7670
7697
  updatedAt: number()
7671
7698
  });
@@ -8433,7 +8460,8 @@ var NcTaxonomyEntrySchema = object({
8433
8460
  /** Macro/category parent for grouping ('car' → 'vehicle'); null for a top. */
8434
8461
  parentKind: string().nullable()
8435
8462
  });
8436
- object({
8463
+ /** The complete NC picker taxonomy — three grouped buckets. */
8464
+ var NcTaxonomySchema = object({
8437
8465
  videoClasses: array(NcTaxonomyEntrySchema),
8438
8466
  audioKinds: array(NcTaxonomyEntrySchema),
8439
8467
  labels: array(NcTaxonomyEntrySchema)
@@ -9375,6 +9403,7 @@ function createRuntimeStateBridge(params) {
9375
9403
  getStatus
9376
9404
  };
9377
9405
  }
9406
+ new Set(["devices", "classes"]);
9378
9407
  /**
9379
9408
  * Shared geometry vocabulary for on-frame shape caps — privacy-mask,
9380
9409
  * motion-zones, and the detection zones/lines editor all speak this one
@@ -9691,12 +9720,60 @@ var NcRuleTargetSchema = object({
9691
9720
  * - `keyFrame` — the clean scene frame (no subject box).
9692
9721
  * - `none` — no attachment.
9693
9722
  */
9694
- var NcMediaPolicySchema = object({ attach: _enum([
9695
- "best",
9696
- "best-matching",
9697
- "keyFrame",
9698
- "none"
9699
- ]).default("best") });
9723
+ /**
9724
+ * WHAT THE PICTURE SHOWS — chosen explicitly, because the implicit ladders
9725
+ * conflate it with the selection strategy and betray the request: asking for
9726
+ * the clean scene frame on an object-event owner used to start at
9727
+ * `fullFrameBoxed`, i.e. the annotated frame (2026-07-30).
9728
+ * - `cropped` — the subject, tight. What "who is at the door" wants.
9729
+ * - `full` — the clean scene, no annotation. What "what is going on" wants.
9730
+ * - `boxed` — the scene WITH the detection boxes drawn, for verifying what
9731
+ * the pipeline actually saw.
9732
+ */
9733
+ var NcMediaFrameSchema = _enum([
9734
+ "cropped",
9735
+ "full",
9736
+ "boxed"
9737
+ ]);
9738
+ var NcMediaPolicySchema = object({
9739
+ attach: _enum([
9740
+ "best",
9741
+ "best-matching",
9742
+ "keyFrame",
9743
+ "none"
9744
+ ]).default("best"),
9745
+ /** What the still shows. Absent = `cropped` (the historical `best` shape). */
9746
+ frame: NcMediaFrameSchema.optional(),
9747
+ /** Crop the attached still to the rule's condition-zone bbox (padded) —
9748
+ * "show me the ZONE", not the whole scene or the subject crop. */
9749
+ zoneCrop: boolean().optional(),
9750
+ /**
9751
+ * Also attach a short GIF cut from the stream broker's clip ring around the
9752
+ * event — NOT from the recording, so the camera does not have to be
9753
+ * recording, and the window sits AROUND the moment instead of a segment
9754
+ * behind it. Fail-closed: a window the ring does not cover contributes no
9755
+ * gif, never a failed notification.
9756
+ */
9757
+ gif: boolean().optional(),
9758
+ /**
9759
+ * Also attach a short MP4 CLIP of the same cut. Same source and the same
9760
+ * fail-closed rule as `gif`. Prefer it where the backend takes video
9761
+ * (telegram, discord, zentik, webhook); backends that do not are handled by
9762
+ * the degrade engine, which drops the video and keeps the still.
9763
+ */
9764
+ clip: boolean().optional(),
9765
+ /** Seconds of footage BEFORE / AFTER the event instant. Defaults 3 / 7. */
9766
+ clipPreRollSec: number().int().min(0).max(30).optional(),
9767
+ clipPostRollSec: number().int().min(0).max(30).optional(),
9768
+ /**
9769
+ * Which stream profile the footage is cut from. Absent = the CHEAPEST
9770
+ * assigned profile: a notification is watched on a phone, so the 4K
9771
+ * rendition would burn CPU to produce a file the client downscales anyway.
9772
+ * A profile that is not assigned falls back to the cheapest, and the render
9773
+ * reports which one actually ran.
9774
+ */
9775
+ profile: CamProfileSchema.optional()
9776
+ });
9700
9777
  /** Throttle — cooldown survives restarts (rebuilt from the outbox on boot). */
9701
9778
  var NcThrottleSchema = object({
9702
9779
  cooldownSec: number().int().min(0).max(86400).default(60),
@@ -9710,7 +9787,19 @@ var NcRuleInputSchema = object({
9710
9787
  delivery: NcDeliverySchema,
9711
9788
  conditions: NcConditionsSchema.default({}),
9712
9789
  schedule: NcScheduleSchema.optional(),
9713
- targets: array(NcRuleTargetSchema).min(1),
9790
+ /** May be empty when `targetUsers` addresses at least one user — the
9791
+ * "at least one addressee" invariant is enforced by the provider, because
9792
+ * a cross-field refine here would break `NcRulePatchSchema.partial()`. */
9793
+ targets: array(NcRuleTargetSchema),
9794
+ /**
9795
+ * USERS this rule addresses in addition to `targets` (Phase 4). At fire
9796
+ * time each user fans out to the personal targets they own
9797
+ * (`config.ownerUserId`), filtered by that user's `allowedDevices` for the
9798
+ * firing camera — a user is never notified about a device they cannot open.
9799
+ * Per-user opt-outs (`disabledTargetIds`) still apply to the fanned-out
9800
+ * targets.
9801
+ */
9802
+ targetUsers: array(string()).optional(),
9714
9803
  media: NcMediaPolicySchema.default({ attach: "best" }),
9715
9804
  throttle: NcThrottleSchema.default({
9716
9805
  cooldownSec: 60,
@@ -9923,7 +10012,10 @@ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), m
9923
10012
  }), object({ results: array(NcTestResultSchema) }), {
9924
10013
  kind: "mutation",
9925
10014
  auth: "admin"
9926
- }), method(object({}), object({ catalog: array(NcConditionDescriptorSchema) })), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" });
10015
+ }), method(object({}), object({
10016
+ catalog: array(NcConditionDescriptorSchema),
10017
+ taxonomy: NcTaxonomySchema.optional()
10018
+ })), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" });
9927
10019
  /**
9928
10020
  * TimelapseRule — the STANDALONE scheduled timelapse producer's rule model.
9929
10021
  *
@@ -10932,6 +11024,28 @@ method(object({
10932
11024
  }), object({ success: literal(true) }), {
10933
11025
  kind: "mutation",
10934
11026
  auth: "admin"
11027
+ }), method(object({
11028
+ deviceId: number(),
11029
+ /** Absent = the LOWEST assigned profile — a notification attachment is
11030
+ * watched on a phone, and the cheap rendition is the right default. */
11031
+ profile: CamProfileSchema.optional(),
11032
+ aroundMs: number(),
11033
+ preRollSec: number().min(0).max(20).default(3),
11034
+ postRollSec: number().min(0).max(20).default(5),
11035
+ format: _enum(["gif", "mp4"]).default("gif"),
11036
+ maxWidth: number().int().min(120).max(1920).default(480),
11037
+ /** GIF only — MP4 keeps the source cadence. */
11038
+ fps: number().int().min(1).max(15).default(5)
11039
+ }), object({
11040
+ base64: string(),
11041
+ mime: string(),
11042
+ bytes: number().int(),
11043
+ /** The profile actually rendered (what the default resolved to). */
11044
+ profile: CamProfileSchema,
11045
+ durationMs: number()
11046
+ }), {
11047
+ kind: "mutation",
11048
+ auth: "admin"
10935
11049
  }), method(_void(), array(CameraStreamSchema).readonly()), method(_void(), array(ProfileSlotSchema).readonly()), method(object({ brokerId: string() }), BrokerStatsSchema), method(object({ brokerId: string() }), object({
10936
11050
  probed: boolean(),
10937
11051
  summary: string()
@@ -19937,6 +20051,36 @@ var TargetKindSchema = object({
19937
20051
  icon: string(),
19938
20052
  /** Stamped by each provider so the concat-fanned catalog stays routable. */
19939
20053
  addonId: string(),
20054
+ /**
20055
+ * URL of the kind's bundled BRAND icon, served by the providing addon over
20056
+ * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
20057
+ * when the addon bundles no icon for that kind — the client then falls back
20058
+ * to a neutral glyph rather than rendering the raw `icon` NAME as text.
20059
+ *
20060
+ * Root-relative on purpose: it resolves against whatever origin serves a web
20061
+ * client, and a native client joins it onto its own hub base.
20062
+ *
20063
+ * DECLARED here deliberately. It used to travel as an undeclared passthrough
20064
+ * field that survived only because the runtime cap-router forwards provider
20065
+ * output verbatim — so every consumer had to re-declare it by hand to stop
20066
+ * its own Zod parse from stripping it, and the whole arrangement would have
20067
+ * broken silently the moment output validation was tightened anywhere.
20068
+ */
20069
+ iconUrl: string().optional(),
20070
+ /**
20071
+ * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
20072
+ *
20073
+ * The server knows this and therefore says it, because the client cannot
20074
+ * safely guess: a React-Native client renders SVG and raster through two
20075
+ * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
20076
+ * not decode SVG on iOS/Android), so without this it silently fell back to a
20077
+ * placeholder glyph for every vector icon while the web build looked fine.
20078
+ *
20079
+ * Absent when {@link iconUrl} is absent, or for a legacy provider that has
20080
+ * not been updated — a client that cannot determine the type should prefer
20081
+ * its raster path, which is the safe default for an unknown image.
20082
+ */
20083
+ iconMediaType: string().optional(),
19940
20084
  configSchema: ConfigSchemaPassthrough,
19941
20085
  supportsDiscovery: boolean(),
19942
20086
  caps: TargetKindCapsSchema
@@ -20668,6 +20812,15 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
20668
20812
  }), method(object({ deviceId: number() }), EventPruneCountsSchema, {
20669
20813
  kind: "mutation",
20670
20814
  auth: "admin"
20815
+ }), method(RelocateMediaInputSchema, object({ jobId: string() }), {
20816
+ kind: "mutation",
20817
+ auth: "admin"
20818
+ }), method(object({}), array(RelocateJobSchema).readonly(), {
20819
+ kind: "query",
20820
+ auth: "admin"
20821
+ }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
20822
+ kind: "mutation",
20823
+ auth: "admin"
20671
20824
  }), method(OpsLogQueryInputSchema, array(OpsLogEntrySchema).readonly(), {
20672
20825
  kind: "query",
20673
20826
  auth: "admin"
@@ -23249,6 +23402,17 @@ var GetConnectionEndpointsResultSchema = object({ endpoints: array(object({
23249
23402
  */
23250
23403
  priority: number()
23251
23404
  })).readonly() });
23405
+ /**
23406
+ * The chosen outbound endpoint for notification artifacts. `baseUrl: null` =
23407
+ * AUTO (resolved from the candidate ranking at send time); `resolved` reports
23408
+ * what AUTO currently picks, so the UI can show the effective value either way.
23409
+ */
23410
+ var NotificationEndpointSchema = object({
23411
+ /** The operator's explicit choice, or null for AUTO. */
23412
+ baseUrl: string().nullable(),
23413
+ /** What the ranking currently resolves to (null when nothing is reachable). */
23414
+ resolved: string().nullable()
23415
+ });
23252
23416
  var AllowedAddressesSchema = object({
23253
23417
  /**
23254
23418
  * Allowlist of interface addresses operators have explicitly opted
@@ -23271,7 +23435,7 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
23271
23435
  * to avoid mixed-content blocks in the browser. The public
23272
23436
  * tunnel always emits `https://` regardless. */
23273
23437
  scheme: _enum(["http", "https"]).optional()
23274
- }), GetConnectionEndpointsResultSchema), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" });
23438
+ }), 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" });
23275
23439
  /**
23276
23440
  * mesh-network — collection cap for mesh-VPN providers.
23277
23441
  *
@@ -24137,7 +24301,12 @@ var RecordingDeviceUsageSchema = object({
24137
24301
  var RecordingLocationUsageSchema = object({
24138
24302
  /** StorageLocation id; null for the legacy/degraded single-root fallback. */
24139
24303
  locationId: string().nullable(),
24140
- /** Bytes of recordings stored on this location. */
24304
+ /** Every location id sharing this row's PHYSICAL volume (aliases). One row
24305
+ * is emitted per physical disk (2026-07-29): two locations on one root
24306
+ * previously rendered as two identical "disks" with a nonsensical used
24307
+ * split — hydrate attribution across aliases is arbitrary by nature. */
24308
+ locationIds: array(string()).optional(),
24309
+ /** Bytes of recordings stored on this PHYSICAL volume (all aliases). */
24141
24310
  usedBytes: number(),
24142
24311
  /** Free bytes on the location's volume; null when capacity is unknown (remote). */
24143
24312
  availableBytes: number().nullable(),
@@ -24249,6 +24418,44 @@ method(object({
24249
24418
  }), method(OpsLogQueryInputSchema, array(OpsLogEntrySchema).readonly(), {
24250
24419
  kind: "query",
24251
24420
  auth: "admin"
24421
+ }), method(object({
24422
+ deviceId: number(),
24423
+ aroundMs: number(),
24424
+ preRollSec: number().min(0).max(30).default(2),
24425
+ postRollSec: number().min(0).max(30).default(5),
24426
+ maxWidth: number().int().min(120).max(1280).default(480),
24427
+ fps: number().int().min(1).max(15).default(5)
24428
+ }), object({
24429
+ gifBase64: string(),
24430
+ fromMs: number(),
24431
+ toMs: number()
24432
+ }), {
24433
+ kind: "mutation",
24434
+ auth: "admin"
24435
+ }), method(object({
24436
+ deviceId: number(),
24437
+ aroundMs: number(),
24438
+ preRollSec: number().min(0).max(30).default(3),
24439
+ postRollSec: number().min(0).max(30).default(7),
24440
+ maxWidth: number().int().min(160).max(1920).default(640)
24441
+ }), object({
24442
+ clipBase64: string(),
24443
+ mime: string(),
24444
+ fromMs: number(),
24445
+ toMs: number(),
24446
+ bytes: number().int()
24447
+ }), {
24448
+ kind: "mutation",
24449
+ auth: "admin"
24450
+ }), method(RelocateFootageInputSchema, object({ jobId: string() }), {
24451
+ kind: "mutation",
24452
+ auth: "admin"
24453
+ }), method(object({}), array(RelocateJobSchema).readonly(), {
24454
+ kind: "query",
24455
+ auth: "admin"
24456
+ }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
24457
+ kind: "mutation",
24458
+ auth: "admin"
24252
24459
  });
24253
24460
  /**
24254
24461
  * `recordingExport` cap — render a footage time range into a single downloadable
@@ -26945,6 +27152,12 @@ Object.freeze({
26945
27152
  addonId: null,
26946
27153
  access: "view"
26947
27154
  },
27155
+ "localNetwork.getNotificationEndpoint": {
27156
+ capName: "local-network",
27157
+ capScope: "system",
27158
+ addonId: null,
27159
+ access: "view"
27160
+ },
26948
27161
  "localNetwork.getPreferred": {
26949
27162
  capName: "local-network",
26950
27163
  capScope: "system",
@@ -26969,6 +27182,12 @@ Object.freeze({
26969
27182
  addonId: null,
26970
27183
  access: "create"
26971
27184
  },
27185
+ "localNetwork.setNotificationEndpoint": {
27186
+ capName: "local-network",
27187
+ capScope: "system",
27188
+ addonId: null,
27189
+ access: "create"
27190
+ },
26972
27191
  "lockControl.lock": {
26973
27192
  capName: "lock-control",
26974
27193
  capScope: "device",
@@ -27611,6 +27830,12 @@ Object.freeze({
27611
27830
  addonId: null,
27612
27831
  access: "create"
27613
27832
  },
27833
+ "pipelineAnalytics.cancelMediaRelocate": {
27834
+ capName: "pipeline-analytics",
27835
+ capScope: "device",
27836
+ addonId: null,
27837
+ access: "create"
27838
+ },
27614
27839
  "pipelineAnalytics.clearTracks": {
27615
27840
  capName: "pipeline-analytics",
27616
27841
  capScope: "device",
@@ -27665,6 +27890,12 @@ Object.freeze({
27665
27890
  addonId: null,
27666
27891
  access: "view"
27667
27892
  },
27893
+ "pipelineAnalytics.getMediaRelocateStatus": {
27894
+ capName: "pipeline-analytics",
27895
+ capScope: "device",
27896
+ addonId: null,
27897
+ access: "view"
27898
+ },
27668
27899
  "pipelineAnalytics.getMotionEvents": {
27669
27900
  capName: "pipeline-analytics",
27670
27901
  capScope: "device",
@@ -27737,6 +27968,12 @@ Object.freeze({
27737
27968
  addonId: null,
27738
27969
  access: "create"
27739
27970
  },
27971
+ "pipelineAnalytics.relocateMedia": {
27972
+ capName: "pipeline-analytics",
27973
+ capScope: "device",
27974
+ addonId: null,
27975
+ access: "create"
27976
+ },
27740
27977
  "pipelineAnalytics.searchObjectEvents": {
27741
27978
  capName: "pipeline-analytics",
27742
27979
  capScope: "device",
@@ -28499,6 +28736,12 @@ Object.freeze({
28499
28736
  addonId: null,
28500
28737
  access: "create"
28501
28738
  },
28739
+ "recording.cancelRelocate": {
28740
+ capName: "recording",
28741
+ capScope: "system",
28742
+ addonId: null,
28743
+ access: "create"
28744
+ },
28502
28745
  "recording.deleteFootprint": {
28503
28746
  capName: "recording",
28504
28747
  capScope: "system",
@@ -28529,6 +28772,12 @@ Object.freeze({
28529
28772
  addonId: null,
28530
28773
  access: "view"
28531
28774
  },
28775
+ "recording.getRelocateStatus": {
28776
+ capName: "recording",
28777
+ capScope: "system",
28778
+ addonId: null,
28779
+ access: "view"
28780
+ },
28532
28781
  "recording.getStorageUsage": {
28533
28782
  capName: "recording",
28534
28783
  capScope: "system",
@@ -28559,6 +28808,24 @@ Object.freeze({
28559
28808
  addonId: null,
28560
28809
  access: "view"
28561
28810
  },
28811
+ "recording.relocateFootage": {
28812
+ capName: "recording",
28813
+ capScope: "system",
28814
+ addonId: null,
28815
+ access: "create"
28816
+ },
28817
+ "recording.renderClip": {
28818
+ capName: "recording",
28819
+ capScope: "system",
28820
+ addonId: null,
28821
+ access: "create"
28822
+ },
28823
+ "recording.renderGif": {
28824
+ capName: "recording",
28825
+ capScope: "system",
28826
+ addonId: null,
28827
+ access: "create"
28828
+ },
28562
28829
  "recording.rescanStorage": {
28563
28830
  capName: "recording",
28564
28831
  capScope: "system",
@@ -29153,6 +29420,12 @@ Object.freeze({
29153
29420
  addonId: null,
29154
29421
  access: "create"
29155
29422
  },
29423
+ "streamBroker.renderPreBufferClip": {
29424
+ capName: "stream-broker",
29425
+ capScope: "system",
29426
+ addonId: null,
29427
+ access: "create"
29428
+ },
29156
29429
  "streamBroker.restartProfile": {
29157
29430
  capName: "stream-broker",
29158
29431
  capScope: "system",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-reolink",
3
- "version": "1.2.13",
3
+ "version": "1.2.14",
4
4
  "description": "Reolink camera device provider addon for CamStack — native Baichuan protocol",
5
5
  "keywords": [
6
6
  "camstack",
@@ -63,7 +63,8 @@
63
63
  }
64
64
  ],
65
65
  "runtimeDependencies": [
66
- "@apocaliss92/nodelink-js"
66
+ "@apocaliss92/nodelink-js",
67
+ "werift"
67
68
  ]
68
69
  },
69
70
  "files": [