@camstack/addon-remote-storage 1.2.19 → 1.2.20

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-CGc-WGwO.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-C-bqmSqc.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-CGc-WGwO.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-C-bqmSqc.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
@@ -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,57 @@ 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
+ * Lazy thumbnail URL, never inlined.
19914
+ *
19915
+ * Recording-derived clips (events-mode keep-window, and the prepared
19916
+ * continuous event+fragment visit) MUST use the snapshot of the **main
19917
+ * event of the interval** — `getEventMedia({ eventId, kind: 'snapshot' })`
19918
+ * of the event that owns `kind` (object > motion > audio). Do not extract
19919
+ * a keyframe from the recorded segments. Other providers (onboard, HKSV)
19920
+ * mint their own stills.
19921
+ *
19922
+ * **VOUCHED, never fabricated.** A provider emits this only for an event it
19923
+ * has CONFIRMED owns at least one media row (its own, or its owning track's).
19924
+ * Absent is meaningful — "this visit has no event still" — never "we did not
19925
+ * look". Stamping it from a URL template made 35% of one camera's clips point
19926
+ * at a 404 (device 3836, 2026-08-17: 64 of 179 clips dead, 63 of them motion).
19927
+ * A read that FAILS drops the claim; it never invents it.
19928
+ */
19929
+ thumbnail: string().optional(),
19930
+ /**
19931
+ * An instant INSIDE this visit's footage, hole-safe, where a recorded still
19932
+ * can be decoded. Present whenever the visit came from recorded availability;
19933
+ * absent on a per-event padded window (there is no footage to promise).
19934
+ *
19935
+ * This is not a thumbnail and not a second byte path: it is the argument to
19936
+ * the recorder's existing still route. The surface — never the provider —
19937
+ * decides whether to use it. It is the midpoint of the visit's LONGEST
19938
+ * contiguous range, not of the visit: a visit spans its holes by
19939
+ * construction, so a naive midpoint lands in dead air.
19940
+ */
19941
+ stillAtMs: number().optional(),
19942
+ /**
19943
+ * Analytics event ids that overlap this visit — a BOUNDED sample, newest
19944
+ * first within kind (object → motion → audio), capped at
19945
+ * {@link MAX_CLIP_EVENT_IDS}. Empty on footage-only clips.
19946
+ *
19947
+ * Bounded because it is not a payload the surface pages through: one visit on
19948
+ * device 615 carried 2 345 ids, and `eventIds` was 99% of a 153 KB
19949
+ * camera-day. Read {@link eventCount} for the true total.
19950
+ */
19951
+ eventIds: array(string()).optional(),
19952
+ /** How many analytics events actually overlap this visit. Differs from
19953
+ * `eventIds.length` exactly when the sample was capped — so a truncated
19954
+ * list is never mistaken for a quiet visit. */
19955
+ eventCount: number().int().nonnegative().optional(),
19956
+ /** Intra-visit footage holes (GOP rolls, discarded segments) still shorter
19957
+ * than `VISIT_MERGE_GAP_MS`. Playback concatenates around them; the timeline
19958
+ * bar keeps showing them via `recording.getAvailability`. */
19959
+ holes: array(object({
19960
+ startMs: number(),
19961
+ endMs: number()
19962
+ })).optional()
19856
19963
  });
19857
19964
  var ClipPlaybackSchema = object({
19858
19965
  /** HLS master URL through the hub data-plane (Range + token in path). */
@@ -20316,7 +20423,14 @@ var SearchResultSchema = object({
20316
20423
  });
20317
20424
  var AutoUpdateSettingsSchema = object({
20318
20425
  channel: ChannelSchema,
20319
- intervalSeconds: number()
20426
+ intervalSeconds: number(),
20427
+ /**
20428
+ * Cadence of the "an update exists" POLLER, in seconds. Independent of
20429
+ * `channel`: the poller runs while auto-apply is `off`, because being told
20430
+ * about a publish and installing it are different decisions. Clamped
20431
+ * server-side to 900 s … 604800 s; defaults to 21600 s (6 h).
20432
+ */
20433
+ updateCheckIntervalSeconds: number()
20320
20434
  });
20321
20435
  var AddonAutoUpdateSchema = ChannelWithInheritSchema;
20322
20436
  var RestartAddonResultSchema = unknown();
@@ -20457,7 +20571,9 @@ method(_void(), array(AddonListItemSchema).readonly()), method(object({
20457
20571
  auth: "admin"
20458
20572
  }), method(_void(), AutoUpdateSettingsSchema, { auth: "admin" }), method(object({
20459
20573
  channel: ChannelSchema,
20460
- intervalSeconds: number().min(300).max(86400).optional()
20574
+ intervalSeconds: number().min(300).max(86400).optional(),
20575
+ /** Availability-poll cadence; see AutoUpdateSettingsSchema. */
20576
+ updateCheckIntervalSeconds: number().min(900).max(604800).optional()
20461
20577
  }), unknown(), {
20462
20578
  kind: "mutation",
20463
20579
  auth: "admin"
@@ -24450,7 +24566,9 @@ var ExportOptionsSchema = object({
24450
24566
  includeAudio: boolean(),
24451
24567
  maxLifeMs: number().int().positive(),
24452
24568
  deleteAfterDownload: boolean(),
24453
- title: string().max(200).optional()
24569
+ title: string().max(200).optional(),
24570
+ /** Notification-output target ids to ping when this export becomes ready. */
24571
+ notifyTargetIds: array(string().min(1)).max(20).optional()
24454
24572
  }).superRefine((v, ctx) => {
24455
24573
  if (v.speed !== void 0 && v.timelapse !== void 0) ctx.addIssue({
24456
24574
  code: ZodIssueCode.custom,
@@ -24509,10 +24627,18 @@ var ExportBytesSchema = object({
24509
24627
  });
24510
24628
  method(object({
24511
24629
  deviceId: number(),
24512
- profile: string(),
24630
+ /** @deprecated Prefer `profiles`. Kept so timelapse/notifiers keep working. */
24631
+ profile: string().optional(),
24632
+ profiles: array(string()).min(1).optional(),
24513
24633
  fromMs: number(),
24514
24634
  toMs: number(),
24515
24635
  options: ExportOptionsSchema
24636
+ }).superRefine((v, ctx) => {
24637
+ if ((v.profiles !== void 0 && v.profiles.length > 0 ? v.profiles : v.profile !== void 0 ? [v.profile] : []).length < 1) ctx.addIssue({
24638
+ code: ZodIssueCode.custom,
24639
+ message: "pass profiles[] (min 1) or legacy profile",
24640
+ path: ["profiles"]
24641
+ });
24516
24642
  }), ExportRecordSchema, {
24517
24643
  kind: "mutation",
24518
24644
  auth: "protected"
@@ -29112,6 +29238,12 @@ Object.freeze({
29112
29238
  addonId: null,
29113
29239
  access: "create"
29114
29240
  },
29241
+ "pipelineAnalytics.reconcileFromDisk": {
29242
+ capName: "pipeline-analytics",
29243
+ capScope: "device",
29244
+ addonId: null,
29245
+ access: "create"
29246
+ },
29115
29247
  "pipelineAnalytics.refreshStorageLocationsForMigration": {
29116
29248
  capName: "pipeline-analytics",
29117
29249
  capScope: "device",
@@ -29514,6 +29646,12 @@ Object.freeze({
29514
29646
  addonId: null,
29515
29647
  access: "view"
29516
29648
  },
29649
+ "pipelineOrchestrator.getReconcileFromDiskStatus": {
29650
+ capName: "pipeline-orchestrator",
29651
+ capScope: "system",
29652
+ addonId: null,
29653
+ access: "view"
29654
+ },
29517
29655
  "pipelineOrchestrator.listAgentSettings": {
29518
29656
  capName: "pipeline-orchestrator",
29519
29657
  capScope: "system",
@@ -29538,6 +29676,12 @@ Object.freeze({
29538
29676
  addonId: null,
29539
29677
  access: "create"
29540
29678
  },
29679
+ "pipelineOrchestrator.reconcileFromDisk": {
29680
+ capName: "pipeline-orchestrator",
29681
+ capScope: "system",
29682
+ addonId: null,
29683
+ access: "create"
29684
+ },
29541
29685
  "pipelineOrchestrator.removeAgentSettings": {
29542
29686
  capName: "pipeline-orchestrator",
29543
29687
  capScope: "system",
@@ -32529,6 +32673,11 @@ Object.freeze({
32529
32673
  form: "single",
32530
32674
  optional: true
32531
32675
  }],
32676
+ "pipelineAnalytics.reconcileFromDisk": [{
32677
+ name: "deviceId",
32678
+ form: "single",
32679
+ optional: false
32680
+ }],
32532
32681
  "pipelineAnalytics.restageRetrainTrack": [{
32533
32682
  name: "deviceId",
32534
32683
  form: "single",
@@ -33594,6 +33743,8 @@ DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb;
33594
33743
  DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
33595
33744
  DEFAULT_NATIVE_LEASE_SETTINGS.tileBudgetMb;
33596
33745
  DEFAULT_NATIVE_LEASE_SETTINGS.admission;
33746
+ var MB = 1024 * 1024;
33747
+ 1024 * MB, 3072 * MB;
33597
33748
  //#endregion
33598
33749
  //#region src/shared.ts
33599
33750
  /**
@@ -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,57 @@ 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
+ * Lazy thumbnail URL, never inlined.
19937
+ *
19938
+ * Recording-derived clips (events-mode keep-window, and the prepared
19939
+ * continuous event+fragment visit) MUST use the snapshot of the **main
19940
+ * event of the interval** — `getEventMedia({ eventId, kind: 'snapshot' })`
19941
+ * of the event that owns `kind` (object > motion > audio). Do not extract
19942
+ * a keyframe from the recorded segments. Other providers (onboard, HKSV)
19943
+ * mint their own stills.
19944
+ *
19945
+ * **VOUCHED, never fabricated.** A provider emits this only for an event it
19946
+ * has CONFIRMED owns at least one media row (its own, or its owning track's).
19947
+ * Absent is meaningful — "this visit has no event still" — never "we did not
19948
+ * look". Stamping it from a URL template made 35% of one camera's clips point
19949
+ * at a 404 (device 3836, 2026-08-17: 64 of 179 clips dead, 63 of them motion).
19950
+ * A read that FAILS drops the claim; it never invents it.
19951
+ */
19952
+ thumbnail: string().optional(),
19953
+ /**
19954
+ * An instant INSIDE this visit's footage, hole-safe, where a recorded still
19955
+ * can be decoded. Present whenever the visit came from recorded availability;
19956
+ * absent on a per-event padded window (there is no footage to promise).
19957
+ *
19958
+ * This is not a thumbnail and not a second byte path: it is the argument to
19959
+ * the recorder's existing still route. The surface — never the provider —
19960
+ * decides whether to use it. It is the midpoint of the visit's LONGEST
19961
+ * contiguous range, not of the visit: a visit spans its holes by
19962
+ * construction, so a naive midpoint lands in dead air.
19963
+ */
19964
+ stillAtMs: number().optional(),
19965
+ /**
19966
+ * Analytics event ids that overlap this visit — a BOUNDED sample, newest
19967
+ * first within kind (object → motion → audio), capped at
19968
+ * {@link MAX_CLIP_EVENT_IDS}. Empty on footage-only clips.
19969
+ *
19970
+ * Bounded because it is not a payload the surface pages through: one visit on
19971
+ * device 615 carried 2 345 ids, and `eventIds` was 99% of a 153 KB
19972
+ * camera-day. Read {@link eventCount} for the true total.
19973
+ */
19974
+ eventIds: array(string()).optional(),
19975
+ /** How many analytics events actually overlap this visit. Differs from
19976
+ * `eventIds.length` exactly when the sample was capped — so a truncated
19977
+ * list is never mistaken for a quiet visit. */
19978
+ eventCount: number().int().nonnegative().optional(),
19979
+ /** Intra-visit footage holes (GOP rolls, discarded segments) still shorter
19980
+ * than `VISIT_MERGE_GAP_MS`. Playback concatenates around them; the timeline
19981
+ * bar keeps showing them via `recording.getAvailability`. */
19982
+ holes: array(object({
19983
+ startMs: number(),
19984
+ endMs: number()
19985
+ })).optional()
19879
19986
  });
19880
19987
  var ClipPlaybackSchema = object({
19881
19988
  /** HLS master URL through the hub data-plane (Range + token in path). */
@@ -20339,7 +20446,14 @@ var SearchResultSchema = object({
20339
20446
  });
20340
20447
  var AutoUpdateSettingsSchema = object({
20341
20448
  channel: ChannelSchema,
20342
- intervalSeconds: number()
20449
+ intervalSeconds: number(),
20450
+ /**
20451
+ * Cadence of the "an update exists" POLLER, in seconds. Independent of
20452
+ * `channel`: the poller runs while auto-apply is `off`, because being told
20453
+ * about a publish and installing it are different decisions. Clamped
20454
+ * server-side to 900 s … 604800 s; defaults to 21600 s (6 h).
20455
+ */
20456
+ updateCheckIntervalSeconds: number()
20343
20457
  });
20344
20458
  var AddonAutoUpdateSchema = ChannelWithInheritSchema;
20345
20459
  var RestartAddonResultSchema = unknown();
@@ -20480,7 +20594,9 @@ method(_void(), array(AddonListItemSchema).readonly()), method(object({
20480
20594
  auth: "admin"
20481
20595
  }), method(_void(), AutoUpdateSettingsSchema, { auth: "admin" }), method(object({
20482
20596
  channel: ChannelSchema,
20483
- intervalSeconds: number().min(300).max(86400).optional()
20597
+ intervalSeconds: number().min(300).max(86400).optional(),
20598
+ /** Availability-poll cadence; see AutoUpdateSettingsSchema. */
20599
+ updateCheckIntervalSeconds: number().min(900).max(604800).optional()
20484
20600
  }), unknown(), {
20485
20601
  kind: "mutation",
20486
20602
  auth: "admin"
@@ -24473,7 +24589,9 @@ var ExportOptionsSchema = object({
24473
24589
  includeAudio: boolean(),
24474
24590
  maxLifeMs: number().int().positive(),
24475
24591
  deleteAfterDownload: boolean(),
24476
- title: string().max(200).optional()
24592
+ title: string().max(200).optional(),
24593
+ /** Notification-output target ids to ping when this export becomes ready. */
24594
+ notifyTargetIds: array(string().min(1)).max(20).optional()
24477
24595
  }).superRefine((v, ctx) => {
24478
24596
  if (v.speed !== void 0 && v.timelapse !== void 0) ctx.addIssue({
24479
24597
  code: ZodIssueCode.custom,
@@ -24532,10 +24650,18 @@ var ExportBytesSchema = object({
24532
24650
  });
24533
24651
  method(object({
24534
24652
  deviceId: number(),
24535
- profile: string(),
24653
+ /** @deprecated Prefer `profiles`. Kept so timelapse/notifiers keep working. */
24654
+ profile: string().optional(),
24655
+ profiles: array(string()).min(1).optional(),
24536
24656
  fromMs: number(),
24537
24657
  toMs: number(),
24538
24658
  options: ExportOptionsSchema
24659
+ }).superRefine((v, ctx) => {
24660
+ if ((v.profiles !== void 0 && v.profiles.length > 0 ? v.profiles : v.profile !== void 0 ? [v.profile] : []).length < 1) ctx.addIssue({
24661
+ code: ZodIssueCode.custom,
24662
+ message: "pass profiles[] (min 1) or legacy profile",
24663
+ path: ["profiles"]
24664
+ });
24539
24665
  }), ExportRecordSchema, {
24540
24666
  kind: "mutation",
24541
24667
  auth: "protected"
@@ -29135,6 +29261,12 @@ Object.freeze({
29135
29261
  addonId: null,
29136
29262
  access: "create"
29137
29263
  },
29264
+ "pipelineAnalytics.reconcileFromDisk": {
29265
+ capName: "pipeline-analytics",
29266
+ capScope: "device",
29267
+ addonId: null,
29268
+ access: "create"
29269
+ },
29138
29270
  "pipelineAnalytics.refreshStorageLocationsForMigration": {
29139
29271
  capName: "pipeline-analytics",
29140
29272
  capScope: "device",
@@ -29537,6 +29669,12 @@ Object.freeze({
29537
29669
  addonId: null,
29538
29670
  access: "view"
29539
29671
  },
29672
+ "pipelineOrchestrator.getReconcileFromDiskStatus": {
29673
+ capName: "pipeline-orchestrator",
29674
+ capScope: "system",
29675
+ addonId: null,
29676
+ access: "view"
29677
+ },
29540
29678
  "pipelineOrchestrator.listAgentSettings": {
29541
29679
  capName: "pipeline-orchestrator",
29542
29680
  capScope: "system",
@@ -29561,6 +29699,12 @@ Object.freeze({
29561
29699
  addonId: null,
29562
29700
  access: "create"
29563
29701
  },
29702
+ "pipelineOrchestrator.reconcileFromDisk": {
29703
+ capName: "pipeline-orchestrator",
29704
+ capScope: "system",
29705
+ addonId: null,
29706
+ access: "create"
29707
+ },
29564
29708
  "pipelineOrchestrator.removeAgentSettings": {
29565
29709
  capName: "pipeline-orchestrator",
29566
29710
  capScope: "system",
@@ -32552,6 +32696,11 @@ Object.freeze({
32552
32696
  form: "single",
32553
32697
  optional: true
32554
32698
  }],
32699
+ "pipelineAnalytics.reconcileFromDisk": [{
32700
+ name: "deviceId",
32701
+ form: "single",
32702
+ optional: false
32703
+ }],
32555
32704
  "pipelineAnalytics.restageRetrainTrack": [{
32556
32705
  name: "deviceId",
32557
32706
  form: "single",
@@ -33617,6 +33766,8 @@ DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb;
33617
33766
  DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
33618
33767
  DEFAULT_NATIVE_LEASE_SETTINGS.tileBudgetMb;
33619
33768
  DEFAULT_NATIVE_LEASE_SETTINGS.admission;
33769
+ var MB = 1024 * 1024;
33770
+ 1024 * MB, 3072 * MB;
33620
33771
  //#endregion
33621
33772
  //#region src/shared.ts
33622
33773
  /**
@@ -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-CGc-WGwO.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-C-bqmSqc.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.20",
4
4
  "description": "Remote storage providers (SFTP, S3, WebDAV) — unifies remote backends behind the storage-provider cap",
5
5
  "keywords": [
6
6
  "camstack",