@camstack/addon-pipeline-orchestrator 1.2.69 → 1.2.75

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 (19) hide show
  1. package/assets/viewer/.viewer-current-files.json +5 -5
  2. package/assets/viewer/.viewer-version +1 -1
  3. package/assets/viewer/_expo/static/js/web/Clipboard-68139000fcc61af593c44b211b675f83.js +8 -0
  4. package/assets/viewer/_expo/static/js/web/Haptics-01ab31b2a1d3325f70b28a24474fe704.js +3 -0
  5. package/assets/viewer/_expo/static/js/web/index-39e5c6b7c7445c1b4a2baa92dd2c98e1.js +3567 -0
  6. package/assets/viewer/_expo/static/js/web/{index-d1a9c6f521e974cc90f1a7eaaf908354.js → index-4fec4dee89e2263869ce0e847239cd29.js} +1 -1
  7. package/assets/viewer/_expo/static/js/web/node-fs-9335d97bb088777ae4eb23bb48049a68.js +1 -0
  8. package/assets/viewer/_expo/static/js/web/node-path-da70216c9d8adb1fa18a6f4efbee784e.js +1 -0
  9. package/assets/viewer/index.html +1 -1
  10. package/dist/_stub.js +2 -2
  11. package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-Cb2gW27C.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-CS2P_brd.mjs} +2 -2
  12. package/dist/_virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Cb_kiYRV.mjs +26 -0
  13. package/dist/{_virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CbIQ4mVS.mjs → _virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-7WxrFeM1.mjs} +1 -1
  14. package/dist/{hostInit-DuuZmzlm.mjs → hostInit-NlhiPlZ_.mjs} +2 -2
  15. package/dist/index.js +256 -1
  16. package/dist/index.mjs +256 -1
  17. package/dist/remoteEntry.js +1 -1
  18. package/package.json +1 -1
  19. package/dist/_virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Di2TvwP9.mjs +0 -26
package/dist/index.js CHANGED
@@ -8461,6 +8461,12 @@ var RecordingConfigSchema = object({
8461
8461
  /** DERIVED summary of `bands`, stamped by the recorder on every save.
8462
8462
  * Authoring it has no effect — see {@link RecordingStorageModeSchema}. */
8463
8463
  mode: RecordingStorageModeSchema.optional(),
8464
+ /**
8465
+ * Which assigned broker slots to record. Absent / empty = {@link
8466
+ * DEFAULT_RECORDING_PROFILES} (`high`+`low`) intersected with the
8467
+ * camera's currently assigned slots — never `mid` unless the operator
8468
+ * picks it, and never a slot the broker has not assigned.
8469
+ */
8464
8470
  profiles: array(CamProfileSchema).optional(),
8465
8471
  segmentSeconds: number().int().positive().optional(),
8466
8472
  /**
@@ -8541,7 +8547,11 @@ var RelocateFootageInputSchema = object({
8541
8547
  profiles: array(string()).optional(),
8542
8548
  /** Copy throttle in MB/s (default 40) — the drain is a background chore,
8543
8549
  * never allowed to starve live writers. */
8544
- throttleMbps: number().min(1).max(1e3).optional()
8550
+ throttleMbps: number().min(1).max(1e3).optional(),
8551
+ /** Move only segments whose startMs is >= this. Absent = the whole source
8552
+ * pile. Used when a full drain is too expensive and the operator only
8553
+ * wants the recent window on the new disk. */
8554
+ sinceMs: number().int().optional()
8545
8555
  });
8546
8556
  /** Internal, lease-scoped participant operation. It is intentionally separate
8547
8557
  * from persistent recording settings: a migration never changes
@@ -17743,6 +17753,12 @@ var TrackCascadeCountsSchema = object({
17743
17753
  /** Per-track CLIP search vectors removed (best-effort). */
17744
17754
  embeddings: number().int()
17745
17755
  });
17756
+ /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17757
+ var DiskReconcileCountsSchema = object({
17758
+ mediaDropped: number().int(),
17759
+ tracks: number().int(),
17760
+ events: number().int()
17761
+ });
17746
17762
  /** Event-store footprint for one camera. */
17747
17763
  var EventStoreDeviceFootprintSchema = object({
17748
17764
  deviceId: number(),
@@ -17919,6 +17935,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17919
17935
  }), method(object({ deviceId: number() }), TrackCascadeCountsSchema, {
17920
17936
  kind: "mutation",
17921
17937
  auth: "admin"
17938
+ }), method(object({ deviceId: number() }), DiskReconcileCountsSchema, {
17939
+ kind: "mutation",
17940
+ auth: "admin"
17922
17941
  }), method(object({
17923
17942
  deviceId: number(),
17924
17943
  trackIds: array(string()).min(1)
@@ -19779,6 +19798,24 @@ var CameraStatusDegradationSchema = object({
19779
19798
  *
19780
19799
  * See spec: `docs/superpowers/specs/2026-06-24-camera-status-aggregator-cap.md`
19781
19800
  */
19801
+ var DiskReconcileJobSchema = object({
19802
+ state: _enum([
19803
+ "idle",
19804
+ "running",
19805
+ "done",
19806
+ "error"
19807
+ ]),
19808
+ total: number().int().nonnegative(),
19809
+ completed: number().int().nonnegative(),
19810
+ currentDeviceId: number().int().nullable(),
19811
+ failed: array(number().int()).readonly(),
19812
+ mediaDropped: number().int().nonnegative(),
19813
+ tracks: number().int().nonnegative(),
19814
+ events: number().int().nonnegative(),
19815
+ startedAtMs: number().int().nullable(),
19816
+ finishedAtMs: number().int().nullable(),
19817
+ error: string().nullable()
19818
+ });
19782
19819
  var CameraStatusSchema = object({
19783
19820
  deviceId: number(),
19784
19821
  assignment: CameraAssignmentStatusSchema,
@@ -20315,6 +20352,18 @@ var pipelineOrchestratorCapability = {
20315
20352
  * rail without issuing N parallel browser round-trips.
20316
20353
  */
20317
20354
  getCameraStatuses: method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()),
20355
+ /**
20356
+ * Disk-wins fleet reconcile after a recordings wipe. Starts the walk in
20357
+ * the addon process and returns immediately with the job snapshot — the
20358
+ * work outlives the tRPC/UDS 60s timeout. Poll `getReconcileFromDiskStatus`.
20359
+ * Admin-only. Idempotent while a job is already running.
20360
+ */
20361
+ reconcileFromDisk: method(_void(), DiskReconcileJobSchema, {
20362
+ kind: "mutation",
20363
+ auth: "admin"
20364
+ }),
20365
+ /** Snapshot of the in-flight or last disk-wins fleet reconcile. */
20366
+ getReconcileFromDiskStatus: method(_void(), DiskReconcileJobSchema, { auth: "admin" }),
20318
20367
  /** List every template the operator has saved. */
20319
20368
  listTemplates: method(_void(), array(PipelineTemplateSchema).readonly()),
20320
20369
  /** Create a new named preset from a given CameraPipelineConfig. */
@@ -30802,6 +30851,12 @@ Object.freeze({
30802
30851
  addonId: null,
30803
30852
  access: "create"
30804
30853
  },
30854
+ "pipelineAnalytics.reconcileFromDisk": {
30855
+ capName: "pipeline-analytics",
30856
+ capScope: "device",
30857
+ addonId: null,
30858
+ access: "create"
30859
+ },
30805
30860
  "pipelineAnalytics.refreshStorageLocationsForMigration": {
30806
30861
  capName: "pipeline-analytics",
30807
30862
  capScope: "device",
@@ -31204,6 +31259,12 @@ Object.freeze({
31204
31259
  addonId: null,
31205
31260
  access: "view"
31206
31261
  },
31262
+ "pipelineOrchestrator.getReconcileFromDiskStatus": {
31263
+ capName: "pipeline-orchestrator",
31264
+ capScope: "system",
31265
+ addonId: null,
31266
+ access: "view"
31267
+ },
31207
31268
  "pipelineOrchestrator.listAgentSettings": {
31208
31269
  capName: "pipeline-orchestrator",
31209
31270
  capScope: "system",
@@ -31228,6 +31289,12 @@ Object.freeze({
31228
31289
  addonId: null,
31229
31290
  access: "create"
31230
31291
  },
31292
+ "pipelineOrchestrator.reconcileFromDisk": {
31293
+ capName: "pipeline-orchestrator",
31294
+ capScope: "system",
31295
+ addonId: null,
31296
+ access: "create"
31297
+ },
31231
31298
  "pipelineOrchestrator.removeAgentSettings": {
31232
31299
  capName: "pipeline-orchestrator",
31233
31300
  capScope: "system",
@@ -34219,6 +34286,11 @@ Object.freeze({
34219
34286
  form: "single",
34220
34287
  optional: true
34221
34288
  }],
34289
+ "pipelineAnalytics.reconcileFromDisk": [{
34290
+ name: "deviceId",
34291
+ form: "single",
34292
+ optional: false
34293
+ }],
34222
34294
  "pipelineAnalytics.restageRetrainTrack": [{
34223
34295
  name: "deviceId",
34224
34296
  form: "single",
@@ -47013,6 +47085,162 @@ function createViewerUiProvider() {
47013
47085
  };
47014
47086
  }
47015
47087
  //#endregion
47088
+ //#region src/disk-reconcile-fleet.ts
47089
+ async function reconcileFleetFromDisk(deps) {
47090
+ const deviceIds = await deps.listDeviceIds();
47091
+ const failed = [];
47092
+ let cameras = 0;
47093
+ let mediaDropped = 0;
47094
+ let tracks = 0;
47095
+ let events = 0;
47096
+ let completed = 0;
47097
+ for (const deviceId of deviceIds) {
47098
+ try {
47099
+ await deps.rescanRecordings(deviceId);
47100
+ const counts = await deps.reconcileAnalytics(deviceId);
47101
+ cameras += 1;
47102
+ mediaDropped += counts.mediaDropped;
47103
+ tracks += counts.tracks;
47104
+ events += counts.events;
47105
+ } catch {
47106
+ failed.push(deviceId);
47107
+ }
47108
+ completed += 1;
47109
+ deps.onProgress?.({
47110
+ deviceId,
47111
+ total: deviceIds.length,
47112
+ completed,
47113
+ failed: [...failed],
47114
+ mediaDropped,
47115
+ tracks,
47116
+ events
47117
+ });
47118
+ }
47119
+ return {
47120
+ cameras,
47121
+ failed,
47122
+ mediaDropped,
47123
+ tracks,
47124
+ events
47125
+ };
47126
+ }
47127
+ //#endregion
47128
+ //#region src/disk-reconcile-job.ts
47129
+ /**
47130
+ * In-memory disk-wins fleet job. The tRPC mutation starts this and returns
47131
+ * immediately; the walk runs in the addon process so a 60s UDS timeout cannot
47132
+ * abort it. Status is polled via getReconcileFromDiskStatus.
47133
+ */
47134
+ function idleDiskReconcileJob() {
47135
+ return {
47136
+ state: "idle",
47137
+ total: 0,
47138
+ completed: 0,
47139
+ currentDeviceId: null,
47140
+ failed: [],
47141
+ mediaDropped: 0,
47142
+ tracks: 0,
47143
+ events: 0,
47144
+ startedAtMs: null,
47145
+ finishedAtMs: null,
47146
+ error: null
47147
+ };
47148
+ }
47149
+ function isTimeoutError(err) {
47150
+ const message = err instanceof Error ? err.message : String(err);
47151
+ return /timed out/i.test(message);
47152
+ }
47153
+ async function withTimeoutRetry(run) {
47154
+ try {
47155
+ return await run();
47156
+ } catch (err) {
47157
+ if (!isTimeoutError(err)) throw err;
47158
+ return await run();
47159
+ }
47160
+ }
47161
+ function createDiskReconcileJobRunner(now = Date.now) {
47162
+ let job = idleDiskReconcileJob();
47163
+ let inFlight = null;
47164
+ const snapshot = () => job;
47165
+ const start = (deps) => {
47166
+ if (job.state === "running" && inFlight) return job;
47167
+ job = {
47168
+ ...idleDiskReconcileJob(),
47169
+ state: "running",
47170
+ startedAtMs: now()
47171
+ };
47172
+ deps.log?.("pipeline disk reconcile started");
47173
+ inFlight = (async () => {
47174
+ try {
47175
+ const result = await reconcileFleetFromDisk({
47176
+ listDeviceIds: deps.listDeviceIds,
47177
+ rescanRecordings: (deviceId) => withTimeoutRetry(() => deps.rescanRecordings(deviceId)),
47178
+ reconcileAnalytics: (deviceId) => withTimeoutRetry(() => deps.reconcileAnalytics(deviceId)),
47179
+ onProgress: (update) => {
47180
+ job = {
47181
+ ...job,
47182
+ total: update.total,
47183
+ completed: update.completed,
47184
+ currentDeviceId: update.deviceId,
47185
+ failed: update.failed,
47186
+ mediaDropped: update.mediaDropped,
47187
+ tracks: update.tracks,
47188
+ events: update.events
47189
+ };
47190
+ deps.onProgress?.(update);
47191
+ deps.log?.("pipeline disk reconcile camera", {
47192
+ deviceId: update.deviceId,
47193
+ completed: update.completed,
47194
+ total: update.total,
47195
+ failed: update.failed.length,
47196
+ mediaDropped: update.mediaDropped,
47197
+ tracks: update.tracks,
47198
+ events: update.events
47199
+ });
47200
+ }
47201
+ });
47202
+ job = {
47203
+ ...job,
47204
+ state: "done",
47205
+ total: result.cameras + result.failed.length,
47206
+ completed: result.cameras + result.failed.length,
47207
+ currentDeviceId: null,
47208
+ failed: result.failed,
47209
+ mediaDropped: result.mediaDropped,
47210
+ tracks: result.tracks,
47211
+ events: result.events,
47212
+ finishedAtMs: now(),
47213
+ error: null
47214
+ };
47215
+ deps.log?.("pipeline disk reconcile", {
47216
+ cameras: result.cameras,
47217
+ failed: result.failed,
47218
+ mediaDropped: result.mediaDropped,
47219
+ tracks: result.tracks,
47220
+ events: result.events
47221
+ });
47222
+ } catch (err) {
47223
+ const error = err instanceof Error ? err.message : String(err);
47224
+ job = {
47225
+ ...job,
47226
+ state: "error",
47227
+ currentDeviceId: null,
47228
+ finishedAtMs: now(),
47229
+ error
47230
+ };
47231
+ deps.log?.("pipeline disk reconcile failed", { error });
47232
+ } finally {
47233
+ inFlight = null;
47234
+ }
47235
+ })();
47236
+ return job;
47237
+ };
47238
+ return {
47239
+ snapshot,
47240
+ start
47241
+ };
47242
+ }
47243
+ //#endregion
47016
47244
  //#region src/widget-catalog.ts
47017
47245
  var pipelineOrchestratorWidgets = [{
47018
47246
  tab: "device-tab",
@@ -47780,6 +48008,8 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
47780
48008
  /** Storage migration maintenance lease. It only gates dispatch; it does not
47781
48009
  * change any camera wrapper or persistent pipeline configuration. */
47782
48010
  storageMigrationLeaseId = null;
48011
+ /** In-memory disk-wins fleet job — start returns immediately; poll status. */
48012
+ diskReconcileJob = createDiskReconcileJobRunner();
47783
48013
  constructor() {
47784
48014
  super({});
47785
48015
  }
@@ -48772,6 +49002,31 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
48772
49002
  async getCameraStatuses(input) {
48773
49003
  return this.cameraStatusService.getCameraStatuses(input.deviceIds);
48774
49004
  }
49005
+ /**
49006
+ * Disk-wins fleet reconcile: reseat the recordings index from the walk, then
49007
+ * drop analytics ghosts. Isolated per camera. Starts in this process and
49008
+ * returns immediately — poll `getReconcileFromDiskStatus` (the walk outlives
49009
+ * the tRPC/UDS 60s timeout).
49010
+ */
49011
+ async reconcileFromDisk() {
49012
+ const api = this.ctx.api;
49013
+ return this.diskReconcileJob.start({
49014
+ listDeviceIds: async () => {
49015
+ return (await api.deviceManager.listAll.query({
49016
+ projection: "slim",
49017
+ isCamera: true
49018
+ })).map((device) => device.id);
49019
+ },
49020
+ rescanRecordings: async (deviceId) => {
49021
+ await api.recording.rescanStorage.mutate({ deviceId });
49022
+ },
49023
+ reconcileAnalytics: async (deviceId) => api.pipelineAnalytics.reconcileFromDisk.mutate({ deviceId }),
49024
+ log: (message, meta) => this.ctx.logger.info(message, { meta })
49025
+ });
49026
+ }
49027
+ async getReconcileFromDiskStatus() {
49028
+ return this.diskReconcileJob.snapshot();
49029
+ }
48775
49030
  /** Emit the `pipeline.camera-updated` event so the internal subscriber hot-reloads the runner. */
48776
49031
  emitCameraUpdated(deviceId, config) {
48777
49032
  if (!this.ctx?.eventBus) return;
package/dist/index.mjs CHANGED
@@ -8433,6 +8433,12 @@ var RecordingConfigSchema = object({
8433
8433
  /** DERIVED summary of `bands`, stamped by the recorder on every save.
8434
8434
  * Authoring it has no effect — see {@link RecordingStorageModeSchema}. */
8435
8435
  mode: RecordingStorageModeSchema.optional(),
8436
+ /**
8437
+ * Which assigned broker slots to record. Absent / empty = {@link
8438
+ * DEFAULT_RECORDING_PROFILES} (`high`+`low`) intersected with the
8439
+ * camera's currently assigned slots — never `mid` unless the operator
8440
+ * picks it, and never a slot the broker has not assigned.
8441
+ */
8436
8442
  profiles: array(CamProfileSchema).optional(),
8437
8443
  segmentSeconds: number().int().positive().optional(),
8438
8444
  /**
@@ -8513,7 +8519,11 @@ var RelocateFootageInputSchema = object({
8513
8519
  profiles: array(string()).optional(),
8514
8520
  /** Copy throttle in MB/s (default 40) — the drain is a background chore,
8515
8521
  * never allowed to starve live writers. */
8516
- throttleMbps: number().min(1).max(1e3).optional()
8522
+ throttleMbps: number().min(1).max(1e3).optional(),
8523
+ /** Move only segments whose startMs is >= this. Absent = the whole source
8524
+ * pile. Used when a full drain is too expensive and the operator only
8525
+ * wants the recent window on the new disk. */
8526
+ sinceMs: number().int().optional()
8517
8527
  });
8518
8528
  /** Internal, lease-scoped participant operation. It is intentionally separate
8519
8529
  * from persistent recording settings: a migration never changes
@@ -17715,6 +17725,12 @@ var TrackCascadeCountsSchema = object({
17715
17725
  /** Per-track CLIP search vectors removed (best-effort). */
17716
17726
  embeddings: number().int()
17717
17727
  });
17728
+ /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17729
+ var DiskReconcileCountsSchema = object({
17730
+ mediaDropped: number().int(),
17731
+ tracks: number().int(),
17732
+ events: number().int()
17733
+ });
17718
17734
  /** Event-store footprint for one camera. */
17719
17735
  var EventStoreDeviceFootprintSchema = object({
17720
17736
  deviceId: number(),
@@ -17891,6 +17907,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17891
17907
  }), method(object({ deviceId: number() }), TrackCascadeCountsSchema, {
17892
17908
  kind: "mutation",
17893
17909
  auth: "admin"
17910
+ }), method(object({ deviceId: number() }), DiskReconcileCountsSchema, {
17911
+ kind: "mutation",
17912
+ auth: "admin"
17894
17913
  }), method(object({
17895
17914
  deviceId: number(),
17896
17915
  trackIds: array(string()).min(1)
@@ -19751,6 +19770,24 @@ var CameraStatusDegradationSchema = object({
19751
19770
  *
19752
19771
  * See spec: `docs/superpowers/specs/2026-06-24-camera-status-aggregator-cap.md`
19753
19772
  */
19773
+ var DiskReconcileJobSchema = object({
19774
+ state: _enum([
19775
+ "idle",
19776
+ "running",
19777
+ "done",
19778
+ "error"
19779
+ ]),
19780
+ total: number().int().nonnegative(),
19781
+ completed: number().int().nonnegative(),
19782
+ currentDeviceId: number().int().nullable(),
19783
+ failed: array(number().int()).readonly(),
19784
+ mediaDropped: number().int().nonnegative(),
19785
+ tracks: number().int().nonnegative(),
19786
+ events: number().int().nonnegative(),
19787
+ startedAtMs: number().int().nullable(),
19788
+ finishedAtMs: number().int().nullable(),
19789
+ error: string().nullable()
19790
+ });
19754
19791
  var CameraStatusSchema = object({
19755
19792
  deviceId: number(),
19756
19793
  assignment: CameraAssignmentStatusSchema,
@@ -20287,6 +20324,18 @@ var pipelineOrchestratorCapability = {
20287
20324
  * rail without issuing N parallel browser round-trips.
20288
20325
  */
20289
20326
  getCameraStatuses: method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()),
20327
+ /**
20328
+ * Disk-wins fleet reconcile after a recordings wipe. Starts the walk in
20329
+ * the addon process and returns immediately with the job snapshot — the
20330
+ * work outlives the tRPC/UDS 60s timeout. Poll `getReconcileFromDiskStatus`.
20331
+ * Admin-only. Idempotent while a job is already running.
20332
+ */
20333
+ reconcileFromDisk: method(_void(), DiskReconcileJobSchema, {
20334
+ kind: "mutation",
20335
+ auth: "admin"
20336
+ }),
20337
+ /** Snapshot of the in-flight or last disk-wins fleet reconcile. */
20338
+ getReconcileFromDiskStatus: method(_void(), DiskReconcileJobSchema, { auth: "admin" }),
20290
20339
  /** List every template the operator has saved. */
20291
20340
  listTemplates: method(_void(), array(PipelineTemplateSchema).readonly()),
20292
20341
  /** Create a new named preset from a given CameraPipelineConfig. */
@@ -30774,6 +30823,12 @@ Object.freeze({
30774
30823
  addonId: null,
30775
30824
  access: "create"
30776
30825
  },
30826
+ "pipelineAnalytics.reconcileFromDisk": {
30827
+ capName: "pipeline-analytics",
30828
+ capScope: "device",
30829
+ addonId: null,
30830
+ access: "create"
30831
+ },
30777
30832
  "pipelineAnalytics.refreshStorageLocationsForMigration": {
30778
30833
  capName: "pipeline-analytics",
30779
30834
  capScope: "device",
@@ -31176,6 +31231,12 @@ Object.freeze({
31176
31231
  addonId: null,
31177
31232
  access: "view"
31178
31233
  },
31234
+ "pipelineOrchestrator.getReconcileFromDiskStatus": {
31235
+ capName: "pipeline-orchestrator",
31236
+ capScope: "system",
31237
+ addonId: null,
31238
+ access: "view"
31239
+ },
31179
31240
  "pipelineOrchestrator.listAgentSettings": {
31180
31241
  capName: "pipeline-orchestrator",
31181
31242
  capScope: "system",
@@ -31200,6 +31261,12 @@ Object.freeze({
31200
31261
  addonId: null,
31201
31262
  access: "create"
31202
31263
  },
31264
+ "pipelineOrchestrator.reconcileFromDisk": {
31265
+ capName: "pipeline-orchestrator",
31266
+ capScope: "system",
31267
+ addonId: null,
31268
+ access: "create"
31269
+ },
31203
31270
  "pipelineOrchestrator.removeAgentSettings": {
31204
31271
  capName: "pipeline-orchestrator",
31205
31272
  capScope: "system",
@@ -34191,6 +34258,11 @@ Object.freeze({
34191
34258
  form: "single",
34192
34259
  optional: true
34193
34260
  }],
34261
+ "pipelineAnalytics.reconcileFromDisk": [{
34262
+ name: "deviceId",
34263
+ form: "single",
34264
+ optional: false
34265
+ }],
34194
34266
  "pipelineAnalytics.restageRetrainTrack": [{
34195
34267
  name: "deviceId",
34196
34268
  form: "single",
@@ -46985,6 +47057,162 @@ function createViewerUiProvider() {
46985
47057
  };
46986
47058
  }
46987
47059
  //#endregion
47060
+ //#region src/disk-reconcile-fleet.ts
47061
+ async function reconcileFleetFromDisk(deps) {
47062
+ const deviceIds = await deps.listDeviceIds();
47063
+ const failed = [];
47064
+ let cameras = 0;
47065
+ let mediaDropped = 0;
47066
+ let tracks = 0;
47067
+ let events = 0;
47068
+ let completed = 0;
47069
+ for (const deviceId of deviceIds) {
47070
+ try {
47071
+ await deps.rescanRecordings(deviceId);
47072
+ const counts = await deps.reconcileAnalytics(deviceId);
47073
+ cameras += 1;
47074
+ mediaDropped += counts.mediaDropped;
47075
+ tracks += counts.tracks;
47076
+ events += counts.events;
47077
+ } catch {
47078
+ failed.push(deviceId);
47079
+ }
47080
+ completed += 1;
47081
+ deps.onProgress?.({
47082
+ deviceId,
47083
+ total: deviceIds.length,
47084
+ completed,
47085
+ failed: [...failed],
47086
+ mediaDropped,
47087
+ tracks,
47088
+ events
47089
+ });
47090
+ }
47091
+ return {
47092
+ cameras,
47093
+ failed,
47094
+ mediaDropped,
47095
+ tracks,
47096
+ events
47097
+ };
47098
+ }
47099
+ //#endregion
47100
+ //#region src/disk-reconcile-job.ts
47101
+ /**
47102
+ * In-memory disk-wins fleet job. The tRPC mutation starts this and returns
47103
+ * immediately; the walk runs in the addon process so a 60s UDS timeout cannot
47104
+ * abort it. Status is polled via getReconcileFromDiskStatus.
47105
+ */
47106
+ function idleDiskReconcileJob() {
47107
+ return {
47108
+ state: "idle",
47109
+ total: 0,
47110
+ completed: 0,
47111
+ currentDeviceId: null,
47112
+ failed: [],
47113
+ mediaDropped: 0,
47114
+ tracks: 0,
47115
+ events: 0,
47116
+ startedAtMs: null,
47117
+ finishedAtMs: null,
47118
+ error: null
47119
+ };
47120
+ }
47121
+ function isTimeoutError(err) {
47122
+ const message = err instanceof Error ? err.message : String(err);
47123
+ return /timed out/i.test(message);
47124
+ }
47125
+ async function withTimeoutRetry(run) {
47126
+ try {
47127
+ return await run();
47128
+ } catch (err) {
47129
+ if (!isTimeoutError(err)) throw err;
47130
+ return await run();
47131
+ }
47132
+ }
47133
+ function createDiskReconcileJobRunner(now = Date.now) {
47134
+ let job = idleDiskReconcileJob();
47135
+ let inFlight = null;
47136
+ const snapshot = () => job;
47137
+ const start = (deps) => {
47138
+ if (job.state === "running" && inFlight) return job;
47139
+ job = {
47140
+ ...idleDiskReconcileJob(),
47141
+ state: "running",
47142
+ startedAtMs: now()
47143
+ };
47144
+ deps.log?.("pipeline disk reconcile started");
47145
+ inFlight = (async () => {
47146
+ try {
47147
+ const result = await reconcileFleetFromDisk({
47148
+ listDeviceIds: deps.listDeviceIds,
47149
+ rescanRecordings: (deviceId) => withTimeoutRetry(() => deps.rescanRecordings(deviceId)),
47150
+ reconcileAnalytics: (deviceId) => withTimeoutRetry(() => deps.reconcileAnalytics(deviceId)),
47151
+ onProgress: (update) => {
47152
+ job = {
47153
+ ...job,
47154
+ total: update.total,
47155
+ completed: update.completed,
47156
+ currentDeviceId: update.deviceId,
47157
+ failed: update.failed,
47158
+ mediaDropped: update.mediaDropped,
47159
+ tracks: update.tracks,
47160
+ events: update.events
47161
+ };
47162
+ deps.onProgress?.(update);
47163
+ deps.log?.("pipeline disk reconcile camera", {
47164
+ deviceId: update.deviceId,
47165
+ completed: update.completed,
47166
+ total: update.total,
47167
+ failed: update.failed.length,
47168
+ mediaDropped: update.mediaDropped,
47169
+ tracks: update.tracks,
47170
+ events: update.events
47171
+ });
47172
+ }
47173
+ });
47174
+ job = {
47175
+ ...job,
47176
+ state: "done",
47177
+ total: result.cameras + result.failed.length,
47178
+ completed: result.cameras + result.failed.length,
47179
+ currentDeviceId: null,
47180
+ failed: result.failed,
47181
+ mediaDropped: result.mediaDropped,
47182
+ tracks: result.tracks,
47183
+ events: result.events,
47184
+ finishedAtMs: now(),
47185
+ error: null
47186
+ };
47187
+ deps.log?.("pipeline disk reconcile", {
47188
+ cameras: result.cameras,
47189
+ failed: result.failed,
47190
+ mediaDropped: result.mediaDropped,
47191
+ tracks: result.tracks,
47192
+ events: result.events
47193
+ });
47194
+ } catch (err) {
47195
+ const error = err instanceof Error ? err.message : String(err);
47196
+ job = {
47197
+ ...job,
47198
+ state: "error",
47199
+ currentDeviceId: null,
47200
+ finishedAtMs: now(),
47201
+ error
47202
+ };
47203
+ deps.log?.("pipeline disk reconcile failed", { error });
47204
+ } finally {
47205
+ inFlight = null;
47206
+ }
47207
+ })();
47208
+ return job;
47209
+ };
47210
+ return {
47211
+ snapshot,
47212
+ start
47213
+ };
47214
+ }
47215
+ //#endregion
46988
47216
  //#region src/widget-catalog.ts
46989
47217
  var pipelineOrchestratorWidgets = [{
46990
47218
  tab: "device-tab",
@@ -47752,6 +47980,8 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
47752
47980
  /** Storage migration maintenance lease. It only gates dispatch; it does not
47753
47981
  * change any camera wrapper or persistent pipeline configuration. */
47754
47982
  storageMigrationLeaseId = null;
47983
+ /** In-memory disk-wins fleet job — start returns immediately; poll status. */
47984
+ diskReconcileJob = createDiskReconcileJobRunner();
47755
47985
  constructor() {
47756
47986
  super({});
47757
47987
  }
@@ -48744,6 +48974,31 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
48744
48974
  async getCameraStatuses(input) {
48745
48975
  return this.cameraStatusService.getCameraStatuses(input.deviceIds);
48746
48976
  }
48977
+ /**
48978
+ * Disk-wins fleet reconcile: reseat the recordings index from the walk, then
48979
+ * drop analytics ghosts. Isolated per camera. Starts in this process and
48980
+ * returns immediately — poll `getReconcileFromDiskStatus` (the walk outlives
48981
+ * the tRPC/UDS 60s timeout).
48982
+ */
48983
+ async reconcileFromDisk() {
48984
+ const api = this.ctx.api;
48985
+ return this.diskReconcileJob.start({
48986
+ listDeviceIds: async () => {
48987
+ return (await api.deviceManager.listAll.query({
48988
+ projection: "slim",
48989
+ isCamera: true
48990
+ })).map((device) => device.id);
48991
+ },
48992
+ rescanRecordings: async (deviceId) => {
48993
+ await api.recording.rescanStorage.mutate({ deviceId });
48994
+ },
48995
+ reconcileAnalytics: async (deviceId) => api.pipelineAnalytics.reconcileFromDisk.mutate({ deviceId }),
48996
+ log: (message, meta) => this.ctx.logger.info(message, { meta })
48997
+ });
48998
+ }
48999
+ async getReconcileFromDiskStatus() {
49000
+ return this.diskReconcileJob.snapshot();
49001
+ }
48747
49002
  /** Emit the `pipeline.camera-updated` event so the internal subscriber hot-reloads the runner. */
48748
49003
  emitCameraUpdated(deviceId, config) {
48749
49004
  if (!this.ctx?.eventBus) return;
@@ -30,7 +30,7 @@ async function d(e) {
30
30
  }
31
31
  }
32
32
  async function f() {
33
- return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-Cb2gW27C.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-CS2P_brd.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-pipeline-orchestrator",
3
- "version": "1.2.69",
3
+ "version": "1.2.75",
4
4
  "description": "Hub-side camera-to-agent load balancer — tracks runner capacity and dispatches attachCamera calls to the optimal pipeline-runner instance",
5
5
  "keywords": [
6
6
  "camstack",