@camstack/addon-export-hap 1.2.31 → 1.2.33

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.
@@ -84,7 +84,7 @@ function carryForward(base, existing, keys) {
84
84
  return out;
85
85
  }
86
86
  //#endregion
87
- //#region ../types/dist/event-category-Bxo5yJjt.mjs
87
+ //#region ../types/dist/event-category-XfKNtfCc.mjs
88
88
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
89
89
  EventCategory["SystemBoot"] = "system.boot";
90
90
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -101,9 +101,10 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
101
101
  EventCategory["SystemRestartCompleted"] = "system.restart-completed";
102
102
  /**
103
103
  * A newer addon or server-root package version was found by the
104
- * authoritative registry check. Emitted once per
105
- * `(target, packageName, currentVersion, latestVersion)` transition; repeated
106
- * polling of the same result is deduplicated by the checker.
104
+ * authoritative registry check. Emitted once when any observed
105
+ * `latestVersion` changes (or a package/node first appears behind);
106
+ * the payload carries the full currently-available list. Repeated
107
+ * polling of the same latests is silent.
107
108
  */
108
109
  EventCategory["UpdateAvailable"] = "update.available";
109
110
  /**
@@ -122,6 +123,22 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
122
123
  EventCategory["AddonInstalled"] = "addon.installed";
123
124
  EventCategory["AddonUninstalled"] = "addon.uninstalled";
124
125
  EventCategory["AddonCrashed"] = "addon.crashed";
126
+ /**
127
+ * A RUNNER the D6 crash circuit-breaker gave up on — terminal.
128
+ *
129
+ * `AddonCrashed` is the routine, self-healing fact ("it crashed; it is being
130
+ * respawned"); this is the one that never resolves itself. Emitted exactly
131
+ * once per trip, by `process-service.ts`, carrying the node, the runner, the
132
+ * addons it hosted and the crash count that tripped the breaker.
133
+ *
134
+ * It exists because a runner marked terminally `failed` used to be SILENT:
135
+ * on 2026-08-18 the `recorder` runner died of three unhandled ffmpeg spawn
136
+ * errors, the breaker stopped respawning it (correctly), `/health` kept
137
+ * returning 200, and fleet-wide recording was gone for 3h15 before the
138
+ * operator's phone told him. The Notification Center's system-event intake
139
+ * consumes this category and maps it to the `addon-crash-loop` kind.
140
+ */
141
+ EventCategory["AddonRunnerFailed"] = "addon.runner-failed";
125
142
  EventCategory["AddonError"] = "addon.error";
126
143
  EventCategory["AddonPageReady"] = "addon.page-ready";
127
144
  EventCategory["AddonWidgetReady"] = "addon.widget-ready";
@@ -8214,6 +8231,10 @@ var RecordingBandSchema = object({
8214
8231
  preBufferSec: number().min(0).optional(),
8215
8232
  postBufferSec: number().min(0).optional()
8216
8233
  });
8234
+ ({
8235
+ preBufferSec: 10,
8236
+ postBufferSec: 30
8237
+ }).postBufferSec * 1e3;
8217
8238
  /**
8218
8239
  * Per-device retention overrides. Every field is optional; an unset or `0`
8219
8240
  * value inherits the node-wide recorder default. Only footage-lifetime limits
@@ -8261,6 +8282,12 @@ var RecordingConfigSchema = object({
8261
8282
  /** DERIVED summary of `bands`, stamped by the recorder on every save.
8262
8283
  * Authoring it has no effect — see {@link RecordingStorageModeSchema}. */
8263
8284
  mode: RecordingStorageModeSchema.optional(),
8285
+ /**
8286
+ * Which assigned broker slots to record. Absent / empty = {@link
8287
+ * DEFAULT_RECORDING_PROFILES} (`high`+`low`) intersected with the
8288
+ * camera's currently assigned slots — never `mid` unless the operator
8289
+ * picks it, and never a slot the broker has not assigned.
8290
+ */
8264
8291
  profiles: array(CamProfileSchema).optional(),
8265
8292
  segmentSeconds: number().int().positive().optional(),
8266
8293
  /**
@@ -8341,7 +8368,11 @@ var RelocateFootageInputSchema = object({
8341
8368
  profiles: array(string()).optional(),
8342
8369
  /** Copy throttle in MB/s (default 40) — the drain is a background chore,
8343
8370
  * never allowed to starve live writers. */
8344
- throttleMbps: number().min(1).max(1e3).optional()
8371
+ throttleMbps: number().min(1).max(1e3).optional(),
8372
+ /** Move only segments whose startMs is >= this. Absent = the whole source
8373
+ * pile. Used when a full drain is too expensive and the operator only
8374
+ * wants the recent window on the new disk. */
8375
+ sinceMs: number().int().optional()
8345
8376
  });
8346
8377
  /** Internal, lease-scoped participant operation. It is intentionally separate
8347
8378
  * from persistent recording settings: a migration never changes
@@ -15039,6 +15070,7 @@ var NcSystemEventKindSchema = _enum([
15039
15070
  "node-offline",
15040
15071
  "node-inference-unavailable",
15041
15072
  "detection-blind",
15073
+ "addon-crash-loop",
15042
15074
  "addon-update-available",
15043
15075
  "server-update-available",
15044
15076
  "alarm-triggered",
@@ -15046,6 +15078,9 @@ var NcSystemEventKindSchema = _enum([
15046
15078
  "alarm-disarmed",
15047
15079
  "alarm-arming",
15048
15080
  "alarm-arm-refused",
15081
+ "addon-updated",
15082
+ "server-updated",
15083
+ "export-completed",
15049
15084
  "camera-online",
15050
15085
  "camera-offline",
15051
15086
  "camera-disabled",
@@ -16939,13 +16974,19 @@ var RetrainStatusSchema = _enum([
16939
16974
  * "never marked" from "already trained" must read `retrainStatus`.
16940
16975
  *
16941
16976
  * `debug` does NOT pin; it is attention, not durability.
16977
+ *
16978
+ * `favourited` IS a BOOLEAN pin (durability bit), not a retrain lifecycle.
16979
+ * A favourited track is skipped by retention the same way `staging` is, but
16980
+ * it does not enter `none|staging|trained` and has no staging budget.
16942
16981
  */
16943
16982
  var TrackFlagFields = {
16944
16983
  /** Operator marked this track as training material — i.e. `retrainStatus` is
16945
16984
  * `'staging'`. */
16946
16985
  markForTrain: boolean().optional(),
16947
16986
  /** Operator marked this track for diagnostic attention. */
16948
- debug: boolean().optional()
16987
+ debug: boolean().optional(),
16988
+ /** Operator favourited this track. Pins it against pruning. */
16989
+ favourited: boolean().optional()
16949
16990
  };
16950
16991
  /**
16951
16992
  * The lifecycle field itself, on the READ surfaces only (`Track`, `KeyEvent`).
@@ -16970,6 +17011,7 @@ var TrackFlagsSchema = object({
16970
17011
  trackId: string(),
16971
17012
  markForTrain: boolean(),
16972
17013
  debug: boolean(),
17014
+ favourited: boolean(),
16973
17015
  /** The lifecycle state the boolean was derived from. Required here (unlike on
16974
17016
  * a track row) because this shape is only ever produced by the write body,
16975
17017
  * which always knows it — and a surface that has just written needs to render
@@ -17602,6 +17644,12 @@ var TrackCascadeCountsSchema = object({
17602
17644
  /** Per-track CLIP search vectors removed (best-effort). */
17603
17645
  embeddings: number().int()
17604
17646
  });
17647
+ /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17648
+ var DiskReconcileCountsSchema = object({
17649
+ mediaDropped: number().int(),
17650
+ tracks: number().int(),
17651
+ events: number().int()
17652
+ });
17605
17653
  /** Event-store footprint for one camera. */
17606
17654
  var EventStoreDeviceFootprintSchema = object({
17607
17655
  deviceId: number(),
@@ -17778,6 +17826,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17778
17826
  }), method(object({ deviceId: number() }), TrackCascadeCountsSchema, {
17779
17827
  kind: "mutation",
17780
17828
  auth: "admin"
17829
+ }), method(object({ deviceId: number() }), DiskReconcileCountsSchema, {
17830
+ kind: "mutation",
17831
+ auth: "admin"
17781
17832
  }), method(object({
17782
17833
  deviceId: number(),
17783
17834
  trackIds: array(string()).min(1)
@@ -19288,6 +19339,24 @@ var CameraStatusDegradationSchema = object({
19288
19339
  *
19289
19340
  * See spec: `docs/superpowers/specs/2026-06-24-camera-status-aggregator-cap.md`
19290
19341
  */
19342
+ var DiskReconcileJobSchema = object({
19343
+ state: _enum([
19344
+ "idle",
19345
+ "running",
19346
+ "done",
19347
+ "error"
19348
+ ]),
19349
+ total: number().int().nonnegative(),
19350
+ completed: number().int().nonnegative(),
19351
+ currentDeviceId: number().int().nullable(),
19352
+ failed: array(number().int()).readonly(),
19353
+ mediaDropped: number().int().nonnegative(),
19354
+ tracks: number().int().nonnegative(),
19355
+ events: number().int().nonnegative(),
19356
+ startedAtMs: number().int().nullable(),
19357
+ finishedAtMs: number().int().nullable(),
19358
+ error: string().nullable()
19359
+ });
19291
19360
  var CameraStatusSchema = object({
19292
19361
  deviceId: number(),
19293
19362
  assignment: CameraAssignmentStatusSchema,
@@ -19519,7 +19588,10 @@ method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
19519
19588
  }), CameraSwitchGroupSchema, {
19520
19589
  kind: "mutation",
19521
19590
  auth: "admin"
19522
- }), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
19591
+ }), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), DiskReconcileJobSchema, {
19592
+ kind: "mutation",
19593
+ auth: "admin"
19594
+ }), method(_void(), DiskReconcileJobSchema, { auth: "admin" }), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
19523
19595
  name: string(),
19524
19596
  description: string().optional(),
19525
19597
  config: CameraPipelineConfigSchema
@@ -20592,20 +20664,6 @@ var VectorStatsResultSchema = object({
20592
20664
  exact: boolean()
20593
20665
  });
20594
20666
  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);
20595
- /**
20596
- * `videoclips` — the unified, navigable-clip surface for a camera.
20597
- *
20598
- * A device-scoped WRAPPER cap (like `pipeline-analytics`): exactly one active
20599
- * provider per device, substitutable. The DEFAULT provider (registered by
20600
- * `addon-post-analysis`, `defaultActive: true`) composes the analytics event
20601
- * log (markers + thumbnails) with the recorder's `getPlaybackManifest` — a clip
20602
- * is a time-WINDOW over existing footage, never a separate file. A camera that
20603
- * exposes NATIVE onboard clips (Reolink/Hikvision NVR) can later substitute the
20604
- * wrapper provider for its device and serve its own clip catalog + URLs.
20605
- *
20606
- * A `Clip` is purely time-based (subtree-blind): playback resolves segments by
20607
- * temporal overlap, so the API never decides `continuous` vs `events`.
20608
- */
20609
20667
  var ClipSchema = object({
20610
20668
  /** Opaque, provider-namespaced id. The default provider encodes the time
20611
20669
  * window so `getClipPlayback` is self-contained (no event re-query). */
@@ -20622,8 +20680,65 @@ var ClipSchema = object({
20622
20680
  startMs: number(),
20623
20681
  endMs: number()
20624
20682
  }),
20625
- /** Thumbnail URL (lazy; e.g. analytics `getEventMedia`). Never inlined. */
20626
- thumbnail: string().optional()
20683
+ /**
20684
+ * Distinct object classes attached to this visit (`person`, `car`, …),
20685
+ * dominant first, capped at {@link MAX_CLIP_LABELS}. The ribbon renders these
20686
+ * instead of the bare kind — "Object" tells the operator nothing a colour bar
20687
+ * did not. Absent (never empty) when the visit attached no classified object
20688
+ * event, so a motion/audio-only visit keeps its kind label.
20689
+ */
20690
+ labels: array(string()).max(3).optional(),
20691
+ /**
20692
+ * Lazy thumbnail URL, never inlined.
20693
+ *
20694
+ * Recording-derived clips (events-mode keep-window, and the prepared
20695
+ * continuous event+fragment visit) MUST use the snapshot of the **main
20696
+ * event of the interval** — `getEventMedia({ eventId, kind: 'snapshot' })`
20697
+ * of the event that owns `kind` (object > motion > audio). Do not extract
20698
+ * a keyframe from the recorded segments. Other providers (onboard, HKSV)
20699
+ * mint their own stills.
20700
+ *
20701
+ * **VOUCHED, never fabricated.** A provider emits this only for an event it
20702
+ * has CONFIRMED owns at least one media row (its own, or its owning track's).
20703
+ * Absent is meaningful — "this visit has no event still" — never "we did not
20704
+ * look". Stamping it from a URL template made 35% of one camera's clips point
20705
+ * at a 404 (device 3836, 2026-08-17: 64 of 179 clips dead, 63 of them motion).
20706
+ * A read that FAILS drops the claim; it never invents it.
20707
+ */
20708
+ thumbnail: string().optional(),
20709
+ /**
20710
+ * An instant INSIDE this visit's footage, hole-safe, where a recorded still
20711
+ * can be decoded. Present whenever the visit came from recorded availability;
20712
+ * absent on a per-event padded window (there is no footage to promise).
20713
+ *
20714
+ * This is not a thumbnail and not a second byte path: it is the argument to
20715
+ * the recorder's existing still route. The surface — never the provider —
20716
+ * decides whether to use it. It is the midpoint of the visit's LONGEST
20717
+ * contiguous range, not of the visit: a visit spans its holes by
20718
+ * construction, so a naive midpoint lands in dead air.
20719
+ */
20720
+ stillAtMs: number().optional(),
20721
+ /**
20722
+ * Analytics event ids that overlap this visit — a BOUNDED sample, newest
20723
+ * first within kind (object → motion → audio), capped at
20724
+ * {@link MAX_CLIP_EVENT_IDS}. Empty on footage-only clips.
20725
+ *
20726
+ * Bounded because it is not a payload the surface pages through: one visit on
20727
+ * device 615 carried 2 345 ids, and `eventIds` was 99% of a 153 KB
20728
+ * camera-day. Read {@link eventCount} for the true total.
20729
+ */
20730
+ eventIds: array(string()).optional(),
20731
+ /** How many analytics events actually overlap this visit. Differs from
20732
+ * `eventIds.length` exactly when the sample was capped — so a truncated
20733
+ * list is never mistaken for a quiet visit. */
20734
+ eventCount: number().int().nonnegative().optional(),
20735
+ /** Intra-visit footage holes (GOP rolls, discarded segments) still shorter
20736
+ * than `VISIT_MERGE_GAP_MS`. Playback concatenates around them; the timeline
20737
+ * bar keeps showing them via `recording.getAvailability`. */
20738
+ holes: array(object({
20739
+ startMs: number(),
20740
+ endMs: number()
20741
+ })).optional()
20627
20742
  });
20628
20743
  var ClipPlaybackSchema = object({
20629
20744
  /** HLS master URL through the hub data-plane (Range + token in path). */
@@ -21087,7 +21202,14 @@ var SearchResultSchema = object({
21087
21202
  });
21088
21203
  var AutoUpdateSettingsSchema = object({
21089
21204
  channel: ChannelSchema,
21090
- intervalSeconds: number()
21205
+ intervalSeconds: number(),
21206
+ /**
21207
+ * Cadence of the "an update exists" POLLER, in seconds. Independent of
21208
+ * `channel`: the poller runs while auto-apply is `off`, because being told
21209
+ * about a publish and installing it are different decisions. Clamped
21210
+ * server-side to 900 s … 604800 s; defaults to 21600 s (6 h).
21211
+ */
21212
+ updateCheckIntervalSeconds: number()
21091
21213
  });
21092
21214
  var AddonAutoUpdateSchema = ChannelWithInheritSchema;
21093
21215
  var RestartAddonResultSchema = unknown();
@@ -21228,7 +21350,9 @@ method(_void(), array(AddonListItemSchema).readonly()), method(object({
21228
21350
  auth: "admin"
21229
21351
  }), method(_void(), AutoUpdateSettingsSchema, { auth: "admin" }), method(object({
21230
21352
  channel: ChannelSchema,
21231
- intervalSeconds: number().min(300).max(86400).optional()
21353
+ intervalSeconds: number().min(300).max(86400).optional(),
21354
+ /** Availability-poll cadence; see AutoUpdateSettingsSchema. */
21355
+ updateCheckIntervalSeconds: number().min(900).max(604800).optional()
21232
21356
  }), unknown(), {
21233
21357
  kind: "mutation",
21234
21358
  auth: "admin"
@@ -25269,7 +25393,9 @@ var ExportOptionsSchema = object({
25269
25393
  includeAudio: boolean(),
25270
25394
  maxLifeMs: number().int().positive(),
25271
25395
  deleteAfterDownload: boolean(),
25272
- title: string().max(200).optional()
25396
+ title: string().max(200).optional(),
25397
+ /** Notification-output target ids to ping when this export becomes ready. */
25398
+ notifyTargetIds: array(string().min(1)).max(20).optional()
25273
25399
  }).superRefine((v, ctx) => {
25274
25400
  if (v.speed !== void 0 && v.timelapse !== void 0) ctx.addIssue({
25275
25401
  code: ZodIssueCode.custom,
@@ -25328,10 +25454,18 @@ var ExportBytesSchema = object({
25328
25454
  });
25329
25455
  method(object({
25330
25456
  deviceId: number(),
25331
- profile: string(),
25457
+ /** @deprecated Prefer `profiles`. Kept so timelapse/notifiers keep working. */
25458
+ profile: string().optional(),
25459
+ profiles: array(string()).min(1).optional(),
25332
25460
  fromMs: number(),
25333
25461
  toMs: number(),
25334
25462
  options: ExportOptionsSchema
25463
+ }).superRefine((v, ctx) => {
25464
+ if ((v.profiles !== void 0 && v.profiles.length > 0 ? v.profiles : v.profile !== void 0 ? [v.profile] : []).length < 1) ctx.addIssue({
25465
+ code: ZodIssueCode.custom,
25466
+ message: "pass profiles[] (min 1) or legacy profile",
25467
+ path: ["profiles"]
25468
+ });
25335
25469
  }), ExportRecordSchema, {
25336
25470
  kind: "mutation",
25337
25471
  auth: "protected"
@@ -29956,6 +30090,12 @@ Object.freeze({
29956
30090
  addonId: null,
29957
30091
  access: "create"
29958
30092
  },
30093
+ "pipelineAnalytics.reconcileFromDisk": {
30094
+ capName: "pipeline-analytics",
30095
+ capScope: "device",
30096
+ addonId: null,
30097
+ access: "create"
30098
+ },
29959
30099
  "pipelineAnalytics.refreshStorageLocationsForMigration": {
29960
30100
  capName: "pipeline-analytics",
29961
30101
  capScope: "device",
@@ -30358,6 +30498,12 @@ Object.freeze({
30358
30498
  addonId: null,
30359
30499
  access: "view"
30360
30500
  },
30501
+ "pipelineOrchestrator.getReconcileFromDiskStatus": {
30502
+ capName: "pipeline-orchestrator",
30503
+ capScope: "system",
30504
+ addonId: null,
30505
+ access: "view"
30506
+ },
30361
30507
  "pipelineOrchestrator.listAgentSettings": {
30362
30508
  capName: "pipeline-orchestrator",
30363
30509
  capScope: "system",
@@ -30382,6 +30528,12 @@ Object.freeze({
30382
30528
  addonId: null,
30383
30529
  access: "create"
30384
30530
  },
30531
+ "pipelineOrchestrator.reconcileFromDisk": {
30532
+ capName: "pipeline-orchestrator",
30533
+ capScope: "system",
30534
+ addonId: null,
30535
+ access: "create"
30536
+ },
30385
30537
  "pipelineOrchestrator.removeAgentSettings": {
30386
30538
  capName: "pipeline-orchestrator",
30387
30539
  capScope: "system",
@@ -33373,6 +33525,11 @@ Object.freeze({
33373
33525
  form: "single",
33374
33526
  optional: true
33375
33527
  }],
33528
+ "pipelineAnalytics.reconcileFromDisk": [{
33529
+ name: "deviceId",
33530
+ form: "single",
33531
+ optional: false
33532
+ }],
33376
33533
  "pipelineAnalytics.restageRetrainTrack": [{
33377
33534
  name: "deviceId",
33378
33535
  form: "single",
@@ -34438,6 +34595,8 @@ DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb;
34438
34595
  DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
34439
34596
  DEFAULT_NATIVE_LEASE_SETTINGS.tileBudgetMb;
34440
34597
  DEFAULT_NATIVE_LEASE_SETTINGS.admission;
34598
+ var MB = 1024 * 1024;
34599
+ 1024 * MB, 3072 * MB;
34441
34600
  /**
34442
34601
  * Compute the stable 64-char lowercase-hex fingerprint of a device's
34443
34602
  * export-relevant shape. Two structurally-equal shapes (any feature order,
@@ -72,7 +72,7 @@ function carryForward(base, existing, keys) {
72
72
  return out;
73
73
  }
74
74
  //#endregion
75
- //#region ../types/dist/event-category-Bxo5yJjt.mjs
75
+ //#region ../types/dist/event-category-XfKNtfCc.mjs
76
76
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
77
77
  EventCategory["SystemBoot"] = "system.boot";
78
78
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -89,9 +89,10 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
89
89
  EventCategory["SystemRestartCompleted"] = "system.restart-completed";
90
90
  /**
91
91
  * A newer addon or server-root package version was found by the
92
- * authoritative registry check. Emitted once per
93
- * `(target, packageName, currentVersion, latestVersion)` transition; repeated
94
- * polling of the same result is deduplicated by the checker.
92
+ * authoritative registry check. Emitted once when any observed
93
+ * `latestVersion` changes (or a package/node first appears behind);
94
+ * the payload carries the full currently-available list. Repeated
95
+ * polling of the same latests is silent.
95
96
  */
96
97
  EventCategory["UpdateAvailable"] = "update.available";
97
98
  /**
@@ -110,6 +111,22 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
110
111
  EventCategory["AddonInstalled"] = "addon.installed";
111
112
  EventCategory["AddonUninstalled"] = "addon.uninstalled";
112
113
  EventCategory["AddonCrashed"] = "addon.crashed";
114
+ /**
115
+ * A RUNNER the D6 crash circuit-breaker gave up on — terminal.
116
+ *
117
+ * `AddonCrashed` is the routine, self-healing fact ("it crashed; it is being
118
+ * respawned"); this is the one that never resolves itself. Emitted exactly
119
+ * once per trip, by `process-service.ts`, carrying the node, the runner, the
120
+ * addons it hosted and the crash count that tripped the breaker.
121
+ *
122
+ * It exists because a runner marked terminally `failed` used to be SILENT:
123
+ * on 2026-08-18 the `recorder` runner died of three unhandled ffmpeg spawn
124
+ * errors, the breaker stopped respawning it (correctly), `/health` kept
125
+ * returning 200, and fleet-wide recording was gone for 3h15 before the
126
+ * operator's phone told him. The Notification Center's system-event intake
127
+ * consumes this category and maps it to the `addon-crash-loop` kind.
128
+ */
129
+ EventCategory["AddonRunnerFailed"] = "addon.runner-failed";
113
130
  EventCategory["AddonError"] = "addon.error";
114
131
  EventCategory["AddonPageReady"] = "addon.page-ready";
115
132
  EventCategory["AddonWidgetReady"] = "addon.widget-ready";
@@ -8202,6 +8219,10 @@ var RecordingBandSchema = object({
8202
8219
  preBufferSec: number().min(0).optional(),
8203
8220
  postBufferSec: number().min(0).optional()
8204
8221
  });
8222
+ ({
8223
+ preBufferSec: 10,
8224
+ postBufferSec: 30
8225
+ }).postBufferSec * 1e3;
8205
8226
  /**
8206
8227
  * Per-device retention overrides. Every field is optional; an unset or `0`
8207
8228
  * value inherits the node-wide recorder default. Only footage-lifetime limits
@@ -8249,6 +8270,12 @@ var RecordingConfigSchema = object({
8249
8270
  /** DERIVED summary of `bands`, stamped by the recorder on every save.
8250
8271
  * Authoring it has no effect — see {@link RecordingStorageModeSchema}. */
8251
8272
  mode: RecordingStorageModeSchema.optional(),
8273
+ /**
8274
+ * Which assigned broker slots to record. Absent / empty = {@link
8275
+ * DEFAULT_RECORDING_PROFILES} (`high`+`low`) intersected with the
8276
+ * camera's currently assigned slots — never `mid` unless the operator
8277
+ * picks it, and never a slot the broker has not assigned.
8278
+ */
8252
8279
  profiles: array(CamProfileSchema).optional(),
8253
8280
  segmentSeconds: number().int().positive().optional(),
8254
8281
  /**
@@ -8329,7 +8356,11 @@ var RelocateFootageInputSchema = object({
8329
8356
  profiles: array(string()).optional(),
8330
8357
  /** Copy throttle in MB/s (default 40) — the drain is a background chore,
8331
8358
  * never allowed to starve live writers. */
8332
- throttleMbps: number().min(1).max(1e3).optional()
8359
+ throttleMbps: number().min(1).max(1e3).optional(),
8360
+ /** Move only segments whose startMs is >= this. Absent = the whole source
8361
+ * pile. Used when a full drain is too expensive and the operator only
8362
+ * wants the recent window on the new disk. */
8363
+ sinceMs: number().int().optional()
8333
8364
  });
8334
8365
  /** Internal, lease-scoped participant operation. It is intentionally separate
8335
8366
  * from persistent recording settings: a migration never changes
@@ -15027,6 +15058,7 @@ var NcSystemEventKindSchema = _enum([
15027
15058
  "node-offline",
15028
15059
  "node-inference-unavailable",
15029
15060
  "detection-blind",
15061
+ "addon-crash-loop",
15030
15062
  "addon-update-available",
15031
15063
  "server-update-available",
15032
15064
  "alarm-triggered",
@@ -15034,6 +15066,9 @@ var NcSystemEventKindSchema = _enum([
15034
15066
  "alarm-disarmed",
15035
15067
  "alarm-arming",
15036
15068
  "alarm-arm-refused",
15069
+ "addon-updated",
15070
+ "server-updated",
15071
+ "export-completed",
15037
15072
  "camera-online",
15038
15073
  "camera-offline",
15039
15074
  "camera-disabled",
@@ -16927,13 +16962,19 @@ var RetrainStatusSchema = _enum([
16927
16962
  * "never marked" from "already trained" must read `retrainStatus`.
16928
16963
  *
16929
16964
  * `debug` does NOT pin; it is attention, not durability.
16965
+ *
16966
+ * `favourited` IS a BOOLEAN pin (durability bit), not a retrain lifecycle.
16967
+ * A favourited track is skipped by retention the same way `staging` is, but
16968
+ * it does not enter `none|staging|trained` and has no staging budget.
16930
16969
  */
16931
16970
  var TrackFlagFields = {
16932
16971
  /** Operator marked this track as training material — i.e. `retrainStatus` is
16933
16972
  * `'staging'`. */
16934
16973
  markForTrain: boolean().optional(),
16935
16974
  /** Operator marked this track for diagnostic attention. */
16936
- debug: boolean().optional()
16975
+ debug: boolean().optional(),
16976
+ /** Operator favourited this track. Pins it against pruning. */
16977
+ favourited: boolean().optional()
16937
16978
  };
16938
16979
  /**
16939
16980
  * The lifecycle field itself, on the READ surfaces only (`Track`, `KeyEvent`).
@@ -16958,6 +16999,7 @@ var TrackFlagsSchema = object({
16958
16999
  trackId: string(),
16959
17000
  markForTrain: boolean(),
16960
17001
  debug: boolean(),
17002
+ favourited: boolean(),
16961
17003
  /** The lifecycle state the boolean was derived from. Required here (unlike on
16962
17004
  * a track row) because this shape is only ever produced by the write body,
16963
17005
  * which always knows it — and a surface that has just written needs to render
@@ -17590,6 +17632,12 @@ var TrackCascadeCountsSchema = object({
17590
17632
  /** Per-track CLIP search vectors removed (best-effort). */
17591
17633
  embeddings: number().int()
17592
17634
  });
17635
+ /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17636
+ var DiskReconcileCountsSchema = object({
17637
+ mediaDropped: number().int(),
17638
+ tracks: number().int(),
17639
+ events: number().int()
17640
+ });
17593
17641
  /** Event-store footprint for one camera. */
17594
17642
  var EventStoreDeviceFootprintSchema = object({
17595
17643
  deviceId: number(),
@@ -17766,6 +17814,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17766
17814
  }), method(object({ deviceId: number() }), TrackCascadeCountsSchema, {
17767
17815
  kind: "mutation",
17768
17816
  auth: "admin"
17817
+ }), method(object({ deviceId: number() }), DiskReconcileCountsSchema, {
17818
+ kind: "mutation",
17819
+ auth: "admin"
17769
17820
  }), method(object({
17770
17821
  deviceId: number(),
17771
17822
  trackIds: array(string()).min(1)
@@ -19276,6 +19327,24 @@ var CameraStatusDegradationSchema = object({
19276
19327
  *
19277
19328
  * See spec: `docs/superpowers/specs/2026-06-24-camera-status-aggregator-cap.md`
19278
19329
  */
19330
+ var DiskReconcileJobSchema = object({
19331
+ state: _enum([
19332
+ "idle",
19333
+ "running",
19334
+ "done",
19335
+ "error"
19336
+ ]),
19337
+ total: number().int().nonnegative(),
19338
+ completed: number().int().nonnegative(),
19339
+ currentDeviceId: number().int().nullable(),
19340
+ failed: array(number().int()).readonly(),
19341
+ mediaDropped: number().int().nonnegative(),
19342
+ tracks: number().int().nonnegative(),
19343
+ events: number().int().nonnegative(),
19344
+ startedAtMs: number().int().nullable(),
19345
+ finishedAtMs: number().int().nullable(),
19346
+ error: string().nullable()
19347
+ });
19279
19348
  var CameraStatusSchema = object({
19280
19349
  deviceId: number(),
19281
19350
  assignment: CameraAssignmentStatusSchema,
@@ -19507,7 +19576,10 @@ method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
19507
19576
  }), CameraSwitchGroupSchema, {
19508
19577
  kind: "mutation",
19509
19578
  auth: "admin"
19510
- }), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
19579
+ }), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), DiskReconcileJobSchema, {
19580
+ kind: "mutation",
19581
+ auth: "admin"
19582
+ }), method(_void(), DiskReconcileJobSchema, { auth: "admin" }), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
19511
19583
  name: string(),
19512
19584
  description: string().optional(),
19513
19585
  config: CameraPipelineConfigSchema
@@ -20580,20 +20652,6 @@ var VectorStatsResultSchema = object({
20580
20652
  exact: boolean()
20581
20653
  });
20582
20654
  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);
20583
- /**
20584
- * `videoclips` — the unified, navigable-clip surface for a camera.
20585
- *
20586
- * A device-scoped WRAPPER cap (like `pipeline-analytics`): exactly one active
20587
- * provider per device, substitutable. The DEFAULT provider (registered by
20588
- * `addon-post-analysis`, `defaultActive: true`) composes the analytics event
20589
- * log (markers + thumbnails) with the recorder's `getPlaybackManifest` — a clip
20590
- * is a time-WINDOW over existing footage, never a separate file. A camera that
20591
- * exposes NATIVE onboard clips (Reolink/Hikvision NVR) can later substitute the
20592
- * wrapper provider for its device and serve its own clip catalog + URLs.
20593
- *
20594
- * A `Clip` is purely time-based (subtree-blind): playback resolves segments by
20595
- * temporal overlap, so the API never decides `continuous` vs `events`.
20596
- */
20597
20655
  var ClipSchema = object({
20598
20656
  /** Opaque, provider-namespaced id. The default provider encodes the time
20599
20657
  * window so `getClipPlayback` is self-contained (no event re-query). */
@@ -20610,8 +20668,65 @@ var ClipSchema = object({
20610
20668
  startMs: number(),
20611
20669
  endMs: number()
20612
20670
  }),
20613
- /** Thumbnail URL (lazy; e.g. analytics `getEventMedia`). Never inlined. */
20614
- thumbnail: string().optional()
20671
+ /**
20672
+ * Distinct object classes attached to this visit (`person`, `car`, …),
20673
+ * dominant first, capped at {@link MAX_CLIP_LABELS}. The ribbon renders these
20674
+ * instead of the bare kind — "Object" tells the operator nothing a colour bar
20675
+ * did not. Absent (never empty) when the visit attached no classified object
20676
+ * event, so a motion/audio-only visit keeps its kind label.
20677
+ */
20678
+ labels: array(string()).max(3).optional(),
20679
+ /**
20680
+ * Lazy thumbnail URL, never inlined.
20681
+ *
20682
+ * Recording-derived clips (events-mode keep-window, and the prepared
20683
+ * continuous event+fragment visit) MUST use the snapshot of the **main
20684
+ * event of the interval** — `getEventMedia({ eventId, kind: 'snapshot' })`
20685
+ * of the event that owns `kind` (object > motion > audio). Do not extract
20686
+ * a keyframe from the recorded segments. Other providers (onboard, HKSV)
20687
+ * mint their own stills.
20688
+ *
20689
+ * **VOUCHED, never fabricated.** A provider emits this only for an event it
20690
+ * has CONFIRMED owns at least one media row (its own, or its owning track's).
20691
+ * Absent is meaningful — "this visit has no event still" — never "we did not
20692
+ * look". Stamping it from a URL template made 35% of one camera's clips point
20693
+ * at a 404 (device 3836, 2026-08-17: 64 of 179 clips dead, 63 of them motion).
20694
+ * A read that FAILS drops the claim; it never invents it.
20695
+ */
20696
+ thumbnail: string().optional(),
20697
+ /**
20698
+ * An instant INSIDE this visit's footage, hole-safe, where a recorded still
20699
+ * can be decoded. Present whenever the visit came from recorded availability;
20700
+ * absent on a per-event padded window (there is no footage to promise).
20701
+ *
20702
+ * This is not a thumbnail and not a second byte path: it is the argument to
20703
+ * the recorder's existing still route. The surface — never the provider —
20704
+ * decides whether to use it. It is the midpoint of the visit's LONGEST
20705
+ * contiguous range, not of the visit: a visit spans its holes by
20706
+ * construction, so a naive midpoint lands in dead air.
20707
+ */
20708
+ stillAtMs: number().optional(),
20709
+ /**
20710
+ * Analytics event ids that overlap this visit — a BOUNDED sample, newest
20711
+ * first within kind (object → motion → audio), capped at
20712
+ * {@link MAX_CLIP_EVENT_IDS}. Empty on footage-only clips.
20713
+ *
20714
+ * Bounded because it is not a payload the surface pages through: one visit on
20715
+ * device 615 carried 2 345 ids, and `eventIds` was 99% of a 153 KB
20716
+ * camera-day. Read {@link eventCount} for the true total.
20717
+ */
20718
+ eventIds: array(string()).optional(),
20719
+ /** How many analytics events actually overlap this visit. Differs from
20720
+ * `eventIds.length` exactly when the sample was capped — so a truncated
20721
+ * list is never mistaken for a quiet visit. */
20722
+ eventCount: number().int().nonnegative().optional(),
20723
+ /** Intra-visit footage holes (GOP rolls, discarded segments) still shorter
20724
+ * than `VISIT_MERGE_GAP_MS`. Playback concatenates around them; the timeline
20725
+ * bar keeps showing them via `recording.getAvailability`. */
20726
+ holes: array(object({
20727
+ startMs: number(),
20728
+ endMs: number()
20729
+ })).optional()
20615
20730
  });
20616
20731
  var ClipPlaybackSchema = object({
20617
20732
  /** HLS master URL through the hub data-plane (Range + token in path). */
@@ -21075,7 +21190,14 @@ var SearchResultSchema = object({
21075
21190
  });
21076
21191
  var AutoUpdateSettingsSchema = object({
21077
21192
  channel: ChannelSchema,
21078
- intervalSeconds: number()
21193
+ intervalSeconds: number(),
21194
+ /**
21195
+ * Cadence of the "an update exists" POLLER, in seconds. Independent of
21196
+ * `channel`: the poller runs while auto-apply is `off`, because being told
21197
+ * about a publish and installing it are different decisions. Clamped
21198
+ * server-side to 900 s … 604800 s; defaults to 21600 s (6 h).
21199
+ */
21200
+ updateCheckIntervalSeconds: number()
21079
21201
  });
21080
21202
  var AddonAutoUpdateSchema = ChannelWithInheritSchema;
21081
21203
  var RestartAddonResultSchema = unknown();
@@ -21216,7 +21338,9 @@ method(_void(), array(AddonListItemSchema).readonly()), method(object({
21216
21338
  auth: "admin"
21217
21339
  }), method(_void(), AutoUpdateSettingsSchema, { auth: "admin" }), method(object({
21218
21340
  channel: ChannelSchema,
21219
- intervalSeconds: number().min(300).max(86400).optional()
21341
+ intervalSeconds: number().min(300).max(86400).optional(),
21342
+ /** Availability-poll cadence; see AutoUpdateSettingsSchema. */
21343
+ updateCheckIntervalSeconds: number().min(900).max(604800).optional()
21220
21344
  }), unknown(), {
21221
21345
  kind: "mutation",
21222
21346
  auth: "admin"
@@ -25257,7 +25381,9 @@ var ExportOptionsSchema = object({
25257
25381
  includeAudio: boolean(),
25258
25382
  maxLifeMs: number().int().positive(),
25259
25383
  deleteAfterDownload: boolean(),
25260
- title: string().max(200).optional()
25384
+ title: string().max(200).optional(),
25385
+ /** Notification-output target ids to ping when this export becomes ready. */
25386
+ notifyTargetIds: array(string().min(1)).max(20).optional()
25261
25387
  }).superRefine((v, ctx) => {
25262
25388
  if (v.speed !== void 0 && v.timelapse !== void 0) ctx.addIssue({
25263
25389
  code: ZodIssueCode.custom,
@@ -25316,10 +25442,18 @@ var ExportBytesSchema = object({
25316
25442
  });
25317
25443
  method(object({
25318
25444
  deviceId: number(),
25319
- profile: string(),
25445
+ /** @deprecated Prefer `profiles`. Kept so timelapse/notifiers keep working. */
25446
+ profile: string().optional(),
25447
+ profiles: array(string()).min(1).optional(),
25320
25448
  fromMs: number(),
25321
25449
  toMs: number(),
25322
25450
  options: ExportOptionsSchema
25451
+ }).superRefine((v, ctx) => {
25452
+ if ((v.profiles !== void 0 && v.profiles.length > 0 ? v.profiles : v.profile !== void 0 ? [v.profile] : []).length < 1) ctx.addIssue({
25453
+ code: ZodIssueCode.custom,
25454
+ message: "pass profiles[] (min 1) or legacy profile",
25455
+ path: ["profiles"]
25456
+ });
25323
25457
  }), ExportRecordSchema, {
25324
25458
  kind: "mutation",
25325
25459
  auth: "protected"
@@ -29944,6 +30078,12 @@ Object.freeze({
29944
30078
  addonId: null,
29945
30079
  access: "create"
29946
30080
  },
30081
+ "pipelineAnalytics.reconcileFromDisk": {
30082
+ capName: "pipeline-analytics",
30083
+ capScope: "device",
30084
+ addonId: null,
30085
+ access: "create"
30086
+ },
29947
30087
  "pipelineAnalytics.refreshStorageLocationsForMigration": {
29948
30088
  capName: "pipeline-analytics",
29949
30089
  capScope: "device",
@@ -30346,6 +30486,12 @@ Object.freeze({
30346
30486
  addonId: null,
30347
30487
  access: "view"
30348
30488
  },
30489
+ "pipelineOrchestrator.getReconcileFromDiskStatus": {
30490
+ capName: "pipeline-orchestrator",
30491
+ capScope: "system",
30492
+ addonId: null,
30493
+ access: "view"
30494
+ },
30349
30495
  "pipelineOrchestrator.listAgentSettings": {
30350
30496
  capName: "pipeline-orchestrator",
30351
30497
  capScope: "system",
@@ -30370,6 +30516,12 @@ Object.freeze({
30370
30516
  addonId: null,
30371
30517
  access: "create"
30372
30518
  },
30519
+ "pipelineOrchestrator.reconcileFromDisk": {
30520
+ capName: "pipeline-orchestrator",
30521
+ capScope: "system",
30522
+ addonId: null,
30523
+ access: "create"
30524
+ },
30373
30525
  "pipelineOrchestrator.removeAgentSettings": {
30374
30526
  capName: "pipeline-orchestrator",
30375
30527
  capScope: "system",
@@ -33361,6 +33513,11 @@ Object.freeze({
33361
33513
  form: "single",
33362
33514
  optional: true
33363
33515
  }],
33516
+ "pipelineAnalytics.reconcileFromDisk": [{
33517
+ name: "deviceId",
33518
+ form: "single",
33519
+ optional: false
33520
+ }],
33364
33521
  "pipelineAnalytics.restageRetrainTrack": [{
33365
33522
  name: "deviceId",
33366
33523
  form: "single",
@@ -34426,6 +34583,8 @@ DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb;
34426
34583
  DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
34427
34584
  DEFAULT_NATIVE_LEASE_SETTINGS.tileBudgetMb;
34428
34585
  DEFAULT_NATIVE_LEASE_SETTINGS.admission;
34586
+ var MB = 1024 * 1024;
34587
+ 1024 * MB, 3072 * MB;
34429
34588
  /**
34430
34589
  * Compute the stable 64-char lowercase-hex fingerprint of a device's
34431
34590
  * export-relevant shape. Two structurally-equal shapes (any feature order,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-export-hap",
3
- "version": "1.2.31",
3
+ "version": "1.2.33",
4
4
  "description": "HomeKit (HAP) exporter for CamStack devices. Publishes each exposed device as its own HomeKit accessory: cameras and doorbells with SRTP streaming, HomeKit Secure Video, motion, two-way audio, PTZ and battery; switches, lights, locks and sensors through a capability→service table.",
5
5
  "keywords": [
6
6
  "camstack",