@camstack/addon-pipeline-orchestrator 1.2.128 → 1.2.129

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/dist/index.mjs CHANGED
@@ -8866,6 +8866,20 @@ var RelocateJobSchema = object({
8866
8866
  * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8867
8867
  */
8868
8868
  rowsReconciled: number().int().nonnegative().optional(),
8869
+ /**
8870
+ * Rows this run FORGOT because the file they name is not on disk.
8871
+ *
8872
+ * The mover derived the path from the row's own fields and `stat`ed it; an
8873
+ * ENOENT there is a per-path confirmation that the segment is gone (D296),
8874
+ * and the durable row is dropped through the same channel eviction uses. It
8875
+ * is reported for the same reason `rowsReconciled` is: this is a durable
8876
+ * mutation nobody asked for, and a migration that quietly erases hour rows is
8877
+ * the same failure as one that quietly skips them (D295).
8878
+ *
8879
+ * The production drain of 2026-08-30 would have reported 11 074 here — the
8880
+ * ledger claimed 5.65 GB of footage that no longer existed.
8881
+ */
8882
+ rowsForgotten: number().int().nonnegative().optional(),
8869
8883
  startedAt: number(),
8870
8884
  finishedAt: number().nullable(),
8871
8885
  error: string().nullable()
@@ -9229,6 +9243,91 @@ var RelocateResidueSchema = object({
9229
9243
  segments: number().int().nonnegative(),
9230
9244
  bytes: number().int().nonnegative()
9231
9245
  }).nullable();
9246
+ /**
9247
+ * Ask one location whether its durable hour rows describe the disk — the walk
9248
+ * (D319).
9249
+ *
9250
+ * `apply` DEFAULTS TO FALSE and that default is the product: the operator's
9251
+ * missing tool is the question, and the dry run is how they sanity-check the
9252
+ * destructive run before authorising it.
9253
+ */
9254
+ var LedgerWalkInputSchema = object({
9255
+ locationId: string().min(1),
9256
+ /** Forget the confirmed-absent rows, rather than only counting them. */
9257
+ apply: boolean().optional(),
9258
+ /** Narrow to one camera. */
9259
+ deviceId: number().int().positive().optional(),
9260
+ /** Narrow to these recording profiles; empty/absent = every profile. */
9261
+ profiles: array(string().min(1)).optional()
9262
+ });
9263
+ /** Why a whole walk did nothing. Every one leaves the ledger untouched. */
9264
+ var LedgerWalkRefusalSchema = _enum([
9265
+ "location-unknown",
9266
+ "source-writable",
9267
+ "no-ledger",
9268
+ "archive-unreadable",
9269
+ "anchor-absent",
9270
+ "anchor-unreadable",
9271
+ "anchor-moved"
9272
+ ]);
9273
+ _enum([
9274
+ "live-tail",
9275
+ "listing-error",
9276
+ "path-mismatch",
9277
+ "durable-refused"
9278
+ ]);
9279
+ /** Every skip reason, always present, always a number — so a reason that never
9280
+ * fired reports as zero rather than absent and the report shape is constant
9281
+ * between passes. Spelled out rather than `z.record` for exactly that. */
9282
+ var LedgerWalkSkipCountsSchema = object({
9283
+ "live-tail": number().int().nonnegative(),
9284
+ "listing-error": number().int().nonnegative(),
9285
+ "path-mismatch": number().int().nonnegative(),
9286
+ "durable-refused": number().int().nonnegative()
9287
+ });
9288
+ /** One camera's share of a walk, so a report names cameras and not rows. */
9289
+ var LedgerWalkDeviceReportSchema = object({
9290
+ deviceId: number().int(),
9291
+ hoursWalked: number().int().nonnegative(),
9292
+ hoursMissing: number().int().nonnegative(),
9293
+ ghostSegments: number().int().nonnegative(),
9294
+ ghostBytes: number().int().nonnegative(),
9295
+ forgottenSegments: number().int().nonnegative(),
9296
+ orphanFiles: number().int().nonnegative()
9297
+ });
9298
+ /**
9299
+ * What one walk claimed, listed, found and (only when armed) forgot.
9300
+ *
9301
+ * `archiveSegments` is the **M** and `segmentsClaimed` the **N** (D295), so a
9302
+ * walk that saw a fraction of the location is visible in its own report rather
9303
+ * than in the absence of one.
9304
+ */
9305
+ var LedgerWalkReportSchema = object({
9306
+ locationId: string(),
9307
+ applied: boolean(),
9308
+ refused: LedgerWalkRefusalSchema.nullable(),
9309
+ archiveSegments: number().int().nonnegative().nullable(),
9310
+ archiveBytes: number().int().nonnegative().nullable(),
9311
+ hoursClaimed: number().int().nonnegative(),
9312
+ hoursWalked: number().int().nonnegative(),
9313
+ hoursMissing: number().int().nonnegative(),
9314
+ /** `readdir` calls issued — the cost, stated in the unit that is paid. */
9315
+ listings: number().int().nonnegative(),
9316
+ segmentsClaimed: number().int().nonnegative(),
9317
+ ghostSegments: number().int().nonnegative(),
9318
+ ghostBytes: number().int().nonnegative(),
9319
+ ghostHoursWhole: number().int().nonnegative(),
9320
+ forgottenSegments: number().int().nonnegative(),
9321
+ forgottenBytes: number().int().nonnegative(),
9322
+ /** Files under a claimed hour that no durable row names. Never deleted. */
9323
+ orphanFiles: number().int().nonnegative(),
9324
+ orphanSample: array(string()).readonly(),
9325
+ hoursSkipped: number().int().nonnegative(),
9326
+ skippedByReason: LedgerWalkSkipCountsSchema,
9327
+ /** The walk stopped at its per-pass hour bound with claims unwalked. */
9328
+ bounded: boolean(),
9329
+ byDevice: array(LedgerWalkDeviceReportSchema).readonly()
9330
+ });
9232
9331
  /** How many rows a media pass would still act on against a given target — the
9233
9332
  * media lane's denominator AND its residue, from ONE derivation so the two can
9234
9333
  * never disagree. `null` = the count could not be taken. */
@@ -19805,13 +19904,15 @@ var ListGroupsPageSchema = object({
19805
19904
  groups: array(AnalyticsGroupRecordSchema).readonly(),
19806
19905
  nextCursor: string().nullable()
19807
19906
  });
19907
+ var KEY_EVENTS_DEFAULT_LIMIT = 50;
19908
+ var KEY_EVENTS_MAX_LIMIT = 200;
19808
19909
  var KeyEventQueryInput = object({
19809
19910
  deviceId: number(),
19810
19911
  /** Window lower bound (track firstSeen ≥ since). */
19811
19912
  since: number(),
19812
19913
  /** Window upper bound (track firstSeen ≤ until). */
19813
19914
  until: number(),
19814
- limit: number().int().min(1).max(200).default(50),
19915
+ limit: number().int().min(1).max(KEY_EVENTS_MAX_LIMIT).default(KEY_EVENTS_DEFAULT_LIMIT),
19815
19916
  /** Drop tracks scoring below this importance. */
19816
19917
  minImportance: number().min(0).max(1).optional(),
19817
19918
  /** Restrict to a single class (e.g. 'person'). */
@@ -19833,6 +19934,32 @@ var KeyEventSchema = object({
19833
19934
  ...TrackFlagFields,
19834
19935
  ...TrackRetrainFields
19835
19936
  });
19937
+ /** `getKeyEvents`' window and filters, asked of a SET of cameras at once. */
19938
+ var KeyEventBatchQueryInput = object({
19939
+ deviceIds: array(number()).min(1).max(200),
19940
+ since: number(),
19941
+ until: number(),
19942
+ /** Applied PER CAMERA, exactly as `getKeyEvents.limit` is — never a total
19943
+ * across the set, which would let a busy camera starve a quiet one of its
19944
+ * rows and change what the merged feed contains. */
19945
+ limit: number().int().min(1).max(KEY_EVENTS_MAX_LIMIT).default(KEY_EVENTS_DEFAULT_LIMIT),
19946
+ minImportance: number().min(0).max(1).optional(),
19947
+ classFilter: string().optional()
19948
+ });
19949
+ /**
19950
+ * One camera's key events in a batch answer.
19951
+ *
19952
+ * The row exists for every requested id. `getKeyEvents` degrades to `[]` on
19953
+ * error rather than throwing, so a camera whose store read failed and one with
19954
+ * no events in the window were ALREADY indistinguishable per camera — the
19955
+ * batch does not make that worse, and the row keeps the deviceId the single
19956
+ * method's output never carried (the caller used to stamp it from the fan-out
19957
+ * key, which only worked because there was one query per camera).
19958
+ */
19959
+ var KeyEventsForDeviceSchema = object({
19960
+ deviceId: number(),
19961
+ events: array(KeyEventSchema).readonly()
19962
+ });
19836
19963
  object({
19837
19964
  trackId: string(),
19838
19965
  className: string(),
@@ -20102,7 +20229,7 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
20102
20229
  until: number().optional(),
20103
20230
  kinds: array(string()).optional(),
20104
20231
  limit: number().int().min(1).max(MAX_EVENT_QUERY_LIMIT).default(DEFAULT_EVENT_QUERY_LIMIT)
20105
- }), array(SensorEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(object({
20232
+ }), array(SensorEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(KeyEventBatchQueryInput, array(KeyEventsForDeviceSchema).readonly()), method(object({
20106
20233
  deviceId: number(),
20107
20234
  since: number(),
20108
20235
  until: number(),
@@ -28235,6 +28362,9 @@ method(object({
28235
28362
  }), method(RelocateResidueInputSchema, RelocateResidueSchema, {
28236
28363
  kind: "query",
28237
28364
  auth: "admin"
28365
+ }), method(LedgerWalkInputSchema, LedgerWalkReportSchema, {
28366
+ kind: "mutation",
28367
+ auth: "admin"
28238
28368
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
28239
28369
  kind: "mutation",
28240
28370
  auth: "admin"
@@ -28626,7 +28756,26 @@ var SceneMonitorStatusSchema = object({
28626
28756
  monitors: array(SceneMonitorSchema),
28627
28757
  lastFetchedAt: number()
28628
28758
  });
28629
- DeviceType.Camera, method(object({ deviceId: number() }), SceneMonitorStatusSchema), method(object({
28759
+ /**
28760
+ * One camera's row in a `listScenesBatch` answer.
28761
+ *
28762
+ * `status` is NULLABLE and the nullability is the whole point. `scene-monitor`
28763
+ * is a `defaultActive` wrapper, so every camera is asked; a camera whose
28764
+ * provider is absent (pipeline-analytics not deployed, the post-processing node
28765
+ * down) cannot answer, and that is not the same fact as a camera with no scenes
28766
+ * configured. Fanned out per camera the difference was visible — one query
28767
+ * errored while the others resolved — and a batch that returned only the rows
28768
+ * it managed would have destroyed it, silently, by making an unreachable camera
28769
+ * indistinguishable from one that answered `monitors: []`.
28770
+ *
28771
+ * So: EVERY requested deviceId gets a row. `status: null` means "this camera
28772
+ * could not be read"; `status.monitors: []` means "read, and it has none".
28773
+ */
28774
+ var SceneMonitorStatusForDeviceSchema = object({
28775
+ deviceId: number(),
28776
+ status: SceneMonitorStatusSchema.nullable()
28777
+ });
28778
+ DeviceType.Camera, method(object({ deviceId: number() }), SceneMonitorStatusSchema), method(object({ deviceIds: array(number()).min(1).max(200) }), array(SceneMonitorStatusForDeviceSchema).readonly()), method(object({
28630
28779
  deviceId: number(),
28631
28780
  label: string(),
28632
28781
  roi: MaskRectShapeSchema,
@@ -29950,6 +30099,27 @@ var CameraOccupancySnapshotSchema = object({
29950
30099
  stationaryObjects: array(StationaryObjectSchema).readonly().optional()
29951
30100
  });
29952
30101
  /**
30102
+ * One camera's row in a `getCurrentSnapshotBatch` answer.
30103
+ *
30104
+ * THREE outcomes, and the single-camera method could only express two of them
30105
+ * because `snapshot: null` was already spoken for:
30106
+ *
30107
+ * - `read: 'read'`, `snapshot` present — the live occupancy reading.
30108
+ * - `read: 'read'`, `snapshot: null` — read, and this camera has no reading
30109
+ * yet: no frame since boot, and no parked-object registry to hydrate from.
30110
+ * - `read: 'unreadable'` — the owner could not answer for this
30111
+ * camera. `snapshot` is null, and it does NOT mean "nothing parked here".
30112
+ *
30113
+ * Collapsing the last two is the failure this field exists to prevent: a
30114
+ * hydration that threw would otherwise render as an empty Stationary section,
30115
+ * which is a definite claim about a camera nobody could read.
30116
+ */
30117
+ var CameraOccupancySnapshotForDeviceSchema = object({
30118
+ deviceId: number(),
30119
+ read: _enum(["read", "unreadable"]),
30120
+ snapshot: CameraOccupancySnapshotSchema.nullable()
30121
+ });
30122
+ /**
29953
30123
  * Time-series resolution. The history methods return one bucket per
29954
30124
  * step over the requested range. Smaller resolutions cost more
29955
30125
  * memory + bandwidth; bound to discrete steps so caller cannot ask
@@ -29973,7 +30143,7 @@ var HistoryPointSchema = object({
29973
30143
  /** Object count averaged over the bucket (rounded to nearest integer). */
29974
30144
  count: number().int().nonnegative()
29975
30145
  });
29976
- DeviceType.Camera, method(object({ deviceId: number() }), CameraOccupancySnapshotSchema.nullable()), method(object({
30146
+ DeviceType.Camera, method(object({ deviceId: number() }), CameraOccupancySnapshotSchema.nullable()), method(object({ deviceIds: array(number()).min(1).max(200) }), array(CameraOccupancySnapshotForDeviceSchema).readonly()), method(object({
29977
30147
  deviceId: number(),
29978
30148
  zoneId: string(),
29979
30149
  className: string().optional()
@@ -33362,6 +33532,12 @@ Object.freeze({
33362
33532
  addonId: null,
33363
33533
  access: "view"
33364
33534
  },
33535
+ "pipelineAnalytics.getKeyEventsBatch": {
33536
+ capName: "pipeline-analytics",
33537
+ capScope: "device",
33538
+ addonId: null,
33539
+ access: "view"
33540
+ },
33365
33541
  "pipelineAnalytics.getMotionEvents": {
33366
33542
  capName: "pipeline-analytics",
33367
33543
  capScope: "device",
@@ -34538,6 +34714,12 @@ Object.freeze({
34538
34714
  addonId: null,
34539
34715
  access: "view"
34540
34716
  },
34717
+ "recording.reconcileLedgerAgainstDisk": {
34718
+ capName: "recording",
34719
+ capScope: "system",
34720
+ addonId: null,
34721
+ access: "create"
34722
+ },
34541
34723
  "recording.refreshStorageLocationsForMigration": {
34542
34724
  capName: "recording",
34543
34725
  capScope: "system",
@@ -34664,6 +34846,12 @@ Object.freeze({
34664
34846
  addonId: null,
34665
34847
  access: "view"
34666
34848
  },
34849
+ "sceneMonitor.listScenesBatch": {
34850
+ capName: "scene-monitor",
34851
+ capScope: "device",
34852
+ addonId: null,
34853
+ access: "view"
34854
+ },
34667
34855
  "sceneMonitor.recheckNow": {
34668
34856
  capName: "scene-monitor",
34669
34857
  capScope: "device",
@@ -36020,6 +36208,12 @@ Object.freeze({
36020
36208
  addonId: null,
36021
36209
  access: "view"
36022
36210
  },
36211
+ "zoneAnalytics.getCurrentSnapshotBatch": {
36212
+ capName: "zone-analytics",
36213
+ capScope: "device",
36214
+ addonId: null,
36215
+ access: "view"
36216
+ },
36023
36217
  "zoneAnalytics.getUnzonedHistory": {
36024
36218
  capName: "zone-analytics",
36025
36219
  capScope: "device",
@@ -37024,6 +37218,11 @@ Object.freeze({
37024
37218
  form: "single",
37025
37219
  optional: false
37026
37220
  }],
37221
+ "pipelineAnalytics.getKeyEventsBatch": [{
37222
+ name: "deviceIds",
37223
+ form: "array",
37224
+ optional: false
37225
+ }],
37027
37226
  "pipelineAnalytics.getMotionEvents": [{
37028
37227
  name: "deviceId",
37029
37228
  form: "single",
@@ -37464,6 +37663,11 @@ Object.freeze({
37464
37663
  form: "single",
37465
37664
  optional: false
37466
37665
  }],
37666
+ "recording.reconcileLedgerAgainstDisk": [{
37667
+ name: "deviceId",
37668
+ form: "single",
37669
+ optional: true
37670
+ }],
37467
37671
  "recording.relocateFootage": [{
37468
37672
  name: "deviceId",
37469
37673
  form: "single",
@@ -37529,6 +37733,11 @@ Object.freeze({
37529
37733
  form: "single",
37530
37734
  optional: false
37531
37735
  }],
37736
+ "sceneMonitor.listScenesBatch": [{
37737
+ name: "deviceIds",
37738
+ form: "array",
37739
+ optional: false
37740
+ }],
37532
37741
  "sceneMonitor.recheckNow": [{
37533
37742
  name: "deviceId",
37534
37743
  form: "single",
@@ -37790,6 +37999,11 @@ Object.freeze({
37790
37999
  form: "single",
37791
38000
  optional: false
37792
38001
  }],
38002
+ "zoneAnalytics.getCurrentSnapshotBatch": [{
38003
+ name: "deviceIds",
38004
+ form: "array",
38005
+ optional: false
38006
+ }],
37793
38007
  "zoneAnalytics.getUnzonedHistory": [{
37794
38008
  name: "deviceId",
37795
38009
  form: "single",
@@ -40899,6 +41113,124 @@ function composeCameraStatus(input) {
40899
41113
  };
40900
41114
  }
40901
41115
  //#endregion
41116
+ //#region src/camera-status/fleet-read-pass.ts
41117
+ /**
41118
+ * Keep a rejection from becoming an UNHANDLED one.
41119
+ *
41120
+ * A shared promise is created by the first camera to ask and consumed by the
41121
+ * others later — `listBindableCapsForDeviceType` is reached in stage 2 of
41122
+ * `readSwitchAuthorities`, after an await, so two cameras genuinely attach
41123
+ * their handlers in different ticks. Without this the first rejection would be
41124
+ * unhandled for the window in between.
41125
+ *
41126
+ * The handler is a no-op ON PURPOSE: it never becomes the answer. Every real
41127
+ * consumer attaches its own `.catch` and reports the failure per camera.
41128
+ */
41129
+ function keepAlive(p) {
41130
+ p.catch(() => void 0);
41131
+ return p;
41132
+ }
41133
+ var FleetReadPass = class {
41134
+ deps;
41135
+ roster = null;
41136
+ bindings = null;
41137
+ mutes = null;
41138
+ bindableByType = /* @__PURE__ */ new Map();
41139
+ slotsByNode = /* @__PURE__ */ new Map();
41140
+ rtspByNode = /* @__PURE__ */ new Map();
41141
+ provisioningByNode = /* @__PURE__ */ new Map();
41142
+ selectedEngineByNode = /* @__PURE__ */ new Map();
41143
+ constructor(deps) {
41144
+ this.deps = deps;
41145
+ }
41146
+ /** The cameras this pass covers — the set both batch reads are issued for. */
41147
+ get deviceIds() {
41148
+ return this.deps.deviceIds;
41149
+ }
41150
+ /**
41151
+ * The device rows for the pass's set.
41152
+ *
41153
+ * **The shape follows the SET, not the caller.** A pass of one asks
41154
+ * `getDevice` — the exact question, and the one the write path
41155
+ * (`CameraSwitchService.setCameraSwitch`) has always asked. A pass of many
41156
+ * asks `listAll({deviceIds})`, which exists precisely because "these N" was
41157
+ * otherwise either N round trips or the whole fleet. Answering a set of one
41158
+ * with the fleet-shaped method would be no cheaper and would move the write
41159
+ * path onto a read it does not need.
41160
+ *
41161
+ * `projection: 'slim'` because the only fields read off it are `type` and
41162
+ * `disabled`: the full projection reads each device's settings row, which is
41163
+ * the per-device round trip this whole change exists to remove.
41164
+ */
41165
+ deviceRoster(api) {
41166
+ const single = this.deps.deviceIds.length === 1 ? this.deps.deviceIds[0] : void 0;
41167
+ const source = single === void 0 ? "deviceManager.listAll" : "deviceManager.getDevice";
41168
+ if (this.roster === null) this.roster = keepAlive(single === void 0 ? api.deviceManager.listAll.query({
41169
+ deviceIds: [...this.deps.deviceIds],
41170
+ projection: "slim"
41171
+ }) : api.deviceManager.getDevice.query({ deviceId: single }).then((row) => row === null ? [] : [row]));
41172
+ return {
41173
+ source,
41174
+ result: this.roster
41175
+ };
41176
+ }
41177
+ /** The binding rows for the pass's set — `getBindings` for one, the batch for many. */
41178
+ bindingRows(api) {
41179
+ const single = this.deps.deviceIds.length === 1 ? this.deps.deviceIds[0] : void 0;
41180
+ const source = single === void 0 ? "deviceManager.getBindingsBatch" : "deviceManager.getBindings";
41181
+ if (this.bindings === null) this.bindings = keepAlive(single === void 0 ? api.deviceManager.getBindingsBatch.query({ deviceIds: [...this.deps.deviceIds] }) : api.deviceManager.getBindings.query({ deviceId: single }).then((row) => [row]));
41182
+ return {
41183
+ source,
41184
+ result: this.bindings
41185
+ };
41186
+ }
41187
+ /** The fleet's muted-camera list. One answer for every camera in the pass. */
41188
+ mutedDevices(api) {
41189
+ if (this.mutes === null) this.mutes = keepAlive(api.notificationRules.listDeviceMutes.query({}));
41190
+ return this.mutes;
41191
+ }
41192
+ /** The bindable caps for one device TYPE. Every camera shares one answer. */
41193
+ bindableCapsFor(api, deviceType) {
41194
+ const existing = this.bindableByType.get(deviceType);
41195
+ if (existing !== void 0) return existing;
41196
+ const created = keepAlive(api.deviceManager.listBindableCapsForDeviceType.query({ deviceType }));
41197
+ this.bindableByType.set(deviceType, created);
41198
+ return created;
41199
+ }
41200
+ /** One source node's whole profile-slot table. */
41201
+ profileSlots(api, sourceNodeId) {
41202
+ const existing = this.slotsByNode.get(sourceNodeId);
41203
+ if (existing !== void 0) return existing;
41204
+ const created = keepAlive(api.streamBroker.listAllProfileSlots.query(void 0, nodePin(sourceNodeId)));
41205
+ this.slotsByNode.set(sourceNodeId, created);
41206
+ return created;
41207
+ }
41208
+ /** One source node's whole RTSP-restream table. */
41209
+ rtspEntries(api, sourceNodeId) {
41210
+ const existing = this.rtspByNode.get(sourceNodeId);
41211
+ if (existing !== void 0) return existing;
41212
+ const created = keepAlive(api.streamBroker.getAllRtspEntries.query({}, nodePin(sourceNodeId)));
41213
+ this.rtspByNode.set(sourceNodeId, created);
41214
+ return created;
41215
+ }
41216
+ /** One detection node's runtime-provisioning snapshot. */
41217
+ engineProvisioning(api, detectionNodeId) {
41218
+ const existing = this.provisioningByNode.get(detectionNodeId);
41219
+ if (existing !== void 0) return existing;
41220
+ const created = keepAlive(api.pipelineExecutor.getEngineProvisioning.query({ nodeId: detectionNodeId }));
41221
+ this.provisioningByNode.set(detectionNodeId, created);
41222
+ return created;
41223
+ }
41224
+ /** The executor's bootstrap engine, as the detection stage reads it. */
41225
+ selectedEngine(api, detectionNodeId) {
41226
+ const existing = this.selectedEngineByNode.get(detectionNodeId);
41227
+ if (existing !== void 0) return existing;
41228
+ const created = keepAlive(api.pipelineExecutor.getSelectedEngine.query({ nodeId: detectionNodeId }));
41229
+ this.selectedEngineByNode.set(detectionNodeId, created);
41230
+ return created;
41231
+ }
41232
+ };
41233
+ //#endregion
40902
41234
  //#region src/camera-status-service.ts
40903
41235
  /** WebRTC consumer kinds counted toward `BrokerResult.webrtcSessions`. */
40904
41236
  var WEBRTC_KINDS = new Set([
@@ -41087,7 +41419,7 @@ var CameraStatusService = class {
41087
41419
  * broker's actual decode-session node into `liveDecoder` (T6) — the first
41088
41420
  * slot that reports one wins.
41089
41421
  */
41090
- buildBrokerStage(api, allSlotsFetch, deviceId, sourceNodeId, liveDecoder, sink) {
41422
+ buildBrokerStage(api, allSlotsFetch, deviceId, sourceNodeId, pass, liveDecoder, sink) {
41091
41423
  if (!api || !allSlotsFetch) return Promise.resolve(null);
41092
41424
  return this.boundedStage(allSlotsFetch.then(async (slots) => {
41093
41425
  const deviceSlots = slots.filter((s) => s.deviceId === deviceId);
@@ -41097,12 +41429,13 @@ var CameraStatusService = class {
41097
41429
  rtspRestream: false
41098
41430
  };
41099
41431
  const [statsAndClients, rtspEntry] = await Promise.all([Promise.all(deviceSlots.map(async (slot) => {
41432
+ const [stats, clients] = await Promise.all([api.streamBroker.getBrokerStats.query({ brokerId: slot.brokerId }, nodePin(sourceNodeId)).catch(() => null), api.streamBroker.listClients.query({ brokerId: slot.brokerId }, nodePin(sourceNodeId)).catch(() => null)]);
41100
41433
  return {
41101
41434
  slot,
41102
- stats: await api.streamBroker.getBrokerStats.query({ brokerId: slot.brokerId }, nodePin(sourceNodeId)).catch(() => null),
41103
- clients: await api.streamBroker.listClients.query({ brokerId: slot.brokerId }, nodePin(sourceNodeId)).catch(() => null)
41435
+ stats,
41436
+ clients
41104
41437
  };
41105
- })), api.streamBroker.getAllRtspEntries.query({}, nodePin(sourceNodeId)).catch(() => null)]);
41438
+ })), pass.rtspEntries(api, sourceNodeId).catch(() => null)]);
41106
41439
  const profileDetails = statsAndClients.map(({ slot, stats, clients }) => ({
41107
41440
  profile: slot.profile,
41108
41441
  status: slot.status,
@@ -41163,13 +41496,16 @@ var CameraStatusService = class {
41163
41496
  };
41164
41497
  }
41165
41498
  /** Detection stage (pipeline-executor + runner metrics). */
41166
- buildDetectionStage(api, detectionNodeId, deviceId, sink) {
41499
+ buildDetectionStage(api, detectionNodeId, deviceId, pass, sink) {
41167
41500
  if (!api || !detectionNodeId) return Promise.resolve(null);
41168
- return this.boundedStage(Promise.all([api.pipelineExecutor.getEngineProvisioning.query({ nodeId: detectionNodeId }).catch(() => null), api.pipelineExecutor.getSelectedEngine.query({ nodeId: detectionNodeId }).catch(() => null)]).then(async ([provisioning, engine]) => {
41169
- const metrics = await api.pipelineRunner.getCameraMetrics.query({
41501
+ return this.boundedStage(Promise.all([
41502
+ pass.engineProvisioning(api, detectionNodeId).catch(() => null),
41503
+ pass.selectedEngine(api, detectionNodeId).catch(() => null),
41504
+ api.pipelineRunner.getCameraMetrics.query({
41170
41505
  deviceId,
41171
41506
  nodeId: detectionNodeId
41172
- }).catch(() => null);
41507
+ }).catch(() => null)
41508
+ ]).then(async ([provisioning, engine, metrics]) => {
41173
41509
  const phase = (() => {
41174
41510
  const p = metrics?.phase;
41175
41511
  if (p === "active") return "active";
@@ -41281,9 +41617,9 @@ var CameraStatusService = class {
41281
41617
  * badge on a working camera); what matters is that the emptiness travels with
41282
41618
  * the reason it is empty.
41283
41619
  */
41284
- buildSwitchStage(deviceId, sink) {
41620
+ buildSwitchStage(deviceId, pass, sink) {
41285
41621
  const startedAt = Date.now();
41286
- return this.boundedStage(this.deps.switchAuthoritiesFor(deviceId), STAGE_TIMEOUT_MS, "switches", deviceId, sink).then((reads) => {
41622
+ return this.boundedStage(this.deps.switchAuthoritiesFor(deviceId, pass), STAGE_TIMEOUT_MS, "switches", deviceId, sink).then((reads) => {
41287
41623
  if (reads === null) return [];
41288
41624
  const { switchedOff, unreadable } = composeSwitchedOff(reads);
41289
41625
  if (unreadable.length > 0) this.recordDegraded(sink, deviceId, "switches", "partial", Date.now() - startedAt, { unreadableAuthorities: unreadable });
@@ -41301,18 +41637,28 @@ var CameraStatusService = class {
41301
41637
  * `null` of a camera that legitimately has no such stage.
41302
41638
  */
41303
41639
  async getCameraStatus(deviceId) {
41640
+ return this.composeOne(deviceId, new FleetReadPass({ deviceIds: [deviceId] }));
41641
+ }
41642
+ /**
41643
+ * One camera's status, composed WITHIN a pass.
41644
+ *
41645
+ * Everything the camera alone can answer is fetched here; everything the pass
41646
+ * already knows (a node's slot table, the fleet's mute list, a device type's
41647
+ * bindable caps) is asked of `pass`, which issues it once for the whole call.
41648
+ */
41649
+ async composeOne(deviceId, pass) {
41304
41650
  const api = this.deps.api();
41305
41651
  const degradations = { entries: [] };
41306
41652
  const { detectionNodeId, sourceNodeId, pinned, detectionReason, audioNodeId, audioPinned } = this.buildAssignmentContext(deviceId);
41307
41653
  const liveDecoder = { nodeId: null };
41308
- const allSlotsFetch = api ? api.streamBroker.listAllProfileSlots.query(void 0, nodePin(sourceNodeId)) : null;
41654
+ const allSlotsFetch = api ? pass.profileSlots(api, sourceNodeId) : null;
41309
41655
  const sourceFetch = this.buildSourceStage(api, allSlotsFetch, deviceId, degradations);
41310
- const brokerFetch = this.buildBrokerStage(api, allSlotsFetch, deviceId, sourceNodeId, liveDecoder, degradations);
41656
+ const brokerFetch = this.buildBrokerStage(api, allSlotsFetch, deviceId, sourceNodeId, pass, liveDecoder, degradations);
41311
41657
  const decoderFetch = this.buildDecoderStage(detectionNodeId);
41312
41658
  const motionResult = this.buildMotionStage(deviceId);
41313
- const detectionFetch = this.buildDetectionStage(api, detectionNodeId, deviceId, degradations);
41659
+ const detectionFetch = this.buildDetectionStage(api, detectionNodeId, deviceId, pass, degradations);
41314
41660
  const recordingFetch = this.buildRecordingStage(api, deviceId, degradations);
41315
- const switchesFetch = this.buildSwitchStage(deviceId, degradations);
41661
+ const switchesFetch = this.buildSwitchStage(deviceId, pass, degradations);
41316
41662
  const [sourceResult, brokerResult, decoderResult, detectionResult, recordingResult, switchedOff] = await Promise.all([
41317
41663
  sourceFetch,
41318
41664
  brokerFetch,
@@ -41355,15 +41701,30 @@ var CameraStatusService = class {
41355
41701
  * `deviceIds` defaults to all cameras currently tracked by the
41356
41702
  * orchestrator's assignment map when omitted.
41357
41703
  *
41358
- * v1: `Promise.all` over per-device composition (no concurrency cap).
41359
- * Note: for large fleets (hundreds of cameras) this may fan out many
41360
- * parallel calls. A concurrency limiter (p-limit / semaphore) should be
41361
- * added if latency measurements show it's necessary deliberately
41362
- * deferred per the YAGNI constraint in the spec.
41704
+ * The whole call runs inside ONE {@link FleetReadPass}, and that is what
41705
+ * makes a five-second poll affordable. Composing a camera reaches seven
41706
+ * stages, but six of the questions those stages ask are about a NODE, a
41707
+ * device TYPE or the whole FLEETthe slot table, the restream table, the
41708
+ * mute list, the engine pair, the bindable caps — and asking them per camera
41709
+ * meant N identical round trips for N identical answers. Two more (the device
41710
+ * row, the binding table) are per-camera questions the device manager already
41711
+ * answers for a set. The pass asks each exactly once and shares the promise;
41712
+ * it holds nothing between calls, so the next poll asks again from scratch.
41713
+ *
41714
+ * What is left is genuinely per camera and stays per camera: the broker slot
41715
+ * stats, the runner metrics, the recorder, the broker mute and the camera's
41716
+ * own privacy provider. Collapsing any of those would be a narrower answer,
41717
+ * not a cheaper one.
41718
+ *
41719
+ * `Promise.all` over per-device composition (no concurrency cap). The fan-out
41720
+ * is now ~8 round trips per camera rather than ~16, and the six fleet reads
41721
+ * no longer multiply — which is the part that made the count grow with the
41722
+ * fleet twice over.
41363
41723
  */
41364
41724
  async getCameraStatuses(deviceIds) {
41365
41725
  const ids = deviceIds !== void 0 && deviceIds.length > 0 ? deviceIds : this.deps.listAssignedDeviceIds();
41366
- return Promise.all(ids.map((deviceId) => this.getCameraStatus(deviceId)));
41726
+ const pass = new FleetReadPass({ deviceIds: ids });
41727
+ return Promise.all(ids.map((deviceId) => this.composeOne(deviceId, pass)));
41367
41728
  }
41368
41729
  };
41369
41730
  //#endregion
@@ -41612,10 +41973,15 @@ async function probeCamera(api, deviceId, deps) {
41612
41973
  * @param deviceId The camera.
41613
41974
  * @param deps Logger + source-owner resolver.
41614
41975
  */
41615
- async function readSwitchAuthorities(api, deviceId, deps) {
41976
+ async function readSwitchAuthorities(api, deviceId, deps, sharedPass) {
41616
41977
  if (!api) return unknownReads(deviceId);
41617
- const devicePromise = bounded(deps, deviceId, "deviceManager.getDevice", api.deviceManager.getDevice.query({ deviceId }).then((d) => isDeviceShape(d) ? d : null).catch((err) => {
41618
- warnUnreachable(deps, deviceId, "getDevice", err);
41978
+ const pass = sharedPass ?? new FleetReadPass({ deviceIds: [deviceId] });
41979
+ const roster = pass.deviceRoster(api);
41980
+ const devicePromise = bounded(deps, deviceId, roster.source, roster.result.then((rows) => {
41981
+ const row = rows.find((d) => d.id === deviceId);
41982
+ return isDeviceShape(row) ? row : null;
41983
+ }).catch((err) => {
41984
+ warnUnreachable(deps, deviceId, roster.source, err);
41619
41985
  return null;
41620
41986
  }), null);
41621
41987
  const unknownBindings = {
@@ -41623,11 +41989,14 @@ async function readSwitchAuthorities(api, deviceId, deps) {
41623
41989
  providerAddonIdByCap: /* @__PURE__ */ new Map(),
41624
41990
  allCapNames: null
41625
41991
  };
41626
- const bindingsPromise = bounded(deps, deviceId, "deviceManager.getBindings", api.deviceManager.getBindings.query({ deviceId }).then((b) => {
41992
+ const bindingRead = pass.bindingRows(api);
41993
+ const bindingsPromise = bounded(deps, deviceId, bindingRead.source, bindingRead.result.then((rows) => {
41994
+ const row = rows.find((r) => r.deviceId === deviceId);
41995
+ if (row === void 0) return unknownBindings;
41627
41996
  const active = [];
41628
41997
  const all = [];
41629
41998
  const providers = /* @__PURE__ */ new Map();
41630
- for (const e of b.entries) {
41999
+ for (const e of row.entries) {
41631
42000
  all.push(e.capName);
41632
42001
  if (e.kind !== "wrapped") continue;
41633
42002
  active.push(e.capName);
@@ -41639,14 +42008,14 @@ async function readSwitchAuthorities(api, deviceId, deps) {
41639
42008
  allCapNames: all
41640
42009
  };
41641
42010
  }).catch((err) => {
41642
- warnUnreachable(deps, deviceId, "getBindings", err);
42011
+ warnUnreachable(deps, deviceId, bindingRead.source, err);
41643
42012
  return unknownBindings;
41644
42013
  }), unknownBindings);
41645
42014
  const recordingPromise = bounded(deps, deviceId, "recording.getDeviceConfig", api.recording.getDeviceConfig.query({ deviceId }).then((c) => isRecordingConfig(c) ? c : null).catch((err) => {
41646
42015
  warnUnreachable(deps, deviceId, "recording.getDeviceConfig", err);
41647
42016
  return null;
41648
42017
  }), null);
41649
- const mutesPromise = bounded(deps, deviceId, "notificationRules.listDeviceMutes", api.notificationRules.listDeviceMutes.query({}).then((r) => r.mutedDeviceIds).catch((err) => {
42018
+ const mutesPromise = bounded(deps, deviceId, "notificationRules.listDeviceMutes", pass.mutedDevices(api).then((r) => r.mutedDeviceIds).catch((err) => {
41650
42019
  warnUnreachable(deps, deviceId, "notificationRules.listDeviceMutes", err);
41651
42020
  return null;
41652
42021
  }), null);
@@ -41661,7 +42030,7 @@ async function readSwitchAuthorities(api, deviceId, deps) {
41661
42030
  mutesPromise,
41662
42031
  brokerAudioPromise
41663
42032
  ]);
41664
- const [bindable, privacy] = await Promise.all([device === null ? Promise.resolve(null) : bounded(deps, deviceId, "deviceManager.listBindableCapsForDeviceType", api.deviceManager.listBindableCapsForDeviceType.query({ deviceType: device.type }).catch((err) => {
42033
+ const [bindable, privacy] = await Promise.all([device === null ? Promise.resolve(null) : bounded(deps, deviceId, "deviceManager.listBindableCapsForDeviceType", pass.bindableCapsFor(api, device.type).catch((err) => {
41665
42034
  warnUnreachable(deps, deviceId, "listBindableCapsForDeviceType", err);
41666
42035
  return null;
41667
42036
  }), null), readPrivacyPlanes(api, deviceId, deps, bindings.allCapNames)]);
@@ -50368,11 +50737,11 @@ async function buildOrchestratorControllers(deps) {
50368
50737
  assignSource: (deviceId) => topology.assignSource(deviceId),
50369
50738
  listAssignedDeviceIds: () => [...new Set([...ledger.listAssignedDeviceIds(), ...detectionWiring.activeDeviceIds()])],
50370
50739
  isSessionCamera: (deviceId) => deps.isSessionCamera(deviceId),
50371
- switchAuthoritiesFor: async (deviceId) => (await readSwitchAuthorities(deps.ctx().api ?? null, deviceId, {
50740
+ switchAuthoritiesFor: async (deviceId, pass) => (await readSwitchAuthorities(deps.ctx().api ?? null, deviceId, {
50372
50741
  logger: deps.ctx().logger,
50373
50742
  assignSource: (id) => topology.assignSource(id),
50374
50743
  warnSampler: switchWarnSampler
50375
- })).derivation
50744
+ }, pass)).derivation
50376
50745
  });
50377
50746
  const reconcile = new ReconcileController({
50378
50747
  api: () => deps.ctx().api ?? null,
@@ -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-D6mD53Q1.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-ChSa_Eh-.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }