@camstack/addon-pipeline 1.1.58 → 1.1.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/audio-analyzer/index.js +4 -4
  2. package/dist/audio-analyzer/index.mjs +2 -2
  3. package/dist/detection-pipeline/index.js +48 -10
  4. package/dist/detection-pipeline/index.mjs +42 -4
  5. package/dist/{dist-CnwwDzX3.mjs → dist-CcqqznPf.mjs} +244 -14
  6. package/dist/{dist-Bn6Uq4vq.js → dist-DynD1EC-.js} +249 -13
  7. package/dist/{model-download-service-C-IHWnXx-3Mmeob3l.mjs → model-download-service-Cp9f4dk6-0wh7OK3s.mjs} +2 -2
  8. package/dist/{model-download-service-C-IHWnXx-D326YNnt.js → model-download-service-Cp9f4dk6-Cp_GTKTw.js} +2 -2
  9. package/dist/motion-wasm/index.js +1 -1
  10. package/dist/motion-wasm/index.mjs +1 -1
  11. package/dist/pipeline-runner/index.js +123 -4
  12. package/dist/pipeline-runner/index.mjs +122 -3
  13. package/dist/recorder/index.js +1021 -7
  14. package/dist/recorder/index.mjs +1018 -5
  15. package/dist/session-decode/decode-worker-child.js +14 -1
  16. package/dist/session-decode/decode-worker-child.mjs +14 -1
  17. package/dist/{hub-hostname-cCknRYKj.mjs → sheet-geometry-BR-ip0CP.js} +56 -1
  18. package/dist/{hub-hostname-DAJXlOgV.js → sheet-geometry-DcpuGTCm.mjs} +39 -6
  19. package/dist/{step-definitions-DhEe57Wn.mjs → step-definitions-CvwB_Otk.mjs} +1 -1
  20. package/dist/{step-definitions-D7NOrnzz.js → step-definitions-DhhwklAL.js} +1 -1
  21. package/dist/stream-broker/_stub.js +2 -2
  22. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-B8FQ1e0a.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-B07QYb7Q.mjs} +3 -3
  23. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DYQ6RHcb.mjs +26 -0
  24. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DSXctoaw.mjs +26 -0
  25. package/dist/stream-broker/{hostInit-D1Vdyu-4.mjs → hostInit-C02YjaoP.mjs} +3 -3
  26. package/dist/stream-broker/index.js +622 -149
  27. package/dist/stream-broker/index.mjs +622 -149
  28. package/dist/stream-broker/remoteEntry.js +1 -1
  29. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-CG5EE0W4.js → MaskShapeCanvas-DI4BY7W2-ColkZGA1.js} +1 -1
  30. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-BFeLUNOl.js → MotionZonesSettings-NcxxQN8r-BIzNJmu0.js} +1 -1
  31. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-pQQunq1F.js → PrivacyMaskSettings-APgPLF7p-BHuYOg7Q.js} +1 -1
  32. package/embed-dist/assets/index-n1_ZSmy0.js +81 -0
  33. package/embed-dist/index.html +1 -1
  34. package/package.json +1 -1
  35. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Ba7Eyd47.mjs +0 -26
  36. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-TWCUWhCm.mjs +0 -26
  37. package/embed-dist/assets/index-BxqQ931j.js +0 -81
@@ -1,4 +1,4 @@
1
- //#region ../types/dist/event-category-CFZs3jI4.mjs
1
+ //#region ../types/dist/event-category-H4AVePnn.mjs
2
2
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
3
3
  EventCategory["SystemBoot"] = "system.boot";
4
4
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -148,6 +148,9 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
148
148
  EventCategory["RecordingSegmentWritten"] = "recording.segment.written";
149
149
  EventCategory["RecordingPolicyFallback"] = "recording.policy.fallback";
150
150
  EventCategory["RecordingRetentionCompleted"] = "recording.retention.completed";
151
+ /** Runner-sampled scrub thumbnail (~1/5 s/camera). Telemetry (D8): a lost
152
+ * thumb is a scrub gap the recorder's keyframe backfill covers. */
153
+ EventCategory["RecordingThumbSampled"] = "recording.thumb-sampled";
151
154
  EventCategory["DetectionEvent"] = "detection.event";
152
155
  EventCategory["SessionTrackNew"] = "session.track.new";
153
156
  EventCategory["SessionTrackExpired"] = "session.track.expired";
@@ -7376,6 +7379,65 @@ var RecordingRetentionSchema = object({
7376
7379
  maxSizeGb: number().min(0).optional()
7377
7380
  });
7378
7381
  /**
7382
+ * Scrub-thumbnail fidelity preset — the single per-camera selector bundling the
7383
+ * sprite tile RESOLUTION + JPEG QUALITY the recorder packs timeline-scrub
7384
+ * previews at. Five graduated steps; absent on a config = `standard` (the
7385
+ * shipped default, matching `sheet-geometry`/`sheet-composer`).
7386
+ *
7387
+ * Existing sheets are IMMUTABLE — a changed preset applies to NEW windows only.
7388
+ * Each window's index sidecar carries its own tile dims, so a camera whose
7389
+ * preset changed over time renders every historical window at the dims it was
7390
+ * written with.
7391
+ */
7392
+ var ScrubThumbnailPresetSchema = _enum([
7393
+ "minimal",
7394
+ "low",
7395
+ "standard",
7396
+ "high",
7397
+ "max"
7398
+ ]);
7399
+ /**
7400
+ * Preset → tile geometry + JPEG quality. Every step is 16:9; the graduated
7401
+ * ladder trades sheet size (and remote fetch cost) for scrub crispness.
7402
+ * `standard` matches the shipped `sheet-geometry`/`sheet-composer` defaults
7403
+ * (480×270 q72), so an unset config is byte-identical to today. The map is
7404
+ * strictly monotone in resolution AND quality across the ordered steps.
7405
+ */
7406
+ var SCRUB_THUMBNAIL_PRESETS = {
7407
+ minimal: {
7408
+ tileWidth: 240,
7409
+ tileHeight: 135,
7410
+ quality: 55
7411
+ },
7412
+ low: {
7413
+ tileWidth: 320,
7414
+ tileHeight: 180,
7415
+ quality: 60
7416
+ },
7417
+ standard: {
7418
+ tileWidth: 480,
7419
+ tileHeight: 270,
7420
+ quality: 72
7421
+ },
7422
+ high: {
7423
+ tileWidth: 640,
7424
+ tileHeight: 360,
7425
+ quality: 80
7426
+ },
7427
+ max: {
7428
+ tileWidth: 960,
7429
+ tileHeight: 540,
7430
+ quality: 85
7431
+ }
7432
+ };
7433
+ /**
7434
+ * Resolve the geometry for a recording config's scrub-thumbnail preset,
7435
+ * defaulting to `standard` when unset. Pure — no I/O.
7436
+ */
7437
+ function resolveScrubThumbnailGeometry(preset) {
7438
+ return SCRUB_THUMBNAIL_PRESETS[preset ?? "standard"];
7439
+ }
7440
+ /**
7379
7441
  * The full per-camera recording intent — the wire shape of a RecordingTarget.
7380
7442
  *
7381
7443
  * `mode` is the authoritative storage choice; `schedule`/`triggers`/`pre`/`post`
@@ -7412,7 +7474,14 @@ var RecordingConfigSchema = object({
7412
7474
  * derived into bands once via `migrateConfigToBands`.
7413
7475
  */
7414
7476
  bands: array(RecordingBandSchema).optional(),
7415
- retention: RecordingRetentionSchema.optional()
7477
+ retention: RecordingRetentionSchema.optional(),
7478
+ /**
7479
+ * Per-camera scrub-thumbnail fidelity preset (resolution + JPEG quality for
7480
+ * timeline-scrub sprite previews). Absent = `standard`. Applies to NEW
7481
+ * windows only — existing sheets are immutable, and each window's index
7482
+ * carries its own tile dims so mixed-preset history renders correctly.
7483
+ */
7484
+ scrubThumbnails: ScrubThumbnailPresetSchema.optional()
7416
7485
  });
7417
7486
  /** All seven weekdays (0 = Sunday … 6 = Saturday), used when a schedule omits a
7418
7487
  * weekday filter. */
@@ -10738,7 +10807,7 @@ object({
10738
10807
  deviceId: number(),
10739
10808
  timestamp: number()
10740
10809
  });
10741
- DeviceType.Button;
10810
+ DeviceType.Button, DeviceType.Camera;
10742
10811
  /**
10743
10812
  * Enum-state sensor — a string value picked from a finite option set.
10744
10813
  * Drives HA `sensor` entries with `state_class: enum` (HVAC action
@@ -15569,6 +15638,14 @@ var ConfigEntrySchema = object({
15569
15638
  value: unknown(),
15570
15639
  description: string().optional()
15571
15640
  });
15641
+ var DeviceLinkModeSchema = _enum(["auto", "manual"]);
15642
+ /** One resolved linked device — the compact projection consumers need. */
15643
+ var LinkedDeviceSchema = object({
15644
+ deviceId: number(),
15645
+ name: string(),
15646
+ location: string().nullable(),
15647
+ features: array(string())
15648
+ });
15572
15649
  var SavedDeviceRowSchema = object({
15573
15650
  /** Numeric id reserved at allocateDeviceId time. */
15574
15651
  id: number(),
@@ -15790,7 +15867,10 @@ method(object({
15790
15867
  }), _void(), {
15791
15868
  kind: "mutation",
15792
15869
  auth: "admin"
15793
- }), method(object({ addonId: string() }), array(SavedDeviceRowSchema)), method(object({ addonId: string().optional() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
15870
+ }), method(object({ addonId: string() }), array(SavedDeviceRowSchema)), method(object({ addonId: string().optional() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
15871
+ mode: DeviceLinkModeSchema,
15872
+ devices: array(LinkedDeviceSchema)
15873
+ })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
15794
15874
  deviceId: number(),
15795
15875
  values: record(string(), unknown())
15796
15876
  }), object({ success: literal(true) }), {
@@ -17059,6 +17139,61 @@ var EventKindSchema = _enum([
17059
17139
  "object",
17060
17140
  "audio"
17061
17141
  ]);
17142
+ /**
17143
+ * Spatial filter for `listTracks` — the rect + polygon variants of the shared
17144
+ * MaskShape vocabulary (see `mask-shape.ts`). Coordinates are NORMALIZED 0..1
17145
+ * of the camera frame (top-left origin), matching the drawing-plane editor.
17146
+ */
17147
+ var TrackZoneFilterSchema = discriminatedUnion("kind", [MaskRectShapeSchema, MaskPolygonShapeSchema]);
17148
+ /** Closed icon vocabulary so clients render a known glyph per kind. */
17149
+ var EventKindIconSchema = _enum([
17150
+ "motion",
17151
+ "audio",
17152
+ "person",
17153
+ "vehicle",
17154
+ "animal",
17155
+ "door",
17156
+ "pir",
17157
+ "smoke",
17158
+ "water",
17159
+ "button",
17160
+ "generic"
17161
+ ]);
17162
+ var EventKindCategorySchema = _enum([
17163
+ "motion",
17164
+ "audio",
17165
+ "detection",
17166
+ "sensor",
17167
+ "custom"
17168
+ ]);
17169
+ var EventKindDescriptorSchema = object({
17170
+ /** Stable kind id (e.g. 'motion', 'person', 'contact'). */
17171
+ kind: string(),
17172
+ label: string(),
17173
+ /** Hex color for timeline/legend rendering. */
17174
+ color: string(),
17175
+ icon: EventKindIconSchema,
17176
+ category: EventKindCategorySchema,
17177
+ /** Which cap + device contributes this kind. For built-ins the camera
17178
+ * itself; for sensor kinds the LINKED source device. */
17179
+ source: object({
17180
+ capName: string(),
17181
+ deviceId: number()
17182
+ })
17183
+ });
17184
+ var SensorEventSchema = object({
17185
+ id: string(),
17186
+ /** The CAMERA the event is attributed to (a sensor linked to N cameras
17187
+ * yields N rows, one per camera). */
17188
+ deviceId: number(),
17189
+ /** The linked sensor device whose state changed. */
17190
+ sourceDeviceId: number(),
17191
+ /** Event kind id — matches an `EventKindDescriptor.kind`. */
17192
+ kind: string(),
17193
+ /** Snapshot of the sensor cap's runtime-state slice at the change. */
17194
+ value: record(string(), unknown()).nullable(),
17195
+ timestamp: number()
17196
+ });
17062
17197
  var TrackPositionSchema = object({
17063
17198
  x: number(),
17064
17199
  y: number(),
@@ -17072,6 +17207,30 @@ var TrackSnapshotSchema = object({
17072
17207
  mediaKey: string()
17073
17208
  });
17074
17209
  /**
17210
+ * Normalized 0..1 trajectory envelope (min/max over every position bbox,
17211
+ * divided by the track's detection-frame dims), computed at persist time.
17212
+ * Absent when the frame dims were unknown when the track was persisted
17213
+ * (legacy rows / dims-less sources) and on active (in-RAM) tracks.
17214
+ */
17215
+ var TrackEnvelopeSchema = object({
17216
+ minX: number(),
17217
+ minY: number(),
17218
+ maxX: number(),
17219
+ maxY: number()
17220
+ });
17221
+ /**
17222
+ * Row projection for track list queries. `full` (default) returns the
17223
+ * complete Track including the frame-rate `positions[]` history and the
17224
+ * `snapshots[]` references — megabytes across a page of tracks. `slim`
17225
+ * keeps every scalar the list surfaces actually render (ids, class(es),
17226
+ * label / audioLabels / importance enrichment, firstSeen/lastSeen, state,
17227
+ * zonesVisited, bestEventId, envelope) and returns `positions` /
17228
+ * `snapshots` as EMPTY arrays — detail views re-fetch the full row via
17229
+ * `getTrack`. Mirrors the event-store `projection` convention
17230
+ * (`getObjectEvents` et al.).
17231
+ */
17232
+ var TrackProjectionSchema = _enum(["full", "slim"]);
17233
+ /**
17075
17234
  * One audio-classification label heard on the track's camera while the
17076
17235
  * track was alive, aggregated per label. An "episode" is one persisted
17077
17236
  * audio event (the confident-classification path: score ≥ the device's
@@ -17122,7 +17281,11 @@ var TrackSchema = object({
17122
17281
  /** Audio-classification labels heard on the camera during the track's
17123
17282
  * life (score ≥ device `classificationMinScore`), aggregated per label.
17124
17283
  * Absent on legacy rows / tracks with no confident audio. */
17125
- audioLabels: array(TrackAudioLabelSchema).readonly().optional()
17284
+ audioLabels: array(TrackAudioLabelSchema).readonly().optional(),
17285
+ /** Normalized 0..1 trajectory envelope (see {@link TrackEnvelopeSchema}).
17286
+ * Populated from the persisted envelope columns on historical reads;
17287
+ * absent on legacy rows, dims-less tracks and active (in-RAM) tracks. */
17288
+ envelope: TrackEnvelopeSchema.optional()
17126
17289
  });
17127
17290
  var BaseEventFields = {
17128
17291
  id: string(),
@@ -17232,11 +17395,13 @@ var MediaFileSchema = object({
17232
17395
  sizeBytes: number(),
17233
17396
  timestamp: number()
17234
17397
  });
17398
+ var DEFAULT_EVENT_QUERY_LIMIT = 1e3;
17399
+ var MAX_EVENT_QUERY_LIMIT = 5e3;
17235
17400
  var DeviceEventQueryInput = object({
17236
17401
  deviceId: number(),
17237
17402
  since: number().optional(),
17238
17403
  until: number().optional(),
17239
- limit: number().int().min(1).max(5e3).default(1e3),
17404
+ limit: number().int().min(1).max(MAX_EVENT_QUERY_LIMIT).default(DEFAULT_EVENT_QUERY_LIMIT),
17240
17405
  /** `slim` drops heavy JSON fields (regions/bbox/zones) and carries an
17241
17406
  * optional `mediaUrl` (populated by B5). `full` (default) keeps today's
17242
17407
  * exact behaviour. Callers may omit this field — the store defaults to
@@ -17244,6 +17409,27 @@ var DeviceEventQueryInput = object({
17244
17409
  projection: _enum(["full", "slim"]).optional()
17245
17410
  });
17246
17411
  var ObjectEventQueryInput = DeviceEventQueryInput.extend({ classFilter: string().optional() });
17412
+ var RecentTracksQueryInput = object({
17413
+ /** Devices to merge. An empty array yields `{ tracks: [], nextCursor: null }`. */
17414
+ deviceIds: array(number()),
17415
+ /** Window lower bound on `lastSeen` (inclusive). */
17416
+ since: number().optional(),
17417
+ /** Window upper bound on `lastSeen` (inclusive). */
17418
+ until: number().optional(),
17419
+ /** Page size. Default 200, max 1000. */
17420
+ limit: number().int().min(1).max(1e3).default(200),
17421
+ /** Opaque continuation cursor from a previous page's `nextCursor`.
17422
+ * Encodes the (lastSeen, trackId) sort position — treat as opaque. */
17423
+ cursor: string().optional(),
17424
+ /** See {@link TrackProjectionSchema}. Default `full`. */
17425
+ projection: TrackProjectionSchema.optional()
17426
+ });
17427
+ var RecentTracksPageSchema = object({
17428
+ /** Merged page, ordered by (`lastSeen` DESC, `trackId` DESC). */
17429
+ tracks: array(TrackSchema).readonly(),
17430
+ /** Cursor for the next page, or null when this page is the last. */
17431
+ nextCursor: string().nullable()
17432
+ });
17247
17433
  var KeyEventQueryInput = object({
17248
17434
  deviceId: number(),
17249
17435
  /** Window lower bound (track firstSeen ≥ since). */
@@ -17326,11 +17512,25 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17326
17512
  deviceId: number(),
17327
17513
  since: number().optional(),
17328
17514
  until: number().optional(),
17329
- limit: number().optional()
17330
- }), array(TrackSchema).readonly()), method(object({ deviceId: number() }), _void(), {
17331
- kind: "mutation",
17332
- auth: "admin"
17333
- }), method(DeviceEventQueryInput, array(MotionEventSchema).readonly()), method(ObjectEventQueryInput, array(ObjectEventSchema).readonly()), method(DeviceEventQueryInput, array(AudioEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(object({
17515
+ limit: number().optional(),
17516
+ /** Spatial filter only tracks whose trajectory intersects the zone
17517
+ * (normalized 0..1 rect or polygon). SQL-prefiltered on the persisted
17518
+ * envelope columns, then precisely tested per position. Tracks with
17519
+ * an unknown envelope (no frame dims at persist time) always match. */
17520
+ zone: TrackZoneFilterSchema.optional(),
17521
+ /** See {@link TrackProjectionSchema}. Default `full` (backward
17522
+ * compatible — omitting the field keeps today's exact behaviour). */
17523
+ projection: TrackProjectionSchema.optional()
17524
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
17525
+ kind: "mutation",
17526
+ auth: "admin"
17527
+ }), method(DeviceEventQueryInput, array(MotionEventSchema).readonly()), method(ObjectEventQueryInput, array(ObjectEventSchema).readonly()), method(DeviceEventQueryInput, array(AudioEventSchema).readonly()), method(object({ deviceId: number() }), array(EventKindDescriptorSchema).readonly()), method(object({
17528
+ deviceId: number(),
17529
+ since: number().optional(),
17530
+ until: number().optional(),
17531
+ kinds: array(string()).optional(),
17532
+ limit: number().int().min(1).max(MAX_EVENT_QUERY_LIMIT).default(DEFAULT_EVENT_QUERY_LIMIT)
17533
+ }), array(SensorEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(object({
17334
17534
  deviceId: number(),
17335
17535
  since: number(),
17336
17536
  until: number(),
@@ -20626,7 +20826,12 @@ var RecordingStorageUsageSchema = object({
20626
20826
  /**
20627
20827
  * Result of locating footage at a wall-clock instant for one device/profile.
20628
20828
  * `segment` carries the covering segment's window; `gap` reports the forward
20629
- * nearest covered edge (`null` past the end of footage / when none exists).
20829
+ * nearest covered edge (`null` past the end of footage / when none exists)
20830
+ * and the backward covered edge `prevEndMs` (exclusive end of the nearest
20831
+ * footage behind the epoch; `null` when none — optional so older providers
20832
+ * that omit it stay valid). `prevEndMs` lets a backward frame-step hop the
20833
+ * small inter-segment cracks (durMs under-covers the span to the next
20834
+ * startMs by ~11-17 ms) instead of no-opping at a segment head.
20630
20835
  */
20631
20836
  var LocateSegmentResultSchema = discriminatedUnion("kind", [object({
20632
20837
  kind: literal("segment"),
@@ -20635,7 +20840,8 @@ var LocateSegmentResultSchema = discriminatedUnion("kind", [object({
20635
20840
  bytes: number()
20636
20841
  }), object({
20637
20842
  kind: literal("gap"),
20638
- nearestEdgeMs: number().nullable()
20843
+ nearestEdgeMs: number().nullable(),
20844
+ prevEndMs: number().nullable().optional()
20639
20845
  })]);
20640
20846
  /** Raw bytes of one finalized footage segment (read off disk on the recording node). */
20641
20847
  var ReadSegmentBytesResultSchema = object({ data: _instanceof(Uint8Array) });
@@ -22237,6 +22443,12 @@ Object.freeze({
22237
22443
  addonId: null,
22238
22444
  access: "view"
22239
22445
  },
22446
+ "deviceManager.getLinkedDevices": {
22447
+ capName: "device-manager",
22448
+ capScope: "system",
22449
+ addonId: null,
22450
+ access: "view"
22451
+ },
22240
22452
  "deviceManager.getRoleDisplayDefaults": {
22241
22453
  capName: "device-manager",
22242
22454
  capScope: "system",
@@ -23791,6 +24003,12 @@ Object.freeze({
23791
24003
  addonId: null,
23792
24004
  access: "view"
23793
24005
  },
24006
+ "pipelineAnalytics.getSensorEvents": {
24007
+ capName: "pipeline-analytics",
24008
+ capScope: "device",
24009
+ addonId: null,
24010
+ access: "view"
24011
+ },
23794
24012
  "pipelineAnalytics.getTrack": {
23795
24013
  capName: "pipeline-analytics",
23796
24014
  capScope: "device",
@@ -23803,6 +24021,18 @@ Object.freeze({
23803
24021
  addonId: null,
23804
24022
  access: "view"
23805
24023
  },
24024
+ "pipelineAnalytics.listEventKinds": {
24025
+ capName: "pipeline-analytics",
24026
+ capScope: "device",
24027
+ addonId: null,
24028
+ access: "view"
24029
+ },
24030
+ "pipelineAnalytics.listRecentTracks": {
24031
+ capName: "pipeline-analytics",
24032
+ capScope: "device",
24033
+ addonId: null,
24034
+ access: "view"
24035
+ },
23806
24036
  "pipelineAnalytics.listTracks": {
23807
24037
  capName: "pipeline-analytics",
23808
24038
  capScope: "device",
@@ -26304,6 +26534,12 @@ Object.defineProperty(exports, "recordingCapability", {
26304
26534
  return recordingCapability;
26305
26535
  }
26306
26536
  });
26537
+ Object.defineProperty(exports, "resolveScrubThumbnailGeometry", {
26538
+ enumerable: true,
26539
+ get: function() {
26540
+ return resolveScrubThumbnailGeometry;
26541
+ }
26542
+ });
26307
26543
  Object.defineProperty(exports, "runtimeDevices", {
26308
26544
  enumerable: true,
26309
26545
  get: function() {
@@ -3,7 +3,7 @@ import { createReadStream, promises } from "node:fs";
3
3
  import * as path$1 from "node:path";
4
4
  import path from "node:path";
5
5
  import "node:crypto";
6
- //#region ../system/dist/model-download-service-C-IHWnXx.mjs
6
+ //#region ../system/dist/model-download-service-Cp9f4dk6.mjs
7
7
  /**
8
8
  * Map a rel path to one candidate absolute path PER root, keeping only roots the
9
9
  * path stays within (traversal guard). The handler serves the first candidate
@@ -121,7 +121,7 @@ function createFileDataPlaneHandler(opts) {
121
121
  const baseHeaders = {
122
122
  "content-type": contentTypeFor(absPath, opts.contentTypes),
123
123
  "accept-ranges": "bytes",
124
- "cache-control": "no-cache"
124
+ "cache-control": opts.cacheControl?.(rel) ?? "no-cache"
125
125
  };
126
126
  const range = parseRangeHeader(req.headers.range, size);
127
127
  if (range) {
@@ -4,7 +4,7 @@ node_fs = require_chunk.__toESM(node_fs, 1);
4
4
  let node_path = require("node:path");
5
5
  node_path = require_chunk.__toESM(node_path, 1);
6
6
  require("node:crypto");
7
- //#region ../system/dist/model-download-service-C-IHWnXx.mjs
7
+ //#region ../system/dist/model-download-service-Cp9f4dk6.mjs
8
8
  /**
9
9
  * Map a rel path to one candidate absolute path PER root, keeping only roots the
10
10
  * path stays within (traversal guard). The handler serves the first candidate
@@ -122,7 +122,7 @@ function createFileDataPlaneHandler(opts) {
122
122
  const baseHeaders = {
123
123
  "content-type": contentTypeFor(absPath, opts.contentTypes),
124
124
  "accept-ranges": "bytes",
125
- "cache-control": "no-cache"
125
+ "cache-control": opts.cacheControl?.(rel) ?? "no-cache"
126
126
  };
127
127
  const range = parseRangeHeader(req.headers.range, size);
128
128
  if (range) {
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-Bn6Uq4vq.js");
5
+ const require_dist = require("../dist-DynD1EC-.js");
6
6
  let node_fs = require("node:fs");
7
7
  let node_path = require("node:path");
8
8
  //#region src/motion-wasm/wasm-motion-detector.ts
@@ -1,4 +1,4 @@
1
- import { B as DeviceType, E as motionDetectionCapability, H as hydrateSchema, L as BaseAddon, x as evaluateZoneRules } from "../dist-CnwwDzX3.mjs";
1
+ import { E as motionDetectionCapability, R as BaseAddon, U as hydrateSchema, V as DeviceType, x as evaluateZoneRules } from "../dist-CcqqznPf.mjs";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  //#region src/motion-wasm/wasm-motion-detector.ts
@@ -3,11 +3,11 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-D6vf50IK.js");
6
- const require_dist = require("../dist-Bn6Uq4vq.js");
6
+ const require_dist = require("../dist-DynD1EC-.js");
7
7
  const require_remote_restream = require("../remote-restream-CO36Sr30.js");
8
- const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
8
+ const require_sheet_geometry = require("../sheet-geometry-BR-ip0CP.js");
9
9
  const require_worker_protocol = require("../worker-protocol-PP4jKHHJ.js");
10
- const require_step_definitions = require("../step-definitions-D7NOrnzz.js");
10
+ const require_step_definitions = require("../step-definitions-DhhwklAL.js");
11
11
  let node_child_process = require("node:child_process");
12
12
  let node_url = require("node:url");
13
13
  let sharp = require("sharp");
@@ -1279,6 +1279,40 @@ function cropRgb(frame, bbox, maxWidth) {
1279
1279
  height: outH
1280
1280
  };
1281
1281
  }
1282
+ var ThumbSamplerPolicy = class {
1283
+ intervalMs;
1284
+ minGapMs;
1285
+ now;
1286
+ /** Last accepted-sample wall clock per device. */
1287
+ lastAcceptedAt = /* @__PURE__ */ new Map();
1288
+ constructor(options) {
1289
+ this.intervalMs = Math.max(0, options.intervalMs);
1290
+ this.minGapMs = Math.max(0, options.minGapMs ?? 1e3);
1291
+ this.now = options.now ?? (() => Date.now());
1292
+ }
1293
+ /**
1294
+ * Decide whether to sample `deviceId` now. Returns true (and records the
1295
+ * acceptance) when the larger of `intervalMs` / `minGapMs` has elapsed since
1296
+ * the last accepted sample for this device; false otherwise. The FIRST call
1297
+ * for a device always samples (cold start).
1298
+ */
1299
+ shouldSample(deviceId) {
1300
+ const now = this.now();
1301
+ const gate = Math.max(this.intervalMs, this.minGapMs);
1302
+ const last = this.lastAcceptedAt.get(deviceId);
1303
+ if (last !== void 0 && now - last < gate) return false;
1304
+ this.lastAcceptedAt.set(deviceId, now);
1305
+ return true;
1306
+ }
1307
+ /** Forget a device (detach / phase exit) so its next frame cold-starts. */
1308
+ forget(deviceId) {
1309
+ this.lastAcceptedAt.delete(deviceId);
1310
+ }
1311
+ /** Drop all per-device state (shutdown). */
1312
+ clear() {
1313
+ this.lastAcceptedAt.clear();
1314
+ }
1315
+ };
1282
1316
  //#endregion
1283
1317
  //#region src/session-decode/session-decode-coordinator.ts
1284
1318
  /** How long to wait after `kill()` before escalating to `kill('SIGKILL')`. */
@@ -2029,6 +2063,8 @@ async function runDetailSubtree(deps, input) {
2029
2063
  //#region src/pipeline-runner/index.ts
2030
2064
  var DEFAULT_CONFIG = {
2031
2065
  maxQueueDepth: 30,
2066
+ thumbnailsEnabled: true,
2067
+ thumbnailSampleMs: 5e3,
2032
2068
  maxConcurrentInferences: 16,
2033
2069
  targetLoadPercent: 80,
2034
2070
  minThrottledFps: 1,
@@ -2244,7 +2280,7 @@ function resolveSessionDecodeOwnerNodeId(config) {
2244
2280
  function resolveOwnerRestreamHost(frameSource, hubUrl) {
2245
2281
  const reachable = frameSource.ownerReachableHost?.trim();
2246
2282
  if (reachable !== void 0 && reachable.length > 0) return reachable;
2247
- return require_hub_hostname.resolveHubHostname(frameSource.hubHostnameOverride, hubUrl);
2283
+ return require_sheet_geometry.resolveHubHostname(frameSource.hubHostnameOverride, hubUrl);
2248
2284
  }
2249
2285
  /**
2250
2286
  * Acquire the broker's COMPRESSED passthrough restream for one camStream and
@@ -2350,6 +2386,12 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
2350
2386
  * Initialised in {@link onInitialize} once `nodeId` is known.
2351
2387
  */
2352
2388
  retainedFrames = null;
2389
+ /**
2390
+ * Scrub-thumbnail sampler policy (2026-07-16). Gates the per-camera JPEG
2391
+ * encode to ~1 / `thumbnailSampleMs` (with a hard 1 Hz ceiling) so the cost
2392
+ * stays a couple ms of sharp every few seconds. Null until `onInitialize`.
2393
+ */
2394
+ thumbSampler = null;
2353
2395
  attached = /* @__PURE__ */ new Map();
2354
2396
  nodeId = "unknown";
2355
2397
  metricsSnapshotTimer = null;
@@ -2470,6 +2512,7 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
2470
2512
  nodeId: this.nodeId,
2471
2513
  logger: this.ctx.logger.child("retained-frames")
2472
2514
  });
2515
+ this.thumbSampler = new ThumbSamplerPolicy({ intervalMs: this.config.thumbnailSampleMs });
2473
2516
  this.runner = new PipelineRunner({
2474
2517
  maxQueueDepth: this.config.maxQueueDepth,
2475
2518
  maxConcurrentInferences: this.config.maxConcurrentInferences,
@@ -2559,6 +2602,8 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
2559
2602
  this.nativeCropRegistry.clear();
2560
2603
  this.retainedFrames?.clear();
2561
2604
  this.retainedFrames = null;
2605
+ this.thumbSampler?.clear();
2606
+ this.thumbSampler = null;
2562
2607
  }
2563
2608
  async cacheBenchFrame(input) {
2564
2609
  const sharp$2 = (await import("sharp")).default;
@@ -3021,6 +3066,59 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
3021
3066
  frameSpace: denormalizeRect(paddedBbox, handle.width, handle.height)
3022
3067
  };
3023
3068
  }
3069
+ /**
3070
+ * Scrub-thumbnail sampler (2026-07-16). Rides the already-decoded rgb
3071
+ * detection frame: at most once per `thumbnailSampleMs` per camera (policy
3072
+ * enforces a 1 Hz hard ceiling), downscale to a ~320px JPEG and emit it as
3073
+ * `recording.thumb-sampled` telemetry. The recorder (single designated node)
3074
+ * packs 5-minute sprite sheets from these. Fire-and-forget — a failed encode
3075
+ * is logged at debug and never disturbs the decode/detect path. Zero extra
3076
+ * decode (frame exists); one sharp encode per cadence per camera (D9: a
3077
+ * compressed payload at 1/5 Hz, never raw pixels at frame-rate).
3078
+ */
3079
+ maybeSampleThumbnail(deviceId, frame) {
3080
+ if (!this.config.thumbnailsEnabled) return;
3081
+ const sampler = this.thumbSampler;
3082
+ if (!sampler || frame.format !== "rgb") return;
3083
+ if (!sampler.shouldSample(deviceId)) return;
3084
+ const capturedAt = typeof frame.capturedAt === "number" && frame.capturedAt > 0 ? frame.capturedAt : Date.now();
3085
+ const width = frame.width;
3086
+ const height = frame.height;
3087
+ const data = frame.data;
3088
+ (async () => {
3089
+ try {
3090
+ const targetW = Math.min(480, width);
3091
+ const image = (0, sharp.default)(data, { raw: {
3092
+ width,
3093
+ height,
3094
+ channels: 3
3095
+ } });
3096
+ if (targetW < width) image.resize(targetW);
3097
+ const jpeg = await image.jpeg({ quality: 60 }).toBuffer();
3098
+ const meta = {
3099
+ width: targetW,
3100
+ height: Math.round(targetW * height / width)
3101
+ };
3102
+ this.ctxIfReady?.eventBus.emit(require_dist.createEvent(require_dist.EventCategory.RecordingThumbSampled, {
3103
+ type: "pipeline",
3104
+ id: deviceId,
3105
+ nodeId: this.nodeId,
3106
+ deviceId
3107
+ }, {
3108
+ deviceId,
3109
+ capturedAt,
3110
+ width: meta.width,
3111
+ height: meta.height,
3112
+ jpeg: new Uint8Array(jpeg)
3113
+ }));
3114
+ } catch (err) {
3115
+ this.ctx?.logger.debug("thumbnail sample encode failed", {
3116
+ tags: { deviceId },
3117
+ meta: { error: err instanceof Error ? err.message : String(err) }
3118
+ });
3119
+ }
3120
+ })();
3121
+ }
3024
3122
  /** Real `runPipeline` dep: node-local `pipelineExecutor.runPipeline` on the `'full'` plane. */
3025
3123
  async runDetailPipeline(steps, imageJpeg, deviceId) {
3026
3124
  const api = this.ctxIfReady?.api;
@@ -3098,6 +3196,7 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
3098
3196
  attachment.detectionUnsubscribe?.();
3099
3197
  this.lastMotionFrameAt.delete(deviceId);
3100
3198
  this.lastDecodeUnexpectedEndAt.delete(deviceId);
3199
+ this.thumbSampler?.forget(deviceId);
3101
3200
  this.attached.delete(deviceId);
3102
3201
  this.lastMotionAt.delete(deviceId);
3103
3202
  this.lastEmittedCameraMetrics.delete(deviceId);
@@ -3339,6 +3438,7 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
3339
3438
  const handle = retainedFrames?.write(config.deviceId, frame) ?? void 0;
3340
3439
  if (handle && nativeCrop) this.nativeCropRegistry.register(handle, nativeCrop);
3341
3440
  this.enqueueSharedDetectionFrame(config.deviceId, frame, handle);
3441
+ this.maybeSampleThumbnail(config.deviceId, frame);
3342
3442
  }
3343
3443
  });
3344
3444
  }
@@ -3993,12 +4093,31 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
3993
4093
  step: 1,
3994
4094
  default: DEFAULT_CONFIG.maxConcurrentDecodeSpawns,
3995
4095
  showValue: true
4096
+ },
4097
+ {
4098
+ type: "boolean",
4099
+ key: "thumbnailsEnabled",
4100
+ label: "Scrub thumbnails",
4101
+ description: "Sample the already-decoded detection frame into small JPEG scrub thumbnails (zero extra decode). The recorder packs them into 5-minute sprite sheets the viewer timeline scrubs. Disable to stop emitting thumbnail telemetry from this node.",
4102
+ default: DEFAULT_CONFIG.thumbnailsEnabled
4103
+ },
4104
+ {
4105
+ type: "slider",
4106
+ key: "thumbnailSampleMs",
4107
+ label: "Thumbnail cadence (ms)",
4108
+ description: "How often (ms) each camera contributes a scrub thumbnail. Lower = denser scrub preview + more storage; a hard 1 Hz ceiling always applies. Default 5000.",
4109
+ min: 1e3,
4110
+ max: 3e4,
4111
+ step: 1e3,
4112
+ default: DEFAULT_CONFIG.thumbnailSampleMs,
4113
+ showValue: true
3996
4114
  }
3997
4115
  ]
3998
4116
  }] });
3999
4117
  }
4000
4118
  async onConfigChanged() {
4001
4119
  this.runner?.updateLimits(this.config);
4120
+ this.thumbSampler = new ThumbSamplerPolicy({ intervalMs: this.config.thumbnailSampleMs });
4002
4121
  this.spawnGate?.resize(Math.max(1, this.config.maxConcurrentDecodeSpawns));
4003
4122
  this.ctx.logger.info("pipeline-runner tuning updated", { meta: {
4004
4123
  maxQueueDepth: this.config.maxQueueDepth,