@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.js CHANGED
@@ -8894,6 +8894,20 @@ var RelocateJobSchema = object({
8894
8894
  * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8895
8895
  */
8896
8896
  rowsReconciled: number().int().nonnegative().optional(),
8897
+ /**
8898
+ * Rows this run FORGOT because the file they name is not on disk.
8899
+ *
8900
+ * The mover derived the path from the row's own fields and `stat`ed it; an
8901
+ * ENOENT there is a per-path confirmation that the segment is gone (D296),
8902
+ * and the durable row is dropped through the same channel eviction uses. It
8903
+ * is reported for the same reason `rowsReconciled` is: this is a durable
8904
+ * mutation nobody asked for, and a migration that quietly erases hour rows is
8905
+ * the same failure as one that quietly skips them (D295).
8906
+ *
8907
+ * The production drain of 2026-08-30 would have reported 11 074 here — the
8908
+ * ledger claimed 5.65 GB of footage that no longer existed.
8909
+ */
8910
+ rowsForgotten: number().int().nonnegative().optional(),
8897
8911
  startedAt: number(),
8898
8912
  finishedAt: number().nullable(),
8899
8913
  error: string().nullable()
@@ -8958,6 +8972,13 @@ var MediaRelocateModeSchema = _enum([
8958
8972
  ]);
8959
8973
  var RelocateMediaInputSchema = object({
8960
8974
  toLocationId: string(),
8975
+ /**
8976
+ * Restrict the pass to rows currently on this location. Omitted / `'*'` =
8977
+ * every row that is not already on `toLocationId` (the historical
8978
+ * behaviour). A named source is what a from→to migration needs: without it
8979
+ * "move events off disk 2" also emptied disk 1.
8980
+ */
8981
+ fromLocationId: string().optional(),
8961
8982
  throttleMbps: number().min(1).max(1e3).optional(),
8962
8983
  /** Omitted = `move`, the pre-existing behaviour. */
8963
8984
  mode: MediaRelocateModeSchema.optional()
@@ -9025,6 +9046,19 @@ var StorageMigrationDestinationsSchema = object({
9025
9046
  galleryMedia: string().min(1).optional()
9026
9047
  }).refine((value) => Object.keys(value).length > 0, { message: "select at least one storage class" });
9027
9048
  /**
9049
+ * Optional named source per class. Omitted = the class's current default
9050
+ * (the historical behaviour). A named source that is NOT the default is a
9051
+ * drain of that disk: bytes move, the default stays, and the source is
9052
+ * disabled when the move finishes.
9053
+ */
9054
+ var StorageMigrationSourcesSchema = object({
9055
+ recordings: string().min(1).optional(),
9056
+ recordingsLow: string().min(1).optional(),
9057
+ eventMedia: string().min(1).optional(),
9058
+ backups: string().min(1).optional(),
9059
+ galleryMedia: string().min(1).optional()
9060
+ }).optional();
9061
+ /**
9028
9062
  * How a migration sequences the cutover against the byte move.
9029
9063
  *
9030
9064
  * - `blocking` — the historical order: pause, move every byte, repoint,
@@ -9046,6 +9080,8 @@ var StorageMigrationModeSchema = _enum(["blocking", "nonBlocking"]);
9046
9080
  /** Shared input for planning and starting an orchestrated storage migration. */
9047
9081
  var StorageMigrationInputSchema = object({
9048
9082
  destinations: StorageMigrationDestinationsSchema,
9083
+ /** Omitted = each class's current default. */
9084
+ sources: StorageMigrationSourcesSchema,
9049
9085
  throttleMbps: number().min(1).max(1e3).optional(),
9050
9086
  /** Omitted = `blocking`, which stays the default. */
9051
9087
  mode: StorageMigrationModeSchema.optional()
@@ -9125,6 +9161,13 @@ var StorageMigrationMoveSchema = object({
9125
9161
  storageClass: StorageMigrationClassSchema,
9126
9162
  fromLocationId: string(),
9127
9163
  toLocationId: string(),
9164
+ /**
9165
+ * True when `from` was NOT the class default at plan time. The move still
9166
+ * copies bytes, but the default is left alone and the source is disabled
9167
+ * once the copy verifies. Absent on jobs planned before this field existed
9168
+ * — those jobs always repointed, which is `false`.
9169
+ */
9170
+ freezeSource: boolean().optional(),
9128
9171
  moverJobId: string().nullable(),
9129
9172
  state: RelocateJobStateSchema.nullable(),
9130
9173
  error: string().nullable(),
@@ -9138,6 +9181,7 @@ var StorageMigrationJobSchema = object({
9138
9181
  * can tell a seconds-long cutover from a thirty-hour one. */
9139
9182
  mode: StorageMigrationModeSchema,
9140
9183
  destinations: StorageMigrationDestinationsSchema,
9184
+ sources: StorageMigrationSourcesSchema,
9141
9185
  throttleMbps: number(),
9142
9186
  moves: array(StorageMigrationMoveSchema),
9143
9187
  pauseLeaseId: string().nullable(),
@@ -9163,6 +9207,7 @@ var StorageMigrationFindingSchema = object({
9163
9207
  });
9164
9208
  var StorageMigrationPlanSchema = object({
9165
9209
  destinations: StorageMigrationDestinationsSchema,
9210
+ sources: StorageMigrationSourcesSchema,
9166
9211
  /** The mode this plan was built for. A plan is only valid for its mode: the
9167
9212
  * `eventMedia` seal gate and the single-cardinality refusal both depend on
9168
9213
  * it. */
@@ -9170,7 +9215,8 @@ var StorageMigrationPlanSchema = object({
9170
9215
  moves: array(object({
9171
9216
  storageClass: StorageMigrationClassSchema,
9172
9217
  fromLocationId: string(),
9173
- toLocationId: string()
9218
+ toLocationId: string(),
9219
+ freezeSource: boolean().optional()
9174
9220
  })),
9175
9221
  findings: array(StorageMigrationFindingSchema)
9176
9222
  });
@@ -9257,16 +9303,142 @@ var RelocateResidueSchema = object({
9257
9303
  segments: number().int().nonnegative(),
9258
9304
  bytes: number().int().nonnegative()
9259
9305
  }).nullable();
9306
+ /**
9307
+ * Ask one location whether its durable hour rows describe the disk — the walk
9308
+ * (D319).
9309
+ *
9310
+ * `apply` DEFAULTS TO FALSE and that default is the product: the operator's
9311
+ * missing tool is the question, and the dry run is how they sanity-check the
9312
+ * destructive run before authorising it.
9313
+ */
9314
+ var LedgerWalkInputSchema = object({
9315
+ locationId: string().min(1),
9316
+ /** Forget the confirmed-absent rows, rather than only counting them. */
9317
+ apply: boolean().optional(),
9318
+ /** Narrow to one camera. */
9319
+ deviceId: number().int().positive().optional(),
9320
+ /** Narrow to these recording profiles; empty/absent = every profile. */
9321
+ profiles: array(string().min(1)).optional()
9322
+ });
9323
+ /** Why a whole walk did nothing. Every one leaves the ledger untouched. */
9324
+ var LedgerWalkRefusalSchema = _enum([
9325
+ "location-unknown",
9326
+ "source-writable",
9327
+ "no-ledger",
9328
+ "archive-unreadable",
9329
+ "anchor-absent",
9330
+ "anchor-unreadable",
9331
+ "anchor-moved"
9332
+ ]);
9333
+ _enum([
9334
+ "live-tail",
9335
+ "listing-error",
9336
+ "path-mismatch",
9337
+ "durable-refused"
9338
+ ]);
9339
+ /** Every skip reason, always present, always a number — so a reason that never
9340
+ * fired reports as zero rather than absent and the report shape is constant
9341
+ * between passes. Spelled out rather than `z.record` for exactly that. */
9342
+ var LedgerWalkSkipCountsSchema = object({
9343
+ "live-tail": number().int().nonnegative(),
9344
+ "listing-error": number().int().nonnegative(),
9345
+ "path-mismatch": number().int().nonnegative(),
9346
+ "durable-refused": number().int().nonnegative()
9347
+ });
9348
+ /** One camera's share of a walk, so a report names cameras and not rows. */
9349
+ var LedgerWalkDeviceReportSchema = object({
9350
+ deviceId: number().int(),
9351
+ hoursWalked: number().int().nonnegative(),
9352
+ hoursMissing: number().int().nonnegative(),
9353
+ ghostSegments: number().int().nonnegative(),
9354
+ ghostBytes: number().int().nonnegative(),
9355
+ forgottenSegments: number().int().nonnegative(),
9356
+ orphanFiles: number().int().nonnegative()
9357
+ });
9358
+ /**
9359
+ * What one walk claimed, listed, found and (only when armed) forgot.
9360
+ *
9361
+ * `archiveSegments` is the **M** and `segmentsClaimed` the **N** (D295), so a
9362
+ * walk that saw a fraction of the location is visible in its own report rather
9363
+ * than in the absence of one.
9364
+ */
9365
+ var LedgerWalkReportSchema = object({
9366
+ locationId: string(),
9367
+ applied: boolean(),
9368
+ refused: LedgerWalkRefusalSchema.nullable(),
9369
+ archiveSegments: number().int().nonnegative().nullable(),
9370
+ archiveBytes: number().int().nonnegative().nullable(),
9371
+ hoursClaimed: number().int().nonnegative(),
9372
+ hoursWalked: number().int().nonnegative(),
9373
+ hoursMissing: number().int().nonnegative(),
9374
+ /** `readdir` calls issued — the cost, stated in the unit that is paid. */
9375
+ listings: number().int().nonnegative(),
9376
+ segmentsClaimed: number().int().nonnegative(),
9377
+ ghostSegments: number().int().nonnegative(),
9378
+ ghostBytes: number().int().nonnegative(),
9379
+ ghostHoursWhole: number().int().nonnegative(),
9380
+ forgottenSegments: number().int().nonnegative(),
9381
+ forgottenBytes: number().int().nonnegative(),
9382
+ /** Files under a claimed hour that no durable row names. Never deleted. */
9383
+ orphanFiles: number().int().nonnegative(),
9384
+ orphanSample: array(string()).readonly(),
9385
+ hoursSkipped: number().int().nonnegative(),
9386
+ skippedByReason: LedgerWalkSkipCountsSchema,
9387
+ /** The walk stopped at its per-pass hour bound with claims unwalked. */
9388
+ bounded: boolean(),
9389
+ byDevice: array(LedgerWalkDeviceReportSchema).readonly()
9390
+ });
9260
9391
  /** How many rows a media pass would still act on against a given target — the
9261
9392
  * media lane's denominator AND its residue, from ONE derivation so the two can
9262
9393
  * never disagree. `null` = the count could not be taken. */
9263
9394
  var RelocatableMediaCountSchema = object({ rows: number().int().nonnegative() }).nullable();
9264
9395
  var RelocatableMediaCountInputSchema = object({
9265
9396
  toLocationId: string().min(1),
9397
+ /** Restrict the count to one source; omitted / `'*'` = every non-target row. */
9398
+ fromLocationId: string().optional(),
9266
9399
  /** Omitted = `move`. */
9267
9400
  mode: MediaRelocateModeSchema.optional()
9268
9401
  });
9269
9402
  /**
9403
+ * Operator cleanup of leftover analytics rows, optional debug media, and
9404
+ * ghost ledger entries on frozen footage locations.
9405
+ *
9406
+ * A pass is tens of minutes on a standing backlog. The hub method RETURNS
9407
+ * `{ jobId }` immediately; progress is `cleanupStatus`. Awaiting the work
9408
+ * is how `addons.custom` hit the 60 s UDS deadline while reclaim continued
9409
+ * with no operator-visible status.
9410
+ */
9411
+ var StorageCleanupPhaseSchema = _enum([
9412
+ "orphans",
9413
+ "debug-media",
9414
+ "ghost-ledger",
9415
+ "done",
9416
+ "failed",
9417
+ "cancelled"
9418
+ ]);
9419
+ var StorageCleanupInputSchema = object({
9420
+ /** Also walk motion stills / track filmstrips. Off by default. */
9421
+ includeDebugMedia: boolean().optional() });
9422
+ var StorageCleanupJobSchema = object({
9423
+ jobId: string(),
9424
+ phase: StorageCleanupPhaseSchema,
9425
+ includeDebugMedia: boolean(),
9426
+ orphansReclaimed: number().int().nonnegative(),
9427
+ orphanBytesReclaimed: number().int().nonnegative(),
9428
+ debugMediaReclaimed: number().int().nonnegative(),
9429
+ debugMediaBytesReclaimed: number().int().nonnegative(),
9430
+ ghostsForgotten: number().int().nonnegative(),
9431
+ ghostBytesForgotten: number().int().nonnegative(),
9432
+ /** Short operator-facing line: current collection, pass, or location. */
9433
+ detail: string().nullable(),
9434
+ cancelRequested: boolean(),
9435
+ startedAt: number(),
9436
+ updatedAt: number(),
9437
+ finishedAt: number().nullable(),
9438
+ error: string().nullable()
9439
+ });
9440
+ var StorageCleanupStatusInputSchema = object({ jobId: string().optional() });
9441
+ /**
9270
9442
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
9271
9443
  * storage a location serves. Defined here (not in `capabilities/storage.cap.ts`)
9272
9444
  * so the persisted record schema and the consumer-facing cap can both consume it
@@ -9294,11 +9466,10 @@ var StorageLocationTypeSchema = string().regex(/^[a-z][a-zA-Z0-9-]*$/);
9294
9466
  * The default location for a type uses `id === <type>:default` by
9295
9467
  * convention (the bare type ref like `'backups'` resolves to it).
9296
9468
  *
9297
- * `isSystem: true` marks a location as orchestrator-seeded and
9298
- * undeletable. The bootstrap-installed defaults (one per type) carry
9299
- * this flag; operator-added locations don't. Editing the config of
9300
- * a system location is allowed (path migration, provider swap) but
9301
- * deleting it is rejected at the cap level.
9469
+ * `isSystem` is a legacy persisted flag. Seed still creates the initial
9470
+ * `<type>:default` locations; the flag is no longer a lock, a badge, or a
9471
+ * prune selector. New writes leave it false. Deletion is gated on uniqueness
9472
+ * / last-enabled, not on this bit.
9302
9473
  */
9303
9474
  var StorageLocationSchema = object({
9304
9475
  id: string().regex(/^[a-z][a-zA-Z0-9-]*:[a-zA-Z0-9-]+$/),
@@ -19833,13 +20004,15 @@ var ListGroupsPageSchema = object({
19833
20004
  groups: array(AnalyticsGroupRecordSchema).readonly(),
19834
20005
  nextCursor: string().nullable()
19835
20006
  });
20007
+ var KEY_EVENTS_DEFAULT_LIMIT = 50;
20008
+ var KEY_EVENTS_MAX_LIMIT = 200;
19836
20009
  var KeyEventQueryInput = object({
19837
20010
  deviceId: number(),
19838
20011
  /** Window lower bound (track firstSeen ≥ since). */
19839
20012
  since: number(),
19840
20013
  /** Window upper bound (track firstSeen ≤ until). */
19841
20014
  until: number(),
19842
- limit: number().int().min(1).max(200).default(50),
20015
+ limit: number().int().min(1).max(KEY_EVENTS_MAX_LIMIT).default(KEY_EVENTS_DEFAULT_LIMIT),
19843
20016
  /** Drop tracks scoring below this importance. */
19844
20017
  minImportance: number().min(0).max(1).optional(),
19845
20018
  /** Restrict to a single class (e.g. 'person'). */
@@ -19861,6 +20034,32 @@ var KeyEventSchema = object({
19861
20034
  ...TrackFlagFields,
19862
20035
  ...TrackRetrainFields
19863
20036
  });
20037
+ /** `getKeyEvents`' window and filters, asked of a SET of cameras at once. */
20038
+ var KeyEventBatchQueryInput = object({
20039
+ deviceIds: array(number()).min(1).max(200),
20040
+ since: number(),
20041
+ until: number(),
20042
+ /** Applied PER CAMERA, exactly as `getKeyEvents.limit` is — never a total
20043
+ * across the set, which would let a busy camera starve a quiet one of its
20044
+ * rows and change what the merged feed contains. */
20045
+ limit: number().int().min(1).max(KEY_EVENTS_MAX_LIMIT).default(KEY_EVENTS_DEFAULT_LIMIT),
20046
+ minImportance: number().min(0).max(1).optional(),
20047
+ classFilter: string().optional()
20048
+ });
20049
+ /**
20050
+ * One camera's key events in a batch answer.
20051
+ *
20052
+ * The row exists for every requested id. `getKeyEvents` degrades to `[]` on
20053
+ * error rather than throwing, so a camera whose store read failed and one with
20054
+ * no events in the window were ALREADY indistinguishable per camera — the
20055
+ * batch does not make that worse, and the row keeps the deviceId the single
20056
+ * method's output never carried (the caller used to stamp it from the fan-out
20057
+ * key, which only worked because there was one query per camera).
20058
+ */
20059
+ var KeyEventsForDeviceSchema = object({
20060
+ deviceId: number(),
20061
+ events: array(KeyEventSchema).readonly()
20062
+ });
19864
20063
  object({
19865
20064
  trackId: string(),
19866
20065
  className: string(),
@@ -20080,6 +20279,47 @@ var RebuildStatusSchema = object({
20080
20279
  /** Present when the pass ended by throwing. */
20081
20280
  error: string().nullable()
20082
20281
  });
20282
+ /**
20283
+ * Acknowledgement that a debug-media reclaim STARTED.
20284
+ *
20285
+ * The pass is paced at ~2 MB/s over a standing 100 GB — tens of minutes — so
20286
+ * it runs detached and this returns immediately. Awaiting it is how the
20287
+ * `addons.custom` door hit the 60 s UDS deadline while the walk carried on
20288
+ * with no status. Poll {@link pipelineAnalyticsCapability.methods.getMediaReclaimStatus}.
20289
+ */
20290
+ var MediaReclaimStartResultSchema = object({
20291
+ started: boolean(),
20292
+ /** True when a pass was already running; the new request is ignored. */
20293
+ alreadyRunning: boolean()
20294
+ });
20295
+ var MediaReclaimInputSchema = object({
20296
+ mode: _enum(["report", "reclaim"]).default("report"),
20297
+ scopes: array(_enum(["motion-still", "track-filmstrip"])).min(1).optional(),
20298
+ deviceIds: array(number().int()).min(1).optional(),
20299
+ restart: boolean().optional(),
20300
+ pageSize: number().int().min(50).max(5e3).optional(),
20301
+ maxRowsPerDevice: number().int().min(1).max(5e5).optional(),
20302
+ maxReclaimPerDevice: number().int().min(1).max(5e5).optional(),
20303
+ maxBytesPerRun: number().int().min(1).optional(),
20304
+ budgetMinutes: number().int().min(1).max(720).optional(),
20305
+ throttleBytesPerSec: number().int().min(64 * 1024).optional(),
20306
+ graceMinutes: number().int().min(1).max(10080).optional()
20307
+ });
20308
+ var MediaReclaimStatusSchema = object({
20309
+ running: boolean(),
20310
+ mode: _enum(["report", "reclaim"]).nullable(),
20311
+ totalExamined: number(),
20312
+ totalEligible: number(),
20313
+ totalReclaimed: number(),
20314
+ totalBytesReclaimed: number(),
20315
+ totalRefused: number(),
20316
+ /** Device+scope windows finished in this pass. */
20317
+ devicesDone: number(),
20318
+ complete: boolean().nullable(),
20319
+ startedAtMs: number().nullable(),
20320
+ finishedAtMs: number().nullable(),
20321
+ error: string().nullable()
20322
+ });
20083
20323
  var ReplayFrameInputSchema = object({
20084
20324
  timestamp: number(),
20085
20325
  frame: PipelineRunResultBridge
@@ -20130,7 +20370,7 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
20130
20370
  until: number().optional(),
20131
20371
  kinds: array(string()).optional(),
20132
20372
  limit: number().int().min(1).max(MAX_EVENT_QUERY_LIMIT).default(DEFAULT_EVENT_QUERY_LIMIT)
20133
- }), array(SensorEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(object({
20373
+ }), array(SensorEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(KeyEventBatchQueryInput, array(KeyEventsForDeviceSchema).readonly()), method(object({
20134
20374
  deviceId: number(),
20135
20375
  since: number(),
20136
20376
  until: number(),
@@ -20221,6 +20461,12 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
20221
20461
  }), method(OpsLogQueryInputSchema, array(OpsLogEntrySchema).readonly(), {
20222
20462
  kind: "query",
20223
20463
  auth: "admin"
20464
+ }), method(MediaReclaimInputSchema, MediaReclaimStartResultSchema, {
20465
+ kind: "mutation",
20466
+ auth: "admin"
20467
+ }), method(object({}), MediaReclaimStatusSchema, {
20468
+ kind: "query",
20469
+ auth: "admin"
20224
20470
  }), method(object({ deviceIds: array(number()).optional() }), TrainingExportSummarySchema, {
20225
20471
  kind: "query",
20226
20472
  auth: "admin"
@@ -22672,7 +22918,13 @@ method(StorageMigrationInputSchema, StorageMigrationPlanSchema, { auth: "admin"
22672
22918
  }), method(object({}), array(StorageMigrationMoverSchema).readonly(), { auth: "admin" }), method(object({}), array(StorageMigrationResidueSchema).readonly(), { auth: "admin" }), method(StorageMigrationDrainInputSchema, object({ jobId: string() }), {
22673
22919
  kind: "mutation",
22674
22920
  auth: "admin"
22675
- });
22921
+ }), method(StorageCleanupInputSchema, object({ jobId: string() }), {
22922
+ kind: "mutation",
22923
+ auth: "admin"
22924
+ }), method(StorageCleanupStatusInputSchema, StorageCleanupJobSchema.nullable(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
22925
+ kind: "mutation",
22926
+ auth: "admin"
22927
+ }), method(object({}), array(StorageMigrationJobSchema).readonly(), { auth: "admin" });
22676
22928
  var ProviderInfoSchema = discriminatedUnion("shouldSaveDiskSpace", [object({
22677
22929
  providerId: string().min(1),
22678
22930
  displayName: string().min(1),
@@ -28050,6 +28302,33 @@ var RecordingRebalanceInputSchema = object({
28050
28302
  minMoveGb: number().min(0).optional()
28051
28303
  });
28052
28304
  /**
28305
+ * Operator-facing placement of one camera onto a recordings location.
28306
+ *
28307
+ * `pinLocationId` is the operator instruction: a location id, or `null` for
28308
+ * Auto (the planner may move this camera). `locationId` is where high/mid
28309
+ * currently write — the plan, which may disagree with the pin when Auto.
28310
+ */
28311
+ var RecordingDevicePlacementSchema = object({
28312
+ deviceId: number().int(),
28313
+ profile: string(),
28314
+ locationId: string()
28315
+ });
28316
+ var RecordingDevicePinSchema = object({
28317
+ deviceId: number().int(),
28318
+ /** Recordings-class location this camera is pinned to. */
28319
+ locationId: string()
28320
+ });
28321
+ var RecordingPlacementViewSchema = object({
28322
+ assignments: array(RecordingDevicePlacementSchema),
28323
+ pins: array(RecordingDevicePinSchema),
28324
+ defaultLocations: record(string(), string())
28325
+ });
28326
+ var RecordingSetDevicePlacementInputSchema = object({
28327
+ deviceId: number().int(),
28328
+ /** `null` = Auto. Otherwise a `recordings:*` location id. */
28329
+ locationId: string().nullable()
28330
+ });
28331
+ /**
28053
28332
  * Result of locating footage at a wall-clock instant for one device/profile.
28054
28333
  * `segment` carries the covering segment's window; `gap` reports the forward
28055
28334
  * nearest covered edge (`null` past the end of footage / when none exists)
@@ -28263,6 +28542,9 @@ method(object({
28263
28542
  }), method(RelocateResidueInputSchema, RelocateResidueSchema, {
28264
28543
  kind: "query",
28265
28544
  auth: "admin"
28545
+ }), method(LedgerWalkInputSchema, LedgerWalkReportSchema, {
28546
+ kind: "mutation",
28547
+ auth: "admin"
28266
28548
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
28267
28549
  kind: "mutation",
28268
28550
  auth: "admin"
@@ -28272,6 +28554,12 @@ method(object({
28272
28554
  }), method(RecordingRebalanceInputSchema, RecordingRebalancePlanSchema, {
28273
28555
  kind: "mutation",
28274
28556
  auth: "admin"
28557
+ }), method(object({}), RecordingPlacementViewSchema, {
28558
+ kind: "query",
28559
+ auth: "admin"
28560
+ }), method(RecordingSetDevicePlacementInputSchema, object({ ok: literal(true) }), {
28561
+ kind: "mutation",
28562
+ auth: "admin"
28275
28563
  });
28276
28564
  /**
28277
28565
  * `recording-export` cap — render a footage time range into a single downloadable
@@ -28654,7 +28942,26 @@ var SceneMonitorStatusSchema = object({
28654
28942
  monitors: array(SceneMonitorSchema),
28655
28943
  lastFetchedAt: number()
28656
28944
  });
28657
- DeviceType.Camera, method(object({ deviceId: number() }), SceneMonitorStatusSchema), method(object({
28945
+ /**
28946
+ * One camera's row in a `listScenesBatch` answer.
28947
+ *
28948
+ * `status` is NULLABLE and the nullability is the whole point. `scene-monitor`
28949
+ * is a `defaultActive` wrapper, so every camera is asked; a camera whose
28950
+ * provider is absent (pipeline-analytics not deployed, the post-processing node
28951
+ * down) cannot answer, and that is not the same fact as a camera with no scenes
28952
+ * configured. Fanned out per camera the difference was visible — one query
28953
+ * errored while the others resolved — and a batch that returned only the rows
28954
+ * it managed would have destroyed it, silently, by making an unreachable camera
28955
+ * indistinguishable from one that answered `monitors: []`.
28956
+ *
28957
+ * So: EVERY requested deviceId gets a row. `status: null` means "this camera
28958
+ * could not be read"; `status.monitors: []` means "read, and it has none".
28959
+ */
28960
+ var SceneMonitorStatusForDeviceSchema = object({
28961
+ deviceId: number(),
28962
+ status: SceneMonitorStatusSchema.nullable()
28963
+ });
28964
+ DeviceType.Camera, method(object({ deviceId: number() }), SceneMonitorStatusSchema), method(object({ deviceIds: array(number()).min(1).max(200) }), array(SceneMonitorStatusForDeviceSchema).readonly()), method(object({
28658
28965
  deviceId: number(),
28659
28966
  label: string(),
28660
28967
  roi: MaskRectShapeSchema,
@@ -29978,6 +30285,27 @@ var CameraOccupancySnapshotSchema = object({
29978
30285
  stationaryObjects: array(StationaryObjectSchema).readonly().optional()
29979
30286
  });
29980
30287
  /**
30288
+ * One camera's row in a `getCurrentSnapshotBatch` answer.
30289
+ *
30290
+ * THREE outcomes, and the single-camera method could only express two of them
30291
+ * because `snapshot: null` was already spoken for:
30292
+ *
30293
+ * - `read: 'read'`, `snapshot` present — the live occupancy reading.
30294
+ * - `read: 'read'`, `snapshot: null` — read, and this camera has no reading
30295
+ * yet: no frame since boot, and no parked-object registry to hydrate from.
30296
+ * - `read: 'unreadable'` — the owner could not answer for this
30297
+ * camera. `snapshot` is null, and it does NOT mean "nothing parked here".
30298
+ *
30299
+ * Collapsing the last two is the failure this field exists to prevent: a
30300
+ * hydration that threw would otherwise render as an empty Stationary section,
30301
+ * which is a definite claim about a camera nobody could read.
30302
+ */
30303
+ var CameraOccupancySnapshotForDeviceSchema = object({
30304
+ deviceId: number(),
30305
+ read: _enum(["read", "unreadable"]),
30306
+ snapshot: CameraOccupancySnapshotSchema.nullable()
30307
+ });
30308
+ /**
29981
30309
  * Time-series resolution. The history methods return one bucket per
29982
30310
  * step over the requested range. Smaller resolutions cost more
29983
30311
  * memory + bandwidth; bound to discrete steps so caller cannot ask
@@ -30001,7 +30329,7 @@ var HistoryPointSchema = object({
30001
30329
  /** Object count averaged over the bucket (rounded to nearest integer). */
30002
30330
  count: number().int().nonnegative()
30003
30331
  });
30004
- DeviceType.Camera, method(object({ deviceId: number() }), CameraOccupancySnapshotSchema.nullable()), method(object({
30332
+ DeviceType.Camera, method(object({ deviceId: number() }), CameraOccupancySnapshotSchema.nullable()), method(object({ deviceIds: array(number()).min(1).max(200) }), array(CameraOccupancySnapshotForDeviceSchema).readonly()), method(object({
30005
30333
  deviceId: number(),
30006
30334
  zoneId: string(),
30007
30335
  className: string().optional()
@@ -33390,6 +33718,18 @@ Object.freeze({
33390
33718
  addonId: null,
33391
33719
  access: "view"
33392
33720
  },
33721
+ "pipelineAnalytics.getKeyEventsBatch": {
33722
+ capName: "pipeline-analytics",
33723
+ capScope: "device",
33724
+ addonId: null,
33725
+ access: "view"
33726
+ },
33727
+ "pipelineAnalytics.getMediaReclaimStatus": {
33728
+ capName: "pipeline-analytics",
33729
+ capScope: "device",
33730
+ addonId: null,
33731
+ access: "view"
33732
+ },
33393
33733
  "pipelineAnalytics.getMotionEvents": {
33394
33734
  capName: "pipeline-analytics",
33395
33735
  capScope: "device",
@@ -33564,6 +33904,12 @@ Object.freeze({
33564
33904
  addonId: null,
33565
33905
  access: "create"
33566
33906
  },
33907
+ "pipelineAnalytics.reclaimDebugMedia": {
33908
+ capName: "pipeline-analytics",
33909
+ capScope: "device",
33910
+ addonId: null,
33911
+ access: "create"
33912
+ },
33567
33913
  "pipelineAnalytics.reconcileFromDisk": {
33568
33914
  capName: "pipeline-analytics",
33569
33915
  capScope: "device",
@@ -34488,6 +34834,12 @@ Object.freeze({
34488
34834
  addonId: null,
34489
34835
  access: "view"
34490
34836
  },
34837
+ "recording.getPlacement": {
34838
+ capName: "recording",
34839
+ capScope: "system",
34840
+ addonId: null,
34841
+ access: "view"
34842
+ },
34491
34843
  "recording.getPlaybackManifest": {
34492
34844
  capName: "recording",
34493
34845
  capScope: "system",
@@ -34566,6 +34918,12 @@ Object.freeze({
34566
34918
  addonId: null,
34567
34919
  access: "view"
34568
34920
  },
34921
+ "recording.reconcileLedgerAgainstDisk": {
34922
+ capName: "recording",
34923
+ capScope: "system",
34924
+ addonId: null,
34925
+ access: "create"
34926
+ },
34569
34927
  "recording.refreshStorageLocationsForMigration": {
34570
34928
  capName: "recording",
34571
34929
  capScope: "system",
@@ -34608,6 +34966,12 @@ Object.freeze({
34608
34966
  addonId: null,
34609
34967
  access: "create"
34610
34968
  },
34969
+ "recording.setDevicePlacement": {
34970
+ capName: "recording",
34971
+ capScope: "system",
34972
+ addonId: null,
34973
+ access: "create"
34974
+ },
34611
34975
  "recording.startStorageMigrationMove": {
34612
34976
  capName: "recording",
34613
34977
  capScope: "system",
@@ -34692,6 +35056,12 @@ Object.freeze({
34692
35056
  addonId: null,
34693
35057
  access: "view"
34694
35058
  },
35059
+ "sceneMonitor.listScenesBatch": {
35060
+ capName: "scene-monitor",
35061
+ capScope: "device",
35062
+ addonId: null,
35063
+ access: "view"
35064
+ },
34695
35065
  "sceneMonitor.recheckNow": {
34696
35066
  capName: "scene-monitor",
34697
35067
  capScope: "device",
@@ -35046,12 +35416,36 @@ Object.freeze({
35046
35416
  addonId: null,
35047
35417
  access: "create"
35048
35418
  },
35419
+ "storageMigration.cleanupCancel": {
35420
+ capName: "storage-migration",
35421
+ capScope: "system",
35422
+ addonId: null,
35423
+ access: "create"
35424
+ },
35425
+ "storageMigration.cleanupStart": {
35426
+ capName: "storage-migration",
35427
+ capScope: "system",
35428
+ addonId: null,
35429
+ access: "create"
35430
+ },
35431
+ "storageMigration.cleanupStatus": {
35432
+ capName: "storage-migration",
35433
+ capScope: "system",
35434
+ addonId: null,
35435
+ access: "view"
35436
+ },
35049
35437
  "storageMigration.drain": {
35050
35438
  capName: "storage-migration",
35051
35439
  capScope: "system",
35052
35440
  addonId: null,
35053
35441
  access: "create"
35054
35442
  },
35443
+ "storageMigration.history": {
35444
+ capName: "storage-migration",
35445
+ capScope: "system",
35446
+ addonId: null,
35447
+ access: "view"
35448
+ },
35055
35449
  "storageMigration.movers": {
35056
35450
  capName: "storage-migration",
35057
35451
  capScope: "system",
@@ -36048,6 +36442,12 @@ Object.freeze({
36048
36442
  addonId: null,
36049
36443
  access: "view"
36050
36444
  },
36445
+ "zoneAnalytics.getCurrentSnapshotBatch": {
36446
+ capName: "zone-analytics",
36447
+ capScope: "device",
36448
+ addonId: null,
36449
+ access: "view"
36450
+ },
36051
36451
  "zoneAnalytics.getUnzonedHistory": {
36052
36452
  capName: "zone-analytics",
36053
36453
  capScope: "device",
@@ -37052,6 +37452,11 @@ Object.freeze({
37052
37452
  form: "single",
37053
37453
  optional: false
37054
37454
  }],
37455
+ "pipelineAnalytics.getKeyEventsBatch": [{
37456
+ name: "deviceIds",
37457
+ form: "array",
37458
+ optional: false
37459
+ }],
37055
37460
  "pipelineAnalytics.getMotionEvents": [{
37056
37461
  name: "deviceId",
37057
37462
  form: "single",
@@ -37157,6 +37562,11 @@ Object.freeze({
37157
37562
  form: "single",
37158
37563
  optional: true
37159
37564
  }],
37565
+ "pipelineAnalytics.reclaimDebugMedia": [{
37566
+ name: "deviceIds",
37567
+ form: "array",
37568
+ optional: true
37569
+ }],
37160
37570
  "pipelineAnalytics.reconcileFromDisk": [{
37161
37571
  name: "deviceId",
37162
37572
  form: "single",
@@ -37492,6 +37902,11 @@ Object.freeze({
37492
37902
  form: "single",
37493
37903
  optional: false
37494
37904
  }],
37905
+ "recording.reconcileLedgerAgainstDisk": [{
37906
+ name: "deviceId",
37907
+ form: "single",
37908
+ optional: true
37909
+ }],
37495
37910
  "recording.relocateFootage": [{
37496
37911
  name: "deviceId",
37497
37912
  form: "single",
@@ -37517,6 +37932,11 @@ Object.freeze({
37517
37932
  form: "single",
37518
37933
  optional: false
37519
37934
  }],
37935
+ "recording.setDevicePlacement": [{
37936
+ name: "deviceId",
37937
+ form: "single",
37938
+ optional: false
37939
+ }],
37520
37940
  "recording.startStorageMigrationMove": [{
37521
37941
  name: "deviceId",
37522
37942
  form: "single",
@@ -37557,6 +37977,11 @@ Object.freeze({
37557
37977
  form: "single",
37558
37978
  optional: false
37559
37979
  }],
37980
+ "sceneMonitor.listScenesBatch": [{
37981
+ name: "deviceIds",
37982
+ form: "array",
37983
+ optional: false
37984
+ }],
37560
37985
  "sceneMonitor.recheckNow": [{
37561
37986
  name: "deviceId",
37562
37987
  form: "single",
@@ -37818,6 +38243,11 @@ Object.freeze({
37818
38243
  form: "single",
37819
38244
  optional: false
37820
38245
  }],
38246
+ "zoneAnalytics.getCurrentSnapshotBatch": [{
38247
+ name: "deviceIds",
38248
+ form: "array",
38249
+ optional: false
38250
+ }],
37821
38251
  "zoneAnalytics.getUnzonedHistory": [{
37822
38252
  name: "deviceId",
37823
38253
  form: "single",
@@ -40927,6 +41357,124 @@ function composeCameraStatus(input) {
40927
41357
  };
40928
41358
  }
40929
41359
  //#endregion
41360
+ //#region src/camera-status/fleet-read-pass.ts
41361
+ /**
41362
+ * Keep a rejection from becoming an UNHANDLED one.
41363
+ *
41364
+ * A shared promise is created by the first camera to ask and consumed by the
41365
+ * others later — `listBindableCapsForDeviceType` is reached in stage 2 of
41366
+ * `readSwitchAuthorities`, after an await, so two cameras genuinely attach
41367
+ * their handlers in different ticks. Without this the first rejection would be
41368
+ * unhandled for the window in between.
41369
+ *
41370
+ * The handler is a no-op ON PURPOSE: it never becomes the answer. Every real
41371
+ * consumer attaches its own `.catch` and reports the failure per camera.
41372
+ */
41373
+ function keepAlive(p) {
41374
+ p.catch(() => void 0);
41375
+ return p;
41376
+ }
41377
+ var FleetReadPass = class {
41378
+ deps;
41379
+ roster = null;
41380
+ bindings = null;
41381
+ mutes = null;
41382
+ bindableByType = /* @__PURE__ */ new Map();
41383
+ slotsByNode = /* @__PURE__ */ new Map();
41384
+ rtspByNode = /* @__PURE__ */ new Map();
41385
+ provisioningByNode = /* @__PURE__ */ new Map();
41386
+ selectedEngineByNode = /* @__PURE__ */ new Map();
41387
+ constructor(deps) {
41388
+ this.deps = deps;
41389
+ }
41390
+ /** The cameras this pass covers — the set both batch reads are issued for. */
41391
+ get deviceIds() {
41392
+ return this.deps.deviceIds;
41393
+ }
41394
+ /**
41395
+ * The device rows for the pass's set.
41396
+ *
41397
+ * **The shape follows the SET, not the caller.** A pass of one asks
41398
+ * `getDevice` — the exact question, and the one the write path
41399
+ * (`CameraSwitchService.setCameraSwitch`) has always asked. A pass of many
41400
+ * asks `listAll({deviceIds})`, which exists precisely because "these N" was
41401
+ * otherwise either N round trips or the whole fleet. Answering a set of one
41402
+ * with the fleet-shaped method would be no cheaper and would move the write
41403
+ * path onto a read it does not need.
41404
+ *
41405
+ * `projection: 'slim'` because the only fields read off it are `type` and
41406
+ * `disabled`: the full projection reads each device's settings row, which is
41407
+ * the per-device round trip this whole change exists to remove.
41408
+ */
41409
+ deviceRoster(api) {
41410
+ const single = this.deps.deviceIds.length === 1 ? this.deps.deviceIds[0] : void 0;
41411
+ const source = single === void 0 ? "deviceManager.listAll" : "deviceManager.getDevice";
41412
+ if (this.roster === null) this.roster = keepAlive(single === void 0 ? api.deviceManager.listAll.query({
41413
+ deviceIds: [...this.deps.deviceIds],
41414
+ projection: "slim"
41415
+ }) : api.deviceManager.getDevice.query({ deviceId: single }).then((row) => row === null ? [] : [row]));
41416
+ return {
41417
+ source,
41418
+ result: this.roster
41419
+ };
41420
+ }
41421
+ /** The binding rows for the pass's set — `getBindings` for one, the batch for many. */
41422
+ bindingRows(api) {
41423
+ const single = this.deps.deviceIds.length === 1 ? this.deps.deviceIds[0] : void 0;
41424
+ const source = single === void 0 ? "deviceManager.getBindingsBatch" : "deviceManager.getBindings";
41425
+ 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]));
41426
+ return {
41427
+ source,
41428
+ result: this.bindings
41429
+ };
41430
+ }
41431
+ /** The fleet's muted-camera list. One answer for every camera in the pass. */
41432
+ mutedDevices(api) {
41433
+ if (this.mutes === null) this.mutes = keepAlive(api.notificationRules.listDeviceMutes.query({}));
41434
+ return this.mutes;
41435
+ }
41436
+ /** The bindable caps for one device TYPE. Every camera shares one answer. */
41437
+ bindableCapsFor(api, deviceType) {
41438
+ const existing = this.bindableByType.get(deviceType);
41439
+ if (existing !== void 0) return existing;
41440
+ const created = keepAlive(api.deviceManager.listBindableCapsForDeviceType.query({ deviceType }));
41441
+ this.bindableByType.set(deviceType, created);
41442
+ return created;
41443
+ }
41444
+ /** One source node's whole profile-slot table. */
41445
+ profileSlots(api, sourceNodeId) {
41446
+ const existing = this.slotsByNode.get(sourceNodeId);
41447
+ if (existing !== void 0) return existing;
41448
+ const created = keepAlive(api.streamBroker.listAllProfileSlots.query(void 0, nodePin(sourceNodeId)));
41449
+ this.slotsByNode.set(sourceNodeId, created);
41450
+ return created;
41451
+ }
41452
+ /** One source node's whole RTSP-restream table. */
41453
+ rtspEntries(api, sourceNodeId) {
41454
+ const existing = this.rtspByNode.get(sourceNodeId);
41455
+ if (existing !== void 0) return existing;
41456
+ const created = keepAlive(api.streamBroker.getAllRtspEntries.query({}, nodePin(sourceNodeId)));
41457
+ this.rtspByNode.set(sourceNodeId, created);
41458
+ return created;
41459
+ }
41460
+ /** One detection node's runtime-provisioning snapshot. */
41461
+ engineProvisioning(api, detectionNodeId) {
41462
+ const existing = this.provisioningByNode.get(detectionNodeId);
41463
+ if (existing !== void 0) return existing;
41464
+ const created = keepAlive(api.pipelineExecutor.getEngineProvisioning.query({ nodeId: detectionNodeId }));
41465
+ this.provisioningByNode.set(detectionNodeId, created);
41466
+ return created;
41467
+ }
41468
+ /** The executor's bootstrap engine, as the detection stage reads it. */
41469
+ selectedEngine(api, detectionNodeId) {
41470
+ const existing = this.selectedEngineByNode.get(detectionNodeId);
41471
+ if (existing !== void 0) return existing;
41472
+ const created = keepAlive(api.pipelineExecutor.getSelectedEngine.query({ nodeId: detectionNodeId }));
41473
+ this.selectedEngineByNode.set(detectionNodeId, created);
41474
+ return created;
41475
+ }
41476
+ };
41477
+ //#endregion
40930
41478
  //#region src/camera-status-service.ts
40931
41479
  /** WebRTC consumer kinds counted toward `BrokerResult.webrtcSessions`. */
40932
41480
  var WEBRTC_KINDS = new Set([
@@ -41115,7 +41663,7 @@ var CameraStatusService = class {
41115
41663
  * broker's actual decode-session node into `liveDecoder` (T6) — the first
41116
41664
  * slot that reports one wins.
41117
41665
  */
41118
- buildBrokerStage(api, allSlotsFetch, deviceId, sourceNodeId, liveDecoder, sink) {
41666
+ buildBrokerStage(api, allSlotsFetch, deviceId, sourceNodeId, pass, liveDecoder, sink) {
41119
41667
  if (!api || !allSlotsFetch) return Promise.resolve(null);
41120
41668
  return this.boundedStage(allSlotsFetch.then(async (slots) => {
41121
41669
  const deviceSlots = slots.filter((s) => s.deviceId === deviceId);
@@ -41125,12 +41673,13 @@ var CameraStatusService = class {
41125
41673
  rtspRestream: false
41126
41674
  };
41127
41675
  const [statsAndClients, rtspEntry] = await Promise.all([Promise.all(deviceSlots.map(async (slot) => {
41676
+ 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)]);
41128
41677
  return {
41129
41678
  slot,
41130
- stats: await api.streamBroker.getBrokerStats.query({ brokerId: slot.brokerId }, nodePin(sourceNodeId)).catch(() => null),
41131
- clients: await api.streamBroker.listClients.query({ brokerId: slot.brokerId }, nodePin(sourceNodeId)).catch(() => null)
41679
+ stats,
41680
+ clients
41132
41681
  };
41133
- })), api.streamBroker.getAllRtspEntries.query({}, nodePin(sourceNodeId)).catch(() => null)]);
41682
+ })), pass.rtspEntries(api, sourceNodeId).catch(() => null)]);
41134
41683
  const profileDetails = statsAndClients.map(({ slot, stats, clients }) => ({
41135
41684
  profile: slot.profile,
41136
41685
  status: slot.status,
@@ -41191,13 +41740,16 @@ var CameraStatusService = class {
41191
41740
  };
41192
41741
  }
41193
41742
  /** Detection stage (pipeline-executor + runner metrics). */
41194
- buildDetectionStage(api, detectionNodeId, deviceId, sink) {
41743
+ buildDetectionStage(api, detectionNodeId, deviceId, pass, sink) {
41195
41744
  if (!api || !detectionNodeId) return Promise.resolve(null);
41196
- 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]) => {
41197
- const metrics = await api.pipelineRunner.getCameraMetrics.query({
41745
+ return this.boundedStage(Promise.all([
41746
+ pass.engineProvisioning(api, detectionNodeId).catch(() => null),
41747
+ pass.selectedEngine(api, detectionNodeId).catch(() => null),
41748
+ api.pipelineRunner.getCameraMetrics.query({
41198
41749
  deviceId,
41199
41750
  nodeId: detectionNodeId
41200
- }).catch(() => null);
41751
+ }).catch(() => null)
41752
+ ]).then(async ([provisioning, engine, metrics]) => {
41201
41753
  const phase = (() => {
41202
41754
  const p = metrics?.phase;
41203
41755
  if (p === "active") return "active";
@@ -41309,9 +41861,9 @@ var CameraStatusService = class {
41309
41861
  * badge on a working camera); what matters is that the emptiness travels with
41310
41862
  * the reason it is empty.
41311
41863
  */
41312
- buildSwitchStage(deviceId, sink) {
41864
+ buildSwitchStage(deviceId, pass, sink) {
41313
41865
  const startedAt = Date.now();
41314
- return this.boundedStage(this.deps.switchAuthoritiesFor(deviceId), STAGE_TIMEOUT_MS, "switches", deviceId, sink).then((reads) => {
41866
+ return this.boundedStage(this.deps.switchAuthoritiesFor(deviceId, pass), STAGE_TIMEOUT_MS, "switches", deviceId, sink).then((reads) => {
41315
41867
  if (reads === null) return [];
41316
41868
  const { switchedOff, unreadable } = composeSwitchedOff(reads);
41317
41869
  if (unreadable.length > 0) this.recordDegraded(sink, deviceId, "switches", "partial", Date.now() - startedAt, { unreadableAuthorities: unreadable });
@@ -41329,18 +41881,28 @@ var CameraStatusService = class {
41329
41881
  * `null` of a camera that legitimately has no such stage.
41330
41882
  */
41331
41883
  async getCameraStatus(deviceId) {
41884
+ return this.composeOne(deviceId, new FleetReadPass({ deviceIds: [deviceId] }));
41885
+ }
41886
+ /**
41887
+ * One camera's status, composed WITHIN a pass.
41888
+ *
41889
+ * Everything the camera alone can answer is fetched here; everything the pass
41890
+ * already knows (a node's slot table, the fleet's mute list, a device type's
41891
+ * bindable caps) is asked of `pass`, which issues it once for the whole call.
41892
+ */
41893
+ async composeOne(deviceId, pass) {
41332
41894
  const api = this.deps.api();
41333
41895
  const degradations = { entries: [] };
41334
41896
  const { detectionNodeId, sourceNodeId, pinned, detectionReason, audioNodeId, audioPinned } = this.buildAssignmentContext(deviceId);
41335
41897
  const liveDecoder = { nodeId: null };
41336
- const allSlotsFetch = api ? api.streamBroker.listAllProfileSlots.query(void 0, nodePin(sourceNodeId)) : null;
41898
+ const allSlotsFetch = api ? pass.profileSlots(api, sourceNodeId) : null;
41337
41899
  const sourceFetch = this.buildSourceStage(api, allSlotsFetch, deviceId, degradations);
41338
- const brokerFetch = this.buildBrokerStage(api, allSlotsFetch, deviceId, sourceNodeId, liveDecoder, degradations);
41900
+ const brokerFetch = this.buildBrokerStage(api, allSlotsFetch, deviceId, sourceNodeId, pass, liveDecoder, degradations);
41339
41901
  const decoderFetch = this.buildDecoderStage(detectionNodeId);
41340
41902
  const motionResult = this.buildMotionStage(deviceId);
41341
- const detectionFetch = this.buildDetectionStage(api, detectionNodeId, deviceId, degradations);
41903
+ const detectionFetch = this.buildDetectionStage(api, detectionNodeId, deviceId, pass, degradations);
41342
41904
  const recordingFetch = this.buildRecordingStage(api, deviceId, degradations);
41343
- const switchesFetch = this.buildSwitchStage(deviceId, degradations);
41905
+ const switchesFetch = this.buildSwitchStage(deviceId, pass, degradations);
41344
41906
  const [sourceResult, brokerResult, decoderResult, detectionResult, recordingResult, switchedOff] = await Promise.all([
41345
41907
  sourceFetch,
41346
41908
  brokerFetch,
@@ -41383,15 +41945,30 @@ var CameraStatusService = class {
41383
41945
  * `deviceIds` defaults to all cameras currently tracked by the
41384
41946
  * orchestrator's assignment map when omitted.
41385
41947
  *
41386
- * v1: `Promise.all` over per-device composition (no concurrency cap).
41387
- * Note: for large fleets (hundreds of cameras) this may fan out many
41388
- * parallel calls. A concurrency limiter (p-limit / semaphore) should be
41389
- * added if latency measurements show it's necessary deliberately
41390
- * deferred per the YAGNI constraint in the spec.
41948
+ * The whole call runs inside ONE {@link FleetReadPass}, and that is what
41949
+ * makes a five-second poll affordable. Composing a camera reaches seven
41950
+ * stages, but six of the questions those stages ask are about a NODE, a
41951
+ * device TYPE or the whole FLEETthe slot table, the restream table, the
41952
+ * mute list, the engine pair, the bindable caps — and asking them per camera
41953
+ * meant N identical round trips for N identical answers. Two more (the device
41954
+ * row, the binding table) are per-camera questions the device manager already
41955
+ * answers for a set. The pass asks each exactly once and shares the promise;
41956
+ * it holds nothing between calls, so the next poll asks again from scratch.
41957
+ *
41958
+ * What is left is genuinely per camera and stays per camera: the broker slot
41959
+ * stats, the runner metrics, the recorder, the broker mute and the camera's
41960
+ * own privacy provider. Collapsing any of those would be a narrower answer,
41961
+ * not a cheaper one.
41962
+ *
41963
+ * `Promise.all` over per-device composition (no concurrency cap). The fan-out
41964
+ * is now ~8 round trips per camera rather than ~16, and the six fleet reads
41965
+ * no longer multiply — which is the part that made the count grow with the
41966
+ * fleet twice over.
41391
41967
  */
41392
41968
  async getCameraStatuses(deviceIds) {
41393
41969
  const ids = deviceIds !== void 0 && deviceIds.length > 0 ? deviceIds : this.deps.listAssignedDeviceIds();
41394
- return Promise.all(ids.map((deviceId) => this.getCameraStatus(deviceId)));
41970
+ const pass = new FleetReadPass({ deviceIds: ids });
41971
+ return Promise.all(ids.map((deviceId) => this.composeOne(deviceId, pass)));
41395
41972
  }
41396
41973
  };
41397
41974
  //#endregion
@@ -41640,10 +42217,15 @@ async function probeCamera(api, deviceId, deps) {
41640
42217
  * @param deviceId The camera.
41641
42218
  * @param deps Logger + source-owner resolver.
41642
42219
  */
41643
- async function readSwitchAuthorities(api, deviceId, deps) {
42220
+ async function readSwitchAuthorities(api, deviceId, deps, sharedPass) {
41644
42221
  if (!api) return unknownReads(deviceId);
41645
- const devicePromise = bounded(deps, deviceId, "deviceManager.getDevice", api.deviceManager.getDevice.query({ deviceId }).then((d) => isDeviceShape(d) ? d : null).catch((err) => {
41646
- warnUnreachable(deps, deviceId, "getDevice", err);
42222
+ const pass = sharedPass ?? new FleetReadPass({ deviceIds: [deviceId] });
42223
+ const roster = pass.deviceRoster(api);
42224
+ const devicePromise = bounded(deps, deviceId, roster.source, roster.result.then((rows) => {
42225
+ const row = rows.find((d) => d.id === deviceId);
42226
+ return isDeviceShape(row) ? row : null;
42227
+ }).catch((err) => {
42228
+ warnUnreachable(deps, deviceId, roster.source, err);
41647
42229
  return null;
41648
42230
  }), null);
41649
42231
  const unknownBindings = {
@@ -41651,11 +42233,14 @@ async function readSwitchAuthorities(api, deviceId, deps) {
41651
42233
  providerAddonIdByCap: /* @__PURE__ */ new Map(),
41652
42234
  allCapNames: null
41653
42235
  };
41654
- const bindingsPromise = bounded(deps, deviceId, "deviceManager.getBindings", api.deviceManager.getBindings.query({ deviceId }).then((b) => {
42236
+ const bindingRead = pass.bindingRows(api);
42237
+ const bindingsPromise = bounded(deps, deviceId, bindingRead.source, bindingRead.result.then((rows) => {
42238
+ const row = rows.find((r) => r.deviceId === deviceId);
42239
+ if (row === void 0) return unknownBindings;
41655
42240
  const active = [];
41656
42241
  const all = [];
41657
42242
  const providers = /* @__PURE__ */ new Map();
41658
- for (const e of b.entries) {
42243
+ for (const e of row.entries) {
41659
42244
  all.push(e.capName);
41660
42245
  if (e.kind !== "wrapped") continue;
41661
42246
  active.push(e.capName);
@@ -41667,14 +42252,14 @@ async function readSwitchAuthorities(api, deviceId, deps) {
41667
42252
  allCapNames: all
41668
42253
  };
41669
42254
  }).catch((err) => {
41670
- warnUnreachable(deps, deviceId, "getBindings", err);
42255
+ warnUnreachable(deps, deviceId, bindingRead.source, err);
41671
42256
  return unknownBindings;
41672
42257
  }), unknownBindings);
41673
42258
  const recordingPromise = bounded(deps, deviceId, "recording.getDeviceConfig", api.recording.getDeviceConfig.query({ deviceId }).then((c) => isRecordingConfig(c) ? c : null).catch((err) => {
41674
42259
  warnUnreachable(deps, deviceId, "recording.getDeviceConfig", err);
41675
42260
  return null;
41676
42261
  }), null);
41677
- const mutesPromise = bounded(deps, deviceId, "notificationRules.listDeviceMutes", api.notificationRules.listDeviceMutes.query({}).then((r) => r.mutedDeviceIds).catch((err) => {
42262
+ const mutesPromise = bounded(deps, deviceId, "notificationRules.listDeviceMutes", pass.mutedDevices(api).then((r) => r.mutedDeviceIds).catch((err) => {
41678
42263
  warnUnreachable(deps, deviceId, "notificationRules.listDeviceMutes", err);
41679
42264
  return null;
41680
42265
  }), null);
@@ -41689,7 +42274,7 @@ async function readSwitchAuthorities(api, deviceId, deps) {
41689
42274
  mutesPromise,
41690
42275
  brokerAudioPromise
41691
42276
  ]);
41692
- 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) => {
42277
+ const [bindable, privacy] = await Promise.all([device === null ? Promise.resolve(null) : bounded(deps, deviceId, "deviceManager.listBindableCapsForDeviceType", pass.bindableCapsFor(api, device.type).catch((err) => {
41693
42278
  warnUnreachable(deps, deviceId, "listBindableCapsForDeviceType", err);
41694
42279
  return null;
41695
42280
  }), null), readPrivacyPlanes(api, deviceId, deps, bindings.allCapNames)]);
@@ -50396,11 +50981,11 @@ async function buildOrchestratorControllers(deps) {
50396
50981
  assignSource: (deviceId) => topology.assignSource(deviceId),
50397
50982
  listAssignedDeviceIds: () => [...new Set([...ledger.listAssignedDeviceIds(), ...detectionWiring.activeDeviceIds()])],
50398
50983
  isSessionCamera: (deviceId) => deps.isSessionCamera(deviceId),
50399
- switchAuthoritiesFor: async (deviceId) => (await readSwitchAuthorities(deps.ctx().api ?? null, deviceId, {
50984
+ switchAuthoritiesFor: async (deviceId, pass) => (await readSwitchAuthorities(deps.ctx().api ?? null, deviceId, {
50400
50985
  logger: deps.ctx().logger,
50401
50986
  assignSource: (id) => topology.assignSource(id),
50402
50987
  warnSampler: switchWarnSampler
50403
- })).derivation
50988
+ }, pass)).derivation
50404
50989
  });
50405
50990
  const reconcile = new ReconcileController({
50406
50991
  api: () => deps.ctx().api ?? null,