@camstack/addon-provider-dreo 0.2.20 → 0.2.21

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 +179 -44
  2. package/dist/addon.mjs +179 -44
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -35,7 +35,7 @@ var __toCommonJS = (mod) => __hasOwnProp.call(mod, "module.exports") ? mod["modu
35
35
  //#endregion
36
36
  let crypto$1 = require("crypto");
37
37
  let events = require("events");
38
- //#region ../types/dist/event-category-Bxo5yJjt.mjs
38
+ //#region ../types/dist/event-category-XfKNtfCc.mjs
39
39
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
40
40
  EventCategory["SystemBoot"] = "system.boot";
41
41
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -52,9 +52,10 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
52
52
  EventCategory["SystemRestartCompleted"] = "system.restart-completed";
53
53
  /**
54
54
  * A newer addon or server-root package version was found by the
55
- * authoritative registry check. Emitted once per
56
- * `(target, packageName, currentVersion, latestVersion)` transition; repeated
57
- * polling of the same result is deduplicated by the checker.
55
+ * authoritative registry check. Emitted once when any observed
56
+ * `latestVersion` changes (or a package/node first appears behind);
57
+ * the payload carries the full currently-available list. Repeated
58
+ * polling of the same latests is silent.
58
59
  */
59
60
  EventCategory["UpdateAvailable"] = "update.available";
60
61
  /**
@@ -73,6 +74,22 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
73
74
  EventCategory["AddonInstalled"] = "addon.installed";
74
75
  EventCategory["AddonUninstalled"] = "addon.uninstalled";
75
76
  EventCategory["AddonCrashed"] = "addon.crashed";
77
+ /**
78
+ * A RUNNER the D6 crash circuit-breaker gave up on — terminal.
79
+ *
80
+ * `AddonCrashed` is the routine, self-healing fact ("it crashed; it is being
81
+ * respawned"); this is the one that never resolves itself. Emitted exactly
82
+ * once per trip, by `process-service.ts`, carrying the node, the runner, the
83
+ * addons it hosted and the crash count that tripped the breaker.
84
+ *
85
+ * It exists because a runner marked terminally `failed` used to be SILENT:
86
+ * on 2026-08-18 the `recorder` runner died of three unhandled ffmpeg spawn
87
+ * errors, the breaker stopped respawning it (correctly), `/health` kept
88
+ * returning 200, and fleet-wide recording was gone for 3h15 before the
89
+ * operator's phone told him. The Notification Center's system-event intake
90
+ * consumes this category and maps it to the `addon-crash-loop` kind.
91
+ */
92
+ EventCategory["AddonRunnerFailed"] = "addon.runner-failed";
76
93
  EventCategory["AddonError"] = "addon.error";
77
94
  EventCategory["AddonPageReady"] = "addon.page-ready";
78
95
  EventCategory["AddonWidgetReady"] = "addon.widget-ready";
@@ -7584,6 +7601,10 @@ var RecordingBandSchema = object({
7584
7601
  preBufferSec: number().min(0).optional(),
7585
7602
  postBufferSec: number().min(0).optional()
7586
7603
  });
7604
+ ({
7605
+ preBufferSec: 10,
7606
+ postBufferSec: 30
7607
+ }).postBufferSec * 1e3;
7587
7608
  /**
7588
7609
  * Per-device retention overrides. Every field is optional; an unset or `0`
7589
7610
  * value inherits the node-wide recorder default. Only footage-lifetime limits
@@ -7631,6 +7652,12 @@ var RecordingConfigSchema = object({
7631
7652
  /** DERIVED summary of `bands`, stamped by the recorder on every save.
7632
7653
  * Authoring it has no effect — see {@link RecordingStorageModeSchema}. */
7633
7654
  mode: RecordingStorageModeSchema.optional(),
7655
+ /**
7656
+ * Which assigned broker slots to record. Absent / empty = {@link
7657
+ * DEFAULT_RECORDING_PROFILES} (`high`+`low`) intersected with the
7658
+ * camera's currently assigned slots — never `mid` unless the operator
7659
+ * picks it, and never a slot the broker has not assigned.
7660
+ */
7634
7661
  profiles: array(CamProfileSchema).optional(),
7635
7662
  segmentSeconds: number().int().positive().optional(),
7636
7663
  /**
@@ -7711,7 +7738,11 @@ var RelocateFootageInputSchema = object({
7711
7738
  profiles: array(string()).optional(),
7712
7739
  /** Copy throttle in MB/s (default 40) — the drain is a background chore,
7713
7740
  * never allowed to starve live writers. */
7714
- throttleMbps: number().min(1).max(1e3).optional()
7741
+ throttleMbps: number().min(1).max(1e3).optional(),
7742
+ /** Move only segments whose startMs is >= this. Absent = the whole source
7743
+ * pile. Used when a full drain is too expensive and the operator only
7744
+ * wants the recent window on the new disk. */
7745
+ sinceMs: number().int().optional()
7715
7746
  });
7716
7747
  /** Internal, lease-scoped participant operation. It is intentionally separate
7717
7748
  * from persistent recording settings: a migration never changes
@@ -14593,6 +14624,7 @@ var NcSystemEventKindSchema = _enum([
14593
14624
  "node-offline",
14594
14625
  "node-inference-unavailable",
14595
14626
  "detection-blind",
14627
+ "addon-crash-loop",
14596
14628
  "addon-update-available",
14597
14629
  "server-update-available",
14598
14630
  "alarm-triggered",
@@ -14600,6 +14632,9 @@ var NcSystemEventKindSchema = _enum([
14600
14632
  "alarm-disarmed",
14601
14633
  "alarm-arming",
14602
14634
  "alarm-arm-refused",
14635
+ "addon-updated",
14636
+ "server-updated",
14637
+ "export-completed",
14603
14638
  "camera-online",
14604
14639
  "camera-offline",
14605
14640
  "camera-disabled",
@@ -16493,13 +16528,19 @@ var RetrainStatusSchema = _enum([
16493
16528
  * "never marked" from "already trained" must read `retrainStatus`.
16494
16529
  *
16495
16530
  * `debug` does NOT pin; it is attention, not durability.
16531
+ *
16532
+ * `favourited` IS a BOOLEAN pin (durability bit), not a retrain lifecycle.
16533
+ * A favourited track is skipped by retention the same way `staging` is, but
16534
+ * it does not enter `none|staging|trained` and has no staging budget.
16496
16535
  */
16497
16536
  var TrackFlagFields = {
16498
16537
  /** Operator marked this track as training material — i.e. `retrainStatus` is
16499
16538
  * `'staging'`. */
16500
16539
  markForTrain: boolean().optional(),
16501
16540
  /** Operator marked this track for diagnostic attention. */
16502
- debug: boolean().optional()
16541
+ debug: boolean().optional(),
16542
+ /** Operator favourited this track. Pins it against pruning. */
16543
+ favourited: boolean().optional()
16503
16544
  };
16504
16545
  /**
16505
16546
  * The lifecycle field itself, on the READ surfaces only (`Track`, `KeyEvent`).
@@ -16524,6 +16565,7 @@ var TrackFlagsSchema = object({
16524
16565
  trackId: string(),
16525
16566
  markForTrain: boolean(),
16526
16567
  debug: boolean(),
16568
+ favourited: boolean(),
16527
16569
  /** The lifecycle state the boolean was derived from. Required here (unlike on
16528
16570
  * a track row) because this shape is only ever produced by the write body,
16529
16571
  * which always knows it — and a surface that has just written needs to render
@@ -17156,6 +17198,12 @@ var TrackCascadeCountsSchema = object({
17156
17198
  /** Per-track CLIP search vectors removed (best-effort). */
17157
17199
  embeddings: number().int()
17158
17200
  });
17201
+ /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17202
+ var DiskReconcileCountsSchema = object({
17203
+ mediaDropped: number().int(),
17204
+ tracks: number().int(),
17205
+ events: number().int()
17206
+ });
17159
17207
  /** Event-store footprint for one camera. */
17160
17208
  var EventStoreDeviceFootprintSchema = object({
17161
17209
  deviceId: number(),
@@ -17332,6 +17380,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17332
17380
  }), method(object({ deviceId: number() }), TrackCascadeCountsSchema, {
17333
17381
  kind: "mutation",
17334
17382
  auth: "admin"
17383
+ }), method(object({ deviceId: number() }), DiskReconcileCountsSchema, {
17384
+ kind: "mutation",
17385
+ auth: "admin"
17335
17386
  }), method(object({
17336
17387
  deviceId: number(),
17337
17388
  trackIds: array(string()).min(1)
@@ -18882,6 +18933,24 @@ var CameraStatusDegradationSchema = object({
18882
18933
  *
18883
18934
  * See spec: `docs/superpowers/specs/2026-06-24-camera-status-aggregator-cap.md`
18884
18935
  */
18936
+ var DiskReconcileJobSchema = object({
18937
+ state: _enum([
18938
+ "idle",
18939
+ "running",
18940
+ "done",
18941
+ "error"
18942
+ ]),
18943
+ total: number().int().nonnegative(),
18944
+ completed: number().int().nonnegative(),
18945
+ currentDeviceId: number().int().nullable(),
18946
+ failed: array(number().int()).readonly(),
18947
+ mediaDropped: number().int().nonnegative(),
18948
+ tracks: number().int().nonnegative(),
18949
+ events: number().int().nonnegative(),
18950
+ startedAtMs: number().int().nullable(),
18951
+ finishedAtMs: number().int().nullable(),
18952
+ error: string().nullable()
18953
+ });
18885
18954
  var CameraStatusSchema = object({
18886
18955
  deviceId: number(),
18887
18956
  assignment: CameraAssignmentStatusSchema,
@@ -19113,7 +19182,10 @@ method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
19113
19182
  }), CameraSwitchGroupSchema, {
19114
19183
  kind: "mutation",
19115
19184
  auth: "admin"
19116
- }), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
19185
+ }), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), DiskReconcileJobSchema, {
19186
+ kind: "mutation",
19187
+ auth: "admin"
19188
+ }), method(_void(), DiskReconcileJobSchema, { auth: "admin" }), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
19117
19189
  name: string(),
19118
19190
  description: string().optional(),
19119
19191
  config: CameraPipelineConfigSchema
@@ -20186,20 +20258,6 @@ var VectorStatsResultSchema = object({
20186
20258
  exact: boolean()
20187
20259
  });
20188
20260
  method(VectorDeclareIndexInputSchema, _void(), { kind: "mutation" }), method(VectorUpsertInputSchema, VectorUpsertResultSchema, { kind: "mutation" }), method(VectorQueryInputSchema, VectorQueryResultSchema), method(VectorGetInputSchema, VectorGetResultSchema), method(VectorDeleteInputSchema, VectorDeleteResultSchema, { kind: "mutation" }), method(VectorDeleteByFilterInputSchema, VectorDeleteResultSchema, { kind: "mutation" }), method(VectorStatsInputSchema, VectorStatsResultSchema);
20189
- /**
20190
- * `videoclips` — the unified, navigable-clip surface for a camera.
20191
- *
20192
- * A device-scoped WRAPPER cap (like `pipeline-analytics`): exactly one active
20193
- * provider per device, substitutable. The DEFAULT provider (registered by
20194
- * `addon-post-analysis`, `defaultActive: true`) composes the analytics event
20195
- * log (markers + thumbnails) with the recorder's `getPlaybackManifest` — a clip
20196
- * is a time-WINDOW over existing footage, never a separate file. A camera that
20197
- * exposes NATIVE onboard clips (Reolink/Hikvision NVR) can later substitute the
20198
- * wrapper provider for its device and serve its own clip catalog + URLs.
20199
- *
20200
- * A `Clip` is purely time-based (subtree-blind): playback resolves segments by
20201
- * temporal overlap, so the API never decides `continuous` vs `events`.
20202
- */
20203
20261
  var ClipSchema = object({
20204
20262
  /** Opaque, provider-namespaced id. The default provider encodes the time
20205
20263
  * window so `getClipPlayback` is self-contained (no event re-query). */
@@ -20216,8 +20274,57 @@ var ClipSchema = object({
20216
20274
  startMs: number(),
20217
20275
  endMs: number()
20218
20276
  }),
20219
- /** Thumbnail URL (lazy; e.g. analytics `getEventMedia`). Never inlined. */
20220
- thumbnail: string().optional()
20277
+ /**
20278
+ * Lazy thumbnail URL, never inlined.
20279
+ *
20280
+ * Recording-derived clips (events-mode keep-window, and the prepared
20281
+ * continuous event+fragment visit) MUST use the snapshot of the **main
20282
+ * event of the interval** — `getEventMedia({ eventId, kind: 'snapshot' })`
20283
+ * of the event that owns `kind` (object > motion > audio). Do not extract
20284
+ * a keyframe from the recorded segments. Other providers (onboard, HKSV)
20285
+ * mint their own stills.
20286
+ *
20287
+ * **VOUCHED, never fabricated.** A provider emits this only for an event it
20288
+ * has CONFIRMED owns at least one media row (its own, or its owning track's).
20289
+ * Absent is meaningful — "this visit has no event still" — never "we did not
20290
+ * look". Stamping it from a URL template made 35% of one camera's clips point
20291
+ * at a 404 (device 3836, 2026-08-17: 64 of 179 clips dead, 63 of them motion).
20292
+ * A read that FAILS drops the claim; it never invents it.
20293
+ */
20294
+ thumbnail: string().optional(),
20295
+ /**
20296
+ * An instant INSIDE this visit's footage, hole-safe, where a recorded still
20297
+ * can be decoded. Present whenever the visit came from recorded availability;
20298
+ * absent on a per-event padded window (there is no footage to promise).
20299
+ *
20300
+ * This is not a thumbnail and not a second byte path: it is the argument to
20301
+ * the recorder's existing still route. The surface — never the provider —
20302
+ * decides whether to use it. It is the midpoint of the visit's LONGEST
20303
+ * contiguous range, not of the visit: a visit spans its holes by
20304
+ * construction, so a naive midpoint lands in dead air.
20305
+ */
20306
+ stillAtMs: number().optional(),
20307
+ /**
20308
+ * Analytics event ids that overlap this visit — a BOUNDED sample, newest
20309
+ * first within kind (object → motion → audio), capped at
20310
+ * {@link MAX_CLIP_EVENT_IDS}. Empty on footage-only clips.
20311
+ *
20312
+ * Bounded because it is not a payload the surface pages through: one visit on
20313
+ * device 615 carried 2 345 ids, and `eventIds` was 99% of a 153 KB
20314
+ * camera-day. Read {@link eventCount} for the true total.
20315
+ */
20316
+ eventIds: array(string()).optional(),
20317
+ /** How many analytics events actually overlap this visit. Differs from
20318
+ * `eventIds.length` exactly when the sample was capped — so a truncated
20319
+ * list is never mistaken for a quiet visit. */
20320
+ eventCount: number().int().nonnegative().optional(),
20321
+ /** Intra-visit footage holes (GOP rolls, discarded segments) still shorter
20322
+ * than `VISIT_MERGE_GAP_MS`. Playback concatenates around them; the timeline
20323
+ * bar keeps showing them via `recording.getAvailability`. */
20324
+ holes: array(object({
20325
+ startMs: number(),
20326
+ endMs: number()
20327
+ })).optional()
20221
20328
  });
20222
20329
  var ClipPlaybackSchema = object({
20223
20330
  /** HLS master URL through the hub data-plane (Range + token in path). */
@@ -20681,7 +20788,14 @@ var SearchResultSchema = object({
20681
20788
  });
20682
20789
  var AutoUpdateSettingsSchema = object({
20683
20790
  channel: ChannelSchema,
20684
- intervalSeconds: number()
20791
+ intervalSeconds: number(),
20792
+ /**
20793
+ * Cadence of the "an update exists" POLLER, in seconds. Independent of
20794
+ * `channel`: the poller runs while auto-apply is `off`, because being told
20795
+ * about a publish and installing it are different decisions. Clamped
20796
+ * server-side to 900 s … 604800 s; defaults to 21600 s (6 h).
20797
+ */
20798
+ updateCheckIntervalSeconds: number()
20685
20799
  });
20686
20800
  var AddonAutoUpdateSchema = ChannelWithInheritSchema;
20687
20801
  var RestartAddonResultSchema = unknown();
@@ -20822,7 +20936,9 @@ method(_void(), array(AddonListItemSchema).readonly()), method(object({
20822
20936
  auth: "admin"
20823
20937
  }), method(_void(), AutoUpdateSettingsSchema, { auth: "admin" }), method(object({
20824
20938
  channel: ChannelSchema,
20825
- intervalSeconds: number().min(300).max(86400).optional()
20939
+ intervalSeconds: number().min(300).max(86400).optional(),
20940
+ /** Availability-poll cadence; see AutoUpdateSettingsSchema. */
20941
+ updateCheckIntervalSeconds: number().min(900).max(604800).optional()
20826
20942
  }), unknown(), {
20827
20943
  kind: "mutation",
20828
20944
  auth: "admin"
@@ -26511,7 +26627,9 @@ var ExportOptionsSchema = object({
26511
26627
  includeAudio: boolean(),
26512
26628
  maxLifeMs: number().int().positive(),
26513
26629
  deleteAfterDownload: boolean(),
26514
- title: string().max(200).optional()
26630
+ title: string().max(200).optional(),
26631
+ /** Notification-output target ids to ping when this export becomes ready. */
26632
+ notifyTargetIds: array(string().min(1)).max(20).optional()
26515
26633
  }).superRefine((v, ctx) => {
26516
26634
  if (v.speed !== void 0 && v.timelapse !== void 0) ctx.addIssue({
26517
26635
  code: ZodIssueCode.custom,
@@ -26570,10 +26688,18 @@ var ExportBytesSchema = object({
26570
26688
  });
26571
26689
  method(object({
26572
26690
  deviceId: number(),
26573
- profile: string(),
26691
+ /** @deprecated Prefer `profiles`. Kept so timelapse/notifiers keep working. */
26692
+ profile: string().optional(),
26693
+ profiles: array(string()).min(1).optional(),
26574
26694
  fromMs: number(),
26575
26695
  toMs: number(),
26576
26696
  options: ExportOptionsSchema
26697
+ }).superRefine((v, ctx) => {
26698
+ if ((v.profiles !== void 0 && v.profiles.length > 0 ? v.profiles : v.profile !== void 0 ? [v.profile] : []).length < 1) ctx.addIssue({
26699
+ code: ZodIssueCode.custom,
26700
+ message: "pass profiles[] (min 1) or legacy profile",
26701
+ path: ["profiles"]
26702
+ });
26577
26703
  }), ExportRecordSchema, {
26578
26704
  kind: "mutation",
26579
26705
  auth: "protected"
@@ -29515,15 +29641,6 @@ var BaseDeviceProvider = class extends BaseAddon {
29515
29641
  labels: ["probe not implemented"]
29516
29642
  };
29517
29643
  }
29518
- /**
29519
- * Top-level devices restored at once in {@link onRestoreDevices}.
29520
- *
29521
- * Four covers the fleets this ships to without turning a boot into a burst a
29522
- * camera NVR answers with a refusal. A provider whose upstream is a single
29523
- * session with a serial command channel (a Baichuan hub, an NVR that
29524
- * serialises ISAPI) should lower it; nothing needs to raise it.
29525
- */
29526
- restoreConcurrency = 4;
29527
29644
  async restoreDevices(savedDevices) {
29528
29645
  await this.onRestoreDevices(savedDevices);
29529
29646
  if (savedDevices.length > 0) this.ctx.logger.info(`Restored ${savedDevices.length} ${this.providerName} device(s)`);
@@ -29578,14 +29695,7 @@ var BaseDeviceProvider = class extends BaseAddon {
29578
29695
  });
29579
29696
  }
29580
29697
  };
29581
- let nextTopLevel = 0;
29582
- await Promise.all(Array.from({ length: Math.min(Math.max(1, this.restoreConcurrency), topLevel.length) }, async () => {
29583
- for (;;) {
29584
- const saved = topLevel[nextTopLevel++];
29585
- if (saved === void 0) return;
29586
- await restoreOne(saved);
29587
- }
29588
- }));
29698
+ await Promise.all(topLevel.map((saved) => restoreOne(saved)));
29589
29699
  const childRows = savedDevices.filter((s) => s.parentDeviceId !== null);
29590
29700
  for (const saved of childRows) {
29591
29701
  const Class = this.deviceClasses[saved.type];
@@ -32858,6 +32968,12 @@ Object.freeze({
32858
32968
  addonId: null,
32859
32969
  access: "create"
32860
32970
  },
32971
+ "pipelineAnalytics.reconcileFromDisk": {
32972
+ capName: "pipeline-analytics",
32973
+ capScope: "device",
32974
+ addonId: null,
32975
+ access: "create"
32976
+ },
32861
32977
  "pipelineAnalytics.refreshStorageLocationsForMigration": {
32862
32978
  capName: "pipeline-analytics",
32863
32979
  capScope: "device",
@@ -33260,6 +33376,12 @@ Object.freeze({
33260
33376
  addonId: null,
33261
33377
  access: "view"
33262
33378
  },
33379
+ "pipelineOrchestrator.getReconcileFromDiskStatus": {
33380
+ capName: "pipeline-orchestrator",
33381
+ capScope: "system",
33382
+ addonId: null,
33383
+ access: "view"
33384
+ },
33263
33385
  "pipelineOrchestrator.listAgentSettings": {
33264
33386
  capName: "pipeline-orchestrator",
33265
33387
  capScope: "system",
@@ -33284,6 +33406,12 @@ Object.freeze({
33284
33406
  addonId: null,
33285
33407
  access: "create"
33286
33408
  },
33409
+ "pipelineOrchestrator.reconcileFromDisk": {
33410
+ capName: "pipeline-orchestrator",
33411
+ capScope: "system",
33412
+ addonId: null,
33413
+ access: "create"
33414
+ },
33287
33415
  "pipelineOrchestrator.removeAgentSettings": {
33288
33416
  capName: "pipeline-orchestrator",
33289
33417
  capScope: "system",
@@ -36275,6 +36403,11 @@ Object.freeze({
36275
36403
  form: "single",
36276
36404
  optional: true
36277
36405
  }],
36406
+ "pipelineAnalytics.reconcileFromDisk": [{
36407
+ name: "deviceId",
36408
+ form: "single",
36409
+ optional: false
36410
+ }],
36278
36411
  "pipelineAnalytics.restageRetrainTrack": [{
36279
36412
  name: "deviceId",
36280
36413
  form: "single",
@@ -37340,6 +37473,8 @@ DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb;
37340
37473
  DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
37341
37474
  DEFAULT_NATIVE_LEASE_SETTINGS.tileBudgetMb;
37342
37475
  DEFAULT_NATIVE_LEASE_SETTINGS.admission;
37476
+ var MB = 1024 * 1024;
37477
+ 1024 * MB, 3072 * MB;
37343
37478
  //#endregion
37344
37479
  //#region src/config.ts
37345
37480
  /**
package/dist/addon.mjs CHANGED
@@ -36,7 +36,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
36
36
  var __toCommonJS = (mod) => __hasOwnProp.call(mod, "module.exports") ? mod["module.exports"] : __copyProps(__defProp({}, "__esModule", { value: true }), mod);
37
37
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
38
38
  //#endregion
39
- //#region ../types/dist/event-category-Bxo5yJjt.mjs
39
+ //#region ../types/dist/event-category-XfKNtfCc.mjs
40
40
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
41
41
  EventCategory["SystemBoot"] = "system.boot";
42
42
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -53,9 +53,10 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
53
53
  EventCategory["SystemRestartCompleted"] = "system.restart-completed";
54
54
  /**
55
55
  * A newer addon or server-root package version was found by the
56
- * authoritative registry check. Emitted once per
57
- * `(target, packageName, currentVersion, latestVersion)` transition; repeated
58
- * polling of the same result is deduplicated by the checker.
56
+ * authoritative registry check. Emitted once when any observed
57
+ * `latestVersion` changes (or a package/node first appears behind);
58
+ * the payload carries the full currently-available list. Repeated
59
+ * polling of the same latests is silent.
59
60
  */
60
61
  EventCategory["UpdateAvailable"] = "update.available";
61
62
  /**
@@ -74,6 +75,22 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
74
75
  EventCategory["AddonInstalled"] = "addon.installed";
75
76
  EventCategory["AddonUninstalled"] = "addon.uninstalled";
76
77
  EventCategory["AddonCrashed"] = "addon.crashed";
78
+ /**
79
+ * A RUNNER the D6 crash circuit-breaker gave up on — terminal.
80
+ *
81
+ * `AddonCrashed` is the routine, self-healing fact ("it crashed; it is being
82
+ * respawned"); this is the one that never resolves itself. Emitted exactly
83
+ * once per trip, by `process-service.ts`, carrying the node, the runner, the
84
+ * addons it hosted and the crash count that tripped the breaker.
85
+ *
86
+ * It exists because a runner marked terminally `failed` used to be SILENT:
87
+ * on 2026-08-18 the `recorder` runner died of three unhandled ffmpeg spawn
88
+ * errors, the breaker stopped respawning it (correctly), `/health` kept
89
+ * returning 200, and fleet-wide recording was gone for 3h15 before the
90
+ * operator's phone told him. The Notification Center's system-event intake
91
+ * consumes this category and maps it to the `addon-crash-loop` kind.
92
+ */
93
+ EventCategory["AddonRunnerFailed"] = "addon.runner-failed";
77
94
  EventCategory["AddonError"] = "addon.error";
78
95
  EventCategory["AddonPageReady"] = "addon.page-ready";
79
96
  EventCategory["AddonWidgetReady"] = "addon.widget-ready";
@@ -7585,6 +7602,10 @@ var RecordingBandSchema = object({
7585
7602
  preBufferSec: number().min(0).optional(),
7586
7603
  postBufferSec: number().min(0).optional()
7587
7604
  });
7605
+ ({
7606
+ preBufferSec: 10,
7607
+ postBufferSec: 30
7608
+ }).postBufferSec * 1e3;
7588
7609
  /**
7589
7610
  * Per-device retention overrides. Every field is optional; an unset or `0`
7590
7611
  * value inherits the node-wide recorder default. Only footage-lifetime limits
@@ -7632,6 +7653,12 @@ var RecordingConfigSchema = object({
7632
7653
  /** DERIVED summary of `bands`, stamped by the recorder on every save.
7633
7654
  * Authoring it has no effect — see {@link RecordingStorageModeSchema}. */
7634
7655
  mode: RecordingStorageModeSchema.optional(),
7656
+ /**
7657
+ * Which assigned broker slots to record. Absent / empty = {@link
7658
+ * DEFAULT_RECORDING_PROFILES} (`high`+`low`) intersected with the
7659
+ * camera's currently assigned slots — never `mid` unless the operator
7660
+ * picks it, and never a slot the broker has not assigned.
7661
+ */
7635
7662
  profiles: array(CamProfileSchema).optional(),
7636
7663
  segmentSeconds: number().int().positive().optional(),
7637
7664
  /**
@@ -7712,7 +7739,11 @@ var RelocateFootageInputSchema = object({
7712
7739
  profiles: array(string()).optional(),
7713
7740
  /** Copy throttle in MB/s (default 40) — the drain is a background chore,
7714
7741
  * never allowed to starve live writers. */
7715
- throttleMbps: number().min(1).max(1e3).optional()
7742
+ throttleMbps: number().min(1).max(1e3).optional(),
7743
+ /** Move only segments whose startMs is >= this. Absent = the whole source
7744
+ * pile. Used when a full drain is too expensive and the operator only
7745
+ * wants the recent window on the new disk. */
7746
+ sinceMs: number().int().optional()
7716
7747
  });
7717
7748
  /** Internal, lease-scoped participant operation. It is intentionally separate
7718
7749
  * from persistent recording settings: a migration never changes
@@ -14594,6 +14625,7 @@ var NcSystemEventKindSchema = _enum([
14594
14625
  "node-offline",
14595
14626
  "node-inference-unavailable",
14596
14627
  "detection-blind",
14628
+ "addon-crash-loop",
14597
14629
  "addon-update-available",
14598
14630
  "server-update-available",
14599
14631
  "alarm-triggered",
@@ -14601,6 +14633,9 @@ var NcSystemEventKindSchema = _enum([
14601
14633
  "alarm-disarmed",
14602
14634
  "alarm-arming",
14603
14635
  "alarm-arm-refused",
14636
+ "addon-updated",
14637
+ "server-updated",
14638
+ "export-completed",
14604
14639
  "camera-online",
14605
14640
  "camera-offline",
14606
14641
  "camera-disabled",
@@ -16494,13 +16529,19 @@ var RetrainStatusSchema = _enum([
16494
16529
  * "never marked" from "already trained" must read `retrainStatus`.
16495
16530
  *
16496
16531
  * `debug` does NOT pin; it is attention, not durability.
16532
+ *
16533
+ * `favourited` IS a BOOLEAN pin (durability bit), not a retrain lifecycle.
16534
+ * A favourited track is skipped by retention the same way `staging` is, but
16535
+ * it does not enter `none|staging|trained` and has no staging budget.
16497
16536
  */
16498
16537
  var TrackFlagFields = {
16499
16538
  /** Operator marked this track as training material — i.e. `retrainStatus` is
16500
16539
  * `'staging'`. */
16501
16540
  markForTrain: boolean().optional(),
16502
16541
  /** Operator marked this track for diagnostic attention. */
16503
- debug: boolean().optional()
16542
+ debug: boolean().optional(),
16543
+ /** Operator favourited this track. Pins it against pruning. */
16544
+ favourited: boolean().optional()
16504
16545
  };
16505
16546
  /**
16506
16547
  * The lifecycle field itself, on the READ surfaces only (`Track`, `KeyEvent`).
@@ -16525,6 +16566,7 @@ var TrackFlagsSchema = object({
16525
16566
  trackId: string(),
16526
16567
  markForTrain: boolean(),
16527
16568
  debug: boolean(),
16569
+ favourited: boolean(),
16528
16570
  /** The lifecycle state the boolean was derived from. Required here (unlike on
16529
16571
  * a track row) because this shape is only ever produced by the write body,
16530
16572
  * which always knows it — and a surface that has just written needs to render
@@ -17157,6 +17199,12 @@ var TrackCascadeCountsSchema = object({
17157
17199
  /** Per-track CLIP search vectors removed (best-effort). */
17158
17200
  embeddings: number().int()
17159
17201
  });
17202
+ /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17203
+ var DiskReconcileCountsSchema = object({
17204
+ mediaDropped: number().int(),
17205
+ tracks: number().int(),
17206
+ events: number().int()
17207
+ });
17160
17208
  /** Event-store footprint for one camera. */
17161
17209
  var EventStoreDeviceFootprintSchema = object({
17162
17210
  deviceId: number(),
@@ -17333,6 +17381,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17333
17381
  }), method(object({ deviceId: number() }), TrackCascadeCountsSchema, {
17334
17382
  kind: "mutation",
17335
17383
  auth: "admin"
17384
+ }), method(object({ deviceId: number() }), DiskReconcileCountsSchema, {
17385
+ kind: "mutation",
17386
+ auth: "admin"
17336
17387
  }), method(object({
17337
17388
  deviceId: number(),
17338
17389
  trackIds: array(string()).min(1)
@@ -18883,6 +18934,24 @@ var CameraStatusDegradationSchema = object({
18883
18934
  *
18884
18935
  * See spec: `docs/superpowers/specs/2026-06-24-camera-status-aggregator-cap.md`
18885
18936
  */
18937
+ var DiskReconcileJobSchema = object({
18938
+ state: _enum([
18939
+ "idle",
18940
+ "running",
18941
+ "done",
18942
+ "error"
18943
+ ]),
18944
+ total: number().int().nonnegative(),
18945
+ completed: number().int().nonnegative(),
18946
+ currentDeviceId: number().int().nullable(),
18947
+ failed: array(number().int()).readonly(),
18948
+ mediaDropped: number().int().nonnegative(),
18949
+ tracks: number().int().nonnegative(),
18950
+ events: number().int().nonnegative(),
18951
+ startedAtMs: number().int().nullable(),
18952
+ finishedAtMs: number().int().nullable(),
18953
+ error: string().nullable()
18954
+ });
18886
18955
  var CameraStatusSchema = object({
18887
18956
  deviceId: number(),
18888
18957
  assignment: CameraAssignmentStatusSchema,
@@ -19114,7 +19183,10 @@ method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
19114
19183
  }), CameraSwitchGroupSchema, {
19115
19184
  kind: "mutation",
19116
19185
  auth: "admin"
19117
- }), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
19186
+ }), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), DiskReconcileJobSchema, {
19187
+ kind: "mutation",
19188
+ auth: "admin"
19189
+ }), method(_void(), DiskReconcileJobSchema, { auth: "admin" }), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
19118
19190
  name: string(),
19119
19191
  description: string().optional(),
19120
19192
  config: CameraPipelineConfigSchema
@@ -20187,20 +20259,6 @@ var VectorStatsResultSchema = object({
20187
20259
  exact: boolean()
20188
20260
  });
20189
20261
  method(VectorDeclareIndexInputSchema, _void(), { kind: "mutation" }), method(VectorUpsertInputSchema, VectorUpsertResultSchema, { kind: "mutation" }), method(VectorQueryInputSchema, VectorQueryResultSchema), method(VectorGetInputSchema, VectorGetResultSchema), method(VectorDeleteInputSchema, VectorDeleteResultSchema, { kind: "mutation" }), method(VectorDeleteByFilterInputSchema, VectorDeleteResultSchema, { kind: "mutation" }), method(VectorStatsInputSchema, VectorStatsResultSchema);
20190
- /**
20191
- * `videoclips` — the unified, navigable-clip surface for a camera.
20192
- *
20193
- * A device-scoped WRAPPER cap (like `pipeline-analytics`): exactly one active
20194
- * provider per device, substitutable. The DEFAULT provider (registered by
20195
- * `addon-post-analysis`, `defaultActive: true`) composes the analytics event
20196
- * log (markers + thumbnails) with the recorder's `getPlaybackManifest` — a clip
20197
- * is a time-WINDOW over existing footage, never a separate file. A camera that
20198
- * exposes NATIVE onboard clips (Reolink/Hikvision NVR) can later substitute the
20199
- * wrapper provider for its device and serve its own clip catalog + URLs.
20200
- *
20201
- * A `Clip` is purely time-based (subtree-blind): playback resolves segments by
20202
- * temporal overlap, so the API never decides `continuous` vs `events`.
20203
- */
20204
20262
  var ClipSchema = object({
20205
20263
  /** Opaque, provider-namespaced id. The default provider encodes the time
20206
20264
  * window so `getClipPlayback` is self-contained (no event re-query). */
@@ -20217,8 +20275,57 @@ var ClipSchema = object({
20217
20275
  startMs: number(),
20218
20276
  endMs: number()
20219
20277
  }),
20220
- /** Thumbnail URL (lazy; e.g. analytics `getEventMedia`). Never inlined. */
20221
- thumbnail: string().optional()
20278
+ /**
20279
+ * Lazy thumbnail URL, never inlined.
20280
+ *
20281
+ * Recording-derived clips (events-mode keep-window, and the prepared
20282
+ * continuous event+fragment visit) MUST use the snapshot of the **main
20283
+ * event of the interval** — `getEventMedia({ eventId, kind: 'snapshot' })`
20284
+ * of the event that owns `kind` (object > motion > audio). Do not extract
20285
+ * a keyframe from the recorded segments. Other providers (onboard, HKSV)
20286
+ * mint their own stills.
20287
+ *
20288
+ * **VOUCHED, never fabricated.** A provider emits this only for an event it
20289
+ * has CONFIRMED owns at least one media row (its own, or its owning track's).
20290
+ * Absent is meaningful — "this visit has no event still" — never "we did not
20291
+ * look". Stamping it from a URL template made 35% of one camera's clips point
20292
+ * at a 404 (device 3836, 2026-08-17: 64 of 179 clips dead, 63 of them motion).
20293
+ * A read that FAILS drops the claim; it never invents it.
20294
+ */
20295
+ thumbnail: string().optional(),
20296
+ /**
20297
+ * An instant INSIDE this visit's footage, hole-safe, where a recorded still
20298
+ * can be decoded. Present whenever the visit came from recorded availability;
20299
+ * absent on a per-event padded window (there is no footage to promise).
20300
+ *
20301
+ * This is not a thumbnail and not a second byte path: it is the argument to
20302
+ * the recorder's existing still route. The surface — never the provider —
20303
+ * decides whether to use it. It is the midpoint of the visit's LONGEST
20304
+ * contiguous range, not of the visit: a visit spans its holes by
20305
+ * construction, so a naive midpoint lands in dead air.
20306
+ */
20307
+ stillAtMs: number().optional(),
20308
+ /**
20309
+ * Analytics event ids that overlap this visit — a BOUNDED sample, newest
20310
+ * first within kind (object → motion → audio), capped at
20311
+ * {@link MAX_CLIP_EVENT_IDS}. Empty on footage-only clips.
20312
+ *
20313
+ * Bounded because it is not a payload the surface pages through: one visit on
20314
+ * device 615 carried 2 345 ids, and `eventIds` was 99% of a 153 KB
20315
+ * camera-day. Read {@link eventCount} for the true total.
20316
+ */
20317
+ eventIds: array(string()).optional(),
20318
+ /** How many analytics events actually overlap this visit. Differs from
20319
+ * `eventIds.length` exactly when the sample was capped — so a truncated
20320
+ * list is never mistaken for a quiet visit. */
20321
+ eventCount: number().int().nonnegative().optional(),
20322
+ /** Intra-visit footage holes (GOP rolls, discarded segments) still shorter
20323
+ * than `VISIT_MERGE_GAP_MS`. Playback concatenates around them; the timeline
20324
+ * bar keeps showing them via `recording.getAvailability`. */
20325
+ holes: array(object({
20326
+ startMs: number(),
20327
+ endMs: number()
20328
+ })).optional()
20222
20329
  });
20223
20330
  var ClipPlaybackSchema = object({
20224
20331
  /** HLS master URL through the hub data-plane (Range + token in path). */
@@ -20682,7 +20789,14 @@ var SearchResultSchema = object({
20682
20789
  });
20683
20790
  var AutoUpdateSettingsSchema = object({
20684
20791
  channel: ChannelSchema,
20685
- intervalSeconds: number()
20792
+ intervalSeconds: number(),
20793
+ /**
20794
+ * Cadence of the "an update exists" POLLER, in seconds. Independent of
20795
+ * `channel`: the poller runs while auto-apply is `off`, because being told
20796
+ * about a publish and installing it are different decisions. Clamped
20797
+ * server-side to 900 s … 604800 s; defaults to 21600 s (6 h).
20798
+ */
20799
+ updateCheckIntervalSeconds: number()
20686
20800
  });
20687
20801
  var AddonAutoUpdateSchema = ChannelWithInheritSchema;
20688
20802
  var RestartAddonResultSchema = unknown();
@@ -20823,7 +20937,9 @@ method(_void(), array(AddonListItemSchema).readonly()), method(object({
20823
20937
  auth: "admin"
20824
20938
  }), method(_void(), AutoUpdateSettingsSchema, { auth: "admin" }), method(object({
20825
20939
  channel: ChannelSchema,
20826
- intervalSeconds: number().min(300).max(86400).optional()
20940
+ intervalSeconds: number().min(300).max(86400).optional(),
20941
+ /** Availability-poll cadence; see AutoUpdateSettingsSchema. */
20942
+ updateCheckIntervalSeconds: number().min(900).max(604800).optional()
20827
20943
  }), unknown(), {
20828
20944
  kind: "mutation",
20829
20945
  auth: "admin"
@@ -26512,7 +26628,9 @@ var ExportOptionsSchema = object({
26512
26628
  includeAudio: boolean(),
26513
26629
  maxLifeMs: number().int().positive(),
26514
26630
  deleteAfterDownload: boolean(),
26515
- title: string().max(200).optional()
26631
+ title: string().max(200).optional(),
26632
+ /** Notification-output target ids to ping when this export becomes ready. */
26633
+ notifyTargetIds: array(string().min(1)).max(20).optional()
26516
26634
  }).superRefine((v, ctx) => {
26517
26635
  if (v.speed !== void 0 && v.timelapse !== void 0) ctx.addIssue({
26518
26636
  code: ZodIssueCode.custom,
@@ -26571,10 +26689,18 @@ var ExportBytesSchema = object({
26571
26689
  });
26572
26690
  method(object({
26573
26691
  deviceId: number(),
26574
- profile: string(),
26692
+ /** @deprecated Prefer `profiles`. Kept so timelapse/notifiers keep working. */
26693
+ profile: string().optional(),
26694
+ profiles: array(string()).min(1).optional(),
26575
26695
  fromMs: number(),
26576
26696
  toMs: number(),
26577
26697
  options: ExportOptionsSchema
26698
+ }).superRefine((v, ctx) => {
26699
+ if ((v.profiles !== void 0 && v.profiles.length > 0 ? v.profiles : v.profile !== void 0 ? [v.profile] : []).length < 1) ctx.addIssue({
26700
+ code: ZodIssueCode.custom,
26701
+ message: "pass profiles[] (min 1) or legacy profile",
26702
+ path: ["profiles"]
26703
+ });
26578
26704
  }), ExportRecordSchema, {
26579
26705
  kind: "mutation",
26580
26706
  auth: "protected"
@@ -29516,15 +29642,6 @@ var BaseDeviceProvider = class extends BaseAddon {
29516
29642
  labels: ["probe not implemented"]
29517
29643
  };
29518
29644
  }
29519
- /**
29520
- * Top-level devices restored at once in {@link onRestoreDevices}.
29521
- *
29522
- * Four covers the fleets this ships to without turning a boot into a burst a
29523
- * camera NVR answers with a refusal. A provider whose upstream is a single
29524
- * session with a serial command channel (a Baichuan hub, an NVR that
29525
- * serialises ISAPI) should lower it; nothing needs to raise it.
29526
- */
29527
- restoreConcurrency = 4;
29528
29645
  async restoreDevices(savedDevices) {
29529
29646
  await this.onRestoreDevices(savedDevices);
29530
29647
  if (savedDevices.length > 0) this.ctx.logger.info(`Restored ${savedDevices.length} ${this.providerName} device(s)`);
@@ -29579,14 +29696,7 @@ var BaseDeviceProvider = class extends BaseAddon {
29579
29696
  });
29580
29697
  }
29581
29698
  };
29582
- let nextTopLevel = 0;
29583
- await Promise.all(Array.from({ length: Math.min(Math.max(1, this.restoreConcurrency), topLevel.length) }, async () => {
29584
- for (;;) {
29585
- const saved = topLevel[nextTopLevel++];
29586
- if (saved === void 0) return;
29587
- await restoreOne(saved);
29588
- }
29589
- }));
29699
+ await Promise.all(topLevel.map((saved) => restoreOne(saved)));
29590
29700
  const childRows = savedDevices.filter((s) => s.parentDeviceId !== null);
29591
29701
  for (const saved of childRows) {
29592
29702
  const Class = this.deviceClasses[saved.type];
@@ -32859,6 +32969,12 @@ Object.freeze({
32859
32969
  addonId: null,
32860
32970
  access: "create"
32861
32971
  },
32972
+ "pipelineAnalytics.reconcileFromDisk": {
32973
+ capName: "pipeline-analytics",
32974
+ capScope: "device",
32975
+ addonId: null,
32976
+ access: "create"
32977
+ },
32862
32978
  "pipelineAnalytics.refreshStorageLocationsForMigration": {
32863
32979
  capName: "pipeline-analytics",
32864
32980
  capScope: "device",
@@ -33261,6 +33377,12 @@ Object.freeze({
33261
33377
  addonId: null,
33262
33378
  access: "view"
33263
33379
  },
33380
+ "pipelineOrchestrator.getReconcileFromDiskStatus": {
33381
+ capName: "pipeline-orchestrator",
33382
+ capScope: "system",
33383
+ addonId: null,
33384
+ access: "view"
33385
+ },
33264
33386
  "pipelineOrchestrator.listAgentSettings": {
33265
33387
  capName: "pipeline-orchestrator",
33266
33388
  capScope: "system",
@@ -33285,6 +33407,12 @@ Object.freeze({
33285
33407
  addonId: null,
33286
33408
  access: "create"
33287
33409
  },
33410
+ "pipelineOrchestrator.reconcileFromDisk": {
33411
+ capName: "pipeline-orchestrator",
33412
+ capScope: "system",
33413
+ addonId: null,
33414
+ access: "create"
33415
+ },
33288
33416
  "pipelineOrchestrator.removeAgentSettings": {
33289
33417
  capName: "pipeline-orchestrator",
33290
33418
  capScope: "system",
@@ -36276,6 +36404,11 @@ Object.freeze({
36276
36404
  form: "single",
36277
36405
  optional: true
36278
36406
  }],
36407
+ "pipelineAnalytics.reconcileFromDisk": [{
36408
+ name: "deviceId",
36409
+ form: "single",
36410
+ optional: false
36411
+ }],
36279
36412
  "pipelineAnalytics.restageRetrainTrack": [{
36280
36413
  name: "deviceId",
36281
36414
  form: "single",
@@ -37341,6 +37474,8 @@ DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb;
37341
37474
  DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
37342
37475
  DEFAULT_NATIVE_LEASE_SETTINGS.tileBudgetMb;
37343
37476
  DEFAULT_NATIVE_LEASE_SETTINGS.admission;
37477
+ var MB = 1024 * 1024;
37478
+ 1024 * MB, 3072 * MB;
37344
37479
  //#endregion
37345
37480
  //#region src/config.ts
37346
37481
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-dreo",
3
- "version": "0.2.20",
3
+ "version": "0.2.21",
4
4
  "description": "Dreo smart-device (fan / air-circulator / purifier / heater / humidifier) device-provider addon for CamStack — wraps the @apocaliss92/nodedreo Dreo cloud client (REST + WebSocket)",
5
5
  "keywords": [
6
6
  "camstack",