@camstack/addon-pipeline-orchestrator 1.2.127 → 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(),
@@ -19904,6 +20031,50 @@ var EventStoreFootprintSchema = object({
19904
20031
  totalBytes: number().int(),
19905
20032
  devices: array(EventStoreDeviceFootprintSchema).readonly()
19906
20033
  });
20034
+ /** Event-media footprint for one {@link MediaFileKind}. */
20035
+ var EventMediaKindFootprintSchema = object({
20036
+ kind: MediaFileKindEnum,
20037
+ /** Media rows of this kind. */
20038
+ rows: number().int(),
20039
+ /** Bytes on disk held by those rows. */
20040
+ bytes: number().int()
20041
+ });
20042
+ /**
20043
+ * The media footprint broken down by KIND — the axis a deletion decision
20044
+ * actually turns on.
20045
+ *
20046
+ * A byte total says how much there is; it cannot say what is safe to remove.
20047
+ * The deletable set (the periodic `snapshot` filmstrip, the surplus per-edge
20048
+ * motion stills) and the keep set (`firstFrame`, rolling `lastFrame`,
20049
+ * `thumbnail`/`thumbnailSmall`, `keyFrame`/`keyFrameSmall`, the face/plate
20050
+ * buffers, gallery media, the CLIP `crop`) are distinguished by `kind` and by
20051
+ * nothing else, so sizing a deletion means summing per kind.
20052
+ *
20053
+ * ## Why `unaccounted*` exists
20054
+ *
20055
+ * `kinds` is enumerated from {@link MediaFileKindEnum} — the closed set the
20056
+ * writers use — and summed one kind at a time. `totalRows` / `totalBytes` come
20057
+ * from a SEPARATE unfiltered aggregate over the same rows, never from adding
20058
+ * `kinds` up. A row whose stored `kind` is not in the enum (written by a
20059
+ * retired code path, or by a version that knew a kind this one does not) would
20060
+ * otherwise vanish from the total silently, and an operator would delete
20061
+ * against a denominator smaller than the disk.
20062
+ *
20063
+ * `unaccountedRows` / `unaccountedBytes` are the difference. They are normally
20064
+ * zero; a non-zero value is a real finding and must be shown, not rounded away.
20065
+ */
20066
+ var EventMediaKindBreakdownSchema = object({
20067
+ /** Every media row in scope, from one unfiltered aggregate. */
20068
+ totalRows: number().int(),
20069
+ /** Every media byte in scope, from that same aggregate. */
20070
+ totalBytes: number().int(),
20071
+ /** Per-kind footprint, ordered by bytes descending. */
20072
+ kinds: array(EventMediaKindFootprintSchema).readonly(),
20073
+ /** `totalRows` minus the summed `kinds` rows — see the schema note. */
20074
+ unaccountedRows: number().int(),
20075
+ /** `totalBytes` minus the summed `kinds` bytes — see the schema note. */
20076
+ unaccountedBytes: number().int()
20077
+ });
19907
20078
  /** Per-kind counts returned by the event-prune / device-delete mutations. */
19908
20079
  var EventPruneCountsSchema = object({
19909
20080
  motion: number().int(),
@@ -20058,7 +20229,7 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
20058
20229
  until: number().optional(),
20059
20230
  kinds: array(string()).optional(),
20060
20231
  limit: number().int().min(1).max(MAX_EVENT_QUERY_LIMIT).default(DEFAULT_EVENT_QUERY_LIMIT)
20061
- }), 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({
20062
20233
  deviceId: number(),
20063
20234
  since: number(),
20064
20235
  until: number(),
@@ -20107,6 +20278,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
20107
20278
  }), TrackFlagsSchema, { kind: "mutation" }), method(object({}), EventStoreFootprintSchema, {
20108
20279
  kind: "query",
20109
20280
  auth: "admin"
20281
+ }), method(object({ deviceId: number().int().optional() }), EventMediaKindBreakdownSchema, {
20282
+ kind: "query",
20283
+ auth: "admin"
20110
20284
  }), method(object({
20111
20285
  olderThanMs: number(),
20112
20286
  reason: OpsLogReasonSchema.optional()
@@ -22539,6 +22713,20 @@ method(object({
22539
22713
  error: string().optional()
22540
22714
  }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
22541
22715
  providerId: string(),
22716
+ /**
22717
+ * The location this config is an UNSAVED edit of, when there is one.
22718
+ *
22719
+ * `listLocations` replaces every declared secret with the redaction
22720
+ * sentinel, so the edit modal's form state holds the sentinel for any
22721
+ * credential the operator did not retype — and posting that here
22722
+ * without a way to resolve it makes the provider try to authenticate
22723
+ * as `__camstack_redacted__` and report the operator's own working
22724
+ * password as wrong. Given this id, the orchestrator restores each
22725
+ * sentinel from the stored config (same rule as `upsertLocation`)
22726
+ * before dispatching. Omitted by the "Add location" wizard, where
22727
+ * every value was typed just now and nothing is stored yet.
22728
+ */
22729
+ locationId: string().optional(),
22542
22730
  config: record(string(), unknown())
22543
22731
  }), object({
22544
22732
  ok: boolean(),
@@ -28174,6 +28362,9 @@ method(object({
28174
28362
  }), method(RelocateResidueInputSchema, RelocateResidueSchema, {
28175
28363
  kind: "query",
28176
28364
  auth: "admin"
28365
+ }), method(LedgerWalkInputSchema, LedgerWalkReportSchema, {
28366
+ kind: "mutation",
28367
+ auth: "admin"
28177
28368
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
28178
28369
  kind: "mutation",
28179
28370
  auth: "admin"
@@ -28565,7 +28756,26 @@ var SceneMonitorStatusSchema = object({
28565
28756
  monitors: array(SceneMonitorSchema),
28566
28757
  lastFetchedAt: number()
28567
28758
  });
28568
- 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({
28569
28779
  deviceId: number(),
28570
28780
  label: string(),
28571
28781
  roi: MaskRectShapeSchema,
@@ -29889,6 +30099,27 @@ var CameraOccupancySnapshotSchema = object({
29889
30099
  stationaryObjects: array(StationaryObjectSchema).readonly().optional()
29890
30100
  });
29891
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
+ /**
29892
30123
  * Time-series resolution. The history methods return one bucket per
29893
30124
  * step over the requested range. Smaller resolutions cost more
29894
30125
  * memory + bandwidth; bound to discrete steps so caller cannot ask
@@ -29912,7 +30143,7 @@ var HistoryPointSchema = object({
29912
30143
  /** Object count averaged over the bucket (rounded to nearest integer). */
29913
30144
  count: number().int().nonnegative()
29914
30145
  });
29915
- 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({
29916
30147
  deviceId: number(),
29917
30148
  zoneId: string(),
29918
30149
  className: string().optional()
@@ -33277,6 +33508,12 @@ Object.freeze({
33277
33508
  addonId: null,
33278
33509
  access: "view"
33279
33510
  },
33511
+ "pipelineAnalytics.getEventMediaFootprintByKind": {
33512
+ capName: "pipeline-analytics",
33513
+ capScope: "device",
33514
+ addonId: null,
33515
+ access: "view"
33516
+ },
33280
33517
  "pipelineAnalytics.getEventStoreFootprint": {
33281
33518
  capName: "pipeline-analytics",
33282
33519
  capScope: "device",
@@ -33295,6 +33532,12 @@ Object.freeze({
33295
33532
  addonId: null,
33296
33533
  access: "view"
33297
33534
  },
33535
+ "pipelineAnalytics.getKeyEventsBatch": {
33536
+ capName: "pipeline-analytics",
33537
+ capScope: "device",
33538
+ addonId: null,
33539
+ access: "view"
33540
+ },
33298
33541
  "pipelineAnalytics.getMotionEvents": {
33299
33542
  capName: "pipeline-analytics",
33300
33543
  capScope: "device",
@@ -34471,6 +34714,12 @@ Object.freeze({
34471
34714
  addonId: null,
34472
34715
  access: "view"
34473
34716
  },
34717
+ "recording.reconcileLedgerAgainstDisk": {
34718
+ capName: "recording",
34719
+ capScope: "system",
34720
+ addonId: null,
34721
+ access: "create"
34722
+ },
34474
34723
  "recording.refreshStorageLocationsForMigration": {
34475
34724
  capName: "recording",
34476
34725
  capScope: "system",
@@ -34597,6 +34846,12 @@ Object.freeze({
34597
34846
  addonId: null,
34598
34847
  access: "view"
34599
34848
  },
34849
+ "sceneMonitor.listScenesBatch": {
34850
+ capName: "scene-monitor",
34851
+ capScope: "device",
34852
+ addonId: null,
34853
+ access: "view"
34854
+ },
34600
34855
  "sceneMonitor.recheckNow": {
34601
34856
  capName: "scene-monitor",
34602
34857
  capScope: "device",
@@ -35953,6 +36208,12 @@ Object.freeze({
35953
36208
  addonId: null,
35954
36209
  access: "view"
35955
36210
  },
36211
+ "zoneAnalytics.getCurrentSnapshotBatch": {
36212
+ capName: "zone-analytics",
36213
+ capScope: "device",
36214
+ addonId: null,
36215
+ access: "view"
36216
+ },
35956
36217
  "zoneAnalytics.getUnzonedHistory": {
35957
36218
  capName: "zone-analytics",
35958
36219
  capScope: "device",
@@ -36942,6 +37203,11 @@ Object.freeze({
36942
37203
  form: "single",
36943
37204
  optional: false
36944
37205
  }],
37206
+ "pipelineAnalytics.getEventMediaFootprintByKind": [{
37207
+ name: "deviceId",
37208
+ form: "single",
37209
+ optional: true
37210
+ }],
36945
37211
  "pipelineAnalytics.getGroup": [{
36946
37212
  name: "deviceId",
36947
37213
  form: "single",
@@ -36952,6 +37218,11 @@ Object.freeze({
36952
37218
  form: "single",
36953
37219
  optional: false
36954
37220
  }],
37221
+ "pipelineAnalytics.getKeyEventsBatch": [{
37222
+ name: "deviceIds",
37223
+ form: "array",
37224
+ optional: false
37225
+ }],
36955
37226
  "pipelineAnalytics.getMotionEvents": [{
36956
37227
  name: "deviceId",
36957
37228
  form: "single",
@@ -37392,6 +37663,11 @@ Object.freeze({
37392
37663
  form: "single",
37393
37664
  optional: false
37394
37665
  }],
37666
+ "recording.reconcileLedgerAgainstDisk": [{
37667
+ name: "deviceId",
37668
+ form: "single",
37669
+ optional: true
37670
+ }],
37395
37671
  "recording.relocateFootage": [{
37396
37672
  name: "deviceId",
37397
37673
  form: "single",
@@ -37457,6 +37733,11 @@ Object.freeze({
37457
37733
  form: "single",
37458
37734
  optional: false
37459
37735
  }],
37736
+ "sceneMonitor.listScenesBatch": [{
37737
+ name: "deviceIds",
37738
+ form: "array",
37739
+ optional: false
37740
+ }],
37460
37741
  "sceneMonitor.recheckNow": [{
37461
37742
  name: "deviceId",
37462
37743
  form: "single",
@@ -37718,6 +37999,11 @@ Object.freeze({
37718
37999
  form: "single",
37719
38000
  optional: false
37720
38001
  }],
38002
+ "zoneAnalytics.getCurrentSnapshotBatch": [{
38003
+ name: "deviceIds",
38004
+ form: "array",
38005
+ optional: false
38006
+ }],
37721
38007
  "zoneAnalytics.getUnzonedHistory": [{
37722
38008
  name: "deviceId",
37723
38009
  form: "single",
@@ -40827,6 +41113,124 @@ function composeCameraStatus(input) {
40827
41113
  };
40828
41114
  }
40829
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
40830
41234
  //#region src/camera-status-service.ts
40831
41235
  /** WebRTC consumer kinds counted toward `BrokerResult.webrtcSessions`. */
40832
41236
  var WEBRTC_KINDS = new Set([
@@ -41015,7 +41419,7 @@ var CameraStatusService = class {
41015
41419
  * broker's actual decode-session node into `liveDecoder` (T6) — the first
41016
41420
  * slot that reports one wins.
41017
41421
  */
41018
- buildBrokerStage(api, allSlotsFetch, deviceId, sourceNodeId, liveDecoder, sink) {
41422
+ buildBrokerStage(api, allSlotsFetch, deviceId, sourceNodeId, pass, liveDecoder, sink) {
41019
41423
  if (!api || !allSlotsFetch) return Promise.resolve(null);
41020
41424
  return this.boundedStage(allSlotsFetch.then(async (slots) => {
41021
41425
  const deviceSlots = slots.filter((s) => s.deviceId === deviceId);
@@ -41025,12 +41429,13 @@ var CameraStatusService = class {
41025
41429
  rtspRestream: false
41026
41430
  };
41027
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)]);
41028
41433
  return {
41029
41434
  slot,
41030
- stats: await api.streamBroker.getBrokerStats.query({ brokerId: slot.brokerId }, nodePin(sourceNodeId)).catch(() => null),
41031
- clients: await api.streamBroker.listClients.query({ brokerId: slot.brokerId }, nodePin(sourceNodeId)).catch(() => null)
41435
+ stats,
41436
+ clients
41032
41437
  };
41033
- })), api.streamBroker.getAllRtspEntries.query({}, nodePin(sourceNodeId)).catch(() => null)]);
41438
+ })), pass.rtspEntries(api, sourceNodeId).catch(() => null)]);
41034
41439
  const profileDetails = statsAndClients.map(({ slot, stats, clients }) => ({
41035
41440
  profile: slot.profile,
41036
41441
  status: slot.status,
@@ -41091,13 +41496,16 @@ var CameraStatusService = class {
41091
41496
  };
41092
41497
  }
41093
41498
  /** Detection stage (pipeline-executor + runner metrics). */
41094
- buildDetectionStage(api, detectionNodeId, deviceId, sink) {
41499
+ buildDetectionStage(api, detectionNodeId, deviceId, pass, sink) {
41095
41500
  if (!api || !detectionNodeId) return Promise.resolve(null);
41096
- 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]) => {
41097
- 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({
41098
41505
  deviceId,
41099
41506
  nodeId: detectionNodeId
41100
- }).catch(() => null);
41507
+ }).catch(() => null)
41508
+ ]).then(async ([provisioning, engine, metrics]) => {
41101
41509
  const phase = (() => {
41102
41510
  const p = metrics?.phase;
41103
41511
  if (p === "active") return "active";
@@ -41209,9 +41617,9 @@ var CameraStatusService = class {
41209
41617
  * badge on a working camera); what matters is that the emptiness travels with
41210
41618
  * the reason it is empty.
41211
41619
  */
41212
- buildSwitchStage(deviceId, sink) {
41620
+ buildSwitchStage(deviceId, pass, sink) {
41213
41621
  const startedAt = Date.now();
41214
- 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) => {
41215
41623
  if (reads === null) return [];
41216
41624
  const { switchedOff, unreadable } = composeSwitchedOff(reads);
41217
41625
  if (unreadable.length > 0) this.recordDegraded(sink, deviceId, "switches", "partial", Date.now() - startedAt, { unreadableAuthorities: unreadable });
@@ -41229,18 +41637,28 @@ var CameraStatusService = class {
41229
41637
  * `null` of a camera that legitimately has no such stage.
41230
41638
  */
41231
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) {
41232
41650
  const api = this.deps.api();
41233
41651
  const degradations = { entries: [] };
41234
41652
  const { detectionNodeId, sourceNodeId, pinned, detectionReason, audioNodeId, audioPinned } = this.buildAssignmentContext(deviceId);
41235
41653
  const liveDecoder = { nodeId: null };
41236
- const allSlotsFetch = api ? api.streamBroker.listAllProfileSlots.query(void 0, nodePin(sourceNodeId)) : null;
41654
+ const allSlotsFetch = api ? pass.profileSlots(api, sourceNodeId) : null;
41237
41655
  const sourceFetch = this.buildSourceStage(api, allSlotsFetch, deviceId, degradations);
41238
- const brokerFetch = this.buildBrokerStage(api, allSlotsFetch, deviceId, sourceNodeId, liveDecoder, degradations);
41656
+ const brokerFetch = this.buildBrokerStage(api, allSlotsFetch, deviceId, sourceNodeId, pass, liveDecoder, degradations);
41239
41657
  const decoderFetch = this.buildDecoderStage(detectionNodeId);
41240
41658
  const motionResult = this.buildMotionStage(deviceId);
41241
- const detectionFetch = this.buildDetectionStage(api, detectionNodeId, deviceId, degradations);
41659
+ const detectionFetch = this.buildDetectionStage(api, detectionNodeId, deviceId, pass, degradations);
41242
41660
  const recordingFetch = this.buildRecordingStage(api, deviceId, degradations);
41243
- const switchesFetch = this.buildSwitchStage(deviceId, degradations);
41661
+ const switchesFetch = this.buildSwitchStage(deviceId, pass, degradations);
41244
41662
  const [sourceResult, brokerResult, decoderResult, detectionResult, recordingResult, switchedOff] = await Promise.all([
41245
41663
  sourceFetch,
41246
41664
  brokerFetch,
@@ -41283,15 +41701,30 @@ var CameraStatusService = class {
41283
41701
  * `deviceIds` defaults to all cameras currently tracked by the
41284
41702
  * orchestrator's assignment map when omitted.
41285
41703
  *
41286
- * v1: `Promise.all` over per-device composition (no concurrency cap).
41287
- * Note: for large fleets (hundreds of cameras) this may fan out many
41288
- * parallel calls. A concurrency limiter (p-limit / semaphore) should be
41289
- * added if latency measurements show it's necessary deliberately
41290
- * 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.
41291
41723
  */
41292
41724
  async getCameraStatuses(deviceIds) {
41293
41725
  const ids = deviceIds !== void 0 && deviceIds.length > 0 ? deviceIds : this.deps.listAssignedDeviceIds();
41294
- 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)));
41295
41728
  }
41296
41729
  };
41297
41730
  //#endregion
@@ -41540,10 +41973,15 @@ async function probeCamera(api, deviceId, deps) {
41540
41973
  * @param deviceId The camera.
41541
41974
  * @param deps Logger + source-owner resolver.
41542
41975
  */
41543
- async function readSwitchAuthorities(api, deviceId, deps) {
41976
+ async function readSwitchAuthorities(api, deviceId, deps, sharedPass) {
41544
41977
  if (!api) return unknownReads(deviceId);
41545
- const devicePromise = bounded(deps, deviceId, "deviceManager.getDevice", api.deviceManager.getDevice.query({ deviceId }).then((d) => isDeviceShape(d) ? d : null).catch((err) => {
41546
- 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);
41547
41985
  return null;
41548
41986
  }), null);
41549
41987
  const unknownBindings = {
@@ -41551,11 +41989,14 @@ async function readSwitchAuthorities(api, deviceId, deps) {
41551
41989
  providerAddonIdByCap: /* @__PURE__ */ new Map(),
41552
41990
  allCapNames: null
41553
41991
  };
41554
- 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;
41555
41996
  const active = [];
41556
41997
  const all = [];
41557
41998
  const providers = /* @__PURE__ */ new Map();
41558
- for (const e of b.entries) {
41999
+ for (const e of row.entries) {
41559
42000
  all.push(e.capName);
41560
42001
  if (e.kind !== "wrapped") continue;
41561
42002
  active.push(e.capName);
@@ -41567,14 +42008,14 @@ async function readSwitchAuthorities(api, deviceId, deps) {
41567
42008
  allCapNames: all
41568
42009
  };
41569
42010
  }).catch((err) => {
41570
- warnUnreachable(deps, deviceId, "getBindings", err);
42011
+ warnUnreachable(deps, deviceId, bindingRead.source, err);
41571
42012
  return unknownBindings;
41572
42013
  }), unknownBindings);
41573
42014
  const recordingPromise = bounded(deps, deviceId, "recording.getDeviceConfig", api.recording.getDeviceConfig.query({ deviceId }).then((c) => isRecordingConfig(c) ? c : null).catch((err) => {
41574
42015
  warnUnreachable(deps, deviceId, "recording.getDeviceConfig", err);
41575
42016
  return null;
41576
42017
  }), null);
41577
- 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) => {
41578
42019
  warnUnreachable(deps, deviceId, "notificationRules.listDeviceMutes", err);
41579
42020
  return null;
41580
42021
  }), null);
@@ -41589,7 +42030,7 @@ async function readSwitchAuthorities(api, deviceId, deps) {
41589
42030
  mutesPromise,
41590
42031
  brokerAudioPromise
41591
42032
  ]);
41592
- 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) => {
41593
42034
  warnUnreachable(deps, deviceId, "listBindableCapsForDeviceType", err);
41594
42035
  return null;
41595
42036
  }), null), readPrivacyPlanes(api, deviceId, deps, bindings.allCapNames)]);
@@ -50296,11 +50737,11 @@ async function buildOrchestratorControllers(deps) {
50296
50737
  assignSource: (deviceId) => topology.assignSource(deviceId),
50297
50738
  listAssignedDeviceIds: () => [...new Set([...ledger.listAssignedDeviceIds(), ...detectionWiring.activeDeviceIds()])],
50298
50739
  isSessionCamera: (deviceId) => deps.isSessionCamera(deviceId),
50299
- switchAuthoritiesFor: async (deviceId) => (await readSwitchAuthorities(deps.ctx().api ?? null, deviceId, {
50740
+ switchAuthoritiesFor: async (deviceId, pass) => (await readSwitchAuthorities(deps.ctx().api ?? null, deviceId, {
50300
50741
  logger: deps.ctx().logger,
50301
50742
  assignSource: (id) => topology.assignSource(id),
50302
50743
  warnSampler: switchWarnSampler
50303
- })).derivation
50744
+ }, pass)).derivation
50304
50745
  });
50305
50746
  const reconcile = new ReconcileController({
50306
50747
  api: () => deps.ctx().api ?? null,