@camstack/addon-pipeline-orchestrator 1.2.128 → 1.2.130

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()
@@ -8930,6 +8944,13 @@ var MediaRelocateModeSchema = _enum([
8930
8944
  ]);
8931
8945
  var RelocateMediaInputSchema = object({
8932
8946
  toLocationId: string(),
8947
+ /**
8948
+ * Restrict the pass to rows currently on this location. Omitted / `'*'` =
8949
+ * every row that is not already on `toLocationId` (the historical
8950
+ * behaviour). A named source is what a from→to migration needs: without it
8951
+ * "move events off disk 2" also emptied disk 1.
8952
+ */
8953
+ fromLocationId: string().optional(),
8933
8954
  throttleMbps: number().min(1).max(1e3).optional(),
8934
8955
  /** Omitted = `move`, the pre-existing behaviour. */
8935
8956
  mode: MediaRelocateModeSchema.optional()
@@ -8997,6 +9018,19 @@ var StorageMigrationDestinationsSchema = object({
8997
9018
  galleryMedia: string().min(1).optional()
8998
9019
  }).refine((value) => Object.keys(value).length > 0, { message: "select at least one storage class" });
8999
9020
  /**
9021
+ * Optional named source per class. Omitted = the class's current default
9022
+ * (the historical behaviour). A named source that is NOT the default is a
9023
+ * drain of that disk: bytes move, the default stays, and the source is
9024
+ * disabled when the move finishes.
9025
+ */
9026
+ var StorageMigrationSourcesSchema = object({
9027
+ recordings: string().min(1).optional(),
9028
+ recordingsLow: string().min(1).optional(),
9029
+ eventMedia: string().min(1).optional(),
9030
+ backups: string().min(1).optional(),
9031
+ galleryMedia: string().min(1).optional()
9032
+ }).optional();
9033
+ /**
9000
9034
  * How a migration sequences the cutover against the byte move.
9001
9035
  *
9002
9036
  * - `blocking` — the historical order: pause, move every byte, repoint,
@@ -9018,6 +9052,8 @@ var StorageMigrationModeSchema = _enum(["blocking", "nonBlocking"]);
9018
9052
  /** Shared input for planning and starting an orchestrated storage migration. */
9019
9053
  var StorageMigrationInputSchema = object({
9020
9054
  destinations: StorageMigrationDestinationsSchema,
9055
+ /** Omitted = each class's current default. */
9056
+ sources: StorageMigrationSourcesSchema,
9021
9057
  throttleMbps: number().min(1).max(1e3).optional(),
9022
9058
  /** Omitted = `blocking`, which stays the default. */
9023
9059
  mode: StorageMigrationModeSchema.optional()
@@ -9097,6 +9133,13 @@ var StorageMigrationMoveSchema = object({
9097
9133
  storageClass: StorageMigrationClassSchema,
9098
9134
  fromLocationId: string(),
9099
9135
  toLocationId: string(),
9136
+ /**
9137
+ * True when `from` was NOT the class default at plan time. The move still
9138
+ * copies bytes, but the default is left alone and the source is disabled
9139
+ * once the copy verifies. Absent on jobs planned before this field existed
9140
+ * — those jobs always repointed, which is `false`.
9141
+ */
9142
+ freezeSource: boolean().optional(),
9100
9143
  moverJobId: string().nullable(),
9101
9144
  state: RelocateJobStateSchema.nullable(),
9102
9145
  error: string().nullable(),
@@ -9110,6 +9153,7 @@ var StorageMigrationJobSchema = object({
9110
9153
  * can tell a seconds-long cutover from a thirty-hour one. */
9111
9154
  mode: StorageMigrationModeSchema,
9112
9155
  destinations: StorageMigrationDestinationsSchema,
9156
+ sources: StorageMigrationSourcesSchema,
9113
9157
  throttleMbps: number(),
9114
9158
  moves: array(StorageMigrationMoveSchema),
9115
9159
  pauseLeaseId: string().nullable(),
@@ -9135,6 +9179,7 @@ var StorageMigrationFindingSchema = object({
9135
9179
  });
9136
9180
  var StorageMigrationPlanSchema = object({
9137
9181
  destinations: StorageMigrationDestinationsSchema,
9182
+ sources: StorageMigrationSourcesSchema,
9138
9183
  /** The mode this plan was built for. A plan is only valid for its mode: the
9139
9184
  * `eventMedia` seal gate and the single-cardinality refusal both depend on
9140
9185
  * it. */
@@ -9142,7 +9187,8 @@ var StorageMigrationPlanSchema = object({
9142
9187
  moves: array(object({
9143
9188
  storageClass: StorageMigrationClassSchema,
9144
9189
  fromLocationId: string(),
9145
- toLocationId: string()
9190
+ toLocationId: string(),
9191
+ freezeSource: boolean().optional()
9146
9192
  })),
9147
9193
  findings: array(StorageMigrationFindingSchema)
9148
9194
  });
@@ -9229,16 +9275,142 @@ var RelocateResidueSchema = object({
9229
9275
  segments: number().int().nonnegative(),
9230
9276
  bytes: number().int().nonnegative()
9231
9277
  }).nullable();
9278
+ /**
9279
+ * Ask one location whether its durable hour rows describe the disk — the walk
9280
+ * (D319).
9281
+ *
9282
+ * `apply` DEFAULTS TO FALSE and that default is the product: the operator's
9283
+ * missing tool is the question, and the dry run is how they sanity-check the
9284
+ * destructive run before authorising it.
9285
+ */
9286
+ var LedgerWalkInputSchema = object({
9287
+ locationId: string().min(1),
9288
+ /** Forget the confirmed-absent rows, rather than only counting them. */
9289
+ apply: boolean().optional(),
9290
+ /** Narrow to one camera. */
9291
+ deviceId: number().int().positive().optional(),
9292
+ /** Narrow to these recording profiles; empty/absent = every profile. */
9293
+ profiles: array(string().min(1)).optional()
9294
+ });
9295
+ /** Why a whole walk did nothing. Every one leaves the ledger untouched. */
9296
+ var LedgerWalkRefusalSchema = _enum([
9297
+ "location-unknown",
9298
+ "source-writable",
9299
+ "no-ledger",
9300
+ "archive-unreadable",
9301
+ "anchor-absent",
9302
+ "anchor-unreadable",
9303
+ "anchor-moved"
9304
+ ]);
9305
+ _enum([
9306
+ "live-tail",
9307
+ "listing-error",
9308
+ "path-mismatch",
9309
+ "durable-refused"
9310
+ ]);
9311
+ /** Every skip reason, always present, always a number — so a reason that never
9312
+ * fired reports as zero rather than absent and the report shape is constant
9313
+ * between passes. Spelled out rather than `z.record` for exactly that. */
9314
+ var LedgerWalkSkipCountsSchema = object({
9315
+ "live-tail": number().int().nonnegative(),
9316
+ "listing-error": number().int().nonnegative(),
9317
+ "path-mismatch": number().int().nonnegative(),
9318
+ "durable-refused": number().int().nonnegative()
9319
+ });
9320
+ /** One camera's share of a walk, so a report names cameras and not rows. */
9321
+ var LedgerWalkDeviceReportSchema = object({
9322
+ deviceId: number().int(),
9323
+ hoursWalked: number().int().nonnegative(),
9324
+ hoursMissing: number().int().nonnegative(),
9325
+ ghostSegments: number().int().nonnegative(),
9326
+ ghostBytes: number().int().nonnegative(),
9327
+ forgottenSegments: number().int().nonnegative(),
9328
+ orphanFiles: number().int().nonnegative()
9329
+ });
9330
+ /**
9331
+ * What one walk claimed, listed, found and (only when armed) forgot.
9332
+ *
9333
+ * `archiveSegments` is the **M** and `segmentsClaimed` the **N** (D295), so a
9334
+ * walk that saw a fraction of the location is visible in its own report rather
9335
+ * than in the absence of one.
9336
+ */
9337
+ var LedgerWalkReportSchema = object({
9338
+ locationId: string(),
9339
+ applied: boolean(),
9340
+ refused: LedgerWalkRefusalSchema.nullable(),
9341
+ archiveSegments: number().int().nonnegative().nullable(),
9342
+ archiveBytes: number().int().nonnegative().nullable(),
9343
+ hoursClaimed: number().int().nonnegative(),
9344
+ hoursWalked: number().int().nonnegative(),
9345
+ hoursMissing: number().int().nonnegative(),
9346
+ /** `readdir` calls issued — the cost, stated in the unit that is paid. */
9347
+ listings: number().int().nonnegative(),
9348
+ segmentsClaimed: number().int().nonnegative(),
9349
+ ghostSegments: number().int().nonnegative(),
9350
+ ghostBytes: number().int().nonnegative(),
9351
+ ghostHoursWhole: number().int().nonnegative(),
9352
+ forgottenSegments: number().int().nonnegative(),
9353
+ forgottenBytes: number().int().nonnegative(),
9354
+ /** Files under a claimed hour that no durable row names. Never deleted. */
9355
+ orphanFiles: number().int().nonnegative(),
9356
+ orphanSample: array(string()).readonly(),
9357
+ hoursSkipped: number().int().nonnegative(),
9358
+ skippedByReason: LedgerWalkSkipCountsSchema,
9359
+ /** The walk stopped at its per-pass hour bound with claims unwalked. */
9360
+ bounded: boolean(),
9361
+ byDevice: array(LedgerWalkDeviceReportSchema).readonly()
9362
+ });
9232
9363
  /** How many rows a media pass would still act on against a given target — the
9233
9364
  * media lane's denominator AND its residue, from ONE derivation so the two can
9234
9365
  * never disagree. `null` = the count could not be taken. */
9235
9366
  var RelocatableMediaCountSchema = object({ rows: number().int().nonnegative() }).nullable();
9236
9367
  var RelocatableMediaCountInputSchema = object({
9237
9368
  toLocationId: string().min(1),
9369
+ /** Restrict the count to one source; omitted / `'*'` = every non-target row. */
9370
+ fromLocationId: string().optional(),
9238
9371
  /** Omitted = `move`. */
9239
9372
  mode: MediaRelocateModeSchema.optional()
9240
9373
  });
9241
9374
  /**
9375
+ * Operator cleanup of leftover analytics rows, optional debug media, and
9376
+ * ghost ledger entries on frozen footage locations.
9377
+ *
9378
+ * A pass is tens of minutes on a standing backlog. The hub method RETURNS
9379
+ * `{ jobId }` immediately; progress is `cleanupStatus`. Awaiting the work
9380
+ * is how `addons.custom` hit the 60 s UDS deadline while reclaim continued
9381
+ * with no operator-visible status.
9382
+ */
9383
+ var StorageCleanupPhaseSchema = _enum([
9384
+ "orphans",
9385
+ "debug-media",
9386
+ "ghost-ledger",
9387
+ "done",
9388
+ "failed",
9389
+ "cancelled"
9390
+ ]);
9391
+ var StorageCleanupInputSchema = object({
9392
+ /** Also walk motion stills / track filmstrips. Off by default. */
9393
+ includeDebugMedia: boolean().optional() });
9394
+ var StorageCleanupJobSchema = object({
9395
+ jobId: string(),
9396
+ phase: StorageCleanupPhaseSchema,
9397
+ includeDebugMedia: boolean(),
9398
+ orphansReclaimed: number().int().nonnegative(),
9399
+ orphanBytesReclaimed: number().int().nonnegative(),
9400
+ debugMediaReclaimed: number().int().nonnegative(),
9401
+ debugMediaBytesReclaimed: number().int().nonnegative(),
9402
+ ghostsForgotten: number().int().nonnegative(),
9403
+ ghostBytesForgotten: number().int().nonnegative(),
9404
+ /** Short operator-facing line: current collection, pass, or location. */
9405
+ detail: string().nullable(),
9406
+ cancelRequested: boolean(),
9407
+ startedAt: number(),
9408
+ updatedAt: number(),
9409
+ finishedAt: number().nullable(),
9410
+ error: string().nullable()
9411
+ });
9412
+ var StorageCleanupStatusInputSchema = object({ jobId: string().optional() });
9413
+ /**
9242
9414
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
9243
9415
  * storage a location serves. Defined here (not in `capabilities/storage.cap.ts`)
9244
9416
  * so the persisted record schema and the consumer-facing cap can both consume it
@@ -9266,11 +9438,10 @@ var StorageLocationTypeSchema = string().regex(/^[a-z][a-zA-Z0-9-]*$/);
9266
9438
  * The default location for a type uses `id === <type>:default` by
9267
9439
  * convention (the bare type ref like `'backups'` resolves to it).
9268
9440
  *
9269
- * `isSystem: true` marks a location as orchestrator-seeded and
9270
- * undeletable. The bootstrap-installed defaults (one per type) carry
9271
- * this flag; operator-added locations don't. Editing the config of
9272
- * a system location is allowed (path migration, provider swap) but
9273
- * deleting it is rejected at the cap level.
9441
+ * `isSystem` is a legacy persisted flag. Seed still creates the initial
9442
+ * `<type>:default` locations; the flag is no longer a lock, a badge, or a
9443
+ * prune selector. New writes leave it false. Deletion is gated on uniqueness
9444
+ * / last-enabled, not on this bit.
9274
9445
  */
9275
9446
  var StorageLocationSchema = object({
9276
9447
  id: string().regex(/^[a-z][a-zA-Z0-9-]*:[a-zA-Z0-9-]+$/),
@@ -19805,13 +19976,15 @@ var ListGroupsPageSchema = object({
19805
19976
  groups: array(AnalyticsGroupRecordSchema).readonly(),
19806
19977
  nextCursor: string().nullable()
19807
19978
  });
19979
+ var KEY_EVENTS_DEFAULT_LIMIT = 50;
19980
+ var KEY_EVENTS_MAX_LIMIT = 200;
19808
19981
  var KeyEventQueryInput = object({
19809
19982
  deviceId: number(),
19810
19983
  /** Window lower bound (track firstSeen ≥ since). */
19811
19984
  since: number(),
19812
19985
  /** Window upper bound (track firstSeen ≤ until). */
19813
19986
  until: number(),
19814
- limit: number().int().min(1).max(200).default(50),
19987
+ limit: number().int().min(1).max(KEY_EVENTS_MAX_LIMIT).default(KEY_EVENTS_DEFAULT_LIMIT),
19815
19988
  /** Drop tracks scoring below this importance. */
19816
19989
  minImportance: number().min(0).max(1).optional(),
19817
19990
  /** Restrict to a single class (e.g. 'person'). */
@@ -19833,6 +20006,32 @@ var KeyEventSchema = object({
19833
20006
  ...TrackFlagFields,
19834
20007
  ...TrackRetrainFields
19835
20008
  });
20009
+ /** `getKeyEvents`' window and filters, asked of a SET of cameras at once. */
20010
+ var KeyEventBatchQueryInput = object({
20011
+ deviceIds: array(number()).min(1).max(200),
20012
+ since: number(),
20013
+ until: number(),
20014
+ /** Applied PER CAMERA, exactly as `getKeyEvents.limit` is — never a total
20015
+ * across the set, which would let a busy camera starve a quiet one of its
20016
+ * rows and change what the merged feed contains. */
20017
+ limit: number().int().min(1).max(KEY_EVENTS_MAX_LIMIT).default(KEY_EVENTS_DEFAULT_LIMIT),
20018
+ minImportance: number().min(0).max(1).optional(),
20019
+ classFilter: string().optional()
20020
+ });
20021
+ /**
20022
+ * One camera's key events in a batch answer.
20023
+ *
20024
+ * The row exists for every requested id. `getKeyEvents` degrades to `[]` on
20025
+ * error rather than throwing, so a camera whose store read failed and one with
20026
+ * no events in the window were ALREADY indistinguishable per camera — the
20027
+ * batch does not make that worse, and the row keeps the deviceId the single
20028
+ * method's output never carried (the caller used to stamp it from the fan-out
20029
+ * key, which only worked because there was one query per camera).
20030
+ */
20031
+ var KeyEventsForDeviceSchema = object({
20032
+ deviceId: number(),
20033
+ events: array(KeyEventSchema).readonly()
20034
+ });
19836
20035
  object({
19837
20036
  trackId: string(),
19838
20037
  className: string(),
@@ -20052,6 +20251,47 @@ var RebuildStatusSchema = object({
20052
20251
  /** Present when the pass ended by throwing. */
20053
20252
  error: string().nullable()
20054
20253
  });
20254
+ /**
20255
+ * Acknowledgement that a debug-media reclaim STARTED.
20256
+ *
20257
+ * The pass is paced at ~2 MB/s over a standing 100 GB — tens of minutes — so
20258
+ * it runs detached and this returns immediately. Awaiting it is how the
20259
+ * `addons.custom` door hit the 60 s UDS deadline while the walk carried on
20260
+ * with no status. Poll {@link pipelineAnalyticsCapability.methods.getMediaReclaimStatus}.
20261
+ */
20262
+ var MediaReclaimStartResultSchema = object({
20263
+ started: boolean(),
20264
+ /** True when a pass was already running; the new request is ignored. */
20265
+ alreadyRunning: boolean()
20266
+ });
20267
+ var MediaReclaimInputSchema = object({
20268
+ mode: _enum(["report", "reclaim"]).default("report"),
20269
+ scopes: array(_enum(["motion-still", "track-filmstrip"])).min(1).optional(),
20270
+ deviceIds: array(number().int()).min(1).optional(),
20271
+ restart: boolean().optional(),
20272
+ pageSize: number().int().min(50).max(5e3).optional(),
20273
+ maxRowsPerDevice: number().int().min(1).max(5e5).optional(),
20274
+ maxReclaimPerDevice: number().int().min(1).max(5e5).optional(),
20275
+ maxBytesPerRun: number().int().min(1).optional(),
20276
+ budgetMinutes: number().int().min(1).max(720).optional(),
20277
+ throttleBytesPerSec: number().int().min(64 * 1024).optional(),
20278
+ graceMinutes: number().int().min(1).max(10080).optional()
20279
+ });
20280
+ var MediaReclaimStatusSchema = object({
20281
+ running: boolean(),
20282
+ mode: _enum(["report", "reclaim"]).nullable(),
20283
+ totalExamined: number(),
20284
+ totalEligible: number(),
20285
+ totalReclaimed: number(),
20286
+ totalBytesReclaimed: number(),
20287
+ totalRefused: number(),
20288
+ /** Device+scope windows finished in this pass. */
20289
+ devicesDone: number(),
20290
+ complete: boolean().nullable(),
20291
+ startedAtMs: number().nullable(),
20292
+ finishedAtMs: number().nullable(),
20293
+ error: string().nullable()
20294
+ });
20055
20295
  var ReplayFrameInputSchema = object({
20056
20296
  timestamp: number(),
20057
20297
  frame: PipelineRunResultBridge
@@ -20102,7 +20342,7 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
20102
20342
  until: number().optional(),
20103
20343
  kinds: array(string()).optional(),
20104
20344
  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({
20345
+ }), array(SensorEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(KeyEventBatchQueryInput, array(KeyEventsForDeviceSchema).readonly()), method(object({
20106
20346
  deviceId: number(),
20107
20347
  since: number(),
20108
20348
  until: number(),
@@ -20193,6 +20433,12 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
20193
20433
  }), method(OpsLogQueryInputSchema, array(OpsLogEntrySchema).readonly(), {
20194
20434
  kind: "query",
20195
20435
  auth: "admin"
20436
+ }), method(MediaReclaimInputSchema, MediaReclaimStartResultSchema, {
20437
+ kind: "mutation",
20438
+ auth: "admin"
20439
+ }), method(object({}), MediaReclaimStatusSchema, {
20440
+ kind: "query",
20441
+ auth: "admin"
20196
20442
  }), method(object({ deviceIds: array(number()).optional() }), TrainingExportSummarySchema, {
20197
20443
  kind: "query",
20198
20444
  auth: "admin"
@@ -22644,7 +22890,13 @@ method(StorageMigrationInputSchema, StorageMigrationPlanSchema, { auth: "admin"
22644
22890
  }), method(object({}), array(StorageMigrationMoverSchema).readonly(), { auth: "admin" }), method(object({}), array(StorageMigrationResidueSchema).readonly(), { auth: "admin" }), method(StorageMigrationDrainInputSchema, object({ jobId: string() }), {
22645
22891
  kind: "mutation",
22646
22892
  auth: "admin"
22647
- });
22893
+ }), method(StorageCleanupInputSchema, object({ jobId: string() }), {
22894
+ kind: "mutation",
22895
+ auth: "admin"
22896
+ }), method(StorageCleanupStatusInputSchema, StorageCleanupJobSchema.nullable(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
22897
+ kind: "mutation",
22898
+ auth: "admin"
22899
+ }), method(object({}), array(StorageMigrationJobSchema).readonly(), { auth: "admin" });
22648
22900
  var ProviderInfoSchema = discriminatedUnion("shouldSaveDiskSpace", [object({
22649
22901
  providerId: string().min(1),
22650
22902
  displayName: string().min(1),
@@ -28022,6 +28274,33 @@ var RecordingRebalanceInputSchema = object({
28022
28274
  minMoveGb: number().min(0).optional()
28023
28275
  });
28024
28276
  /**
28277
+ * Operator-facing placement of one camera onto a recordings location.
28278
+ *
28279
+ * `pinLocationId` is the operator instruction: a location id, or `null` for
28280
+ * Auto (the planner may move this camera). `locationId` is where high/mid
28281
+ * currently write — the plan, which may disagree with the pin when Auto.
28282
+ */
28283
+ var RecordingDevicePlacementSchema = object({
28284
+ deviceId: number().int(),
28285
+ profile: string(),
28286
+ locationId: string()
28287
+ });
28288
+ var RecordingDevicePinSchema = object({
28289
+ deviceId: number().int(),
28290
+ /** Recordings-class location this camera is pinned to. */
28291
+ locationId: string()
28292
+ });
28293
+ var RecordingPlacementViewSchema = object({
28294
+ assignments: array(RecordingDevicePlacementSchema),
28295
+ pins: array(RecordingDevicePinSchema),
28296
+ defaultLocations: record(string(), string())
28297
+ });
28298
+ var RecordingSetDevicePlacementInputSchema = object({
28299
+ deviceId: number().int(),
28300
+ /** `null` = Auto. Otherwise a `recordings:*` location id. */
28301
+ locationId: string().nullable()
28302
+ });
28303
+ /**
28025
28304
  * Result of locating footage at a wall-clock instant for one device/profile.
28026
28305
  * `segment` carries the covering segment's window; `gap` reports the forward
28027
28306
  * nearest covered edge (`null` past the end of footage / when none exists)
@@ -28235,6 +28514,9 @@ method(object({
28235
28514
  }), method(RelocateResidueInputSchema, RelocateResidueSchema, {
28236
28515
  kind: "query",
28237
28516
  auth: "admin"
28517
+ }), method(LedgerWalkInputSchema, LedgerWalkReportSchema, {
28518
+ kind: "mutation",
28519
+ auth: "admin"
28238
28520
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
28239
28521
  kind: "mutation",
28240
28522
  auth: "admin"
@@ -28244,6 +28526,12 @@ method(object({
28244
28526
  }), method(RecordingRebalanceInputSchema, RecordingRebalancePlanSchema, {
28245
28527
  kind: "mutation",
28246
28528
  auth: "admin"
28529
+ }), method(object({}), RecordingPlacementViewSchema, {
28530
+ kind: "query",
28531
+ auth: "admin"
28532
+ }), method(RecordingSetDevicePlacementInputSchema, object({ ok: literal(true) }), {
28533
+ kind: "mutation",
28534
+ auth: "admin"
28247
28535
  });
28248
28536
  /**
28249
28537
  * `recording-export` cap — render a footage time range into a single downloadable
@@ -28626,7 +28914,26 @@ var SceneMonitorStatusSchema = object({
28626
28914
  monitors: array(SceneMonitorSchema),
28627
28915
  lastFetchedAt: number()
28628
28916
  });
28629
- DeviceType.Camera, method(object({ deviceId: number() }), SceneMonitorStatusSchema), method(object({
28917
+ /**
28918
+ * One camera's row in a `listScenesBatch` answer.
28919
+ *
28920
+ * `status` is NULLABLE and the nullability is the whole point. `scene-monitor`
28921
+ * is a `defaultActive` wrapper, so every camera is asked; a camera whose
28922
+ * provider is absent (pipeline-analytics not deployed, the post-processing node
28923
+ * down) cannot answer, and that is not the same fact as a camera with no scenes
28924
+ * configured. Fanned out per camera the difference was visible — one query
28925
+ * errored while the others resolved — and a batch that returned only the rows
28926
+ * it managed would have destroyed it, silently, by making an unreachable camera
28927
+ * indistinguishable from one that answered `monitors: []`.
28928
+ *
28929
+ * So: EVERY requested deviceId gets a row. `status: null` means "this camera
28930
+ * could not be read"; `status.monitors: []` means "read, and it has none".
28931
+ */
28932
+ var SceneMonitorStatusForDeviceSchema = object({
28933
+ deviceId: number(),
28934
+ status: SceneMonitorStatusSchema.nullable()
28935
+ });
28936
+ DeviceType.Camera, method(object({ deviceId: number() }), SceneMonitorStatusSchema), method(object({ deviceIds: array(number()).min(1).max(200) }), array(SceneMonitorStatusForDeviceSchema).readonly()), method(object({
28630
28937
  deviceId: number(),
28631
28938
  label: string(),
28632
28939
  roi: MaskRectShapeSchema,
@@ -29950,6 +30257,27 @@ var CameraOccupancySnapshotSchema = object({
29950
30257
  stationaryObjects: array(StationaryObjectSchema).readonly().optional()
29951
30258
  });
29952
30259
  /**
30260
+ * One camera's row in a `getCurrentSnapshotBatch` answer.
30261
+ *
30262
+ * THREE outcomes, and the single-camera method could only express two of them
30263
+ * because `snapshot: null` was already spoken for:
30264
+ *
30265
+ * - `read: 'read'`, `snapshot` present — the live occupancy reading.
30266
+ * - `read: 'read'`, `snapshot: null` — read, and this camera has no reading
30267
+ * yet: no frame since boot, and no parked-object registry to hydrate from.
30268
+ * - `read: 'unreadable'` — the owner could not answer for this
30269
+ * camera. `snapshot` is null, and it does NOT mean "nothing parked here".
30270
+ *
30271
+ * Collapsing the last two is the failure this field exists to prevent: a
30272
+ * hydration that threw would otherwise render as an empty Stationary section,
30273
+ * which is a definite claim about a camera nobody could read.
30274
+ */
30275
+ var CameraOccupancySnapshotForDeviceSchema = object({
30276
+ deviceId: number(),
30277
+ read: _enum(["read", "unreadable"]),
30278
+ snapshot: CameraOccupancySnapshotSchema.nullable()
30279
+ });
30280
+ /**
29953
30281
  * Time-series resolution. The history methods return one bucket per
29954
30282
  * step over the requested range. Smaller resolutions cost more
29955
30283
  * memory + bandwidth; bound to discrete steps so caller cannot ask
@@ -29973,7 +30301,7 @@ var HistoryPointSchema = object({
29973
30301
  /** Object count averaged over the bucket (rounded to nearest integer). */
29974
30302
  count: number().int().nonnegative()
29975
30303
  });
29976
- DeviceType.Camera, method(object({ deviceId: number() }), CameraOccupancySnapshotSchema.nullable()), method(object({
30304
+ DeviceType.Camera, method(object({ deviceId: number() }), CameraOccupancySnapshotSchema.nullable()), method(object({ deviceIds: array(number()).min(1).max(200) }), array(CameraOccupancySnapshotForDeviceSchema).readonly()), method(object({
29977
30305
  deviceId: number(),
29978
30306
  zoneId: string(),
29979
30307
  className: string().optional()
@@ -33362,6 +33690,18 @@ Object.freeze({
33362
33690
  addonId: null,
33363
33691
  access: "view"
33364
33692
  },
33693
+ "pipelineAnalytics.getKeyEventsBatch": {
33694
+ capName: "pipeline-analytics",
33695
+ capScope: "device",
33696
+ addonId: null,
33697
+ access: "view"
33698
+ },
33699
+ "pipelineAnalytics.getMediaReclaimStatus": {
33700
+ capName: "pipeline-analytics",
33701
+ capScope: "device",
33702
+ addonId: null,
33703
+ access: "view"
33704
+ },
33365
33705
  "pipelineAnalytics.getMotionEvents": {
33366
33706
  capName: "pipeline-analytics",
33367
33707
  capScope: "device",
@@ -33536,6 +33876,12 @@ Object.freeze({
33536
33876
  addonId: null,
33537
33877
  access: "create"
33538
33878
  },
33879
+ "pipelineAnalytics.reclaimDebugMedia": {
33880
+ capName: "pipeline-analytics",
33881
+ capScope: "device",
33882
+ addonId: null,
33883
+ access: "create"
33884
+ },
33539
33885
  "pipelineAnalytics.reconcileFromDisk": {
33540
33886
  capName: "pipeline-analytics",
33541
33887
  capScope: "device",
@@ -34460,6 +34806,12 @@ Object.freeze({
34460
34806
  addonId: null,
34461
34807
  access: "view"
34462
34808
  },
34809
+ "recording.getPlacement": {
34810
+ capName: "recording",
34811
+ capScope: "system",
34812
+ addonId: null,
34813
+ access: "view"
34814
+ },
34463
34815
  "recording.getPlaybackManifest": {
34464
34816
  capName: "recording",
34465
34817
  capScope: "system",
@@ -34538,6 +34890,12 @@ Object.freeze({
34538
34890
  addonId: null,
34539
34891
  access: "view"
34540
34892
  },
34893
+ "recording.reconcileLedgerAgainstDisk": {
34894
+ capName: "recording",
34895
+ capScope: "system",
34896
+ addonId: null,
34897
+ access: "create"
34898
+ },
34541
34899
  "recording.refreshStorageLocationsForMigration": {
34542
34900
  capName: "recording",
34543
34901
  capScope: "system",
@@ -34580,6 +34938,12 @@ Object.freeze({
34580
34938
  addonId: null,
34581
34939
  access: "create"
34582
34940
  },
34941
+ "recording.setDevicePlacement": {
34942
+ capName: "recording",
34943
+ capScope: "system",
34944
+ addonId: null,
34945
+ access: "create"
34946
+ },
34583
34947
  "recording.startStorageMigrationMove": {
34584
34948
  capName: "recording",
34585
34949
  capScope: "system",
@@ -34664,6 +35028,12 @@ Object.freeze({
34664
35028
  addonId: null,
34665
35029
  access: "view"
34666
35030
  },
35031
+ "sceneMonitor.listScenesBatch": {
35032
+ capName: "scene-monitor",
35033
+ capScope: "device",
35034
+ addonId: null,
35035
+ access: "view"
35036
+ },
34667
35037
  "sceneMonitor.recheckNow": {
34668
35038
  capName: "scene-monitor",
34669
35039
  capScope: "device",
@@ -35018,12 +35388,36 @@ Object.freeze({
35018
35388
  addonId: null,
35019
35389
  access: "create"
35020
35390
  },
35391
+ "storageMigration.cleanupCancel": {
35392
+ capName: "storage-migration",
35393
+ capScope: "system",
35394
+ addonId: null,
35395
+ access: "create"
35396
+ },
35397
+ "storageMigration.cleanupStart": {
35398
+ capName: "storage-migration",
35399
+ capScope: "system",
35400
+ addonId: null,
35401
+ access: "create"
35402
+ },
35403
+ "storageMigration.cleanupStatus": {
35404
+ capName: "storage-migration",
35405
+ capScope: "system",
35406
+ addonId: null,
35407
+ access: "view"
35408
+ },
35021
35409
  "storageMigration.drain": {
35022
35410
  capName: "storage-migration",
35023
35411
  capScope: "system",
35024
35412
  addonId: null,
35025
35413
  access: "create"
35026
35414
  },
35415
+ "storageMigration.history": {
35416
+ capName: "storage-migration",
35417
+ capScope: "system",
35418
+ addonId: null,
35419
+ access: "view"
35420
+ },
35027
35421
  "storageMigration.movers": {
35028
35422
  capName: "storage-migration",
35029
35423
  capScope: "system",
@@ -36020,6 +36414,12 @@ Object.freeze({
36020
36414
  addonId: null,
36021
36415
  access: "view"
36022
36416
  },
36417
+ "zoneAnalytics.getCurrentSnapshotBatch": {
36418
+ capName: "zone-analytics",
36419
+ capScope: "device",
36420
+ addonId: null,
36421
+ access: "view"
36422
+ },
36023
36423
  "zoneAnalytics.getUnzonedHistory": {
36024
36424
  capName: "zone-analytics",
36025
36425
  capScope: "device",
@@ -37024,6 +37424,11 @@ Object.freeze({
37024
37424
  form: "single",
37025
37425
  optional: false
37026
37426
  }],
37427
+ "pipelineAnalytics.getKeyEventsBatch": [{
37428
+ name: "deviceIds",
37429
+ form: "array",
37430
+ optional: false
37431
+ }],
37027
37432
  "pipelineAnalytics.getMotionEvents": [{
37028
37433
  name: "deviceId",
37029
37434
  form: "single",
@@ -37129,6 +37534,11 @@ Object.freeze({
37129
37534
  form: "single",
37130
37535
  optional: true
37131
37536
  }],
37537
+ "pipelineAnalytics.reclaimDebugMedia": [{
37538
+ name: "deviceIds",
37539
+ form: "array",
37540
+ optional: true
37541
+ }],
37132
37542
  "pipelineAnalytics.reconcileFromDisk": [{
37133
37543
  name: "deviceId",
37134
37544
  form: "single",
@@ -37464,6 +37874,11 @@ Object.freeze({
37464
37874
  form: "single",
37465
37875
  optional: false
37466
37876
  }],
37877
+ "recording.reconcileLedgerAgainstDisk": [{
37878
+ name: "deviceId",
37879
+ form: "single",
37880
+ optional: true
37881
+ }],
37467
37882
  "recording.relocateFootage": [{
37468
37883
  name: "deviceId",
37469
37884
  form: "single",
@@ -37489,6 +37904,11 @@ Object.freeze({
37489
37904
  form: "single",
37490
37905
  optional: false
37491
37906
  }],
37907
+ "recording.setDevicePlacement": [{
37908
+ name: "deviceId",
37909
+ form: "single",
37910
+ optional: false
37911
+ }],
37492
37912
  "recording.startStorageMigrationMove": [{
37493
37913
  name: "deviceId",
37494
37914
  form: "single",
@@ -37529,6 +37949,11 @@ Object.freeze({
37529
37949
  form: "single",
37530
37950
  optional: false
37531
37951
  }],
37952
+ "sceneMonitor.listScenesBatch": [{
37953
+ name: "deviceIds",
37954
+ form: "array",
37955
+ optional: false
37956
+ }],
37532
37957
  "sceneMonitor.recheckNow": [{
37533
37958
  name: "deviceId",
37534
37959
  form: "single",
@@ -37790,6 +38215,11 @@ Object.freeze({
37790
38215
  form: "single",
37791
38216
  optional: false
37792
38217
  }],
38218
+ "zoneAnalytics.getCurrentSnapshotBatch": [{
38219
+ name: "deviceIds",
38220
+ form: "array",
38221
+ optional: false
38222
+ }],
37793
38223
  "zoneAnalytics.getUnzonedHistory": [{
37794
38224
  name: "deviceId",
37795
38225
  form: "single",
@@ -40899,6 +41329,124 @@ function composeCameraStatus(input) {
40899
41329
  };
40900
41330
  }
40901
41331
  //#endregion
41332
+ //#region src/camera-status/fleet-read-pass.ts
41333
+ /**
41334
+ * Keep a rejection from becoming an UNHANDLED one.
41335
+ *
41336
+ * A shared promise is created by the first camera to ask and consumed by the
41337
+ * others later — `listBindableCapsForDeviceType` is reached in stage 2 of
41338
+ * `readSwitchAuthorities`, after an await, so two cameras genuinely attach
41339
+ * their handlers in different ticks. Without this the first rejection would be
41340
+ * unhandled for the window in between.
41341
+ *
41342
+ * The handler is a no-op ON PURPOSE: it never becomes the answer. Every real
41343
+ * consumer attaches its own `.catch` and reports the failure per camera.
41344
+ */
41345
+ function keepAlive(p) {
41346
+ p.catch(() => void 0);
41347
+ return p;
41348
+ }
41349
+ var FleetReadPass = class {
41350
+ deps;
41351
+ roster = null;
41352
+ bindings = null;
41353
+ mutes = null;
41354
+ bindableByType = /* @__PURE__ */ new Map();
41355
+ slotsByNode = /* @__PURE__ */ new Map();
41356
+ rtspByNode = /* @__PURE__ */ new Map();
41357
+ provisioningByNode = /* @__PURE__ */ new Map();
41358
+ selectedEngineByNode = /* @__PURE__ */ new Map();
41359
+ constructor(deps) {
41360
+ this.deps = deps;
41361
+ }
41362
+ /** The cameras this pass covers — the set both batch reads are issued for. */
41363
+ get deviceIds() {
41364
+ return this.deps.deviceIds;
41365
+ }
41366
+ /**
41367
+ * The device rows for the pass's set.
41368
+ *
41369
+ * **The shape follows the SET, not the caller.** A pass of one asks
41370
+ * `getDevice` — the exact question, and the one the write path
41371
+ * (`CameraSwitchService.setCameraSwitch`) has always asked. A pass of many
41372
+ * asks `listAll({deviceIds})`, which exists precisely because "these N" was
41373
+ * otherwise either N round trips or the whole fleet. Answering a set of one
41374
+ * with the fleet-shaped method would be no cheaper and would move the write
41375
+ * path onto a read it does not need.
41376
+ *
41377
+ * `projection: 'slim'` because the only fields read off it are `type` and
41378
+ * `disabled`: the full projection reads each device's settings row, which is
41379
+ * the per-device round trip this whole change exists to remove.
41380
+ */
41381
+ deviceRoster(api) {
41382
+ const single = this.deps.deviceIds.length === 1 ? this.deps.deviceIds[0] : void 0;
41383
+ const source = single === void 0 ? "deviceManager.listAll" : "deviceManager.getDevice";
41384
+ if (this.roster === null) this.roster = keepAlive(single === void 0 ? api.deviceManager.listAll.query({
41385
+ deviceIds: [...this.deps.deviceIds],
41386
+ projection: "slim"
41387
+ }) : api.deviceManager.getDevice.query({ deviceId: single }).then((row) => row === null ? [] : [row]));
41388
+ return {
41389
+ source,
41390
+ result: this.roster
41391
+ };
41392
+ }
41393
+ /** The binding rows for the pass's set — `getBindings` for one, the batch for many. */
41394
+ bindingRows(api) {
41395
+ const single = this.deps.deviceIds.length === 1 ? this.deps.deviceIds[0] : void 0;
41396
+ const source = single === void 0 ? "deviceManager.getBindingsBatch" : "deviceManager.getBindings";
41397
+ 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]));
41398
+ return {
41399
+ source,
41400
+ result: this.bindings
41401
+ };
41402
+ }
41403
+ /** The fleet's muted-camera list. One answer for every camera in the pass. */
41404
+ mutedDevices(api) {
41405
+ if (this.mutes === null) this.mutes = keepAlive(api.notificationRules.listDeviceMutes.query({}));
41406
+ return this.mutes;
41407
+ }
41408
+ /** The bindable caps for one device TYPE. Every camera shares one answer. */
41409
+ bindableCapsFor(api, deviceType) {
41410
+ const existing = this.bindableByType.get(deviceType);
41411
+ if (existing !== void 0) return existing;
41412
+ const created = keepAlive(api.deviceManager.listBindableCapsForDeviceType.query({ deviceType }));
41413
+ this.bindableByType.set(deviceType, created);
41414
+ return created;
41415
+ }
41416
+ /** One source node's whole profile-slot table. */
41417
+ profileSlots(api, sourceNodeId) {
41418
+ const existing = this.slotsByNode.get(sourceNodeId);
41419
+ if (existing !== void 0) return existing;
41420
+ const created = keepAlive(api.streamBroker.listAllProfileSlots.query(void 0, nodePin(sourceNodeId)));
41421
+ this.slotsByNode.set(sourceNodeId, created);
41422
+ return created;
41423
+ }
41424
+ /** One source node's whole RTSP-restream table. */
41425
+ rtspEntries(api, sourceNodeId) {
41426
+ const existing = this.rtspByNode.get(sourceNodeId);
41427
+ if (existing !== void 0) return existing;
41428
+ const created = keepAlive(api.streamBroker.getAllRtspEntries.query({}, nodePin(sourceNodeId)));
41429
+ this.rtspByNode.set(sourceNodeId, created);
41430
+ return created;
41431
+ }
41432
+ /** One detection node's runtime-provisioning snapshot. */
41433
+ engineProvisioning(api, detectionNodeId) {
41434
+ const existing = this.provisioningByNode.get(detectionNodeId);
41435
+ if (existing !== void 0) return existing;
41436
+ const created = keepAlive(api.pipelineExecutor.getEngineProvisioning.query({ nodeId: detectionNodeId }));
41437
+ this.provisioningByNode.set(detectionNodeId, created);
41438
+ return created;
41439
+ }
41440
+ /** The executor's bootstrap engine, as the detection stage reads it. */
41441
+ selectedEngine(api, detectionNodeId) {
41442
+ const existing = this.selectedEngineByNode.get(detectionNodeId);
41443
+ if (existing !== void 0) return existing;
41444
+ const created = keepAlive(api.pipelineExecutor.getSelectedEngine.query({ nodeId: detectionNodeId }));
41445
+ this.selectedEngineByNode.set(detectionNodeId, created);
41446
+ return created;
41447
+ }
41448
+ };
41449
+ //#endregion
40902
41450
  //#region src/camera-status-service.ts
40903
41451
  /** WebRTC consumer kinds counted toward `BrokerResult.webrtcSessions`. */
40904
41452
  var WEBRTC_KINDS = new Set([
@@ -41087,7 +41635,7 @@ var CameraStatusService = class {
41087
41635
  * broker's actual decode-session node into `liveDecoder` (T6) — the first
41088
41636
  * slot that reports one wins.
41089
41637
  */
41090
- buildBrokerStage(api, allSlotsFetch, deviceId, sourceNodeId, liveDecoder, sink) {
41638
+ buildBrokerStage(api, allSlotsFetch, deviceId, sourceNodeId, pass, liveDecoder, sink) {
41091
41639
  if (!api || !allSlotsFetch) return Promise.resolve(null);
41092
41640
  return this.boundedStage(allSlotsFetch.then(async (slots) => {
41093
41641
  const deviceSlots = slots.filter((s) => s.deviceId === deviceId);
@@ -41097,12 +41645,13 @@ var CameraStatusService = class {
41097
41645
  rtspRestream: false
41098
41646
  };
41099
41647
  const [statsAndClients, rtspEntry] = await Promise.all([Promise.all(deviceSlots.map(async (slot) => {
41648
+ 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
41649
  return {
41101
41650
  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)
41651
+ stats,
41652
+ clients
41104
41653
  };
41105
- })), api.streamBroker.getAllRtspEntries.query({}, nodePin(sourceNodeId)).catch(() => null)]);
41654
+ })), pass.rtspEntries(api, sourceNodeId).catch(() => null)]);
41106
41655
  const profileDetails = statsAndClients.map(({ slot, stats, clients }) => ({
41107
41656
  profile: slot.profile,
41108
41657
  status: slot.status,
@@ -41163,13 +41712,16 @@ var CameraStatusService = class {
41163
41712
  };
41164
41713
  }
41165
41714
  /** Detection stage (pipeline-executor + runner metrics). */
41166
- buildDetectionStage(api, detectionNodeId, deviceId, sink) {
41715
+ buildDetectionStage(api, detectionNodeId, deviceId, pass, sink) {
41167
41716
  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({
41717
+ return this.boundedStage(Promise.all([
41718
+ pass.engineProvisioning(api, detectionNodeId).catch(() => null),
41719
+ pass.selectedEngine(api, detectionNodeId).catch(() => null),
41720
+ api.pipelineRunner.getCameraMetrics.query({
41170
41721
  deviceId,
41171
41722
  nodeId: detectionNodeId
41172
- }).catch(() => null);
41723
+ }).catch(() => null)
41724
+ ]).then(async ([provisioning, engine, metrics]) => {
41173
41725
  const phase = (() => {
41174
41726
  const p = metrics?.phase;
41175
41727
  if (p === "active") return "active";
@@ -41281,9 +41833,9 @@ var CameraStatusService = class {
41281
41833
  * badge on a working camera); what matters is that the emptiness travels with
41282
41834
  * the reason it is empty.
41283
41835
  */
41284
- buildSwitchStage(deviceId, sink) {
41836
+ buildSwitchStage(deviceId, pass, sink) {
41285
41837
  const startedAt = Date.now();
41286
- return this.boundedStage(this.deps.switchAuthoritiesFor(deviceId), STAGE_TIMEOUT_MS, "switches", deviceId, sink).then((reads) => {
41838
+ return this.boundedStage(this.deps.switchAuthoritiesFor(deviceId, pass), STAGE_TIMEOUT_MS, "switches", deviceId, sink).then((reads) => {
41287
41839
  if (reads === null) return [];
41288
41840
  const { switchedOff, unreadable } = composeSwitchedOff(reads);
41289
41841
  if (unreadable.length > 0) this.recordDegraded(sink, deviceId, "switches", "partial", Date.now() - startedAt, { unreadableAuthorities: unreadable });
@@ -41301,18 +41853,28 @@ var CameraStatusService = class {
41301
41853
  * `null` of a camera that legitimately has no such stage.
41302
41854
  */
41303
41855
  async getCameraStatus(deviceId) {
41856
+ return this.composeOne(deviceId, new FleetReadPass({ deviceIds: [deviceId] }));
41857
+ }
41858
+ /**
41859
+ * One camera's status, composed WITHIN a pass.
41860
+ *
41861
+ * Everything the camera alone can answer is fetched here; everything the pass
41862
+ * already knows (a node's slot table, the fleet's mute list, a device type's
41863
+ * bindable caps) is asked of `pass`, which issues it once for the whole call.
41864
+ */
41865
+ async composeOne(deviceId, pass) {
41304
41866
  const api = this.deps.api();
41305
41867
  const degradations = { entries: [] };
41306
41868
  const { detectionNodeId, sourceNodeId, pinned, detectionReason, audioNodeId, audioPinned } = this.buildAssignmentContext(deviceId);
41307
41869
  const liveDecoder = { nodeId: null };
41308
- const allSlotsFetch = api ? api.streamBroker.listAllProfileSlots.query(void 0, nodePin(sourceNodeId)) : null;
41870
+ const allSlotsFetch = api ? pass.profileSlots(api, sourceNodeId) : null;
41309
41871
  const sourceFetch = this.buildSourceStage(api, allSlotsFetch, deviceId, degradations);
41310
- const brokerFetch = this.buildBrokerStage(api, allSlotsFetch, deviceId, sourceNodeId, liveDecoder, degradations);
41872
+ const brokerFetch = this.buildBrokerStage(api, allSlotsFetch, deviceId, sourceNodeId, pass, liveDecoder, degradations);
41311
41873
  const decoderFetch = this.buildDecoderStage(detectionNodeId);
41312
41874
  const motionResult = this.buildMotionStage(deviceId);
41313
- const detectionFetch = this.buildDetectionStage(api, detectionNodeId, deviceId, degradations);
41875
+ const detectionFetch = this.buildDetectionStage(api, detectionNodeId, deviceId, pass, degradations);
41314
41876
  const recordingFetch = this.buildRecordingStage(api, deviceId, degradations);
41315
- const switchesFetch = this.buildSwitchStage(deviceId, degradations);
41877
+ const switchesFetch = this.buildSwitchStage(deviceId, pass, degradations);
41316
41878
  const [sourceResult, brokerResult, decoderResult, detectionResult, recordingResult, switchedOff] = await Promise.all([
41317
41879
  sourceFetch,
41318
41880
  brokerFetch,
@@ -41355,15 +41917,30 @@ var CameraStatusService = class {
41355
41917
  * `deviceIds` defaults to all cameras currently tracked by the
41356
41918
  * orchestrator's assignment map when omitted.
41357
41919
  *
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.
41920
+ * The whole call runs inside ONE {@link FleetReadPass}, and that is what
41921
+ * makes a five-second poll affordable. Composing a camera reaches seven
41922
+ * stages, but six of the questions those stages ask are about a NODE, a
41923
+ * device TYPE or the whole FLEETthe slot table, the restream table, the
41924
+ * mute list, the engine pair, the bindable caps — and asking them per camera
41925
+ * meant N identical round trips for N identical answers. Two more (the device
41926
+ * row, the binding table) are per-camera questions the device manager already
41927
+ * answers for a set. The pass asks each exactly once and shares the promise;
41928
+ * it holds nothing between calls, so the next poll asks again from scratch.
41929
+ *
41930
+ * What is left is genuinely per camera and stays per camera: the broker slot
41931
+ * stats, the runner metrics, the recorder, the broker mute and the camera's
41932
+ * own privacy provider. Collapsing any of those would be a narrower answer,
41933
+ * not a cheaper one.
41934
+ *
41935
+ * `Promise.all` over per-device composition (no concurrency cap). The fan-out
41936
+ * is now ~8 round trips per camera rather than ~16, and the six fleet reads
41937
+ * no longer multiply — which is the part that made the count grow with the
41938
+ * fleet twice over.
41363
41939
  */
41364
41940
  async getCameraStatuses(deviceIds) {
41365
41941
  const ids = deviceIds !== void 0 && deviceIds.length > 0 ? deviceIds : this.deps.listAssignedDeviceIds();
41366
- return Promise.all(ids.map((deviceId) => this.getCameraStatus(deviceId)));
41942
+ const pass = new FleetReadPass({ deviceIds: ids });
41943
+ return Promise.all(ids.map((deviceId) => this.composeOne(deviceId, pass)));
41367
41944
  }
41368
41945
  };
41369
41946
  //#endregion
@@ -41612,10 +42189,15 @@ async function probeCamera(api, deviceId, deps) {
41612
42189
  * @param deviceId The camera.
41613
42190
  * @param deps Logger + source-owner resolver.
41614
42191
  */
41615
- async function readSwitchAuthorities(api, deviceId, deps) {
42192
+ async function readSwitchAuthorities(api, deviceId, deps, sharedPass) {
41616
42193
  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);
42194
+ const pass = sharedPass ?? new FleetReadPass({ deviceIds: [deviceId] });
42195
+ const roster = pass.deviceRoster(api);
42196
+ const devicePromise = bounded(deps, deviceId, roster.source, roster.result.then((rows) => {
42197
+ const row = rows.find((d) => d.id === deviceId);
42198
+ return isDeviceShape(row) ? row : null;
42199
+ }).catch((err) => {
42200
+ warnUnreachable(deps, deviceId, roster.source, err);
41619
42201
  return null;
41620
42202
  }), null);
41621
42203
  const unknownBindings = {
@@ -41623,11 +42205,14 @@ async function readSwitchAuthorities(api, deviceId, deps) {
41623
42205
  providerAddonIdByCap: /* @__PURE__ */ new Map(),
41624
42206
  allCapNames: null
41625
42207
  };
41626
- const bindingsPromise = bounded(deps, deviceId, "deviceManager.getBindings", api.deviceManager.getBindings.query({ deviceId }).then((b) => {
42208
+ const bindingRead = pass.bindingRows(api);
42209
+ const bindingsPromise = bounded(deps, deviceId, bindingRead.source, bindingRead.result.then((rows) => {
42210
+ const row = rows.find((r) => r.deviceId === deviceId);
42211
+ if (row === void 0) return unknownBindings;
41627
42212
  const active = [];
41628
42213
  const all = [];
41629
42214
  const providers = /* @__PURE__ */ new Map();
41630
- for (const e of b.entries) {
42215
+ for (const e of row.entries) {
41631
42216
  all.push(e.capName);
41632
42217
  if (e.kind !== "wrapped") continue;
41633
42218
  active.push(e.capName);
@@ -41639,14 +42224,14 @@ async function readSwitchAuthorities(api, deviceId, deps) {
41639
42224
  allCapNames: all
41640
42225
  };
41641
42226
  }).catch((err) => {
41642
- warnUnreachable(deps, deviceId, "getBindings", err);
42227
+ warnUnreachable(deps, deviceId, bindingRead.source, err);
41643
42228
  return unknownBindings;
41644
42229
  }), unknownBindings);
41645
42230
  const recordingPromise = bounded(deps, deviceId, "recording.getDeviceConfig", api.recording.getDeviceConfig.query({ deviceId }).then((c) => isRecordingConfig(c) ? c : null).catch((err) => {
41646
42231
  warnUnreachable(deps, deviceId, "recording.getDeviceConfig", err);
41647
42232
  return null;
41648
42233
  }), null);
41649
- const mutesPromise = bounded(deps, deviceId, "notificationRules.listDeviceMutes", api.notificationRules.listDeviceMutes.query({}).then((r) => r.mutedDeviceIds).catch((err) => {
42234
+ const mutesPromise = bounded(deps, deviceId, "notificationRules.listDeviceMutes", pass.mutedDevices(api).then((r) => r.mutedDeviceIds).catch((err) => {
41650
42235
  warnUnreachable(deps, deviceId, "notificationRules.listDeviceMutes", err);
41651
42236
  return null;
41652
42237
  }), null);
@@ -41661,7 +42246,7 @@ async function readSwitchAuthorities(api, deviceId, deps) {
41661
42246
  mutesPromise,
41662
42247
  brokerAudioPromise
41663
42248
  ]);
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) => {
42249
+ const [bindable, privacy] = await Promise.all([device === null ? Promise.resolve(null) : bounded(deps, deviceId, "deviceManager.listBindableCapsForDeviceType", pass.bindableCapsFor(api, device.type).catch((err) => {
41665
42250
  warnUnreachable(deps, deviceId, "listBindableCapsForDeviceType", err);
41666
42251
  return null;
41667
42252
  }), null), readPrivacyPlanes(api, deviceId, deps, bindings.allCapNames)]);
@@ -50368,11 +50953,11 @@ async function buildOrchestratorControllers(deps) {
50368
50953
  assignSource: (deviceId) => topology.assignSource(deviceId),
50369
50954
  listAssignedDeviceIds: () => [...new Set([...ledger.listAssignedDeviceIds(), ...detectionWiring.activeDeviceIds()])],
50370
50955
  isSessionCamera: (deviceId) => deps.isSessionCamera(deviceId),
50371
- switchAuthoritiesFor: async (deviceId) => (await readSwitchAuthorities(deps.ctx().api ?? null, deviceId, {
50956
+ switchAuthoritiesFor: async (deviceId, pass) => (await readSwitchAuthorities(deps.ctx().api ?? null, deviceId, {
50372
50957
  logger: deps.ctx().logger,
50373
50958
  assignSource: (id) => topology.assignSource(id),
50374
50959
  warnSampler: switchWarnSampler
50375
- })).derivation
50960
+ }, pass)).derivation
50376
50961
  });
50377
50962
  const reconcile = new ReconcileController({
50378
50963
  api: () => deps.ctx().api ?? null,