@camstack/addon-static-turn 1.2.18 → 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.
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- //#region ../types/dist/event-category-Bxo5yJjt.mjs
2
+ //#region ../types/dist/event-category-XfKNtfCc.mjs
3
3
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
4
4
  EventCategory["SystemBoot"] = "system.boot";
5
5
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -16,9 +16,10 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
16
16
  EventCategory["SystemRestartCompleted"] = "system.restart-completed";
17
17
  /**
18
18
  * A newer addon or server-root package version was found by the
19
- * authoritative registry check. Emitted once per
20
- * `(target, packageName, currentVersion, latestVersion)` transition; repeated
21
- * polling of the same result is deduplicated by the checker.
19
+ * authoritative registry check. Emitted once when any observed
20
+ * `latestVersion` changes (or a package/node first appears behind);
21
+ * the payload carries the full currently-available list. Repeated
22
+ * polling of the same latests is silent.
22
23
  */
23
24
  EventCategory["UpdateAvailable"] = "update.available";
24
25
  /**
@@ -37,6 +38,22 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
37
38
  EventCategory["AddonInstalled"] = "addon.installed";
38
39
  EventCategory["AddonUninstalled"] = "addon.uninstalled";
39
40
  EventCategory["AddonCrashed"] = "addon.crashed";
41
+ /**
42
+ * A RUNNER the D6 crash circuit-breaker gave up on — terminal.
43
+ *
44
+ * `AddonCrashed` is the routine, self-healing fact ("it crashed; it is being
45
+ * respawned"); this is the one that never resolves itself. Emitted exactly
46
+ * once per trip, by `process-service.ts`, carrying the node, the runner, the
47
+ * addons it hosted and the crash count that tripped the breaker.
48
+ *
49
+ * It exists because a runner marked terminally `failed` used to be SILENT:
50
+ * on 2026-08-18 the `recorder` runner died of three unhandled ffmpeg spawn
51
+ * errors, the breaker stopped respawning it (correctly), `/health` kept
52
+ * returning 200, and fleet-wide recording was gone for 3h15 before the
53
+ * operator's phone told him. The Notification Center's system-event intake
54
+ * consumes this category and maps it to the `addon-crash-loop` kind.
55
+ */
56
+ EventCategory["AddonRunnerFailed"] = "addon.runner-failed";
40
57
  EventCategory["AddonError"] = "addon.error";
41
58
  EventCategory["AddonPageReady"] = "addon.page-ready";
42
59
  EventCategory["AddonWidgetReady"] = "addon.widget-ready";
@@ -7532,6 +7549,10 @@ var RecordingBandSchema = object({
7532
7549
  preBufferSec: number().min(0).optional(),
7533
7550
  postBufferSec: number().min(0).optional()
7534
7551
  });
7552
+ ({
7553
+ preBufferSec: 10,
7554
+ postBufferSec: 30
7555
+ }).postBufferSec * 1e3;
7535
7556
  /**
7536
7557
  * Per-device retention overrides. Every field is optional; an unset or `0`
7537
7558
  * value inherits the node-wide recorder default. Only footage-lifetime limits
@@ -7579,6 +7600,12 @@ var RecordingConfigSchema = object({
7579
7600
  /** DERIVED summary of `bands`, stamped by the recorder on every save.
7580
7601
  * Authoring it has no effect — see {@link RecordingStorageModeSchema}. */
7581
7602
  mode: RecordingStorageModeSchema.optional(),
7603
+ /**
7604
+ * Which assigned broker slots to record. Absent / empty = {@link
7605
+ * DEFAULT_RECORDING_PROFILES} (`high`+`low`) intersected with the
7606
+ * camera's currently assigned slots — never `mid` unless the operator
7607
+ * picks it, and never a slot the broker has not assigned.
7608
+ */
7582
7609
  profiles: array(CamProfileSchema).optional(),
7583
7610
  segmentSeconds: number().int().positive().optional(),
7584
7611
  /**
@@ -7659,7 +7686,11 @@ var RelocateFootageInputSchema = object({
7659
7686
  profiles: array(string()).optional(),
7660
7687
  /** Copy throttle in MB/s (default 40) — the drain is a background chore,
7661
7688
  * never allowed to starve live writers. */
7662
- throttleMbps: number().min(1).max(1e3).optional()
7689
+ throttleMbps: number().min(1).max(1e3).optional(),
7690
+ /** Move only segments whose startMs is >= this. Absent = the whole source
7691
+ * pile. Used when a full drain is too expensive and the operator only
7692
+ * wants the recent window on the new disk. */
7693
+ sinceMs: number().int().optional()
7663
7694
  });
7664
7695
  /** Internal, lease-scoped participant operation. It is intentionally separate
7665
7696
  * from persistent recording settings: a migration never changes
@@ -14222,6 +14253,7 @@ var NcSystemEventKindSchema = _enum([
14222
14253
  "node-offline",
14223
14254
  "node-inference-unavailable",
14224
14255
  "detection-blind",
14256
+ "addon-crash-loop",
14225
14257
  "addon-update-available",
14226
14258
  "server-update-available",
14227
14259
  "alarm-triggered",
@@ -14229,6 +14261,9 @@ var NcSystemEventKindSchema = _enum([
14229
14261
  "alarm-disarmed",
14230
14262
  "alarm-arming",
14231
14263
  "alarm-arm-refused",
14264
+ "addon-updated",
14265
+ "server-updated",
14266
+ "export-completed",
14232
14267
  "camera-online",
14233
14268
  "camera-offline",
14234
14269
  "camera-disabled",
@@ -16122,13 +16157,19 @@ var RetrainStatusSchema = _enum([
16122
16157
  * "never marked" from "already trained" must read `retrainStatus`.
16123
16158
  *
16124
16159
  * `debug` does NOT pin; it is attention, not durability.
16160
+ *
16161
+ * `favourited` IS a BOOLEAN pin (durability bit), not a retrain lifecycle.
16162
+ * A favourited track is skipped by retention the same way `staging` is, but
16163
+ * it does not enter `none|staging|trained` and has no staging budget.
16125
16164
  */
16126
16165
  var TrackFlagFields = {
16127
16166
  /** Operator marked this track as training material — i.e. `retrainStatus` is
16128
16167
  * `'staging'`. */
16129
16168
  markForTrain: boolean().optional(),
16130
16169
  /** Operator marked this track for diagnostic attention. */
16131
- debug: boolean().optional()
16170
+ debug: boolean().optional(),
16171
+ /** Operator favourited this track. Pins it against pruning. */
16172
+ favourited: boolean().optional()
16132
16173
  };
16133
16174
  /**
16134
16175
  * The lifecycle field itself, on the READ surfaces only (`Track`, `KeyEvent`).
@@ -16153,6 +16194,7 @@ var TrackFlagsSchema = object({
16153
16194
  trackId: string(),
16154
16195
  markForTrain: boolean(),
16155
16196
  debug: boolean(),
16197
+ favourited: boolean(),
16156
16198
  /** The lifecycle state the boolean was derived from. Required here (unlike on
16157
16199
  * a track row) because this shape is only ever produced by the write body,
16158
16200
  * which always knows it — and a surface that has just written needs to render
@@ -16785,6 +16827,12 @@ var TrackCascadeCountsSchema = object({
16785
16827
  /** Per-track CLIP search vectors removed (best-effort). */
16786
16828
  embeddings: number().int()
16787
16829
  });
16830
+ /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
16831
+ var DiskReconcileCountsSchema = object({
16832
+ mediaDropped: number().int(),
16833
+ tracks: number().int(),
16834
+ events: number().int()
16835
+ });
16788
16836
  /** Event-store footprint for one camera. */
16789
16837
  var EventStoreDeviceFootprintSchema = object({
16790
16838
  deviceId: number(),
@@ -16961,6 +17009,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
16961
17009
  }), method(object({ deviceId: number() }), TrackCascadeCountsSchema, {
16962
17010
  kind: "mutation",
16963
17011
  auth: "admin"
17012
+ }), method(object({ deviceId: number() }), DiskReconcileCountsSchema, {
17013
+ kind: "mutation",
17014
+ auth: "admin"
16964
17015
  }), method(object({
16965
17016
  deviceId: number(),
16966
17017
  trackIds: array(string()).min(1)
@@ -18471,6 +18522,24 @@ var CameraStatusDegradationSchema = object({
18471
18522
  *
18472
18523
  * See spec: `docs/superpowers/specs/2026-06-24-camera-status-aggregator-cap.md`
18473
18524
  */
18525
+ var DiskReconcileJobSchema = object({
18526
+ state: _enum([
18527
+ "idle",
18528
+ "running",
18529
+ "done",
18530
+ "error"
18531
+ ]),
18532
+ total: number().int().nonnegative(),
18533
+ completed: number().int().nonnegative(),
18534
+ currentDeviceId: number().int().nullable(),
18535
+ failed: array(number().int()).readonly(),
18536
+ mediaDropped: number().int().nonnegative(),
18537
+ tracks: number().int().nonnegative(),
18538
+ events: number().int().nonnegative(),
18539
+ startedAtMs: number().int().nullable(),
18540
+ finishedAtMs: number().int().nullable(),
18541
+ error: string().nullable()
18542
+ });
18474
18543
  var CameraStatusSchema = object({
18475
18544
  deviceId: number(),
18476
18545
  assignment: CameraAssignmentStatusSchema,
@@ -18702,7 +18771,10 @@ method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
18702
18771
  }), CameraSwitchGroupSchema, {
18703
18772
  kind: "mutation",
18704
18773
  auth: "admin"
18705
- }), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
18774
+ }), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), DiskReconcileJobSchema, {
18775
+ kind: "mutation",
18776
+ auth: "admin"
18777
+ }), method(_void(), DiskReconcileJobSchema, { auth: "admin" }), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
18706
18778
  name: string(),
18707
18779
  description: string().optional(),
18708
18780
  config: CameraPipelineConfigSchema
@@ -19107,7 +19179,14 @@ targets: array(object({
19107
19179
  "sleeping",
19108
19180
  "unreachable",
19109
19181
  "waking"
19110
- ]).nullable()
19182
+ ]).nullable(),
19183
+ /** A battery camera (whatever its current state). An AWAKE battery
19184
+ * camera is deliberately NOT recaptured on the poll cadence — every
19185
+ * capture is a camera hit that would keep it out of sleep — so its
19186
+ * cached frame legitimately ages past the currency ceiling while
19187
+ * nothing is streaming. A surface must keep painting it (a fresh
19188
+ * frame is captured at each wake), not blank to "unavailable". */
19189
+ battery: boolean()
19111
19190
  })));
19112
19191
  /**
19113
19192
  * `sso-bridge` — internal hub-only cap that lets SSO-style auth
@@ -19791,20 +19870,6 @@ var VectorStatsResultSchema = object({
19791
19870
  exact: boolean()
19792
19871
  });
19793
19872
  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);
19794
- /**
19795
- * `videoclips` — the unified, navigable-clip surface for a camera.
19796
- *
19797
- * A device-scoped WRAPPER cap (like `pipeline-analytics`): exactly one active
19798
- * provider per device, substitutable. The DEFAULT provider (registered by
19799
- * `addon-post-analysis`, `defaultActive: true`) composes the analytics event
19800
- * log (markers + thumbnails) with the recorder's `getPlaybackManifest` — a clip
19801
- * is a time-WINDOW over existing footage, never a separate file. A camera that
19802
- * exposes NATIVE onboard clips (Reolink/Hikvision NVR) can later substitute the
19803
- * wrapper provider for its device and serve its own clip catalog + URLs.
19804
- *
19805
- * A `Clip` is purely time-based (subtree-blind): playback resolves segments by
19806
- * temporal overlap, so the API never decides `continuous` vs `events`.
19807
- */
19808
19873
  var ClipSchema = object({
19809
19874
  /** Opaque, provider-namespaced id. The default provider encodes the time
19810
19875
  * window so `getClipPlayback` is self-contained (no event re-query). */
@@ -19821,8 +19886,57 @@ var ClipSchema = object({
19821
19886
  startMs: number(),
19822
19887
  endMs: number()
19823
19888
  }),
19824
- /** Thumbnail URL (lazy; e.g. analytics `getEventMedia`). Never inlined. */
19825
- thumbnail: string().optional()
19889
+ /**
19890
+ * Lazy thumbnail URL, never inlined.
19891
+ *
19892
+ * Recording-derived clips (events-mode keep-window, and the prepared
19893
+ * continuous event+fragment visit) MUST use the snapshot of the **main
19894
+ * event of the interval** — `getEventMedia({ eventId, kind: 'snapshot' })`
19895
+ * of the event that owns `kind` (object > motion > audio). Do not extract
19896
+ * a keyframe from the recorded segments. Other providers (onboard, HKSV)
19897
+ * mint their own stills.
19898
+ *
19899
+ * **VOUCHED, never fabricated.** A provider emits this only for an event it
19900
+ * has CONFIRMED owns at least one media row (its own, or its owning track's).
19901
+ * Absent is meaningful — "this visit has no event still" — never "we did not
19902
+ * look". Stamping it from a URL template made 35% of one camera's clips point
19903
+ * at a 404 (device 3836, 2026-08-17: 64 of 179 clips dead, 63 of them motion).
19904
+ * A read that FAILS drops the claim; it never invents it.
19905
+ */
19906
+ thumbnail: string().optional(),
19907
+ /**
19908
+ * An instant INSIDE this visit's footage, hole-safe, where a recorded still
19909
+ * can be decoded. Present whenever the visit came from recorded availability;
19910
+ * absent on a per-event padded window (there is no footage to promise).
19911
+ *
19912
+ * This is not a thumbnail and not a second byte path: it is the argument to
19913
+ * the recorder's existing still route. The surface — never the provider —
19914
+ * decides whether to use it. It is the midpoint of the visit's LONGEST
19915
+ * contiguous range, not of the visit: a visit spans its holes by
19916
+ * construction, so a naive midpoint lands in dead air.
19917
+ */
19918
+ stillAtMs: number().optional(),
19919
+ /**
19920
+ * Analytics event ids that overlap this visit — a BOUNDED sample, newest
19921
+ * first within kind (object → motion → audio), capped at
19922
+ * {@link MAX_CLIP_EVENT_IDS}. Empty on footage-only clips.
19923
+ *
19924
+ * Bounded because it is not a payload the surface pages through: one visit on
19925
+ * device 615 carried 2 345 ids, and `eventIds` was 99% of a 153 KB
19926
+ * camera-day. Read {@link eventCount} for the true total.
19927
+ */
19928
+ eventIds: array(string()).optional(),
19929
+ /** How many analytics events actually overlap this visit. Differs from
19930
+ * `eventIds.length` exactly when the sample was capped — so a truncated
19931
+ * list is never mistaken for a quiet visit. */
19932
+ eventCount: number().int().nonnegative().optional(),
19933
+ /** Intra-visit footage holes (GOP rolls, discarded segments) still shorter
19934
+ * than `VISIT_MERGE_GAP_MS`. Playback concatenates around them; the timeline
19935
+ * bar keeps showing them via `recording.getAvailability`. */
19936
+ holes: array(object({
19937
+ startMs: number(),
19938
+ endMs: number()
19939
+ })).optional()
19826
19940
  });
19827
19941
  var ClipPlaybackSchema = object({
19828
19942
  /** HLS master URL through the hub data-plane (Range + token in path). */
@@ -20286,7 +20400,14 @@ var SearchResultSchema = object({
20286
20400
  });
20287
20401
  var AutoUpdateSettingsSchema = object({
20288
20402
  channel: ChannelSchema,
20289
- intervalSeconds: number()
20403
+ intervalSeconds: number(),
20404
+ /**
20405
+ * Cadence of the "an update exists" POLLER, in seconds. Independent of
20406
+ * `channel`: the poller runs while auto-apply is `off`, because being told
20407
+ * about a publish and installing it are different decisions. Clamped
20408
+ * server-side to 900 s … 604800 s; defaults to 21600 s (6 h).
20409
+ */
20410
+ updateCheckIntervalSeconds: number()
20290
20411
  });
20291
20412
  var AddonAutoUpdateSchema = ChannelWithInheritSchema;
20292
20413
  var RestartAddonResultSchema = unknown();
@@ -20427,7 +20548,9 @@ method(_void(), array(AddonListItemSchema).readonly()), method(object({
20427
20548
  auth: "admin"
20428
20549
  }), method(_void(), AutoUpdateSettingsSchema, { auth: "admin" }), method(object({
20429
20550
  channel: ChannelSchema,
20430
- intervalSeconds: number().min(300).max(86400).optional()
20551
+ intervalSeconds: number().min(300).max(86400).optional(),
20552
+ /** Availability-poll cadence; see AutoUpdateSettingsSchema. */
20553
+ updateCheckIntervalSeconds: number().min(900).max(604800).optional()
20431
20554
  }), unknown(), {
20432
20555
  kind: "mutation",
20433
20556
  auth: "admin"
@@ -24420,7 +24543,9 @@ var ExportOptionsSchema = object({
24420
24543
  includeAudio: boolean(),
24421
24544
  maxLifeMs: number().int().positive(),
24422
24545
  deleteAfterDownload: boolean(),
24423
- title: string().max(200).optional()
24546
+ title: string().max(200).optional(),
24547
+ /** Notification-output target ids to ping when this export becomes ready. */
24548
+ notifyTargetIds: array(string().min(1)).max(20).optional()
24424
24549
  }).superRefine((v, ctx) => {
24425
24550
  if (v.speed !== void 0 && v.timelapse !== void 0) ctx.addIssue({
24426
24551
  code: ZodIssueCode.custom,
@@ -24479,10 +24604,18 @@ var ExportBytesSchema = object({
24479
24604
  });
24480
24605
  method(object({
24481
24606
  deviceId: number(),
24482
- profile: string(),
24607
+ /** @deprecated Prefer `profiles`. Kept so timelapse/notifiers keep working. */
24608
+ profile: string().optional(),
24609
+ profiles: array(string()).min(1).optional(),
24483
24610
  fromMs: number(),
24484
24611
  toMs: number(),
24485
24612
  options: ExportOptionsSchema
24613
+ }).superRefine((v, ctx) => {
24614
+ if ((v.profiles !== void 0 && v.profiles.length > 0 ? v.profiles : v.profile !== void 0 ? [v.profile] : []).length < 1) ctx.addIssue({
24615
+ code: ZodIssueCode.custom,
24616
+ message: "pass profiles[] (min 1) or legacy profile",
24617
+ path: ["profiles"]
24618
+ });
24486
24619
  }), ExportRecordSchema, {
24487
24620
  kind: "mutation",
24488
24621
  auth: "protected"
@@ -29082,6 +29215,12 @@ Object.freeze({
29082
29215
  addonId: null,
29083
29216
  access: "create"
29084
29217
  },
29218
+ "pipelineAnalytics.reconcileFromDisk": {
29219
+ capName: "pipeline-analytics",
29220
+ capScope: "device",
29221
+ addonId: null,
29222
+ access: "create"
29223
+ },
29085
29224
  "pipelineAnalytics.refreshStorageLocationsForMigration": {
29086
29225
  capName: "pipeline-analytics",
29087
29226
  capScope: "device",
@@ -29484,6 +29623,12 @@ Object.freeze({
29484
29623
  addonId: null,
29485
29624
  access: "view"
29486
29625
  },
29626
+ "pipelineOrchestrator.getReconcileFromDiskStatus": {
29627
+ capName: "pipeline-orchestrator",
29628
+ capScope: "system",
29629
+ addonId: null,
29630
+ access: "view"
29631
+ },
29487
29632
  "pipelineOrchestrator.listAgentSettings": {
29488
29633
  capName: "pipeline-orchestrator",
29489
29634
  capScope: "system",
@@ -29508,6 +29653,12 @@ Object.freeze({
29508
29653
  addonId: null,
29509
29654
  access: "create"
29510
29655
  },
29656
+ "pipelineOrchestrator.reconcileFromDisk": {
29657
+ capName: "pipeline-orchestrator",
29658
+ capScope: "system",
29659
+ addonId: null,
29660
+ access: "create"
29661
+ },
29511
29662
  "pipelineOrchestrator.removeAgentSettings": {
29512
29663
  capName: "pipeline-orchestrator",
29513
29664
  capScope: "system",
@@ -32499,6 +32650,11 @@ Object.freeze({
32499
32650
  form: "single",
32500
32651
  optional: true
32501
32652
  }],
32653
+ "pipelineAnalytics.reconcileFromDisk": [{
32654
+ name: "deviceId",
32655
+ form: "single",
32656
+ optional: false
32657
+ }],
32502
32658
  "pipelineAnalytics.restageRetrainTrack": [{
32503
32659
  name: "deviceId",
32504
32660
  form: "single",
@@ -33564,6 +33720,8 @@ DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb;
33564
33720
  DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
33565
33721
  DEFAULT_NATIVE_LEASE_SETTINGS.tileBudgetMb;
33566
33722
  DEFAULT_NATIVE_LEASE_SETTINGS.admission;
33723
+ var MB = 1024 * 1024;
33724
+ 1024 * MB, 3072 * MB;
33567
33725
  //#endregion
33568
33726
  //#region src/static-turn.addon.ts
33569
33727
  /**
@@ -1,4 +1,4 @@
1
- //#region ../types/dist/event-category-Bxo5yJjt.mjs
1
+ //#region ../types/dist/event-category-XfKNtfCc.mjs
2
2
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
3
3
  EventCategory["SystemBoot"] = "system.boot";
4
4
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -15,9 +15,10 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
15
15
  EventCategory["SystemRestartCompleted"] = "system.restart-completed";
16
16
  /**
17
17
  * A newer addon or server-root package version was found by the
18
- * authoritative registry check. Emitted once per
19
- * `(target, packageName, currentVersion, latestVersion)` transition; repeated
20
- * polling of the same result is deduplicated by the checker.
18
+ * authoritative registry check. Emitted once when any observed
19
+ * `latestVersion` changes (or a package/node first appears behind);
20
+ * the payload carries the full currently-available list. Repeated
21
+ * polling of the same latests is silent.
21
22
  */
22
23
  EventCategory["UpdateAvailable"] = "update.available";
23
24
  /**
@@ -36,6 +37,22 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
36
37
  EventCategory["AddonInstalled"] = "addon.installed";
37
38
  EventCategory["AddonUninstalled"] = "addon.uninstalled";
38
39
  EventCategory["AddonCrashed"] = "addon.crashed";
40
+ /**
41
+ * A RUNNER the D6 crash circuit-breaker gave up on — terminal.
42
+ *
43
+ * `AddonCrashed` is the routine, self-healing fact ("it crashed; it is being
44
+ * respawned"); this is the one that never resolves itself. Emitted exactly
45
+ * once per trip, by `process-service.ts`, carrying the node, the runner, the
46
+ * addons it hosted and the crash count that tripped the breaker.
47
+ *
48
+ * It exists because a runner marked terminally `failed` used to be SILENT:
49
+ * on 2026-08-18 the `recorder` runner died of three unhandled ffmpeg spawn
50
+ * errors, the breaker stopped respawning it (correctly), `/health` kept
51
+ * returning 200, and fleet-wide recording was gone for 3h15 before the
52
+ * operator's phone told him. The Notification Center's system-event intake
53
+ * consumes this category and maps it to the `addon-crash-loop` kind.
54
+ */
55
+ EventCategory["AddonRunnerFailed"] = "addon.runner-failed";
39
56
  EventCategory["AddonError"] = "addon.error";
40
57
  EventCategory["AddonPageReady"] = "addon.page-ready";
41
58
  EventCategory["AddonWidgetReady"] = "addon.widget-ready";
@@ -7531,6 +7548,10 @@ var RecordingBandSchema = object({
7531
7548
  preBufferSec: number().min(0).optional(),
7532
7549
  postBufferSec: number().min(0).optional()
7533
7550
  });
7551
+ ({
7552
+ preBufferSec: 10,
7553
+ postBufferSec: 30
7554
+ }).postBufferSec * 1e3;
7534
7555
  /**
7535
7556
  * Per-device retention overrides. Every field is optional; an unset or `0`
7536
7557
  * value inherits the node-wide recorder default. Only footage-lifetime limits
@@ -7578,6 +7599,12 @@ var RecordingConfigSchema = object({
7578
7599
  /** DERIVED summary of `bands`, stamped by the recorder on every save.
7579
7600
  * Authoring it has no effect — see {@link RecordingStorageModeSchema}. */
7580
7601
  mode: RecordingStorageModeSchema.optional(),
7602
+ /**
7603
+ * Which assigned broker slots to record. Absent / empty = {@link
7604
+ * DEFAULT_RECORDING_PROFILES} (`high`+`low`) intersected with the
7605
+ * camera's currently assigned slots — never `mid` unless the operator
7606
+ * picks it, and never a slot the broker has not assigned.
7607
+ */
7581
7608
  profiles: array(CamProfileSchema).optional(),
7582
7609
  segmentSeconds: number().int().positive().optional(),
7583
7610
  /**
@@ -7658,7 +7685,11 @@ var RelocateFootageInputSchema = object({
7658
7685
  profiles: array(string()).optional(),
7659
7686
  /** Copy throttle in MB/s (default 40) — the drain is a background chore,
7660
7687
  * never allowed to starve live writers. */
7661
- throttleMbps: number().min(1).max(1e3).optional()
7688
+ throttleMbps: number().min(1).max(1e3).optional(),
7689
+ /** Move only segments whose startMs is >= this. Absent = the whole source
7690
+ * pile. Used when a full drain is too expensive and the operator only
7691
+ * wants the recent window on the new disk. */
7692
+ sinceMs: number().int().optional()
7662
7693
  });
7663
7694
  /** Internal, lease-scoped participant operation. It is intentionally separate
7664
7695
  * from persistent recording settings: a migration never changes
@@ -14221,6 +14252,7 @@ var NcSystemEventKindSchema = _enum([
14221
14252
  "node-offline",
14222
14253
  "node-inference-unavailable",
14223
14254
  "detection-blind",
14255
+ "addon-crash-loop",
14224
14256
  "addon-update-available",
14225
14257
  "server-update-available",
14226
14258
  "alarm-triggered",
@@ -14228,6 +14260,9 @@ var NcSystemEventKindSchema = _enum([
14228
14260
  "alarm-disarmed",
14229
14261
  "alarm-arming",
14230
14262
  "alarm-arm-refused",
14263
+ "addon-updated",
14264
+ "server-updated",
14265
+ "export-completed",
14231
14266
  "camera-online",
14232
14267
  "camera-offline",
14233
14268
  "camera-disabled",
@@ -16121,13 +16156,19 @@ var RetrainStatusSchema = _enum([
16121
16156
  * "never marked" from "already trained" must read `retrainStatus`.
16122
16157
  *
16123
16158
  * `debug` does NOT pin; it is attention, not durability.
16159
+ *
16160
+ * `favourited` IS a BOOLEAN pin (durability bit), not a retrain lifecycle.
16161
+ * A favourited track is skipped by retention the same way `staging` is, but
16162
+ * it does not enter `none|staging|trained` and has no staging budget.
16124
16163
  */
16125
16164
  var TrackFlagFields = {
16126
16165
  /** Operator marked this track as training material — i.e. `retrainStatus` is
16127
16166
  * `'staging'`. */
16128
16167
  markForTrain: boolean().optional(),
16129
16168
  /** Operator marked this track for diagnostic attention. */
16130
- debug: boolean().optional()
16169
+ debug: boolean().optional(),
16170
+ /** Operator favourited this track. Pins it against pruning. */
16171
+ favourited: boolean().optional()
16131
16172
  };
16132
16173
  /**
16133
16174
  * The lifecycle field itself, on the READ surfaces only (`Track`, `KeyEvent`).
@@ -16152,6 +16193,7 @@ var TrackFlagsSchema = object({
16152
16193
  trackId: string(),
16153
16194
  markForTrain: boolean(),
16154
16195
  debug: boolean(),
16196
+ favourited: boolean(),
16155
16197
  /** The lifecycle state the boolean was derived from. Required here (unlike on
16156
16198
  * a track row) because this shape is only ever produced by the write body,
16157
16199
  * which always knows it — and a surface that has just written needs to render
@@ -16784,6 +16826,12 @@ var TrackCascadeCountsSchema = object({
16784
16826
  /** Per-track CLIP search vectors removed (best-effort). */
16785
16827
  embeddings: number().int()
16786
16828
  });
16829
+ /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
16830
+ var DiskReconcileCountsSchema = object({
16831
+ mediaDropped: number().int(),
16832
+ tracks: number().int(),
16833
+ events: number().int()
16834
+ });
16787
16835
  /** Event-store footprint for one camera. */
16788
16836
  var EventStoreDeviceFootprintSchema = object({
16789
16837
  deviceId: number(),
@@ -16960,6 +17008,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
16960
17008
  }), method(object({ deviceId: number() }), TrackCascadeCountsSchema, {
16961
17009
  kind: "mutation",
16962
17010
  auth: "admin"
17011
+ }), method(object({ deviceId: number() }), DiskReconcileCountsSchema, {
17012
+ kind: "mutation",
17013
+ auth: "admin"
16963
17014
  }), method(object({
16964
17015
  deviceId: number(),
16965
17016
  trackIds: array(string()).min(1)
@@ -18470,6 +18521,24 @@ var CameraStatusDegradationSchema = object({
18470
18521
  *
18471
18522
  * See spec: `docs/superpowers/specs/2026-06-24-camera-status-aggregator-cap.md`
18472
18523
  */
18524
+ var DiskReconcileJobSchema = object({
18525
+ state: _enum([
18526
+ "idle",
18527
+ "running",
18528
+ "done",
18529
+ "error"
18530
+ ]),
18531
+ total: number().int().nonnegative(),
18532
+ completed: number().int().nonnegative(),
18533
+ currentDeviceId: number().int().nullable(),
18534
+ failed: array(number().int()).readonly(),
18535
+ mediaDropped: number().int().nonnegative(),
18536
+ tracks: number().int().nonnegative(),
18537
+ events: number().int().nonnegative(),
18538
+ startedAtMs: number().int().nullable(),
18539
+ finishedAtMs: number().int().nullable(),
18540
+ error: string().nullable()
18541
+ });
18473
18542
  var CameraStatusSchema = object({
18474
18543
  deviceId: number(),
18475
18544
  assignment: CameraAssignmentStatusSchema,
@@ -18701,7 +18770,10 @@ method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
18701
18770
  }), CameraSwitchGroupSchema, {
18702
18771
  kind: "mutation",
18703
18772
  auth: "admin"
18704
- }), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
18773
+ }), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), DiskReconcileJobSchema, {
18774
+ kind: "mutation",
18775
+ auth: "admin"
18776
+ }), method(_void(), DiskReconcileJobSchema, { auth: "admin" }), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
18705
18777
  name: string(),
18706
18778
  description: string().optional(),
18707
18779
  config: CameraPipelineConfigSchema
@@ -19106,7 +19178,14 @@ targets: array(object({
19106
19178
  "sleeping",
19107
19179
  "unreachable",
19108
19180
  "waking"
19109
- ]).nullable()
19181
+ ]).nullable(),
19182
+ /** A battery camera (whatever its current state). An AWAKE battery
19183
+ * camera is deliberately NOT recaptured on the poll cadence — every
19184
+ * capture is a camera hit that would keep it out of sleep — so its
19185
+ * cached frame legitimately ages past the currency ceiling while
19186
+ * nothing is streaming. A surface must keep painting it (a fresh
19187
+ * frame is captured at each wake), not blank to "unavailable". */
19188
+ battery: boolean()
19110
19189
  })));
19111
19190
  /**
19112
19191
  * `sso-bridge` — internal hub-only cap that lets SSO-style auth
@@ -19790,20 +19869,6 @@ var VectorStatsResultSchema = object({
19790
19869
  exact: boolean()
19791
19870
  });
19792
19871
  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);
19793
- /**
19794
- * `videoclips` — the unified, navigable-clip surface for a camera.
19795
- *
19796
- * A device-scoped WRAPPER cap (like `pipeline-analytics`): exactly one active
19797
- * provider per device, substitutable. The DEFAULT provider (registered by
19798
- * `addon-post-analysis`, `defaultActive: true`) composes the analytics event
19799
- * log (markers + thumbnails) with the recorder's `getPlaybackManifest` — a clip
19800
- * is a time-WINDOW over existing footage, never a separate file. A camera that
19801
- * exposes NATIVE onboard clips (Reolink/Hikvision NVR) can later substitute the
19802
- * wrapper provider for its device and serve its own clip catalog + URLs.
19803
- *
19804
- * A `Clip` is purely time-based (subtree-blind): playback resolves segments by
19805
- * temporal overlap, so the API never decides `continuous` vs `events`.
19806
- */
19807
19872
  var ClipSchema = object({
19808
19873
  /** Opaque, provider-namespaced id. The default provider encodes the time
19809
19874
  * window so `getClipPlayback` is self-contained (no event re-query). */
@@ -19820,8 +19885,57 @@ var ClipSchema = object({
19820
19885
  startMs: number(),
19821
19886
  endMs: number()
19822
19887
  }),
19823
- /** Thumbnail URL (lazy; e.g. analytics `getEventMedia`). Never inlined. */
19824
- thumbnail: string().optional()
19888
+ /**
19889
+ * Lazy thumbnail URL, never inlined.
19890
+ *
19891
+ * Recording-derived clips (events-mode keep-window, and the prepared
19892
+ * continuous event+fragment visit) MUST use the snapshot of the **main
19893
+ * event of the interval** — `getEventMedia({ eventId, kind: 'snapshot' })`
19894
+ * of the event that owns `kind` (object > motion > audio). Do not extract
19895
+ * a keyframe from the recorded segments. Other providers (onboard, HKSV)
19896
+ * mint their own stills.
19897
+ *
19898
+ * **VOUCHED, never fabricated.** A provider emits this only for an event it
19899
+ * has CONFIRMED owns at least one media row (its own, or its owning track's).
19900
+ * Absent is meaningful — "this visit has no event still" — never "we did not
19901
+ * look". Stamping it from a URL template made 35% of one camera's clips point
19902
+ * at a 404 (device 3836, 2026-08-17: 64 of 179 clips dead, 63 of them motion).
19903
+ * A read that FAILS drops the claim; it never invents it.
19904
+ */
19905
+ thumbnail: string().optional(),
19906
+ /**
19907
+ * An instant INSIDE this visit's footage, hole-safe, where a recorded still
19908
+ * can be decoded. Present whenever the visit came from recorded availability;
19909
+ * absent on a per-event padded window (there is no footage to promise).
19910
+ *
19911
+ * This is not a thumbnail and not a second byte path: it is the argument to
19912
+ * the recorder's existing still route. The surface — never the provider —
19913
+ * decides whether to use it. It is the midpoint of the visit's LONGEST
19914
+ * contiguous range, not of the visit: a visit spans its holes by
19915
+ * construction, so a naive midpoint lands in dead air.
19916
+ */
19917
+ stillAtMs: number().optional(),
19918
+ /**
19919
+ * Analytics event ids that overlap this visit — a BOUNDED sample, newest
19920
+ * first within kind (object → motion → audio), capped at
19921
+ * {@link MAX_CLIP_EVENT_IDS}. Empty on footage-only clips.
19922
+ *
19923
+ * Bounded because it is not a payload the surface pages through: one visit on
19924
+ * device 615 carried 2 345 ids, and `eventIds` was 99% of a 153 KB
19925
+ * camera-day. Read {@link eventCount} for the true total.
19926
+ */
19927
+ eventIds: array(string()).optional(),
19928
+ /** How many analytics events actually overlap this visit. Differs from
19929
+ * `eventIds.length` exactly when the sample was capped — so a truncated
19930
+ * list is never mistaken for a quiet visit. */
19931
+ eventCount: number().int().nonnegative().optional(),
19932
+ /** Intra-visit footage holes (GOP rolls, discarded segments) still shorter
19933
+ * than `VISIT_MERGE_GAP_MS`. Playback concatenates around them; the timeline
19934
+ * bar keeps showing them via `recording.getAvailability`. */
19935
+ holes: array(object({
19936
+ startMs: number(),
19937
+ endMs: number()
19938
+ })).optional()
19825
19939
  });
19826
19940
  var ClipPlaybackSchema = object({
19827
19941
  /** HLS master URL through the hub data-plane (Range + token in path). */
@@ -20285,7 +20399,14 @@ var SearchResultSchema = object({
20285
20399
  });
20286
20400
  var AutoUpdateSettingsSchema = object({
20287
20401
  channel: ChannelSchema,
20288
- intervalSeconds: number()
20402
+ intervalSeconds: number(),
20403
+ /**
20404
+ * Cadence of the "an update exists" POLLER, in seconds. Independent of
20405
+ * `channel`: the poller runs while auto-apply is `off`, because being told
20406
+ * about a publish and installing it are different decisions. Clamped
20407
+ * server-side to 900 s … 604800 s; defaults to 21600 s (6 h).
20408
+ */
20409
+ updateCheckIntervalSeconds: number()
20289
20410
  });
20290
20411
  var AddonAutoUpdateSchema = ChannelWithInheritSchema;
20291
20412
  var RestartAddonResultSchema = unknown();
@@ -20426,7 +20547,9 @@ method(_void(), array(AddonListItemSchema).readonly()), method(object({
20426
20547
  auth: "admin"
20427
20548
  }), method(_void(), AutoUpdateSettingsSchema, { auth: "admin" }), method(object({
20428
20549
  channel: ChannelSchema,
20429
- intervalSeconds: number().min(300).max(86400).optional()
20550
+ intervalSeconds: number().min(300).max(86400).optional(),
20551
+ /** Availability-poll cadence; see AutoUpdateSettingsSchema. */
20552
+ updateCheckIntervalSeconds: number().min(900).max(604800).optional()
20430
20553
  }), unknown(), {
20431
20554
  kind: "mutation",
20432
20555
  auth: "admin"
@@ -24419,7 +24542,9 @@ var ExportOptionsSchema = object({
24419
24542
  includeAudio: boolean(),
24420
24543
  maxLifeMs: number().int().positive(),
24421
24544
  deleteAfterDownload: boolean(),
24422
- title: string().max(200).optional()
24545
+ title: string().max(200).optional(),
24546
+ /** Notification-output target ids to ping when this export becomes ready. */
24547
+ notifyTargetIds: array(string().min(1)).max(20).optional()
24423
24548
  }).superRefine((v, ctx) => {
24424
24549
  if (v.speed !== void 0 && v.timelapse !== void 0) ctx.addIssue({
24425
24550
  code: ZodIssueCode.custom,
@@ -24478,10 +24603,18 @@ var ExportBytesSchema = object({
24478
24603
  });
24479
24604
  method(object({
24480
24605
  deviceId: number(),
24481
- profile: string(),
24606
+ /** @deprecated Prefer `profiles`. Kept so timelapse/notifiers keep working. */
24607
+ profile: string().optional(),
24608
+ profiles: array(string()).min(1).optional(),
24482
24609
  fromMs: number(),
24483
24610
  toMs: number(),
24484
24611
  options: ExportOptionsSchema
24612
+ }).superRefine((v, ctx) => {
24613
+ if ((v.profiles !== void 0 && v.profiles.length > 0 ? v.profiles : v.profile !== void 0 ? [v.profile] : []).length < 1) ctx.addIssue({
24614
+ code: ZodIssueCode.custom,
24615
+ message: "pass profiles[] (min 1) or legacy profile",
24616
+ path: ["profiles"]
24617
+ });
24485
24618
  }), ExportRecordSchema, {
24486
24619
  kind: "mutation",
24487
24620
  auth: "protected"
@@ -29081,6 +29214,12 @@ Object.freeze({
29081
29214
  addonId: null,
29082
29215
  access: "create"
29083
29216
  },
29217
+ "pipelineAnalytics.reconcileFromDisk": {
29218
+ capName: "pipeline-analytics",
29219
+ capScope: "device",
29220
+ addonId: null,
29221
+ access: "create"
29222
+ },
29084
29223
  "pipelineAnalytics.refreshStorageLocationsForMigration": {
29085
29224
  capName: "pipeline-analytics",
29086
29225
  capScope: "device",
@@ -29483,6 +29622,12 @@ Object.freeze({
29483
29622
  addonId: null,
29484
29623
  access: "view"
29485
29624
  },
29625
+ "pipelineOrchestrator.getReconcileFromDiskStatus": {
29626
+ capName: "pipeline-orchestrator",
29627
+ capScope: "system",
29628
+ addonId: null,
29629
+ access: "view"
29630
+ },
29486
29631
  "pipelineOrchestrator.listAgentSettings": {
29487
29632
  capName: "pipeline-orchestrator",
29488
29633
  capScope: "system",
@@ -29507,6 +29652,12 @@ Object.freeze({
29507
29652
  addonId: null,
29508
29653
  access: "create"
29509
29654
  },
29655
+ "pipelineOrchestrator.reconcileFromDisk": {
29656
+ capName: "pipeline-orchestrator",
29657
+ capScope: "system",
29658
+ addonId: null,
29659
+ access: "create"
29660
+ },
29510
29661
  "pipelineOrchestrator.removeAgentSettings": {
29511
29662
  capName: "pipeline-orchestrator",
29512
29663
  capScope: "system",
@@ -32498,6 +32649,11 @@ Object.freeze({
32498
32649
  form: "single",
32499
32650
  optional: true
32500
32651
  }],
32652
+ "pipelineAnalytics.reconcileFromDisk": [{
32653
+ name: "deviceId",
32654
+ form: "single",
32655
+ optional: false
32656
+ }],
32501
32657
  "pipelineAnalytics.restageRetrainTrack": [{
32502
32658
  name: "deviceId",
32503
32659
  form: "single",
@@ -33563,6 +33719,8 @@ DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb;
33563
33719
  DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
33564
33720
  DEFAULT_NATIVE_LEASE_SETTINGS.tileBudgetMb;
33565
33721
  DEFAULT_NATIVE_LEASE_SETTINGS.admission;
33722
+ var MB = 1024 * 1024;
33723
+ 1024 * MB, 3072 * MB;
33566
33724
  //#endregion
33567
33725
  //#region src/static-turn.addon.ts
33568
33726
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-static-turn",
3
- "version": "1.2.18",
3
+ "version": "1.2.20",
4
4
  "description": "Static / self-hosted (coturn) TURN provider for CamStack",
5
5
  "keywords": [
6
6
  "camstack",