@camstack/addon-remote-storage 1.2.19 → 1.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.
package/dist/s3.addon.js CHANGED
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_shared = require("./shared-C7kobCEN.js");
5
+ const require_shared = require("./shared-CrotCkhF.js");
6
6
  let node_stream = require("node:stream");
7
7
  let _aws_sdk_client_s3 = require("@aws-sdk/client-s3");
8
8
  let _aws_sdk_lib_storage = require("@aws-sdk/lib-storage");
package/dist/s3.addon.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { c as BaseAddon, i as rearmIdleAbort, n as getOptionalBasePath, o as scheduleIdleAbort, s as storageProviderCapability, t as createSessionId } from "./shared-CN_vleSm.mjs";
1
+ import { c as BaseAddon, i as rearmIdleAbort, n as getOptionalBasePath, o as scheduleIdleAbort, s as storageProviderCapability, t as createSessionId } from "./shared-Toqenw3n.mjs";
2
2
  import { PassThrough } from "node:stream";
3
3
  import { DeleteObjectCommand, GetObjectCommand, HeadBucketCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
4
4
  import { Upload } from "@aws-sdk/lib-storage";
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_shared = require("./shared-C7kobCEN.js");
5
+ const require_shared = require("./shared-CrotCkhF.js");
6
6
  let ssh2 = require("ssh2");
7
7
  let node_path = require("node:path");
8
8
  node_path = require_shared.__toESM(node_path);
@@ -1,4 +1,4 @@
1
- import { a as safeJoinRemotePath, c as BaseAddon, i as rearmIdleAbort, o as scheduleIdleAbort, r as getRequiredBasePath, s as storageProviderCapability, t as createSessionId } from "./shared-CN_vleSm.mjs";
1
+ import { a as safeJoinRemotePath, c as BaseAddon, i as rearmIdleAbort, o as scheduleIdleAbort, r as getRequiredBasePath, s as storageProviderCapability, t as createSessionId } from "./shared-Toqenw3n.mjs";
2
2
  import { Client } from "ssh2";
3
3
  import * as path from "node:path";
4
4
  //#region src/providers/sftp/sftp-config-schema.ts
@@ -23,7 +23,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
23
  let node_crypto = require("node:crypto");
24
24
  let node_path = require("node:path");
25
25
  node_path = __toESM(node_path);
26
- //#region ../types/dist/event-category-Bxo5yJjt.mjs
26
+ //#region ../types/dist/event-category-XfKNtfCc.mjs
27
27
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
28
28
  EventCategory["SystemBoot"] = "system.boot";
29
29
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -40,9 +40,10 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
40
40
  EventCategory["SystemRestartCompleted"] = "system.restart-completed";
41
41
  /**
42
42
  * A newer addon or server-root package version was found by the
43
- * authoritative registry check. Emitted once per
44
- * `(target, packageName, currentVersion, latestVersion)` transition; repeated
45
- * polling of the same result is deduplicated by the checker.
43
+ * authoritative registry check. Emitted once when any observed
44
+ * `latestVersion` changes (or a package/node first appears behind);
45
+ * the payload carries the full currently-available list. Repeated
46
+ * polling of the same latests is silent.
46
47
  */
47
48
  EventCategory["UpdateAvailable"] = "update.available";
48
49
  /**
@@ -61,6 +62,22 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
61
62
  EventCategory["AddonInstalled"] = "addon.installed";
62
63
  EventCategory["AddonUninstalled"] = "addon.uninstalled";
63
64
  EventCategory["AddonCrashed"] = "addon.crashed";
65
+ /**
66
+ * A RUNNER the D6 crash circuit-breaker gave up on — terminal.
67
+ *
68
+ * `AddonCrashed` is the routine, self-healing fact ("it crashed; it is being
69
+ * respawned"); this is the one that never resolves itself. Emitted exactly
70
+ * once per trip, by `process-service.ts`, carrying the node, the runner, the
71
+ * addons it hosted and the crash count that tripped the breaker.
72
+ *
73
+ * It exists because a runner marked terminally `failed` used to be SILENT:
74
+ * on 2026-08-18 the `recorder` runner died of three unhandled ffmpeg spawn
75
+ * errors, the breaker stopped respawning it (correctly), `/health` kept
76
+ * returning 200, and fleet-wide recording was gone for 3h15 before the
77
+ * operator's phone told him. The Notification Center's system-event intake
78
+ * consumes this category and maps it to the `addon-crash-loop` kind.
79
+ */
80
+ EventCategory["AddonRunnerFailed"] = "addon.runner-failed";
64
81
  EventCategory["AddonError"] = "addon.error";
65
82
  EventCategory["AddonPageReady"] = "addon.page-ready";
66
83
  EventCategory["AddonWidgetReady"] = "addon.widget-ready";
@@ -7556,6 +7573,10 @@ var RecordingBandSchema = object({
7556
7573
  preBufferSec: number().min(0).optional(),
7557
7574
  postBufferSec: number().min(0).optional()
7558
7575
  });
7576
+ ({
7577
+ preBufferSec: 10,
7578
+ postBufferSec: 30
7579
+ }).postBufferSec * 1e3;
7559
7580
  /**
7560
7581
  * Per-device retention overrides. Every field is optional; an unset or `0`
7561
7582
  * value inherits the node-wide recorder default. Only footage-lifetime limits
@@ -7603,6 +7624,12 @@ var RecordingConfigSchema = object({
7603
7624
  /** DERIVED summary of `bands`, stamped by the recorder on every save.
7604
7625
  * Authoring it has no effect — see {@link RecordingStorageModeSchema}. */
7605
7626
  mode: RecordingStorageModeSchema.optional(),
7627
+ /**
7628
+ * Which assigned broker slots to record. Absent / empty = {@link
7629
+ * DEFAULT_RECORDING_PROFILES} (`high`+`low`) intersected with the
7630
+ * camera's currently assigned slots — never `mid` unless the operator
7631
+ * picks it, and never a slot the broker has not assigned.
7632
+ */
7606
7633
  profiles: array(CamProfileSchema).optional(),
7607
7634
  segmentSeconds: number().int().positive().optional(),
7608
7635
  /**
@@ -7683,7 +7710,11 @@ var RelocateFootageInputSchema = object({
7683
7710
  profiles: array(string()).optional(),
7684
7711
  /** Copy throttle in MB/s (default 40) — the drain is a background chore,
7685
7712
  * never allowed to starve live writers. */
7686
- throttleMbps: number().min(1).max(1e3).optional()
7713
+ throttleMbps: number().min(1).max(1e3).optional(),
7714
+ /** Move only segments whose startMs is >= this. Absent = the whole source
7715
+ * pile. Used when a full drain is too expensive and the operator only
7716
+ * wants the recent window on the new disk. */
7717
+ sinceMs: number().int().optional()
7687
7718
  });
7688
7719
  /** Internal, lease-scoped participant operation. It is intentionally separate
7689
7720
  * from persistent recording settings: a migration never changes
@@ -14246,6 +14277,7 @@ var NcSystemEventKindSchema = _enum([
14246
14277
  "node-offline",
14247
14278
  "node-inference-unavailable",
14248
14279
  "detection-blind",
14280
+ "addon-crash-loop",
14249
14281
  "addon-update-available",
14250
14282
  "server-update-available",
14251
14283
  "alarm-triggered",
@@ -14253,6 +14285,9 @@ var NcSystemEventKindSchema = _enum([
14253
14285
  "alarm-disarmed",
14254
14286
  "alarm-arming",
14255
14287
  "alarm-arm-refused",
14288
+ "addon-updated",
14289
+ "server-updated",
14290
+ "export-completed",
14256
14291
  "camera-online",
14257
14292
  "camera-offline",
14258
14293
  "camera-disabled",
@@ -16146,13 +16181,19 @@ var RetrainStatusSchema = _enum([
16146
16181
  * "never marked" from "already trained" must read `retrainStatus`.
16147
16182
  *
16148
16183
  * `debug` does NOT pin; it is attention, not durability.
16184
+ *
16185
+ * `favourited` IS a BOOLEAN pin (durability bit), not a retrain lifecycle.
16186
+ * A favourited track is skipped by retention the same way `staging` is, but
16187
+ * it does not enter `none|staging|trained` and has no staging budget.
16149
16188
  */
16150
16189
  var TrackFlagFields = {
16151
16190
  /** Operator marked this track as training material — i.e. `retrainStatus` is
16152
16191
  * `'staging'`. */
16153
16192
  markForTrain: boolean().optional(),
16154
16193
  /** Operator marked this track for diagnostic attention. */
16155
- debug: boolean().optional()
16194
+ debug: boolean().optional(),
16195
+ /** Operator favourited this track. Pins it against pruning. */
16196
+ favourited: boolean().optional()
16156
16197
  };
16157
16198
  /**
16158
16199
  * The lifecycle field itself, on the READ surfaces only (`Track`, `KeyEvent`).
@@ -16177,6 +16218,7 @@ var TrackFlagsSchema = object({
16177
16218
  trackId: string(),
16178
16219
  markForTrain: boolean(),
16179
16220
  debug: boolean(),
16221
+ favourited: boolean(),
16180
16222
  /** The lifecycle state the boolean was derived from. Required here (unlike on
16181
16223
  * a track row) because this shape is only ever produced by the write body,
16182
16224
  * which always knows it — and a surface that has just written needs to render
@@ -16809,6 +16851,12 @@ var TrackCascadeCountsSchema = object({
16809
16851
  /** Per-track CLIP search vectors removed (best-effort). */
16810
16852
  embeddings: number().int()
16811
16853
  });
16854
+ /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
16855
+ var DiskReconcileCountsSchema = object({
16856
+ mediaDropped: number().int(),
16857
+ tracks: number().int(),
16858
+ events: number().int()
16859
+ });
16812
16860
  /** Event-store footprint for one camera. */
16813
16861
  var EventStoreDeviceFootprintSchema = object({
16814
16862
  deviceId: number(),
@@ -16985,6 +17033,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
16985
17033
  }), method(object({ deviceId: number() }), TrackCascadeCountsSchema, {
16986
17034
  kind: "mutation",
16987
17035
  auth: "admin"
17036
+ }), method(object({ deviceId: number() }), DiskReconcileCountsSchema, {
17037
+ kind: "mutation",
17038
+ auth: "admin"
16988
17039
  }), method(object({
16989
17040
  deviceId: number(),
16990
17041
  trackIds: array(string()).min(1)
@@ -18495,6 +18546,24 @@ var CameraStatusDegradationSchema = object({
18495
18546
  *
18496
18547
  * See spec: `docs/superpowers/specs/2026-06-24-camera-status-aggregator-cap.md`
18497
18548
  */
18549
+ var DiskReconcileJobSchema = object({
18550
+ state: _enum([
18551
+ "idle",
18552
+ "running",
18553
+ "done",
18554
+ "error"
18555
+ ]),
18556
+ total: number().int().nonnegative(),
18557
+ completed: number().int().nonnegative(),
18558
+ currentDeviceId: number().int().nullable(),
18559
+ failed: array(number().int()).readonly(),
18560
+ mediaDropped: number().int().nonnegative(),
18561
+ tracks: number().int().nonnegative(),
18562
+ events: number().int().nonnegative(),
18563
+ startedAtMs: number().int().nullable(),
18564
+ finishedAtMs: number().int().nullable(),
18565
+ error: string().nullable()
18566
+ });
18498
18567
  var CameraStatusSchema = object({
18499
18568
  deviceId: number(),
18500
18569
  assignment: CameraAssignmentStatusSchema,
@@ -18726,7 +18795,10 @@ method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
18726
18795
  }), CameraSwitchGroupSchema, {
18727
18796
  kind: "mutation",
18728
18797
  auth: "admin"
18729
- }), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
18798
+ }), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), DiskReconcileJobSchema, {
18799
+ kind: "mutation",
18800
+ auth: "admin"
18801
+ }), method(_void(), DiskReconcileJobSchema, { auth: "admin" }), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
18730
18802
  name: string(),
18731
18803
  description: string().optional(),
18732
18804
  config: CameraPipelineConfigSchema
@@ -19844,20 +19916,6 @@ var VectorStatsResultSchema = object({
19844
19916
  exact: boolean()
19845
19917
  });
19846
19918
  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);
19847
- /**
19848
- * `videoclips` — the unified, navigable-clip surface for a camera.
19849
- *
19850
- * A device-scoped WRAPPER cap (like `pipeline-analytics`): exactly one active
19851
- * provider per device, substitutable. The DEFAULT provider (registered by
19852
- * `addon-post-analysis`, `defaultActive: true`) composes the analytics event
19853
- * log (markers + thumbnails) with the recorder's `getPlaybackManifest` — a clip
19854
- * is a time-WINDOW over existing footage, never a separate file. A camera that
19855
- * exposes NATIVE onboard clips (Reolink/Hikvision NVR) can later substitute the
19856
- * wrapper provider for its device and serve its own clip catalog + URLs.
19857
- *
19858
- * A `Clip` is purely time-based (subtree-blind): playback resolves segments by
19859
- * temporal overlap, so the API never decides `continuous` vs `events`.
19860
- */
19861
19919
  var ClipSchema = object({
19862
19920
  /** Opaque, provider-namespaced id. The default provider encodes the time
19863
19921
  * window so `getClipPlayback` is self-contained (no event re-query). */
@@ -19874,8 +19932,65 @@ var ClipSchema = object({
19874
19932
  startMs: number(),
19875
19933
  endMs: number()
19876
19934
  }),
19877
- /** Thumbnail URL (lazy; e.g. analytics `getEventMedia`). Never inlined. */
19878
- thumbnail: string().optional()
19935
+ /**
19936
+ * Distinct object classes attached to this visit (`person`, `car`, …),
19937
+ * dominant first, capped at {@link MAX_CLIP_LABELS}. The ribbon renders these
19938
+ * instead of the bare kind — "Object" tells the operator nothing a colour bar
19939
+ * did not. Absent (never empty) when the visit attached no classified object
19940
+ * event, so a motion/audio-only visit keeps its kind label.
19941
+ */
19942
+ labels: array(string()).max(3).optional(),
19943
+ /**
19944
+ * Lazy thumbnail URL, never inlined.
19945
+ *
19946
+ * Recording-derived clips (events-mode keep-window, and the prepared
19947
+ * continuous event+fragment visit) MUST use the snapshot of the **main
19948
+ * event of the interval** — `getEventMedia({ eventId, kind: 'snapshot' })`
19949
+ * of the event that owns `kind` (object > motion > audio). Do not extract
19950
+ * a keyframe from the recorded segments. Other providers (onboard, HKSV)
19951
+ * mint their own stills.
19952
+ *
19953
+ * **VOUCHED, never fabricated.** A provider emits this only for an event it
19954
+ * has CONFIRMED owns at least one media row (its own, or its owning track's).
19955
+ * Absent is meaningful — "this visit has no event still" — never "we did not
19956
+ * look". Stamping it from a URL template made 35% of one camera's clips point
19957
+ * at a 404 (device 3836, 2026-08-17: 64 of 179 clips dead, 63 of them motion).
19958
+ * A read that FAILS drops the claim; it never invents it.
19959
+ */
19960
+ thumbnail: string().optional(),
19961
+ /**
19962
+ * An instant INSIDE this visit's footage, hole-safe, where a recorded still
19963
+ * can be decoded. Present whenever the visit came from recorded availability;
19964
+ * absent on a per-event padded window (there is no footage to promise).
19965
+ *
19966
+ * This is not a thumbnail and not a second byte path: it is the argument to
19967
+ * the recorder's existing still route. The surface — never the provider —
19968
+ * decides whether to use it. It is the midpoint of the visit's LONGEST
19969
+ * contiguous range, not of the visit: a visit spans its holes by
19970
+ * construction, so a naive midpoint lands in dead air.
19971
+ */
19972
+ stillAtMs: number().optional(),
19973
+ /**
19974
+ * Analytics event ids that overlap this visit — a BOUNDED sample, newest
19975
+ * first within kind (object → motion → audio), capped at
19976
+ * {@link MAX_CLIP_EVENT_IDS}. Empty on footage-only clips.
19977
+ *
19978
+ * Bounded because it is not a payload the surface pages through: one visit on
19979
+ * device 615 carried 2 345 ids, and `eventIds` was 99% of a 153 KB
19980
+ * camera-day. Read {@link eventCount} for the true total.
19981
+ */
19982
+ eventIds: array(string()).optional(),
19983
+ /** How many analytics events actually overlap this visit. Differs from
19984
+ * `eventIds.length` exactly when the sample was capped — so a truncated
19985
+ * list is never mistaken for a quiet visit. */
19986
+ eventCount: number().int().nonnegative().optional(),
19987
+ /** Intra-visit footage holes (GOP rolls, discarded segments) still shorter
19988
+ * than `VISIT_MERGE_GAP_MS`. Playback concatenates around them; the timeline
19989
+ * bar keeps showing them via `recording.getAvailability`. */
19990
+ holes: array(object({
19991
+ startMs: number(),
19992
+ endMs: number()
19993
+ })).optional()
19879
19994
  });
19880
19995
  var ClipPlaybackSchema = object({
19881
19996
  /** HLS master URL through the hub data-plane (Range + token in path). */
@@ -20339,7 +20454,14 @@ var SearchResultSchema = object({
20339
20454
  });
20340
20455
  var AutoUpdateSettingsSchema = object({
20341
20456
  channel: ChannelSchema,
20342
- intervalSeconds: number()
20457
+ intervalSeconds: number(),
20458
+ /**
20459
+ * Cadence of the "an update exists" POLLER, in seconds. Independent of
20460
+ * `channel`: the poller runs while auto-apply is `off`, because being told
20461
+ * about a publish and installing it are different decisions. Clamped
20462
+ * server-side to 900 s … 604800 s; defaults to 21600 s (6 h).
20463
+ */
20464
+ updateCheckIntervalSeconds: number()
20343
20465
  });
20344
20466
  var AddonAutoUpdateSchema = ChannelWithInheritSchema;
20345
20467
  var RestartAddonResultSchema = unknown();
@@ -20480,7 +20602,9 @@ method(_void(), array(AddonListItemSchema).readonly()), method(object({
20480
20602
  auth: "admin"
20481
20603
  }), method(_void(), AutoUpdateSettingsSchema, { auth: "admin" }), method(object({
20482
20604
  channel: ChannelSchema,
20483
- intervalSeconds: number().min(300).max(86400).optional()
20605
+ intervalSeconds: number().min(300).max(86400).optional(),
20606
+ /** Availability-poll cadence; see AutoUpdateSettingsSchema. */
20607
+ updateCheckIntervalSeconds: number().min(900).max(604800).optional()
20484
20608
  }), unknown(), {
20485
20609
  kind: "mutation",
20486
20610
  auth: "admin"
@@ -24473,7 +24597,9 @@ var ExportOptionsSchema = object({
24473
24597
  includeAudio: boolean(),
24474
24598
  maxLifeMs: number().int().positive(),
24475
24599
  deleteAfterDownload: boolean(),
24476
- title: string().max(200).optional()
24600
+ title: string().max(200).optional(),
24601
+ /** Notification-output target ids to ping when this export becomes ready. */
24602
+ notifyTargetIds: array(string().min(1)).max(20).optional()
24477
24603
  }).superRefine((v, ctx) => {
24478
24604
  if (v.speed !== void 0 && v.timelapse !== void 0) ctx.addIssue({
24479
24605
  code: ZodIssueCode.custom,
@@ -24532,10 +24658,18 @@ var ExportBytesSchema = object({
24532
24658
  });
24533
24659
  method(object({
24534
24660
  deviceId: number(),
24535
- profile: string(),
24661
+ /** @deprecated Prefer `profiles`. Kept so timelapse/notifiers keep working. */
24662
+ profile: string().optional(),
24663
+ profiles: array(string()).min(1).optional(),
24536
24664
  fromMs: number(),
24537
24665
  toMs: number(),
24538
24666
  options: ExportOptionsSchema
24667
+ }).superRefine((v, ctx) => {
24668
+ if ((v.profiles !== void 0 && v.profiles.length > 0 ? v.profiles : v.profile !== void 0 ? [v.profile] : []).length < 1) ctx.addIssue({
24669
+ code: ZodIssueCode.custom,
24670
+ message: "pass profiles[] (min 1) or legacy profile",
24671
+ path: ["profiles"]
24672
+ });
24539
24673
  }), ExportRecordSchema, {
24540
24674
  kind: "mutation",
24541
24675
  auth: "protected"
@@ -29135,6 +29269,12 @@ Object.freeze({
29135
29269
  addonId: null,
29136
29270
  access: "create"
29137
29271
  },
29272
+ "pipelineAnalytics.reconcileFromDisk": {
29273
+ capName: "pipeline-analytics",
29274
+ capScope: "device",
29275
+ addonId: null,
29276
+ access: "create"
29277
+ },
29138
29278
  "pipelineAnalytics.refreshStorageLocationsForMigration": {
29139
29279
  capName: "pipeline-analytics",
29140
29280
  capScope: "device",
@@ -29537,6 +29677,12 @@ Object.freeze({
29537
29677
  addonId: null,
29538
29678
  access: "view"
29539
29679
  },
29680
+ "pipelineOrchestrator.getReconcileFromDiskStatus": {
29681
+ capName: "pipeline-orchestrator",
29682
+ capScope: "system",
29683
+ addonId: null,
29684
+ access: "view"
29685
+ },
29540
29686
  "pipelineOrchestrator.listAgentSettings": {
29541
29687
  capName: "pipeline-orchestrator",
29542
29688
  capScope: "system",
@@ -29561,6 +29707,12 @@ Object.freeze({
29561
29707
  addonId: null,
29562
29708
  access: "create"
29563
29709
  },
29710
+ "pipelineOrchestrator.reconcileFromDisk": {
29711
+ capName: "pipeline-orchestrator",
29712
+ capScope: "system",
29713
+ addonId: null,
29714
+ access: "create"
29715
+ },
29564
29716
  "pipelineOrchestrator.removeAgentSettings": {
29565
29717
  capName: "pipeline-orchestrator",
29566
29718
  capScope: "system",
@@ -32552,6 +32704,11 @@ Object.freeze({
32552
32704
  form: "single",
32553
32705
  optional: true
32554
32706
  }],
32707
+ "pipelineAnalytics.reconcileFromDisk": [{
32708
+ name: "deviceId",
32709
+ form: "single",
32710
+ optional: false
32711
+ }],
32555
32712
  "pipelineAnalytics.restageRetrainTrack": [{
32556
32713
  name: "deviceId",
32557
32714
  form: "single",
@@ -33617,6 +33774,8 @@ DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb;
33617
33774
  DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
33618
33775
  DEFAULT_NATIVE_LEASE_SETTINGS.tileBudgetMb;
33619
33776
  DEFAULT_NATIVE_LEASE_SETTINGS.admission;
33777
+ var MB = 1024 * 1024;
33778
+ 1024 * MB, 3072 * MB;
33620
33779
  //#endregion
33621
33780
  //#region src/shared.ts
33622
33781
  /**
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import * as path from "node:path";
3
- //#region ../types/dist/event-category-Bxo5yJjt.mjs
3
+ //#region ../types/dist/event-category-XfKNtfCc.mjs
4
4
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
5
5
  EventCategory["SystemBoot"] = "system.boot";
6
6
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -17,9 +17,10 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
17
17
  EventCategory["SystemRestartCompleted"] = "system.restart-completed";
18
18
  /**
19
19
  * A newer addon or server-root package version was found by the
20
- * authoritative registry check. Emitted once per
21
- * `(target, packageName, currentVersion, latestVersion)` transition; repeated
22
- * polling of the same result is deduplicated by the checker.
20
+ * authoritative registry check. Emitted once when any observed
21
+ * `latestVersion` changes (or a package/node first appears behind);
22
+ * the payload carries the full currently-available list. Repeated
23
+ * polling of the same latests is silent.
23
24
  */
24
25
  EventCategory["UpdateAvailable"] = "update.available";
25
26
  /**
@@ -38,6 +39,22 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
38
39
  EventCategory["AddonInstalled"] = "addon.installed";
39
40
  EventCategory["AddonUninstalled"] = "addon.uninstalled";
40
41
  EventCategory["AddonCrashed"] = "addon.crashed";
42
+ /**
43
+ * A RUNNER the D6 crash circuit-breaker gave up on — terminal.
44
+ *
45
+ * `AddonCrashed` is the routine, self-healing fact ("it crashed; it is being
46
+ * respawned"); this is the one that never resolves itself. Emitted exactly
47
+ * once per trip, by `process-service.ts`, carrying the node, the runner, the
48
+ * addons it hosted and the crash count that tripped the breaker.
49
+ *
50
+ * It exists because a runner marked terminally `failed` used to be SILENT:
51
+ * on 2026-08-18 the `recorder` runner died of three unhandled ffmpeg spawn
52
+ * errors, the breaker stopped respawning it (correctly), `/health` kept
53
+ * returning 200, and fleet-wide recording was gone for 3h15 before the
54
+ * operator's phone told him. The Notification Center's system-event intake
55
+ * consumes this category and maps it to the `addon-crash-loop` kind.
56
+ */
57
+ EventCategory["AddonRunnerFailed"] = "addon.runner-failed";
41
58
  EventCategory["AddonError"] = "addon.error";
42
59
  EventCategory["AddonPageReady"] = "addon.page-ready";
43
60
  EventCategory["AddonWidgetReady"] = "addon.widget-ready";
@@ -7533,6 +7550,10 @@ var RecordingBandSchema = object({
7533
7550
  preBufferSec: number().min(0).optional(),
7534
7551
  postBufferSec: number().min(0).optional()
7535
7552
  });
7553
+ ({
7554
+ preBufferSec: 10,
7555
+ postBufferSec: 30
7556
+ }).postBufferSec * 1e3;
7536
7557
  /**
7537
7558
  * Per-device retention overrides. Every field is optional; an unset or `0`
7538
7559
  * value inherits the node-wide recorder default. Only footage-lifetime limits
@@ -7580,6 +7601,12 @@ var RecordingConfigSchema = object({
7580
7601
  /** DERIVED summary of `bands`, stamped by the recorder on every save.
7581
7602
  * Authoring it has no effect — see {@link RecordingStorageModeSchema}. */
7582
7603
  mode: RecordingStorageModeSchema.optional(),
7604
+ /**
7605
+ * Which assigned broker slots to record. Absent / empty = {@link
7606
+ * DEFAULT_RECORDING_PROFILES} (`high`+`low`) intersected with the
7607
+ * camera's currently assigned slots — never `mid` unless the operator
7608
+ * picks it, and never a slot the broker has not assigned.
7609
+ */
7583
7610
  profiles: array(CamProfileSchema).optional(),
7584
7611
  segmentSeconds: number().int().positive().optional(),
7585
7612
  /**
@@ -7660,7 +7687,11 @@ var RelocateFootageInputSchema = object({
7660
7687
  profiles: array(string()).optional(),
7661
7688
  /** Copy throttle in MB/s (default 40) — the drain is a background chore,
7662
7689
  * never allowed to starve live writers. */
7663
- throttleMbps: number().min(1).max(1e3).optional()
7690
+ throttleMbps: number().min(1).max(1e3).optional(),
7691
+ /** Move only segments whose startMs is >= this. Absent = the whole source
7692
+ * pile. Used when a full drain is too expensive and the operator only
7693
+ * wants the recent window on the new disk. */
7694
+ sinceMs: number().int().optional()
7664
7695
  });
7665
7696
  /** Internal, lease-scoped participant operation. It is intentionally separate
7666
7697
  * from persistent recording settings: a migration never changes
@@ -14223,6 +14254,7 @@ var NcSystemEventKindSchema = _enum([
14223
14254
  "node-offline",
14224
14255
  "node-inference-unavailable",
14225
14256
  "detection-blind",
14257
+ "addon-crash-loop",
14226
14258
  "addon-update-available",
14227
14259
  "server-update-available",
14228
14260
  "alarm-triggered",
@@ -14230,6 +14262,9 @@ var NcSystemEventKindSchema = _enum([
14230
14262
  "alarm-disarmed",
14231
14263
  "alarm-arming",
14232
14264
  "alarm-arm-refused",
14265
+ "addon-updated",
14266
+ "server-updated",
14267
+ "export-completed",
14233
14268
  "camera-online",
14234
14269
  "camera-offline",
14235
14270
  "camera-disabled",
@@ -16123,13 +16158,19 @@ var RetrainStatusSchema = _enum([
16123
16158
  * "never marked" from "already trained" must read `retrainStatus`.
16124
16159
  *
16125
16160
  * `debug` does NOT pin; it is attention, not durability.
16161
+ *
16162
+ * `favourited` IS a BOOLEAN pin (durability bit), not a retrain lifecycle.
16163
+ * A favourited track is skipped by retention the same way `staging` is, but
16164
+ * it does not enter `none|staging|trained` and has no staging budget.
16126
16165
  */
16127
16166
  var TrackFlagFields = {
16128
16167
  /** Operator marked this track as training material — i.e. `retrainStatus` is
16129
16168
  * `'staging'`. */
16130
16169
  markForTrain: boolean().optional(),
16131
16170
  /** Operator marked this track for diagnostic attention. */
16132
- debug: boolean().optional()
16171
+ debug: boolean().optional(),
16172
+ /** Operator favourited this track. Pins it against pruning. */
16173
+ favourited: boolean().optional()
16133
16174
  };
16134
16175
  /**
16135
16176
  * The lifecycle field itself, on the READ surfaces only (`Track`, `KeyEvent`).
@@ -16154,6 +16195,7 @@ var TrackFlagsSchema = object({
16154
16195
  trackId: string(),
16155
16196
  markForTrain: boolean(),
16156
16197
  debug: boolean(),
16198
+ favourited: boolean(),
16157
16199
  /** The lifecycle state the boolean was derived from. Required here (unlike on
16158
16200
  * a track row) because this shape is only ever produced by the write body,
16159
16201
  * which always knows it — and a surface that has just written needs to render
@@ -16786,6 +16828,12 @@ var TrackCascadeCountsSchema = object({
16786
16828
  /** Per-track CLIP search vectors removed (best-effort). */
16787
16829
  embeddings: number().int()
16788
16830
  });
16831
+ /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
16832
+ var DiskReconcileCountsSchema = object({
16833
+ mediaDropped: number().int(),
16834
+ tracks: number().int(),
16835
+ events: number().int()
16836
+ });
16789
16837
  /** Event-store footprint for one camera. */
16790
16838
  var EventStoreDeviceFootprintSchema = object({
16791
16839
  deviceId: number(),
@@ -16962,6 +17010,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
16962
17010
  }), method(object({ deviceId: number() }), TrackCascadeCountsSchema, {
16963
17011
  kind: "mutation",
16964
17012
  auth: "admin"
17013
+ }), method(object({ deviceId: number() }), DiskReconcileCountsSchema, {
17014
+ kind: "mutation",
17015
+ auth: "admin"
16965
17016
  }), method(object({
16966
17017
  deviceId: number(),
16967
17018
  trackIds: array(string()).min(1)
@@ -18472,6 +18523,24 @@ var CameraStatusDegradationSchema = object({
18472
18523
  *
18473
18524
  * See spec: `docs/superpowers/specs/2026-06-24-camera-status-aggregator-cap.md`
18474
18525
  */
18526
+ var DiskReconcileJobSchema = object({
18527
+ state: _enum([
18528
+ "idle",
18529
+ "running",
18530
+ "done",
18531
+ "error"
18532
+ ]),
18533
+ total: number().int().nonnegative(),
18534
+ completed: number().int().nonnegative(),
18535
+ currentDeviceId: number().int().nullable(),
18536
+ failed: array(number().int()).readonly(),
18537
+ mediaDropped: number().int().nonnegative(),
18538
+ tracks: number().int().nonnegative(),
18539
+ events: number().int().nonnegative(),
18540
+ startedAtMs: number().int().nullable(),
18541
+ finishedAtMs: number().int().nullable(),
18542
+ error: string().nullable()
18543
+ });
18475
18544
  var CameraStatusSchema = object({
18476
18545
  deviceId: number(),
18477
18546
  assignment: CameraAssignmentStatusSchema,
@@ -18703,7 +18772,10 @@ method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
18703
18772
  }), CameraSwitchGroupSchema, {
18704
18773
  kind: "mutation",
18705
18774
  auth: "admin"
18706
- }), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
18775
+ }), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), DiskReconcileJobSchema, {
18776
+ kind: "mutation",
18777
+ auth: "admin"
18778
+ }), method(_void(), DiskReconcileJobSchema, { auth: "admin" }), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
18707
18779
  name: string(),
18708
18780
  description: string().optional(),
18709
18781
  config: CameraPipelineConfigSchema
@@ -19821,20 +19893,6 @@ var VectorStatsResultSchema = object({
19821
19893
  exact: boolean()
19822
19894
  });
19823
19895
  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);
19824
- /**
19825
- * `videoclips` — the unified, navigable-clip surface for a camera.
19826
- *
19827
- * A device-scoped WRAPPER cap (like `pipeline-analytics`): exactly one active
19828
- * provider per device, substitutable. The DEFAULT provider (registered by
19829
- * `addon-post-analysis`, `defaultActive: true`) composes the analytics event
19830
- * log (markers + thumbnails) with the recorder's `getPlaybackManifest` — a clip
19831
- * is a time-WINDOW over existing footage, never a separate file. A camera that
19832
- * exposes NATIVE onboard clips (Reolink/Hikvision NVR) can later substitute the
19833
- * wrapper provider for its device and serve its own clip catalog + URLs.
19834
- *
19835
- * A `Clip` is purely time-based (subtree-blind): playback resolves segments by
19836
- * temporal overlap, so the API never decides `continuous` vs `events`.
19837
- */
19838
19896
  var ClipSchema = object({
19839
19897
  /** Opaque, provider-namespaced id. The default provider encodes the time
19840
19898
  * window so `getClipPlayback` is self-contained (no event re-query). */
@@ -19851,8 +19909,65 @@ var ClipSchema = object({
19851
19909
  startMs: number(),
19852
19910
  endMs: number()
19853
19911
  }),
19854
- /** Thumbnail URL (lazy; e.g. analytics `getEventMedia`). Never inlined. */
19855
- thumbnail: string().optional()
19912
+ /**
19913
+ * Distinct object classes attached to this visit (`person`, `car`, …),
19914
+ * dominant first, capped at {@link MAX_CLIP_LABELS}. The ribbon renders these
19915
+ * instead of the bare kind — "Object" tells the operator nothing a colour bar
19916
+ * did not. Absent (never empty) when the visit attached no classified object
19917
+ * event, so a motion/audio-only visit keeps its kind label.
19918
+ */
19919
+ labels: array(string()).max(3).optional(),
19920
+ /**
19921
+ * Lazy thumbnail URL, never inlined.
19922
+ *
19923
+ * Recording-derived clips (events-mode keep-window, and the prepared
19924
+ * continuous event+fragment visit) MUST use the snapshot of the **main
19925
+ * event of the interval** — `getEventMedia({ eventId, kind: 'snapshot' })`
19926
+ * of the event that owns `kind` (object > motion > audio). Do not extract
19927
+ * a keyframe from the recorded segments. Other providers (onboard, HKSV)
19928
+ * mint their own stills.
19929
+ *
19930
+ * **VOUCHED, never fabricated.** A provider emits this only for an event it
19931
+ * has CONFIRMED owns at least one media row (its own, or its owning track's).
19932
+ * Absent is meaningful — "this visit has no event still" — never "we did not
19933
+ * look". Stamping it from a URL template made 35% of one camera's clips point
19934
+ * at a 404 (device 3836, 2026-08-17: 64 of 179 clips dead, 63 of them motion).
19935
+ * A read that FAILS drops the claim; it never invents it.
19936
+ */
19937
+ thumbnail: string().optional(),
19938
+ /**
19939
+ * An instant INSIDE this visit's footage, hole-safe, where a recorded still
19940
+ * can be decoded. Present whenever the visit came from recorded availability;
19941
+ * absent on a per-event padded window (there is no footage to promise).
19942
+ *
19943
+ * This is not a thumbnail and not a second byte path: it is the argument to
19944
+ * the recorder's existing still route. The surface — never the provider —
19945
+ * decides whether to use it. It is the midpoint of the visit's LONGEST
19946
+ * contiguous range, not of the visit: a visit spans its holes by
19947
+ * construction, so a naive midpoint lands in dead air.
19948
+ */
19949
+ stillAtMs: number().optional(),
19950
+ /**
19951
+ * Analytics event ids that overlap this visit — a BOUNDED sample, newest
19952
+ * first within kind (object → motion → audio), capped at
19953
+ * {@link MAX_CLIP_EVENT_IDS}. Empty on footage-only clips.
19954
+ *
19955
+ * Bounded because it is not a payload the surface pages through: one visit on
19956
+ * device 615 carried 2 345 ids, and `eventIds` was 99% of a 153 KB
19957
+ * camera-day. Read {@link eventCount} for the true total.
19958
+ */
19959
+ eventIds: array(string()).optional(),
19960
+ /** How many analytics events actually overlap this visit. Differs from
19961
+ * `eventIds.length` exactly when the sample was capped — so a truncated
19962
+ * list is never mistaken for a quiet visit. */
19963
+ eventCount: number().int().nonnegative().optional(),
19964
+ /** Intra-visit footage holes (GOP rolls, discarded segments) still shorter
19965
+ * than `VISIT_MERGE_GAP_MS`. Playback concatenates around them; the timeline
19966
+ * bar keeps showing them via `recording.getAvailability`. */
19967
+ holes: array(object({
19968
+ startMs: number(),
19969
+ endMs: number()
19970
+ })).optional()
19856
19971
  });
19857
19972
  var ClipPlaybackSchema = object({
19858
19973
  /** HLS master URL through the hub data-plane (Range + token in path). */
@@ -20316,7 +20431,14 @@ var SearchResultSchema = object({
20316
20431
  });
20317
20432
  var AutoUpdateSettingsSchema = object({
20318
20433
  channel: ChannelSchema,
20319
- intervalSeconds: number()
20434
+ intervalSeconds: number(),
20435
+ /**
20436
+ * Cadence of the "an update exists" POLLER, in seconds. Independent of
20437
+ * `channel`: the poller runs while auto-apply is `off`, because being told
20438
+ * about a publish and installing it are different decisions. Clamped
20439
+ * server-side to 900 s … 604800 s; defaults to 21600 s (6 h).
20440
+ */
20441
+ updateCheckIntervalSeconds: number()
20320
20442
  });
20321
20443
  var AddonAutoUpdateSchema = ChannelWithInheritSchema;
20322
20444
  var RestartAddonResultSchema = unknown();
@@ -20457,7 +20579,9 @@ method(_void(), array(AddonListItemSchema).readonly()), method(object({
20457
20579
  auth: "admin"
20458
20580
  }), method(_void(), AutoUpdateSettingsSchema, { auth: "admin" }), method(object({
20459
20581
  channel: ChannelSchema,
20460
- intervalSeconds: number().min(300).max(86400).optional()
20582
+ intervalSeconds: number().min(300).max(86400).optional(),
20583
+ /** Availability-poll cadence; see AutoUpdateSettingsSchema. */
20584
+ updateCheckIntervalSeconds: number().min(900).max(604800).optional()
20461
20585
  }), unknown(), {
20462
20586
  kind: "mutation",
20463
20587
  auth: "admin"
@@ -24450,7 +24574,9 @@ var ExportOptionsSchema = object({
24450
24574
  includeAudio: boolean(),
24451
24575
  maxLifeMs: number().int().positive(),
24452
24576
  deleteAfterDownload: boolean(),
24453
- title: string().max(200).optional()
24577
+ title: string().max(200).optional(),
24578
+ /** Notification-output target ids to ping when this export becomes ready. */
24579
+ notifyTargetIds: array(string().min(1)).max(20).optional()
24454
24580
  }).superRefine((v, ctx) => {
24455
24581
  if (v.speed !== void 0 && v.timelapse !== void 0) ctx.addIssue({
24456
24582
  code: ZodIssueCode.custom,
@@ -24509,10 +24635,18 @@ var ExportBytesSchema = object({
24509
24635
  });
24510
24636
  method(object({
24511
24637
  deviceId: number(),
24512
- profile: string(),
24638
+ /** @deprecated Prefer `profiles`. Kept so timelapse/notifiers keep working. */
24639
+ profile: string().optional(),
24640
+ profiles: array(string()).min(1).optional(),
24513
24641
  fromMs: number(),
24514
24642
  toMs: number(),
24515
24643
  options: ExportOptionsSchema
24644
+ }).superRefine((v, ctx) => {
24645
+ if ((v.profiles !== void 0 && v.profiles.length > 0 ? v.profiles : v.profile !== void 0 ? [v.profile] : []).length < 1) ctx.addIssue({
24646
+ code: ZodIssueCode.custom,
24647
+ message: "pass profiles[] (min 1) or legacy profile",
24648
+ path: ["profiles"]
24649
+ });
24516
24650
  }), ExportRecordSchema, {
24517
24651
  kind: "mutation",
24518
24652
  auth: "protected"
@@ -29112,6 +29246,12 @@ Object.freeze({
29112
29246
  addonId: null,
29113
29247
  access: "create"
29114
29248
  },
29249
+ "pipelineAnalytics.reconcileFromDisk": {
29250
+ capName: "pipeline-analytics",
29251
+ capScope: "device",
29252
+ addonId: null,
29253
+ access: "create"
29254
+ },
29115
29255
  "pipelineAnalytics.refreshStorageLocationsForMigration": {
29116
29256
  capName: "pipeline-analytics",
29117
29257
  capScope: "device",
@@ -29514,6 +29654,12 @@ Object.freeze({
29514
29654
  addonId: null,
29515
29655
  access: "view"
29516
29656
  },
29657
+ "pipelineOrchestrator.getReconcileFromDiskStatus": {
29658
+ capName: "pipeline-orchestrator",
29659
+ capScope: "system",
29660
+ addonId: null,
29661
+ access: "view"
29662
+ },
29517
29663
  "pipelineOrchestrator.listAgentSettings": {
29518
29664
  capName: "pipeline-orchestrator",
29519
29665
  capScope: "system",
@@ -29538,6 +29684,12 @@ Object.freeze({
29538
29684
  addonId: null,
29539
29685
  access: "create"
29540
29686
  },
29687
+ "pipelineOrchestrator.reconcileFromDisk": {
29688
+ capName: "pipeline-orchestrator",
29689
+ capScope: "system",
29690
+ addonId: null,
29691
+ access: "create"
29692
+ },
29541
29693
  "pipelineOrchestrator.removeAgentSettings": {
29542
29694
  capName: "pipeline-orchestrator",
29543
29695
  capScope: "system",
@@ -32529,6 +32681,11 @@ Object.freeze({
32529
32681
  form: "single",
32530
32682
  optional: true
32531
32683
  }],
32684
+ "pipelineAnalytics.reconcileFromDisk": [{
32685
+ name: "deviceId",
32686
+ form: "single",
32687
+ optional: false
32688
+ }],
32532
32689
  "pipelineAnalytics.restageRetrainTrack": [{
32533
32690
  name: "deviceId",
32534
32691
  form: "single",
@@ -33594,6 +33751,8 @@ DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb;
33594
33751
  DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
33595
33752
  DEFAULT_NATIVE_LEASE_SETTINGS.tileBudgetMb;
33596
33753
  DEFAULT_NATIVE_LEASE_SETTINGS.admission;
33754
+ var MB = 1024 * 1024;
33755
+ 1024 * MB, 3072 * MB;
33597
33756
  //#endregion
33598
33757
  //#region src/shared.ts
33599
33758
  /**
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_shared = require("./shared-C7kobCEN.js");
5
+ const require_shared = require("./shared-CrotCkhF.js");
6
6
  let node_stream = require("node:stream");
7
7
  let webdav = require("webdav");
8
8
  //#region src/providers/webdav/webdav-config-schema.ts
@@ -1,4 +1,4 @@
1
- import { a as safeJoinRemotePath, c as BaseAddon, i as rearmIdleAbort, o as scheduleIdleAbort, r as getRequiredBasePath, s as storageProviderCapability, t as createSessionId } from "./shared-CN_vleSm.mjs";
1
+ import { a as safeJoinRemotePath, c as BaseAddon, i as rearmIdleAbort, o as scheduleIdleAbort, r as getRequiredBasePath, s as storageProviderCapability, t as createSessionId } from "./shared-Toqenw3n.mjs";
2
2
  import { PassThrough } from "node:stream";
3
3
  import { AuthType, createClient } from "webdav";
4
4
  //#region src/providers/webdav/webdav-config-schema.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-remote-storage",
3
- "version": "1.2.19",
3
+ "version": "1.2.21",
4
4
  "description": "Remote storage providers (SFTP, S3, WebDAV) — unifies remote backends behind the storage-provider cap",
5
5
  "keywords": [
6
6
  "camstack",