@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.
- package/assets/viewer/.viewer-current-files.json +5 -5
- package/assets/viewer/.viewer-version +1 -1
- package/assets/viewer/_expo/static/js/web/Clipboard-68139000fcc61af593c44b211b675f83.js +8 -0
- package/assets/viewer/_expo/static/js/web/Haptics-01ab31b2a1d3325f70b28a24474fe704.js +3 -0
- package/assets/viewer/_expo/static/js/web/index-39e5c6b7c7445c1b4a2baa92dd2c98e1.js +3567 -0
- package/assets/viewer/_expo/static/js/web/{index-d1a9c6f521e974cc90f1a7eaaf908354.js → index-4fec4dee89e2263869ce0e847239cd29.js} +1 -1
- package/assets/viewer/_expo/static/js/web/node-fs-9335d97bb088777ae4eb23bb48049a68.js +1 -0
- package/assets/viewer/_expo/static/js/web/node-path-da70216c9d8adb1fa18a6f4efbee784e.js +1 -0
- package/assets/viewer/index.html +1 -1
- package/dist/_stub.js +2 -2
- 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
- package/dist/_virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Cb_kiYRV.mjs +26 -0
- 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
- package/dist/{hostInit-DuuZmzlm.mjs → hostInit-NlhiPlZ_.mjs} +2 -2
- package/dist/index.js +256 -1
- package/dist/index.mjs +256 -1
- package/dist/remoteEntry.js +1 -1
- package/package.json +1 -1
- 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;
|
package/dist/remoteEntry.js
CHANGED
|
@@ -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-
|
|
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.
|
|
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",
|