@camstack/addon-terminal 0.1.53 → 0.1.55

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.
Files changed (3) hide show
  1. package/dist/addon.js +963 -537
  2. package/dist/addon.mjs +963 -537
  3. package/package.json +1 -1
package/dist/addon.mjs CHANGED
@@ -8094,6 +8094,20 @@ var RelocateJobSchema = object({
8094
8094
  * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8095
8095
  */
8096
8096
  rowsReconciled: number().int().nonnegative().optional(),
8097
+ /**
8098
+ * Rows this run FORGOT because the file they name is not on disk.
8099
+ *
8100
+ * The mover derived the path from the row's own fields and `stat`ed it; an
8101
+ * ENOENT there is a per-path confirmation that the segment is gone (D296),
8102
+ * and the durable row is dropped through the same channel eviction uses. It
8103
+ * is reported for the same reason `rowsReconciled` is: this is a durable
8104
+ * mutation nobody asked for, and a migration that quietly erases hour rows is
8105
+ * the same failure as one that quietly skips them (D295).
8106
+ *
8107
+ * The production drain of 2026-08-30 would have reported 11 074 here — the
8108
+ * ledger claimed 5.65 GB of footage that no longer existed.
8109
+ */
8110
+ rowsForgotten: number().int().nonnegative().optional(),
8097
8111
  startedAt: number(),
8098
8112
  finishedAt: number().nullable(),
8099
8113
  error: string().nullable()
@@ -8158,6 +8172,13 @@ var MediaRelocateModeSchema = _enum([
8158
8172
  ]);
8159
8173
  var RelocateMediaInputSchema = object({
8160
8174
  toLocationId: string(),
8175
+ /**
8176
+ * Restrict the pass to rows currently on this location. Omitted / `'*'` =
8177
+ * every row that is not already on `toLocationId` (the historical
8178
+ * behaviour). A named source is what a from→to migration needs: without it
8179
+ * "move events off disk 2" also emptied disk 1.
8180
+ */
8181
+ fromLocationId: string().optional(),
8161
8182
  throttleMbps: number().min(1).max(1e3).optional(),
8162
8183
  /** Omitted = `move`, the pre-existing behaviour. */
8163
8184
  mode: MediaRelocateModeSchema.optional()
@@ -8225,6 +8246,19 @@ var StorageMigrationDestinationsSchema = object({
8225
8246
  galleryMedia: string().min(1).optional()
8226
8247
  }).refine((value) => Object.keys(value).length > 0, { message: "select at least one storage class" });
8227
8248
  /**
8249
+ * Optional named source per class. Omitted = the class's current default
8250
+ * (the historical behaviour). A named source that is NOT the default is a
8251
+ * drain of that disk: bytes move, the default stays, and the source is
8252
+ * disabled when the move finishes.
8253
+ */
8254
+ var StorageMigrationSourcesSchema = object({
8255
+ recordings: string().min(1).optional(),
8256
+ recordingsLow: string().min(1).optional(),
8257
+ eventMedia: string().min(1).optional(),
8258
+ backups: string().min(1).optional(),
8259
+ galleryMedia: string().min(1).optional()
8260
+ }).optional();
8261
+ /**
8228
8262
  * How a migration sequences the cutover against the byte move.
8229
8263
  *
8230
8264
  * - `blocking` — the historical order: pause, move every byte, repoint,
@@ -8246,6 +8280,8 @@ var StorageMigrationModeSchema = _enum(["blocking", "nonBlocking"]);
8246
8280
  /** Shared input for planning and starting an orchestrated storage migration. */
8247
8281
  var StorageMigrationInputSchema = object({
8248
8282
  destinations: StorageMigrationDestinationsSchema,
8283
+ /** Omitted = each class's current default. */
8284
+ sources: StorageMigrationSourcesSchema,
8249
8285
  throttleMbps: number().min(1).max(1e3).optional(),
8250
8286
  /** Omitted = `blocking`, which stays the default. */
8251
8287
  mode: StorageMigrationModeSchema.optional()
@@ -8325,6 +8361,13 @@ var StorageMigrationMoveSchema = object({
8325
8361
  storageClass: StorageMigrationClassSchema,
8326
8362
  fromLocationId: string(),
8327
8363
  toLocationId: string(),
8364
+ /**
8365
+ * True when `from` was NOT the class default at plan time. The move still
8366
+ * copies bytes, but the default is left alone and the source is disabled
8367
+ * once the copy verifies. Absent on jobs planned before this field existed
8368
+ * — those jobs always repointed, which is `false`.
8369
+ */
8370
+ freezeSource: boolean().optional(),
8328
8371
  moverJobId: string().nullable(),
8329
8372
  state: RelocateJobStateSchema.nullable(),
8330
8373
  error: string().nullable(),
@@ -8338,6 +8381,7 @@ var StorageMigrationJobSchema = object({
8338
8381
  * can tell a seconds-long cutover from a thirty-hour one. */
8339
8382
  mode: StorageMigrationModeSchema,
8340
8383
  destinations: StorageMigrationDestinationsSchema,
8384
+ sources: StorageMigrationSourcesSchema,
8341
8385
  throttleMbps: number(),
8342
8386
  moves: array(StorageMigrationMoveSchema),
8343
8387
  pauseLeaseId: string().nullable(),
@@ -8363,6 +8407,7 @@ var StorageMigrationFindingSchema = object({
8363
8407
  });
8364
8408
  var StorageMigrationPlanSchema = object({
8365
8409
  destinations: StorageMigrationDestinationsSchema,
8410
+ sources: StorageMigrationSourcesSchema,
8366
8411
  /** The mode this plan was built for. A plan is only valid for its mode: the
8367
8412
  * `eventMedia` seal gate and the single-cardinality refusal both depend on
8368
8413
  * it. */
@@ -8370,7 +8415,8 @@ var StorageMigrationPlanSchema = object({
8370
8415
  moves: array(object({
8371
8416
  storageClass: StorageMigrationClassSchema,
8372
8417
  fromLocationId: string(),
8373
- toLocationId: string()
8418
+ toLocationId: string(),
8419
+ freezeSource: boolean().optional()
8374
8420
  })),
8375
8421
  findings: array(StorageMigrationFindingSchema)
8376
8422
  });
@@ -8457,16 +8503,142 @@ var RelocateResidueSchema = object({
8457
8503
  segments: number().int().nonnegative(),
8458
8504
  bytes: number().int().nonnegative()
8459
8505
  }).nullable();
8506
+ /**
8507
+ * Ask one location whether its durable hour rows describe the disk — the walk
8508
+ * (D319).
8509
+ *
8510
+ * `apply` DEFAULTS TO FALSE and that default is the product: the operator's
8511
+ * missing tool is the question, and the dry run is how they sanity-check the
8512
+ * destructive run before authorising it.
8513
+ */
8514
+ var LedgerWalkInputSchema = object({
8515
+ locationId: string().min(1),
8516
+ /** Forget the confirmed-absent rows, rather than only counting them. */
8517
+ apply: boolean().optional(),
8518
+ /** Narrow to one camera. */
8519
+ deviceId: number().int().positive().optional(),
8520
+ /** Narrow to these recording profiles; empty/absent = every profile. */
8521
+ profiles: array(string().min(1)).optional()
8522
+ });
8523
+ /** Why a whole walk did nothing. Every one leaves the ledger untouched. */
8524
+ var LedgerWalkRefusalSchema = _enum([
8525
+ "location-unknown",
8526
+ "source-writable",
8527
+ "no-ledger",
8528
+ "archive-unreadable",
8529
+ "anchor-absent",
8530
+ "anchor-unreadable",
8531
+ "anchor-moved"
8532
+ ]);
8533
+ _enum([
8534
+ "live-tail",
8535
+ "listing-error",
8536
+ "path-mismatch",
8537
+ "durable-refused"
8538
+ ]);
8539
+ /** Every skip reason, always present, always a number — so a reason that never
8540
+ * fired reports as zero rather than absent and the report shape is constant
8541
+ * between passes. Spelled out rather than `z.record` for exactly that. */
8542
+ var LedgerWalkSkipCountsSchema = object({
8543
+ "live-tail": number().int().nonnegative(),
8544
+ "listing-error": number().int().nonnegative(),
8545
+ "path-mismatch": number().int().nonnegative(),
8546
+ "durable-refused": number().int().nonnegative()
8547
+ });
8548
+ /** One camera's share of a walk, so a report names cameras and not rows. */
8549
+ var LedgerWalkDeviceReportSchema = object({
8550
+ deviceId: number().int(),
8551
+ hoursWalked: number().int().nonnegative(),
8552
+ hoursMissing: number().int().nonnegative(),
8553
+ ghostSegments: number().int().nonnegative(),
8554
+ ghostBytes: number().int().nonnegative(),
8555
+ forgottenSegments: number().int().nonnegative(),
8556
+ orphanFiles: number().int().nonnegative()
8557
+ });
8558
+ /**
8559
+ * What one walk claimed, listed, found and (only when armed) forgot.
8560
+ *
8561
+ * `archiveSegments` is the **M** and `segmentsClaimed` the **N** (D295), so a
8562
+ * walk that saw a fraction of the location is visible in its own report rather
8563
+ * than in the absence of one.
8564
+ */
8565
+ var LedgerWalkReportSchema = object({
8566
+ locationId: string(),
8567
+ applied: boolean(),
8568
+ refused: LedgerWalkRefusalSchema.nullable(),
8569
+ archiveSegments: number().int().nonnegative().nullable(),
8570
+ archiveBytes: number().int().nonnegative().nullable(),
8571
+ hoursClaimed: number().int().nonnegative(),
8572
+ hoursWalked: number().int().nonnegative(),
8573
+ hoursMissing: number().int().nonnegative(),
8574
+ /** `readdir` calls issued — the cost, stated in the unit that is paid. */
8575
+ listings: number().int().nonnegative(),
8576
+ segmentsClaimed: number().int().nonnegative(),
8577
+ ghostSegments: number().int().nonnegative(),
8578
+ ghostBytes: number().int().nonnegative(),
8579
+ ghostHoursWhole: number().int().nonnegative(),
8580
+ forgottenSegments: number().int().nonnegative(),
8581
+ forgottenBytes: number().int().nonnegative(),
8582
+ /** Files under a claimed hour that no durable row names. Never deleted. */
8583
+ orphanFiles: number().int().nonnegative(),
8584
+ orphanSample: array(string()).readonly(),
8585
+ hoursSkipped: number().int().nonnegative(),
8586
+ skippedByReason: LedgerWalkSkipCountsSchema,
8587
+ /** The walk stopped at its per-pass hour bound with claims unwalked. */
8588
+ bounded: boolean(),
8589
+ byDevice: array(LedgerWalkDeviceReportSchema).readonly()
8590
+ });
8460
8591
  /** How many rows a media pass would still act on against a given target — the
8461
8592
  * media lane's denominator AND its residue, from ONE derivation so the two can
8462
8593
  * never disagree. `null` = the count could not be taken. */
8463
8594
  var RelocatableMediaCountSchema = object({ rows: number().int().nonnegative() }).nullable();
8464
8595
  var RelocatableMediaCountInputSchema = object({
8465
8596
  toLocationId: string().min(1),
8597
+ /** Restrict the count to one source; omitted / `'*'` = every non-target row. */
8598
+ fromLocationId: string().optional(),
8466
8599
  /** Omitted = `move`. */
8467
8600
  mode: MediaRelocateModeSchema.optional()
8468
8601
  });
8469
8602
  /**
8603
+ * Operator cleanup of leftover analytics rows, optional debug media, and
8604
+ * ghost ledger entries on frozen footage locations.
8605
+ *
8606
+ * A pass is tens of minutes on a standing backlog. The hub method RETURNS
8607
+ * `{ jobId }` immediately; progress is `cleanupStatus`. Awaiting the work
8608
+ * is how `addons.custom` hit the 60 s UDS deadline while reclaim continued
8609
+ * with no operator-visible status.
8610
+ */
8611
+ var StorageCleanupPhaseSchema = _enum([
8612
+ "orphans",
8613
+ "debug-media",
8614
+ "ghost-ledger",
8615
+ "done",
8616
+ "failed",
8617
+ "cancelled"
8618
+ ]);
8619
+ var StorageCleanupInputSchema = object({
8620
+ /** Also walk motion stills / track filmstrips. Off by default. */
8621
+ includeDebugMedia: boolean().optional() });
8622
+ var StorageCleanupJobSchema = object({
8623
+ jobId: string(),
8624
+ phase: StorageCleanupPhaseSchema,
8625
+ includeDebugMedia: boolean(),
8626
+ orphansReclaimed: number().int().nonnegative(),
8627
+ orphanBytesReclaimed: number().int().nonnegative(),
8628
+ debugMediaReclaimed: number().int().nonnegative(),
8629
+ debugMediaBytesReclaimed: number().int().nonnegative(),
8630
+ ghostsForgotten: number().int().nonnegative(),
8631
+ ghostBytesForgotten: number().int().nonnegative(),
8632
+ /** Short operator-facing line: current collection, pass, or location. */
8633
+ detail: string().nullable(),
8634
+ cancelRequested: boolean(),
8635
+ startedAt: number(),
8636
+ updatedAt: number(),
8637
+ finishedAt: number().nullable(),
8638
+ error: string().nullable()
8639
+ });
8640
+ var StorageCleanupStatusInputSchema = object({ jobId: string().optional() });
8641
+ /**
8470
8642
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
8471
8643
  * storage a location serves. Defined here (not in `capabilities/storage.cap.ts`)
8472
8644
  * so the persisted record schema and the consumer-facing cap can both consume it
@@ -8494,11 +8666,10 @@ var StorageLocationTypeSchema = string().regex(/^[a-z][a-zA-Z0-9-]*$/);
8494
8666
  * The default location for a type uses `id === <type>:default` by
8495
8667
  * convention (the bare type ref like `'backups'` resolves to it).
8496
8668
  *
8497
- * `isSystem: true` marks a location as orchestrator-seeded and
8498
- * undeletable. The bootstrap-installed defaults (one per type) carry
8499
- * this flag; operator-added locations don't. Editing the config of
8500
- * a system location is allowed (path migration, provider swap) but
8501
- * deleting it is rejected at the cap level.
8669
+ * `isSystem` is a legacy persisted flag. Seed still creates the initial
8670
+ * `<type>:default` locations; the flag is no longer a lock, a badge, or a
8671
+ * prune selector. New writes leave it false. Deletion is gated on uniqueness
8672
+ * / last-enabled, not on this bit.
8502
8673
  */
8503
8674
  var StorageLocationSchema = object({
8504
8675
  id: string().regex(/^[a-z][a-zA-Z0-9-]*:[a-zA-Z0-9-]+$/),
@@ -12969,6 +13140,12 @@ method(object({
12969
13140
  }), _void(), {
12970
13141
  kind: "mutation",
12971
13142
  auth: "admin"
13143
+ }), method(object({
13144
+ from: string(),
13145
+ to: string()
13146
+ }), object({ moved: number() }), {
13147
+ kind: "mutation",
13148
+ auth: "admin"
12972
13149
  }), method(object({
12973
13150
  deviceId: number(),
12974
13151
  disabled: boolean()
@@ -18977,53 +19154,15 @@ var RecentTracksPageSchema = object({
18977
19154
  /** Cursor for the next page, or null when this page is the last. */
18978
19155
  nextCursor: string().nullable()
18979
19156
  });
18980
- var LIST_GROUPS_DEFAULT_LIMIT = 40;
18981
- var LIST_GROUPS_MAX_LIMIT = 100;
18982
- var AnalyticsGroupRecordSchema = object({
18983
- id: string(),
18984
- deviceId: number().int(),
18985
- openedAt: number().int(),
18986
- closedAt: number().int(),
18987
- timestamp: number().int(),
18988
- memberCount: number().int(),
18989
- memberTrackIds: array(string()).readonly(),
18990
- className: string(),
18991
- classes: array(string()).readonly(),
18992
- /** Relative event-media path, or null when the group has no picture yet. */
18993
- mediaUrl: string().nullable(),
18994
- singleton: boolean()
18995
- });
18996
- var AnalyticsGroupMemberSchema = object({
18997
- trackId: string(),
18998
- deviceId: number().int(),
18999
- className: string(),
19000
- firstSeen: number().int(),
19001
- lastSeen: number().int(),
19002
- mediaUrl: string().nullable()
19003
- });
19004
- var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
19005
- var ListGroupsQueryInput = object({
19006
- /** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
19007
- deviceIds: array(number()),
19008
- /** Window lower bound on `closedAt` (inclusive). */
19009
- since: number().optional(),
19010
- /** Window upper bound on `openedAt` (inclusive). */
19011
- until: number().optional(),
19012
- limit: number().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
19013
- /** Opaque continuation cursor from a previous page's `nextCursor`. */
19014
- cursor: string().optional()
19015
- });
19016
- var ListGroupsPageSchema = object({
19017
- groups: array(AnalyticsGroupRecordSchema).readonly(),
19018
- nextCursor: string().nullable()
19019
- });
19157
+ var KEY_EVENTS_DEFAULT_LIMIT = 50;
19158
+ var KEY_EVENTS_MAX_LIMIT = 200;
19020
19159
  var KeyEventQueryInput = object({
19021
19160
  deviceId: number(),
19022
19161
  /** Window lower bound (track firstSeen ≥ since). */
19023
19162
  since: number(),
19024
19163
  /** Window upper bound (track firstSeen ≤ until). */
19025
19164
  until: number(),
19026
- limit: number().int().min(1).max(200).default(50),
19165
+ limit: number().int().min(1).max(KEY_EVENTS_MAX_LIMIT).default(KEY_EVENTS_DEFAULT_LIMIT),
19027
19166
  /** Drop tracks scoring below this importance. */
19028
19167
  minImportance: number().min(0).max(1).optional(),
19029
19168
  /** Restrict to a single class (e.g. 'person'). */
@@ -19045,6 +19184,32 @@ var KeyEventSchema = object({
19045
19184
  ...TrackFlagFields,
19046
19185
  ...TrackRetrainFields
19047
19186
  });
19187
+ /** `getKeyEvents`' window and filters, asked of a SET of cameras at once. */
19188
+ var KeyEventBatchQueryInput = object({
19189
+ deviceIds: array(number()).min(1).max(200),
19190
+ since: number(),
19191
+ until: number(),
19192
+ /** Applied PER CAMERA, exactly as `getKeyEvents.limit` is — never a total
19193
+ * across the set, which would let a busy camera starve a quiet one of its
19194
+ * rows and change what the merged feed contains. */
19195
+ limit: number().int().min(1).max(KEY_EVENTS_MAX_LIMIT).default(KEY_EVENTS_DEFAULT_LIMIT),
19196
+ minImportance: number().min(0).max(1).optional(),
19197
+ classFilter: string().optional()
19198
+ });
19199
+ /**
19200
+ * One camera's key events in a batch answer.
19201
+ *
19202
+ * The row exists for every requested id. `getKeyEvents` degrades to `[]` on
19203
+ * error rather than throwing, so a camera whose store read failed and one with
19204
+ * no events in the window were ALREADY indistinguishable per camera — the
19205
+ * batch does not make that worse, and the row keeps the deviceId the single
19206
+ * method's output never carried (the caller used to stamp it from the fan-out
19207
+ * key, which only worked because there was one query per camera).
19208
+ */
19209
+ var KeyEventsForDeviceSchema = object({
19210
+ deviceId: number(),
19211
+ events: array(KeyEventSchema).readonly()
19212
+ });
19048
19213
  object({
19049
19214
  trackId: string(),
19050
19215
  className: string(),
@@ -19092,9 +19257,7 @@ var TrackCascadeCountsSchema = object({
19092
19257
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
19093
19258
  plates: number().int(),
19094
19259
  /** Per-track CLIP search vectors removed (best-effort). */
19095
- embeddings: number().int(),
19096
- /** Group membership + group rows removed with their last member (best-effort). */
19097
- groups: number().int()
19260
+ embeddings: number().int()
19098
19261
  });
19099
19262
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
19100
19263
  var DiskReconcileCountsSchema = object({
@@ -19264,6 +19427,47 @@ var RebuildStatusSchema = object({
19264
19427
  /** Present when the pass ended by throwing. */
19265
19428
  error: string().nullable()
19266
19429
  });
19430
+ /**
19431
+ * Acknowledgement that a debug-media reclaim STARTED.
19432
+ *
19433
+ * The pass is paced at ~2 MB/s over a standing 100 GB — tens of minutes — so
19434
+ * it runs detached and this returns immediately. Awaiting it is how the
19435
+ * `addons.custom` door hit the 60 s UDS deadline while the walk carried on
19436
+ * with no status. Poll {@link pipelineAnalyticsCapability.methods.getMediaReclaimStatus}.
19437
+ */
19438
+ var MediaReclaimStartResultSchema = object({
19439
+ started: boolean(),
19440
+ /** True when a pass was already running; the new request is ignored. */
19441
+ alreadyRunning: boolean()
19442
+ });
19443
+ var MediaReclaimInputSchema = object({
19444
+ mode: _enum(["report", "reclaim"]).default("report"),
19445
+ scopes: array(_enum(["motion-still", "track-filmstrip"])).min(1).optional(),
19446
+ deviceIds: array(number().int()).min(1).optional(),
19447
+ restart: boolean().optional(),
19448
+ pageSize: number().int().min(50).max(5e3).optional(),
19449
+ maxRowsPerDevice: number().int().min(1).max(5e5).optional(),
19450
+ maxReclaimPerDevice: number().int().min(1).max(5e5).optional(),
19451
+ maxBytesPerRun: number().int().min(1).optional(),
19452
+ budgetMinutes: number().int().min(1).max(720).optional(),
19453
+ throttleBytesPerSec: number().int().min(64 * 1024).optional(),
19454
+ graceMinutes: number().int().min(1).max(10080).optional()
19455
+ });
19456
+ var MediaReclaimStatusSchema = object({
19457
+ running: boolean(),
19458
+ mode: _enum(["report", "reclaim"]).nullable(),
19459
+ totalExamined: number(),
19460
+ totalEligible: number(),
19461
+ totalReclaimed: number(),
19462
+ totalBytesReclaimed: number(),
19463
+ totalRefused: number(),
19464
+ /** Device+scope windows finished in this pass. */
19465
+ devicesDone: number(),
19466
+ complete: boolean().nullable(),
19467
+ startedAtMs: number().nullable(),
19468
+ finishedAtMs: number().nullable(),
19469
+ error: string().nullable()
19470
+ });
19267
19471
  var ReplayFrameInputSchema = object({
19268
19472
  timestamp: number(),
19269
19473
  frame: PipelineRunResultBridge
@@ -19302,10 +19506,7 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19302
19506
  * stationary registry). Default false: the timeline lists passages,
19303
19507
  * not parking records (operator decision, 2026-08-15). */
19304
19508
  includeStationary: boolean().optional()
19305
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(ListGroupsQueryInput, ListGroupsPageSchema), method(object({
19306
- deviceId: number(),
19307
- groupId: string().min(1)
19308
- }), AnalyticsGroupDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
19509
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
19309
19510
  kind: "mutation",
19310
19511
  auth: "admin"
19311
19512
  }), method(DeviceEventQueryInput, array(MotionEventSchema).readonly()), method(ObjectEventQueryInput, array(ObjectEventSchema).readonly()), method(DeviceEventQueryInput, array(AudioEventSchema).readonly()), method(object({ deviceId: number() }), array(EventKindDescriptorSchema).readonly()), method(object({ deviceIds: array(number()).min(1).max(200) }), array(EventKindsForDeviceSchema).readonly()), method(object({
@@ -19314,7 +19515,7 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19314
19515
  until: number().optional(),
19315
19516
  kinds: array(string()).optional(),
19316
19517
  limit: number().int().min(1).max(MAX_EVENT_QUERY_LIMIT).default(DEFAULT_EVENT_QUERY_LIMIT)
19317
- }), array(SensorEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(object({
19518
+ }), array(SensorEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(KeyEventBatchQueryInput, array(KeyEventsForDeviceSchema).readonly()), method(object({
19318
19519
  deviceId: number(),
19319
19520
  since: number(),
19320
19521
  until: number(),
@@ -19405,6 +19606,12 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19405
19606
  }), method(OpsLogQueryInputSchema, array(OpsLogEntrySchema).readonly(), {
19406
19607
  kind: "query",
19407
19608
  auth: "admin"
19609
+ }), method(MediaReclaimInputSchema, MediaReclaimStartResultSchema, {
19610
+ kind: "mutation",
19611
+ auth: "admin"
19612
+ }), method(object({}), MediaReclaimStatusSchema, {
19613
+ kind: "query",
19614
+ auth: "admin"
19408
19615
  }), method(object({ deviceIds: array(number()).optional() }), TrainingExportSummarySchema, {
19409
19616
  kind: "query",
19410
19617
  auth: "admin"
@@ -21479,7 +21686,13 @@ method(StorageMigrationInputSchema, StorageMigrationPlanSchema, { auth: "admin"
21479
21686
  }), method(object({}), array(StorageMigrationMoverSchema).readonly(), { auth: "admin" }), method(object({}), array(StorageMigrationResidueSchema).readonly(), { auth: "admin" }), method(StorageMigrationDrainInputSchema, object({ jobId: string() }), {
21480
21687
  kind: "mutation",
21481
21688
  auth: "admin"
21482
- });
21689
+ }), method(StorageCleanupInputSchema, object({ jobId: string() }), {
21690
+ kind: "mutation",
21691
+ auth: "admin"
21692
+ }), method(StorageCleanupStatusInputSchema, StorageCleanupJobSchema.nullable(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
21693
+ kind: "mutation",
21694
+ auth: "admin"
21695
+ }), method(object({}), array(StorageMigrationJobSchema).readonly(), { auth: "admin" });
21483
21696
  var ProviderInfoSchema = discriminatedUnion("shouldSaveDiskSpace", [object({
21484
21697
  providerId: string().min(1),
21485
21698
  displayName: string().min(1),
@@ -28616,6 +28829,33 @@ var RecordingRebalanceInputSchema = object({
28616
28829
  minMoveGb: number().min(0).optional()
28617
28830
  });
28618
28831
  /**
28832
+ * Operator-facing placement of one camera onto a recordings location.
28833
+ *
28834
+ * `pinLocationId` is the operator instruction: a location id, or `null` for
28835
+ * Auto (the planner may move this camera). `locationId` is where high/mid
28836
+ * currently write — the plan, which may disagree with the pin when Auto.
28837
+ */
28838
+ var RecordingDevicePlacementSchema = object({
28839
+ deviceId: number().int(),
28840
+ profile: string(),
28841
+ locationId: string()
28842
+ });
28843
+ var RecordingDevicePinSchema = object({
28844
+ deviceId: number().int(),
28845
+ /** Recordings-class location this camera is pinned to. */
28846
+ locationId: string()
28847
+ });
28848
+ var RecordingPlacementViewSchema = object({
28849
+ assignments: array(RecordingDevicePlacementSchema),
28850
+ pins: array(RecordingDevicePinSchema),
28851
+ defaultLocations: record(string(), string())
28852
+ });
28853
+ var RecordingSetDevicePlacementInputSchema = object({
28854
+ deviceId: number().int(),
28855
+ /** `null` = Auto. Otherwise a `recordings:*` location id. */
28856
+ locationId: string().nullable()
28857
+ });
28858
+ /**
28619
28859
  * Result of locating footage at a wall-clock instant for one device/profile.
28620
28860
  * `segment` carries the covering segment's window; `gap` reports the forward
28621
28861
  * nearest covered edge (`null` past the end of footage / when none exists)
@@ -28829,6 +29069,9 @@ method(object({
28829
29069
  }), method(RelocateResidueInputSchema, RelocateResidueSchema, {
28830
29070
  kind: "query",
28831
29071
  auth: "admin"
29072
+ }), method(LedgerWalkInputSchema, LedgerWalkReportSchema, {
29073
+ kind: "mutation",
29074
+ auth: "admin"
28832
29075
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
28833
29076
  kind: "mutation",
28834
29077
  auth: "admin"
@@ -28838,6 +29081,12 @@ method(object({
28838
29081
  }), method(RecordingRebalanceInputSchema, RecordingRebalancePlanSchema, {
28839
29082
  kind: "mutation",
28840
29083
  auth: "admin"
29084
+ }), method(object({}), RecordingPlacementViewSchema, {
29085
+ kind: "query",
29086
+ auth: "admin"
29087
+ }), method(RecordingSetDevicePlacementInputSchema, object({ ok: literal(true) }), {
29088
+ kind: "mutation",
29089
+ auth: "admin"
28841
29090
  });
28842
29091
  /**
28843
29092
  * `recording-export` cap — render a footage time range into a single downloadable
@@ -29220,6 +29469,25 @@ var SceneMonitorStatusSchema = object({
29220
29469
  monitors: array(SceneMonitorSchema),
29221
29470
  lastFetchedAt: number()
29222
29471
  });
29472
+ /**
29473
+ * One camera's row in a `listScenesBatch` answer.
29474
+ *
29475
+ * `status` is NULLABLE and the nullability is the whole point. `scene-monitor`
29476
+ * is a `defaultActive` wrapper, so every camera is asked; a camera whose
29477
+ * provider is absent (pipeline-analytics not deployed, the post-processing node
29478
+ * down) cannot answer, and that is not the same fact as a camera with no scenes
29479
+ * configured. Fanned out per camera the difference was visible — one query
29480
+ * errored while the others resolved — and a batch that returned only the rows
29481
+ * it managed would have destroyed it, silently, by making an unreachable camera
29482
+ * indistinguishable from one that answered `monitors: []`.
29483
+ *
29484
+ * So: EVERY requested deviceId gets a row. `status: null` means "this camera
29485
+ * could not be read"; `status.monitors: []` means "read, and it has none".
29486
+ */
29487
+ var SceneMonitorStatusForDeviceSchema = object({
29488
+ deviceId: number(),
29489
+ status: SceneMonitorStatusSchema.nullable()
29490
+ });
29223
29491
  var sceneMonitorCapability = {
29224
29492
  name: "scene-monitor",
29225
29493
  scope: "device",
@@ -29229,6 +29497,22 @@ var sceneMonitorCapability = {
29229
29497
  deviceTypes: [DeviceType.Camera],
29230
29498
  methods: {
29231
29499
  listScenes: method(object({ deviceId: number() }), SceneMonitorStatusSchema),
29500
+ /**
29501
+ * The same answer, for a SET of cameras, in one round trip.
29502
+ *
29503
+ * `/scenes` renders every camera and re-reads them on a 30s safety-net
29504
+ * poll behind the push slice. Fanned out client-side that was one query
29505
+ * per camera — 29 round trips through the browser, the hub and the
29506
+ * post-analysis runner every 30 seconds to read an in-memory map the
29507
+ * owner had already merged. The work is unchanged (`statusFor` per
29508
+ * device, all in-process at the owner); what collapses is the transport.
29509
+ *
29510
+ * A camera that cannot answer still gets a row, with `status: null` —
29511
+ * see {@link SceneMonitorStatusForDeviceSchema}. Never fewer rows than
29512
+ * ids: a caller that asked for twenty-nine and got twenty-seven cannot
29513
+ * tell which two are missing, or that any are.
29514
+ */
29515
+ listScenesBatch: method(object({ deviceIds: array(number()).min(1).max(200) }), array(SceneMonitorStatusForDeviceSchema).readonly()),
29232
29516
  createScene: method(object({
29233
29517
  deviceId: number(),
29234
29518
  label: string(),
@@ -31064,6 +31348,27 @@ var CameraOccupancySnapshotSchema = object({
31064
31348
  stationaryObjects: array(StationaryObjectSchema).readonly().optional()
31065
31349
  });
31066
31350
  /**
31351
+ * One camera's row in a `getCurrentSnapshotBatch` answer.
31352
+ *
31353
+ * THREE outcomes, and the single-camera method could only express two of them
31354
+ * because `snapshot: null` was already spoken for:
31355
+ *
31356
+ * - `read: 'read'`, `snapshot` present — the live occupancy reading.
31357
+ * - `read: 'read'`, `snapshot: null` — read, and this camera has no reading
31358
+ * yet: no frame since boot, and no parked-object registry to hydrate from.
31359
+ * - `read: 'unreadable'` — the owner could not answer for this
31360
+ * camera. `snapshot` is null, and it does NOT mean "nothing parked here".
31361
+ *
31362
+ * Collapsing the last two is the failure this field exists to prevent: a
31363
+ * hydration that threw would otherwise render as an empty Stationary section,
31364
+ * which is a definite claim about a camera nobody could read.
31365
+ */
31366
+ var CameraOccupancySnapshotForDeviceSchema = object({
31367
+ deviceId: number(),
31368
+ read: _enum(["read", "unreadable"]),
31369
+ snapshot: CameraOccupancySnapshotSchema.nullable()
31370
+ });
31371
+ /**
31067
31372
  * Time-series resolution. The history methods return one bucket per
31068
31373
  * step over the requested range. Smaller resolutions cost more
31069
31374
  * memory + bandwidth; bound to discrete steps so caller cannot ask
@@ -31120,6 +31425,20 @@ var zoneAnalyticsCapability = {
31120
31425
  * (no inference result emitted since boot or since binding was
31121
31426
  * activated). */
31122
31427
  getCurrentSnapshot: method(object({ deviceId: number() }), CameraOccupancySnapshotSchema.nullable()),
31428
+ /**
31429
+ * The same snapshot, for a SET of cameras, in one round trip.
31430
+ *
31431
+ * The Events page's Stationary section polls this every 15s for every
31432
+ * selected camera. Fanned out client-side that is one query per camera to
31433
+ * read a `Map.get` at the owner — the answer costs nothing, the round trip
31434
+ * costs everything. Batched, N transports become one and the per-device
31435
+ * work is unchanged.
31436
+ *
31437
+ * Every requested deviceId gets a row, tagged `read` — see
31438
+ * {@link CameraOccupancySnapshotForDeviceSchema}. A camera the owner could
31439
+ * not answer for is `'unreadable'`, never an empty reading.
31440
+ */
31441
+ getCurrentSnapshotBatch: method(object({ deviceIds: array(number()).min(1).max(200) }), array(CameraOccupancySnapshotForDeviceSchema).readonly()),
31123
31442
  /** Time-series object count inside one zone. `className` optional —
31124
31443
  * omit to count every class in the zone. */
31125
31444
  getZoneHistory: method(object({
@@ -33835,6 +34154,12 @@ Object.freeze({
33835
34154
  addonId: null,
33836
34155
  access: "delete"
33837
34156
  },
34157
+ "deviceManager.renameLocation": {
34158
+ capName: "device-manager",
34159
+ capScope: "system",
34160
+ addonId: null,
34161
+ access: "create"
34162
+ },
33838
34163
  "deviceManager.runDeviceAction": {
33839
34164
  capName: "device-manager",
33840
34165
  capScope: "system",
@@ -35527,13 +35852,19 @@ Object.freeze({
35527
35852
  addonId: null,
35528
35853
  access: "view"
35529
35854
  },
35530
- "pipelineAnalytics.getGroup": {
35855
+ "pipelineAnalytics.getKeyEvents": {
35531
35856
  capName: "pipeline-analytics",
35532
35857
  capScope: "device",
35533
35858
  addonId: null,
35534
35859
  access: "view"
35535
35860
  },
35536
- "pipelineAnalytics.getKeyEvents": {
35861
+ "pipelineAnalytics.getKeyEventsBatch": {
35862
+ capName: "pipeline-analytics",
35863
+ capScope: "device",
35864
+ addonId: null,
35865
+ access: "view"
35866
+ },
35867
+ "pipelineAnalytics.getMediaReclaimStatus": {
35537
35868
  capName: "pipeline-analytics",
35538
35869
  capScope: "device",
35539
35870
  addonId: null,
@@ -35623,12 +35954,6 @@ Object.freeze({
35623
35954
  addonId: null,
35624
35955
  access: "view"
35625
35956
  },
35626
- "pipelineAnalytics.listGroups": {
35627
- capName: "pipeline-analytics",
35628
- capScope: "device",
35629
- addonId: null,
35630
- access: "view"
35631
- },
35632
35957
  "pipelineAnalytics.listOpsLog": {
35633
35958
  capName: "pipeline-analytics",
35634
35959
  capScope: "device",
@@ -35713,6 +36038,12 @@ Object.freeze({
35713
36038
  addonId: null,
35714
36039
  access: "create"
35715
36040
  },
36041
+ "pipelineAnalytics.reclaimDebugMedia": {
36042
+ capName: "pipeline-analytics",
36043
+ capScope: "device",
36044
+ addonId: null,
36045
+ access: "create"
36046
+ },
35716
36047
  "pipelineAnalytics.reconcileFromDisk": {
35717
36048
  capName: "pipeline-analytics",
35718
36049
  capScope: "device",
@@ -36637,6 +36968,12 @@ Object.freeze({
36637
36968
  addonId: null,
36638
36969
  access: "view"
36639
36970
  },
36971
+ "recording.getPlacement": {
36972
+ capName: "recording",
36973
+ capScope: "system",
36974
+ addonId: null,
36975
+ access: "view"
36976
+ },
36640
36977
  "recording.getPlaybackManifest": {
36641
36978
  capName: "recording",
36642
36979
  capScope: "system",
@@ -36715,6 +37052,12 @@ Object.freeze({
36715
37052
  addonId: null,
36716
37053
  access: "view"
36717
37054
  },
37055
+ "recording.reconcileLedgerAgainstDisk": {
37056
+ capName: "recording",
37057
+ capScope: "system",
37058
+ addonId: null,
37059
+ access: "create"
37060
+ },
36718
37061
  "recording.refreshStorageLocationsForMigration": {
36719
37062
  capName: "recording",
36720
37063
  capScope: "system",
@@ -36757,6 +37100,12 @@ Object.freeze({
36757
37100
  addonId: null,
36758
37101
  access: "create"
36759
37102
  },
37103
+ "recording.setDevicePlacement": {
37104
+ capName: "recording",
37105
+ capScope: "system",
37106
+ addonId: null,
37107
+ access: "create"
37108
+ },
36760
37109
  "recording.startStorageMigrationMove": {
36761
37110
  capName: "recording",
36762
37111
  capScope: "system",
@@ -36841,6 +37190,12 @@ Object.freeze({
36841
37190
  addonId: null,
36842
37191
  access: "view"
36843
37192
  },
37193
+ "sceneMonitor.listScenesBatch": {
37194
+ capName: "scene-monitor",
37195
+ capScope: "device",
37196
+ addonId: null,
37197
+ access: "view"
37198
+ },
36844
37199
  "sceneMonitor.recheckNow": {
36845
37200
  capName: "scene-monitor",
36846
37201
  capScope: "device",
@@ -37195,12 +37550,36 @@ Object.freeze({
37195
37550
  addonId: null,
37196
37551
  access: "create"
37197
37552
  },
37553
+ "storageMigration.cleanupCancel": {
37554
+ capName: "storage-migration",
37555
+ capScope: "system",
37556
+ addonId: null,
37557
+ access: "create"
37558
+ },
37559
+ "storageMigration.cleanupStart": {
37560
+ capName: "storage-migration",
37561
+ capScope: "system",
37562
+ addonId: null,
37563
+ access: "create"
37564
+ },
37565
+ "storageMigration.cleanupStatus": {
37566
+ capName: "storage-migration",
37567
+ capScope: "system",
37568
+ addonId: null,
37569
+ access: "view"
37570
+ },
37198
37571
  "storageMigration.drain": {
37199
37572
  capName: "storage-migration",
37200
37573
  capScope: "system",
37201
37574
  addonId: null,
37202
37575
  access: "create"
37203
37576
  },
37577
+ "storageMigration.history": {
37578
+ capName: "storage-migration",
37579
+ capScope: "system",
37580
+ addonId: null,
37581
+ access: "view"
37582
+ },
37204
37583
  "storageMigration.movers": {
37205
37584
  capName: "storage-migration",
37206
37585
  capScope: "system",
@@ -38197,6 +38576,12 @@ Object.freeze({
38197
38576
  addonId: null,
38198
38577
  access: "view"
38199
38578
  },
38579
+ "zoneAnalytics.getCurrentSnapshotBatch": {
38580
+ capName: "zone-analytics",
38581
+ capScope: "device",
38582
+ addonId: null,
38583
+ access: "view"
38584
+ },
38200
38585
  "zoneAnalytics.getUnzonedHistory": {
38201
38586
  capName: "zone-analytics",
38202
38587
  capScope: "device",
@@ -39191,14 +39576,14 @@ Object.freeze({
39191
39576
  form: "single",
39192
39577
  optional: true
39193
39578
  }],
39194
- "pipelineAnalytics.getGroup": [{
39579
+ "pipelineAnalytics.getKeyEvents": [{
39195
39580
  name: "deviceId",
39196
39581
  form: "single",
39197
39582
  optional: false
39198
39583
  }],
39199
- "pipelineAnalytics.getKeyEvents": [{
39200
- name: "deviceId",
39201
- form: "single",
39584
+ "pipelineAnalytics.getKeyEventsBatch": [{
39585
+ name: "deviceIds",
39586
+ form: "array",
39202
39587
  optional: false
39203
39588
  }],
39204
39589
  "pipelineAnalytics.getMotionEvents": [{
@@ -39256,11 +39641,6 @@ Object.freeze({
39256
39641
  form: "single",
39257
39642
  optional: false
39258
39643
  }],
39259
- "pipelineAnalytics.listGroups": [{
39260
- name: "deviceIds",
39261
- form: "array",
39262
- optional: false
39263
- }],
39264
39644
  "pipelineAnalytics.listOpsLog": [{
39265
39645
  name: "deviceId",
39266
39646
  form: "single",
@@ -39306,6 +39686,11 @@ Object.freeze({
39306
39686
  form: "single",
39307
39687
  optional: true
39308
39688
  }],
39689
+ "pipelineAnalytics.reclaimDebugMedia": [{
39690
+ name: "deviceIds",
39691
+ form: "array",
39692
+ optional: true
39693
+ }],
39309
39694
  "pipelineAnalytics.reconcileFromDisk": [{
39310
39695
  name: "deviceId",
39311
39696
  form: "single",
@@ -39641,6 +40026,11 @@ Object.freeze({
39641
40026
  form: "single",
39642
40027
  optional: false
39643
40028
  }],
40029
+ "recording.reconcileLedgerAgainstDisk": [{
40030
+ name: "deviceId",
40031
+ form: "single",
40032
+ optional: true
40033
+ }],
39644
40034
  "recording.relocateFootage": [{
39645
40035
  name: "deviceId",
39646
40036
  form: "single",
@@ -39666,6 +40056,11 @@ Object.freeze({
39666
40056
  form: "single",
39667
40057
  optional: false
39668
40058
  }],
40059
+ "recording.setDevicePlacement": [{
40060
+ name: "deviceId",
40061
+ form: "single",
40062
+ optional: false
40063
+ }],
39669
40064
  "recording.startStorageMigrationMove": [{
39670
40065
  name: "deviceId",
39671
40066
  form: "single",
@@ -39706,6 +40101,11 @@ Object.freeze({
39706
40101
  form: "single",
39707
40102
  optional: false
39708
40103
  }],
40104
+ "sceneMonitor.listScenesBatch": [{
40105
+ name: "deviceIds",
40106
+ form: "array",
40107
+ optional: false
40108
+ }],
39709
40109
  "sceneMonitor.recheckNow": [{
39710
40110
  name: "deviceId",
39711
40111
  form: "single",
@@ -39967,6 +40367,11 @@ Object.freeze({
39967
40367
  form: "single",
39968
40368
  optional: false
39969
40369
  }],
40370
+ "zoneAnalytics.getCurrentSnapshotBatch": [{
40371
+ name: "deviceIds",
40372
+ form: "array",
40373
+ optional: false
40374
+ }],
39970
40375
  "zoneAnalytics.getUnzonedHistory": [{
39971
40376
  name: "deviceId",
39972
40377
  form: "single",
@@ -40540,6 +40945,111 @@ function resolveGlancesCursesShimEnv(options) {
40540
40945
  return { PYTHONPATH: existing ? `${options.shimDir}:${existing}` : options.shimDir };
40541
40946
  }
40542
40947
  //#endregion
40948
+ //#region src/profile-settings.ts
40949
+ var GLANCES_PLUGINS = [
40950
+ {
40951
+ key: "showCpu",
40952
+ plugin: "cpu",
40953
+ label: "CPU"
40954
+ },
40955
+ {
40956
+ key: "showMem",
40957
+ plugin: "mem",
40958
+ label: "Memory"
40959
+ },
40960
+ {
40961
+ key: "showLoad",
40962
+ plugin: "load",
40963
+ label: "Load"
40964
+ },
40965
+ {
40966
+ key: "showNetwork",
40967
+ plugin: "network",
40968
+ label: "Network"
40969
+ },
40970
+ {
40971
+ key: "showDiskIo",
40972
+ plugin: "diskio",
40973
+ label: "Disk I/O"
40974
+ },
40975
+ {
40976
+ key: "showFs",
40977
+ plugin: "fs",
40978
+ label: "Filesystems"
40979
+ },
40980
+ {
40981
+ key: "showProcessList",
40982
+ plugin: "processlist",
40983
+ label: "Process list"
40984
+ },
40985
+ {
40986
+ key: "showContainers",
40987
+ plugin: "containers",
40988
+ label: "Containers"
40989
+ },
40990
+ {
40991
+ key: "showSensors",
40992
+ plugin: "sensors",
40993
+ label: "Sensors"
40994
+ }
40995
+ ];
40996
+ function glancesBooleanField(key, label) {
40997
+ return {
40998
+ type: "boolean",
40999
+ key,
41000
+ label,
41001
+ default: true,
41002
+ style: "switch"
41003
+ };
41004
+ }
41005
+ function glancesSettingsSchema() {
41006
+ return { sections: [{
41007
+ id: "glances-panels",
41008
+ title: "Glances panels",
41009
+ description: "Turn off a panel to pass --disable-plugin to this Terminal only. All on is the measured default (~1% of one core at the camera grid).",
41010
+ columns: 2,
41011
+ fields: GLANCES_PLUGINS.map((plugin) => glancesBooleanField(plugin.key, plugin.label))
41012
+ }] };
41013
+ }
41014
+ function settingsSchemaForProfile(profileId) {
41015
+ if (profileId === "glances") return glancesSettingsSchema();
41016
+ return null;
41017
+ }
41018
+ function glancesSettingsToArgs(settings) {
41019
+ const disabled = GLANCES_PLUGINS.filter((plugin) => settings[plugin.key] === false).map((plugin) => plugin.plugin);
41020
+ if (disabled.length === 0) return [];
41021
+ return ["--disable-plugin", disabled.join(",")];
41022
+ }
41023
+ function sanitizeProfileSettings(profileId, raw) {
41024
+ if (profileId !== "glances") return {};
41025
+ const bag = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
41026
+ const out = {
41027
+ showCpu: true,
41028
+ showMem: true,
41029
+ showLoad: true,
41030
+ showNetwork: true,
41031
+ showDiskIo: true,
41032
+ showFs: true,
41033
+ showProcessList: true,
41034
+ showContainers: true,
41035
+ showSensors: true
41036
+ };
41037
+ for (const plugin of GLANCES_PLUGINS) {
41038
+ const value = bag[plugin.key];
41039
+ if (typeof value === "boolean") out[plugin.key] = value;
41040
+ }
41041
+ return out;
41042
+ }
41043
+ function profileSettingsToArgs(profileId, settings) {
41044
+ if (profileId !== "glances") return [];
41045
+ return glancesSettingsToArgs(sanitizeProfileSettings(profileId, settings));
41046
+ }
41047
+ function spawnArgsForInstance(input) {
41048
+ const extra = profileSettingsToArgs(input.profileId, input.profileSettings);
41049
+ if (input.instanceArgs.length > 0) return [...input.instanceArgs, ...extra];
41050
+ if (extra.length > 0) return [...input.profileArgs, ...extra];
41051
+ }
41052
+ //#endregion
40543
41053
  //#region src/pty.ts
40544
41054
  /**
40545
41055
  * Minimal pty abstraction. The manager depends on this interface, never on
@@ -40721,6 +41231,356 @@ async function silenceAnalysisFor(deps, deviceId) {
40721
41231
  if (failures.length > 0) throw new Error(`terminal camera ${deviceId}: could not switch off ${failures.length} analyzer(s) — it will run at full detection cost (${failures.join("; ")})`);
40722
41232
  }
40723
41233
  //#endregion
41234
+ //#region src/terminal-camera-declarations.ts
41235
+ /**
41236
+ * Feed DeclaredDevices every live declaration plus one deterministic orphan
41237
+ * batch. The generic sweep intentionally refuses an over-limit set; selecting
41238
+ * a batch here drains large historical Terminal orphan sets across convergence
41239
+ * passes without weakening that global safety guard.
41240
+ */
41241
+ function terminalCameraReconciliationIndex(rows, integrationId, declarations, managedStableIds = /* @__PURE__ */ new Set()) {
41242
+ if (!integrationId) return [];
41243
+ const declared = new Set(declarations.map((camera) => camera.stableId));
41244
+ const owned = rows.filter((row) => row.integrationId === integrationId && (declared.has(row.stableId) || managedStableIds.has(row.stableId) || row.stableId.startsWith("terminal-camera-instance-")));
41245
+ return [...owned.filter((row) => declared.has(row.stableId)), ...owned.filter((row) => !declared.has(row.stableId)).sort((left, right) => left.id - right.id).slice(0, 32)];
41246
+ }
41247
+ /** Explicit persisted instances, never the node × profile template matrix. */
41248
+ function buildTerminalInstanceCameraDeclarations(instances) {
41249
+ return instances.filter((instance) => instance.enabled).map((instance) => ({
41250
+ stableId: instance.cameraStableId,
41251
+ name: instance.name,
41252
+ config: {
41253
+ instanceId: instance.id,
41254
+ nodeId: instance.nodeId,
41255
+ profileId: instance.profileId,
41256
+ profileLabel: instance.profileLabel
41257
+ }
41258
+ }));
41259
+ }
41260
+ /**
41261
+ * `DeviceConfig` materializes schema defaults in memory, so comparing
41262
+ * `config.get()` cannot detect a legacy `{ nodeId }` row. Reconciliation must
41263
+ * inspect the raw persisted blob to make the profile migration durable.
41264
+ */
41265
+ function needsTerminalCameraConfigMigration(persistedConfig, declaration) {
41266
+ return persistedConfig.instanceId !== declaration.config.instanceId || persistedConfig.nodeId !== declaration.config.nodeId || persistedConfig.profileId !== declaration.config.profileId || persistedConfig.profileLabel !== declaration.config.profileLabel;
41267
+ }
41268
+ //#endregion
41269
+ //#region src/terminal-cell-runs.ts
41270
+ var TERMINAL_DEFAULT_FG = "#d7dce2";
41271
+ var TERMINAL_DEFAULT_BG = "#0b0d10";
41272
+ /**
41273
+ * The 16 ANSI colours, in a dark-theme variant chosen to sit with the existing
41274
+ * frame (`#0b0d10` ground, `#d7dce2` text) rather than the raw xterm defaults,
41275
+ * whose pure `#0000ff` blue and `#008000` green are close to unreadable on a
41276
+ * near-black background at 13px. Index 7 IS the default foreground, so plain
41277
+ * `CSI 37m` text renders identically to unstyled text.
41278
+ */
41279
+ var TERMINAL_ANSI_PALETTE = [
41280
+ "#282c34",
41281
+ "#e06c75",
41282
+ "#98c379",
41283
+ "#e5c07b",
41284
+ "#61afef",
41285
+ "#c678dd",
41286
+ "#56b6c2",
41287
+ TERMINAL_DEFAULT_FG,
41288
+ "#5c6370",
41289
+ "#ef596f",
41290
+ "#89ca78",
41291
+ "#f0c674",
41292
+ "#6cb6ff",
41293
+ "#d55fde",
41294
+ "#2bbac5",
41295
+ "#ffffff"
41296
+ ];
41297
+ /** The six levels of the xterm 6×6×6 colour cube (indices 16-231). */
41298
+ var TERMINAL_CUBE_LEVELS = [
41299
+ 0,
41300
+ 95,
41301
+ 135,
41302
+ 175,
41303
+ 215,
41304
+ 255
41305
+ ];
41306
+ var TERMINAL_CUBE_FIRST = 16;
41307
+ var TERMINAL_GRAYSCALE_FIRST = 232;
41308
+ var TERMINAL_GRAYSCALE_BASE = 8;
41309
+ var TERMINAL_GRAYSCALE_STEP = 10;
41310
+ /** SGR 2 keeps the foreground legible; it must not become the background. */
41311
+ var TERMINAL_DIM_WEIGHT = .6;
41312
+ function channel(value) {
41313
+ return Math.max(0, Math.min(255, Math.round(value))).toString(16).padStart(2, "0");
41314
+ }
41315
+ function hex(red, green, blue) {
41316
+ return `#${channel(red)}${channel(green)}${channel(blue)}`;
41317
+ }
41318
+ function parseHex(color) {
41319
+ return [
41320
+ Number.parseInt(color.slice(1, 3), 16),
41321
+ Number.parseInt(color.slice(3, 5), 16),
41322
+ Number.parseInt(color.slice(5, 7), 16)
41323
+ ];
41324
+ }
41325
+ /** Resolve an xterm palette index (0-255) to a hex colour. */
41326
+ function terminalPaletteColor(index) {
41327
+ const ansi = TERMINAL_ANSI_PALETTE[index];
41328
+ if (ansi !== void 0) return ansi;
41329
+ if (index >= TERMINAL_GRAYSCALE_FIRST) {
41330
+ const level = TERMINAL_GRAYSCALE_BASE + (index - TERMINAL_GRAYSCALE_FIRST) * TERMINAL_GRAYSCALE_STEP;
41331
+ return hex(level, level, level);
41332
+ }
41333
+ if (index >= TERMINAL_CUBE_FIRST) {
41334
+ const offset = index - TERMINAL_CUBE_FIRST;
41335
+ return hex(TERMINAL_CUBE_LEVELS[Math.floor(offset / 36) % 6] ?? 0, TERMINAL_CUBE_LEVELS[Math.floor(offset / 6) % 6] ?? 0, TERMINAL_CUBE_LEVELS[offset % 6] ?? 0);
41336
+ }
41337
+ return TERMINAL_DEFAULT_FG;
41338
+ }
41339
+ /** Resolve a 0xRRGGBB truecolor value to a hex colour. */
41340
+ function terminalRgbColor(value) {
41341
+ return hex(value >> 16 & 255, value >> 8 & 255, value & 255);
41342
+ }
41343
+ function blend(color, toward, weight) {
41344
+ const [red, green, blue] = parseHex(color);
41345
+ const [targetRed, targetGreen, targetBlue] = parseHex(toward);
41346
+ return hex(red * weight + targetRed * (1 - weight), green * weight + targetGreen * (1 - weight), blue * weight + targetBlue * (1 - weight));
41347
+ }
41348
+ function resolveForeground(cell) {
41349
+ if (cell.isFgRGB()) return terminalRgbColor(cell.getFgColor());
41350
+ if (cell.isFgPalette()) return terminalPaletteColor(cell.getFgColor());
41351
+ return TERMINAL_DEFAULT_FG;
41352
+ }
41353
+ function resolveBackground(cell) {
41354
+ if (cell.isBgRGB()) return terminalRgbColor(cell.getBgColor());
41355
+ if (cell.isBgPalette()) return terminalPaletteColor(cell.getBgColor());
41356
+ return TERMINAL_DEFAULT_BG;
41357
+ }
41358
+ /**
41359
+ * Resolve one cell's attributes into concrete colours.
41360
+ *
41361
+ * Inverse is applied by SWAPPING the already-resolved pair, so inverse over two
41362
+ * defaults is still a visible swap rather than a no-op — that is how a selected
41363
+ * or highlighted row in Glances reads. Invisible is then conceal-by-equality
41364
+ * (foreground painted in its own background): the cell keeps its columns, which
41365
+ * a dropped cell would not, and dropping it would shift the whole rest of the
41366
+ * row left.
41367
+ */
41368
+ function resolveCellStyle(cell) {
41369
+ const inverse = cell.isInverse() !== 0;
41370
+ const plainFg = resolveForeground(cell);
41371
+ const plainBg = resolveBackground(cell);
41372
+ const background = inverse ? plainFg : plainBg;
41373
+ let foreground = inverse ? plainBg : plainFg;
41374
+ if (cell.isDim() !== 0) foreground = blend(foreground, background, TERMINAL_DIM_WEIGHT);
41375
+ if (cell.isInvisible() !== 0) foreground = background;
41376
+ return {
41377
+ fg: foreground === "#d7dce2" ? null : foreground,
41378
+ bg: background === "#0b0d10" ? null : background,
41379
+ bold: cell.isBold() !== 0
41380
+ };
41381
+ }
41382
+ function sameStyle(left, right) {
41383
+ return left.fg === right.fg && left.bg === right.bg && left.bold === right.bold;
41384
+ }
41385
+ /**
41386
+ * Merge adjacent same-style cells into runs, then drop the trailing run of
41387
+ * default-styled whitespace so a row costs what it draws — the same trim
41388
+ * `translateToString(true)` performs. Whitespace carrying a BACKGROUND is kept:
41389
+ * a green bar of spaces out to the right margin is a pixel Glances drew.
41390
+ */
41391
+ function buildCellRuns(cells) {
41392
+ const runs = [];
41393
+ let text = "";
41394
+ let style = null;
41395
+ for (const cell of cells) {
41396
+ if (style !== null && sameStyle(style, cell.style)) {
41397
+ text += cell.text;
41398
+ continue;
41399
+ }
41400
+ if (style !== null) runs.push({
41401
+ text,
41402
+ ...style
41403
+ });
41404
+ text = cell.text;
41405
+ style = cell.style;
41406
+ }
41407
+ if (style !== null) runs.push({
41408
+ text,
41409
+ ...style
41410
+ });
41411
+ while (runs.length > 0) {
41412
+ const last = runs[runs.length - 1];
41413
+ if (last === void 0 || last.bg !== null) break;
41414
+ const trimmed = last.text.replace(/\s+$/u, "");
41415
+ if (trimmed === last.text) break;
41416
+ if (trimmed === "") {
41417
+ runs.pop();
41418
+ continue;
41419
+ }
41420
+ runs[runs.length - 1] = {
41421
+ ...last,
41422
+ text: trimmed
41423
+ };
41424
+ break;
41425
+ }
41426
+ return runs;
41427
+ }
41428
+ /**
41429
+ * Monospace families to try, in order — NOT one family and a generic.
41430
+ *
41431
+ * A terminal screen is mostly box-drawing and block characters, and a font
41432
+ * without them renders the frame as noise rather than as missing detail.
41433
+ * `DejaVu Sans Mono` covers them and ships in the hub image; macOS has neither
41434
+ * DejaVu nor fontconfig, so the Mac agent fell through to a generic whose
41435
+ * coverage is not, and its Glances camera came out unreadable while the hub's
41436
+ * was pixel-perfect (measured 2026-08-11, same Glances, two nodes).
41437
+ *
41438
+ * `Menlo` is the fix rather than a guess: it is macOS's default terminal face,
41439
+ * present on every install, and derived from DejaVu Sans Mono — the same glyph
41440
+ * coverage by ancestry. Monaco and Courier New follow as older fallbacks; the
41441
+ * generic stays last so a host with none of them still draws something.
41442
+ */
41443
+ var TERMINAL_FONT_STACK = "DejaVu Sans Mono,Menlo,Monaco,Courier New,monospace";
41444
+ var TERMINAL_FONT_SIZE = 13;
41445
+ var TERMINAL_TEXT_MARGIN_X = 8;
41446
+ var TERMINAL_ROW_HEIGHT = 15;
41447
+ var TERMINAL_BASELINE_Y = 18;
41448
+ /**
41449
+ * Distance from a row's baseline up to the top of its cell box. Chosen so
41450
+ * consecutive rows tile exactly: row N's box runs from `baseline - this` for
41451
+ * `TERMINAL_ROW_HEIGHT`, and row N+1's box starts where it ends. A background
41452
+ * bar that stopped short would draw as stripes across a `CSI 42m` panel.
41453
+ */
41454
+ var TERMINAL_CELL_ASCENT = 11.5;
41455
+ var TERMINAL_CELL_WIDTH = TERMINAL_FONT_SIZE * (1233 / 2048);
41456
+ /** Two decimals is under a tenth of a pixel and keeps the SVG small. */
41457
+ function coordinate(value) {
41458
+ return String(Number(value.toFixed(2)));
41459
+ }
41460
+ function escapeXml(value) {
41461
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;");
41462
+ }
41463
+ /**
41464
+ * Render already-interpreted terminal rows into a compact MJPEG frame.
41465
+ *
41466
+ * `xml:space="preserve"` is load-bearing, not tidiness. SVG `<text>` collapses
41467
+ * runs of whitespace by default, and a terminal's entire column alignment IS
41468
+ * runs of whitespace — Glances pads every field with spaces. Without it the
41469
+ * frame drew each line at roughly half its true width, crammed into the
41470
+ * top-left of a mostly-black image, while the SAME session over `attach`
41471
+ * looked perfect — which is exactly how the operator reported it. Measured in
41472
+ * the hub's own rasterizer 2026-08-11: `AAA<10 spaces>BBB<3>CCC` drew 92 px
41473
+ * collapsed against 178 px preserved.
41474
+ *
41475
+ * Every run is anchored at its OWN column (`x = margin + startCol * cellW`),
41476
+ * never appended to the one before it, so the background rects and the glyphs
41477
+ * are placed off the same grid and cannot drift apart. `textLength` is emitted
41478
+ * with it because it is the correct declaration and renderers that honour it
41479
+ * get an exact grid — but it is not what makes this work: librsvg, which sharp
41480
+ * uses, ignores it outright (measured 2026-08-12, a 120-glyph run pinned to
41481
+ * 600 px still drew its natural 937 px). The anchoring is the guarantee.
41482
+ */
41483
+ function renderTerminalSvg(rows) {
41484
+ const backgrounds = [];
41485
+ const texts = [];
41486
+ rows.slice(0, 40).forEach((row, index) => {
41487
+ const baseline = TERMINAL_BASELINE_Y + index * TERMINAL_ROW_HEIGHT;
41488
+ const top = baseline - TERMINAL_CELL_ASCENT;
41489
+ let column = 0;
41490
+ for (const run of row) {
41491
+ if (column >= 120) break;
41492
+ const clipped = clipRun(run, 120 - column);
41493
+ const columns = [...clipped].length;
41494
+ if (columns === 0) continue;
41495
+ const x = TERMINAL_TEXT_MARGIN_X + column * TERMINAL_CELL_WIDTH;
41496
+ const width = columns * TERMINAL_CELL_WIDTH;
41497
+ if (run.bg !== null) backgrounds.push(`<rect x="${coordinate(x)}" y="${coordinate(top)}" width="${coordinate(width)}" height="${String(TERMINAL_ROW_HEIGHT)}" fill="${run.bg}"/>`);
41498
+ if (clipped.trim() !== "") {
41499
+ const fill = run.fg === null ? "" : ` fill="${run.fg}"`;
41500
+ const weight = run.bold ? " font-weight=\"bold\"" : "";
41501
+ texts.push(`<text xml:space="preserve" x="${coordinate(x)}" y="${coordinate(baseline)}" textLength="${coordinate(width)}" lengthAdjust="spacingAndGlyphs"${fill}${weight}>${escapeXml(clipped)}</text>`);
41502
+ }
41503
+ column += columns;
41504
+ }
41505
+ });
41506
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${String(960)}" height="${String(640)}"><rect width="100%" height="100%" fill="${TERMINAL_DEFAULT_BG}"/>${backgrounds.join("")}<g fill="${TERMINAL_DEFAULT_FG}" font-family="${TERMINAL_FONT_STACK}" font-size="${String(TERMINAL_FONT_SIZE)}">${texts.join("")}</g></svg>`;
41507
+ }
41508
+ /** Cut a run to the columns still left in the row, by code point not unit. */
41509
+ function clipRun(run, remaining) {
41510
+ const points = [...run.text];
41511
+ return points.length <= remaining ? run.text : points.slice(0, remaining).join("");
41512
+ }
41513
+ async function renderTerminalJpeg(rows) {
41514
+ return sharp(Buffer.from(renderTerminalSvg(rows))).jpeg({
41515
+ quality: 82,
41516
+ chromaSubsampling: "4:2:0"
41517
+ }).toBuffer();
41518
+ }
41519
+ //#endregion
41520
+ //#region src/terminal-camera-device.ts
41521
+ var terminalCameraSchema = object({
41522
+ instanceId: string().min(1).optional(),
41523
+ nodeId: string().min(1),
41524
+ profileId: string().min(1).default("monitor"),
41525
+ profileLabel: string().min(1).default("BTM")
41526
+ });
41527
+ var relay = null;
41528
+ function installTerminalCameraRelay(next) {
41529
+ relay = next;
41530
+ }
41531
+ var TerminalCameraDevice = class extends BaseDevice {
41532
+ features = [DeviceFeature.NativeSnapshot];
41533
+ constructor(ctx) {
41534
+ super(ctx, terminalCameraSchema, { type: ctx.deviceMeta.type });
41535
+ this.ctx.registerNativeCap(streamCatalogCapability, { getCatalog: async ({ deviceId }) => {
41536
+ if (deviceId !== this.id) return [];
41537
+ return this.catalog();
41538
+ } });
41539
+ this.ctx.registerNativeCap(snapshotCapability, {
41540
+ getSnapshot: async ({ deviceId }) => {
41541
+ if (deviceId !== this.id) throw new Error(`TerminalCameraDevice: deviceId mismatch, expected ${String(this.id)}, got ${String(deviceId)}`);
41542
+ const activeRelay = relay;
41543
+ if (!activeRelay) throw new Error("terminal camera relay is unavailable");
41544
+ return {
41545
+ base64: (await activeRelay.snapshot(this.relayInstanceId(), this.config.get("nodeId"), this.config.get("profileId"))).toString("base64"),
41546
+ contentType: "image/jpeg"
41547
+ };
41548
+ },
41549
+ invalidateCache: async () => {}
41550
+ });
41551
+ this.markOnline(true);
41552
+ }
41553
+ async catalog() {
41554
+ const activeRelay = relay;
41555
+ if (!activeRelay) throw new Error("terminal camera relay is unavailable");
41556
+ const nodeId = this.config.get("nodeId");
41557
+ const profileId = this.config.get("profileId");
41558
+ const instanceId = this.relayInstanceId();
41559
+ return [{
41560
+ camStreamId: profileId,
41561
+ kind: "pull-http",
41562
+ url: activeRelay.streamUrl(instanceId, nodeId, profileId),
41563
+ codec: "h264",
41564
+ resolution: {
41565
+ width: 960,
41566
+ height: 640
41567
+ },
41568
+ fps: 2,
41569
+ label: this.config.get("profileLabel")
41570
+ }];
41571
+ }
41572
+ setNodeOnline(online) {
41573
+ this.markOnline(online);
41574
+ if (!online) relay?.closeInstance(this.relayInstanceId());
41575
+ }
41576
+ async removeDevice() {
41577
+ await relay?.closeInstance(this.relayInstanceId());
41578
+ }
41579
+ relayInstanceId() {
41580
+ return this.config.get("instanceId") ?? `legacy:${this.stableId}`;
41581
+ }
41582
+ };
41583
+ //#endregion
40724
41584
  //#region ../../node_modules/@xterm/addon-serialize/lib/addon-serialize.js
40725
41585
  var require_addon_serialize = /* @__PURE__ */ __commonJSMin(((exports, module) => {
40726
41586
  (function(e, t) {
@@ -45528,174 +46388,9 @@ var require_xterm_headless = /* @__PURE__ */ __commonJSMin(((exports) => {
45528
46388
  })();
45529
46389
  }));
45530
46390
  //#endregion
45531
- //#region src/terminal-cell-runs.ts
46391
+ //#region src/xterm-screen.ts
45532
46392
  var import_addon_serialize = require_addon_serialize();
45533
46393
  var import_xterm_headless = require_xterm_headless();
45534
- var TERMINAL_DEFAULT_FG = "#d7dce2";
45535
- var TERMINAL_DEFAULT_BG = "#0b0d10";
45536
- /**
45537
- * The 16 ANSI colours, in a dark-theme variant chosen to sit with the existing
45538
- * frame (`#0b0d10` ground, `#d7dce2` text) rather than the raw xterm defaults,
45539
- * whose pure `#0000ff` blue and `#008000` green are close to unreadable on a
45540
- * near-black background at 13px. Index 7 IS the default foreground, so plain
45541
- * `CSI 37m` text renders identically to unstyled text.
45542
- */
45543
- var TERMINAL_ANSI_PALETTE = [
45544
- "#282c34",
45545
- "#e06c75",
45546
- "#98c379",
45547
- "#e5c07b",
45548
- "#61afef",
45549
- "#c678dd",
45550
- "#56b6c2",
45551
- TERMINAL_DEFAULT_FG,
45552
- "#5c6370",
45553
- "#ef596f",
45554
- "#89ca78",
45555
- "#f0c674",
45556
- "#6cb6ff",
45557
- "#d55fde",
45558
- "#2bbac5",
45559
- "#ffffff"
45560
- ];
45561
- /** The six levels of the xterm 6×6×6 colour cube (indices 16-231). */
45562
- var TERMINAL_CUBE_LEVELS = [
45563
- 0,
45564
- 95,
45565
- 135,
45566
- 175,
45567
- 215,
45568
- 255
45569
- ];
45570
- var TERMINAL_CUBE_FIRST = 16;
45571
- var TERMINAL_GRAYSCALE_FIRST = 232;
45572
- var TERMINAL_GRAYSCALE_BASE = 8;
45573
- var TERMINAL_GRAYSCALE_STEP = 10;
45574
- /** SGR 2 keeps the foreground legible; it must not become the background. */
45575
- var TERMINAL_DIM_WEIGHT = .6;
45576
- function channel(value) {
45577
- return Math.max(0, Math.min(255, Math.round(value))).toString(16).padStart(2, "0");
45578
- }
45579
- function hex(red, green, blue) {
45580
- return `#${channel(red)}${channel(green)}${channel(blue)}`;
45581
- }
45582
- function parseHex(color) {
45583
- return [
45584
- Number.parseInt(color.slice(1, 3), 16),
45585
- Number.parseInt(color.slice(3, 5), 16),
45586
- Number.parseInt(color.slice(5, 7), 16)
45587
- ];
45588
- }
45589
- /** Resolve an xterm palette index (0-255) to a hex colour. */
45590
- function terminalPaletteColor(index) {
45591
- const ansi = TERMINAL_ANSI_PALETTE[index];
45592
- if (ansi !== void 0) return ansi;
45593
- if (index >= TERMINAL_GRAYSCALE_FIRST) {
45594
- const level = TERMINAL_GRAYSCALE_BASE + (index - TERMINAL_GRAYSCALE_FIRST) * TERMINAL_GRAYSCALE_STEP;
45595
- return hex(level, level, level);
45596
- }
45597
- if (index >= TERMINAL_CUBE_FIRST) {
45598
- const offset = index - TERMINAL_CUBE_FIRST;
45599
- return hex(TERMINAL_CUBE_LEVELS[Math.floor(offset / 36) % 6] ?? 0, TERMINAL_CUBE_LEVELS[Math.floor(offset / 6) % 6] ?? 0, TERMINAL_CUBE_LEVELS[offset % 6] ?? 0);
45600
- }
45601
- return TERMINAL_DEFAULT_FG;
45602
- }
45603
- /** Resolve a 0xRRGGBB truecolor value to a hex colour. */
45604
- function terminalRgbColor(value) {
45605
- return hex(value >> 16 & 255, value >> 8 & 255, value & 255);
45606
- }
45607
- function blend(color, toward, weight) {
45608
- const [red, green, blue] = parseHex(color);
45609
- const [targetRed, targetGreen, targetBlue] = parseHex(toward);
45610
- return hex(red * weight + targetRed * (1 - weight), green * weight + targetGreen * (1 - weight), blue * weight + targetBlue * (1 - weight));
45611
- }
45612
- function resolveForeground(cell) {
45613
- if (cell.isFgRGB()) return terminalRgbColor(cell.getFgColor());
45614
- if (cell.isFgPalette()) return terminalPaletteColor(cell.getFgColor());
45615
- return TERMINAL_DEFAULT_FG;
45616
- }
45617
- function resolveBackground(cell) {
45618
- if (cell.isBgRGB()) return terminalRgbColor(cell.getBgColor());
45619
- if (cell.isBgPalette()) return terminalPaletteColor(cell.getBgColor());
45620
- return TERMINAL_DEFAULT_BG;
45621
- }
45622
- /**
45623
- * Resolve one cell's attributes into concrete colours.
45624
- *
45625
- * Inverse is applied by SWAPPING the already-resolved pair, so inverse over two
45626
- * defaults is still a visible swap rather than a no-op — that is how a selected
45627
- * or highlighted row in Glances reads. Invisible is then conceal-by-equality
45628
- * (foreground painted in its own background): the cell keeps its columns, which
45629
- * a dropped cell would not, and dropping it would shift the whole rest of the
45630
- * row left.
45631
- */
45632
- function resolveCellStyle(cell) {
45633
- const inverse = cell.isInverse() !== 0;
45634
- const plainFg = resolveForeground(cell);
45635
- const plainBg = resolveBackground(cell);
45636
- const background = inverse ? plainFg : plainBg;
45637
- let foreground = inverse ? plainBg : plainFg;
45638
- if (cell.isDim() !== 0) foreground = blend(foreground, background, TERMINAL_DIM_WEIGHT);
45639
- if (cell.isInvisible() !== 0) foreground = background;
45640
- return {
45641
- fg: foreground === "#d7dce2" ? null : foreground,
45642
- bg: background === "#0b0d10" ? null : background,
45643
- bold: cell.isBold() !== 0
45644
- };
45645
- }
45646
- function sameStyle(left, right) {
45647
- return left.fg === right.fg && left.bg === right.bg && left.bold === right.bold;
45648
- }
45649
- /**
45650
- * Merge adjacent same-style cells into runs, then drop the trailing run of
45651
- * default-styled whitespace so a row costs what it draws — the same trim
45652
- * `translateToString(true)` performs. Whitespace carrying a BACKGROUND is kept:
45653
- * a green bar of spaces out to the right margin is a pixel Glances drew.
45654
- */
45655
- function buildCellRuns(cells) {
45656
- const runs = [];
45657
- let text = "";
45658
- let style = null;
45659
- for (const cell of cells) {
45660
- if (style !== null && sameStyle(style, cell.style)) {
45661
- text += cell.text;
45662
- continue;
45663
- }
45664
- if (style !== null) runs.push({
45665
- text,
45666
- ...style
45667
- });
45668
- text = cell.text;
45669
- style = cell.style;
45670
- }
45671
- if (style !== null) runs.push({
45672
- text,
45673
- ...style
45674
- });
45675
- while (runs.length > 0) {
45676
- const last = runs[runs.length - 1];
45677
- if (last === void 0 || last.bg !== null) break;
45678
- const trimmed = last.text.replace(/\s+$/u, "");
45679
- if (trimmed === last.text) break;
45680
- if (trimmed === "") {
45681
- runs.pop();
45682
- continue;
45683
- }
45684
- runs[runs.length - 1] = {
45685
- ...last,
45686
- text: trimmed
45687
- };
45688
- break;
45689
- }
45690
- return runs;
45691
- }
45692
- //#endregion
45693
- //#region src/xterm-screen.ts
45694
- /**
45695
- * Real {@link ScreenBuffer} backed by `@xterm/headless` — the DOM-less xterm
45696
- * build — plus the serialize addon, which turns the current buffer into a
45697
- * self-contained repaint escape sequence for reconnecting clients.
45698
- */
45699
46394
  var SCROLLBACK_LINES = 2e3;
45700
46395
  function createXtermScreen(cols, rows) {
45701
46396
  const term = new import_xterm_headless.Terminal({
@@ -45762,196 +46457,6 @@ function createXtermScreen(cols, rows) {
45762
46457
  };
45763
46458
  }
45764
46459
  //#endregion
45765
- //#region src/terminal-camera-declarations.ts
45766
- /**
45767
- * Feed DeclaredDevices every live declaration plus one deterministic orphan
45768
- * batch. The generic sweep intentionally refuses an over-limit set; selecting
45769
- * a batch here drains large historical Terminal orphan sets across convergence
45770
- * passes without weakening that global safety guard.
45771
- */
45772
- function terminalCameraReconciliationIndex(rows, integrationId, declarations, managedStableIds = /* @__PURE__ */ new Set()) {
45773
- if (!integrationId) return [];
45774
- const declared = new Set(declarations.map((camera) => camera.stableId));
45775
- const owned = rows.filter((row) => row.integrationId === integrationId && (declared.has(row.stableId) || managedStableIds.has(row.stableId) || row.stableId.startsWith("terminal-camera-instance-")));
45776
- return [...owned.filter((row) => declared.has(row.stableId)), ...owned.filter((row) => !declared.has(row.stableId)).sort((left, right) => left.id - right.id).slice(0, 32)];
45777
- }
45778
- /** Explicit persisted instances, never the node × profile template matrix. */
45779
- function buildTerminalInstanceCameraDeclarations(instances) {
45780
- return instances.filter((instance) => instance.enabled).map((instance) => ({
45781
- stableId: instance.cameraStableId,
45782
- name: instance.name,
45783
- config: {
45784
- instanceId: instance.id,
45785
- nodeId: instance.nodeId,
45786
- profileId: instance.profileId,
45787
- profileLabel: instance.profileLabel
45788
- }
45789
- }));
45790
- }
45791
- /**
45792
- * `DeviceConfig` materializes schema defaults in memory, so comparing
45793
- * `config.get()` cannot detect a legacy `{ nodeId }` row. Reconciliation must
45794
- * inspect the raw persisted blob to make the profile migration durable.
45795
- */
45796
- function needsTerminalCameraConfigMigration(persistedConfig, declaration) {
45797
- return persistedConfig.instanceId !== declaration.config.instanceId || persistedConfig.nodeId !== declaration.config.nodeId || persistedConfig.profileId !== declaration.config.profileId || persistedConfig.profileLabel !== declaration.config.profileLabel;
45798
- }
45799
- /**
45800
- * Monospace families to try, in order — NOT one family and a generic.
45801
- *
45802
- * A terminal screen is mostly box-drawing and block characters, and a font
45803
- * without them renders the frame as noise rather than as missing detail.
45804
- * `DejaVu Sans Mono` covers them and ships in the hub image; macOS has neither
45805
- * DejaVu nor fontconfig, so the Mac agent fell through to a generic whose
45806
- * coverage is not, and its Glances camera came out unreadable while the hub's
45807
- * was pixel-perfect (measured 2026-08-11, same Glances, two nodes).
45808
- *
45809
- * `Menlo` is the fix rather than a guess: it is macOS's default terminal face,
45810
- * present on every install, and derived from DejaVu Sans Mono — the same glyph
45811
- * coverage by ancestry. Monaco and Courier New follow as older fallbacks; the
45812
- * generic stays last so a host with none of them still draws something.
45813
- */
45814
- var TERMINAL_FONT_STACK = "DejaVu Sans Mono,Menlo,Monaco,Courier New,monospace";
45815
- var TERMINAL_FONT_SIZE = 13;
45816
- var TERMINAL_TEXT_MARGIN_X = 8;
45817
- var TERMINAL_ROW_HEIGHT = 15;
45818
- var TERMINAL_BASELINE_Y = 18;
45819
- /**
45820
- * Distance from a row's baseline up to the top of its cell box. Chosen so
45821
- * consecutive rows tile exactly: row N's box runs from `baseline - this` for
45822
- * `TERMINAL_ROW_HEIGHT`, and row N+1's box starts where it ends. A background
45823
- * bar that stopped short would draw as stripes across a `CSI 42m` panel.
45824
- */
45825
- var TERMINAL_CELL_ASCENT = 11.5;
45826
- var TERMINAL_CELL_WIDTH = TERMINAL_FONT_SIZE * (1233 / 2048);
45827
- /** Two decimals is under a tenth of a pixel and keeps the SVG small. */
45828
- function coordinate(value) {
45829
- return String(Number(value.toFixed(2)));
45830
- }
45831
- function escapeXml(value) {
45832
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;");
45833
- }
45834
- /**
45835
- * Render already-interpreted terminal rows into a compact MJPEG frame.
45836
- *
45837
- * `xml:space="preserve"` is load-bearing, not tidiness. SVG `<text>` collapses
45838
- * runs of whitespace by default, and a terminal's entire column alignment IS
45839
- * runs of whitespace — Glances pads every field with spaces. Without it the
45840
- * frame drew each line at roughly half its true width, crammed into the
45841
- * top-left of a mostly-black image, while the SAME session over `attach`
45842
- * looked perfect — which is exactly how the operator reported it. Measured in
45843
- * the hub's own rasterizer 2026-08-11: `AAA<10 spaces>BBB<3>CCC` drew 92 px
45844
- * collapsed against 178 px preserved.
45845
- *
45846
- * Every run is anchored at its OWN column (`x = margin + startCol * cellW`),
45847
- * never appended to the one before it, so the background rects and the glyphs
45848
- * are placed off the same grid and cannot drift apart. `textLength` is emitted
45849
- * with it because it is the correct declaration and renderers that honour it
45850
- * get an exact grid — but it is not what makes this work: librsvg, which sharp
45851
- * uses, ignores it outright (measured 2026-08-12, a 120-glyph run pinned to
45852
- * 600 px still drew its natural 937 px). The anchoring is the guarantee.
45853
- */
45854
- function renderTerminalSvg(rows) {
45855
- const backgrounds = [];
45856
- const texts = [];
45857
- rows.slice(0, 40).forEach((row, index) => {
45858
- const baseline = TERMINAL_BASELINE_Y + index * TERMINAL_ROW_HEIGHT;
45859
- const top = baseline - TERMINAL_CELL_ASCENT;
45860
- let column = 0;
45861
- for (const run of row) {
45862
- if (column >= 120) break;
45863
- const clipped = clipRun(run, 120 - column);
45864
- const columns = [...clipped].length;
45865
- if (columns === 0) continue;
45866
- const x = TERMINAL_TEXT_MARGIN_X + column * TERMINAL_CELL_WIDTH;
45867
- const width = columns * TERMINAL_CELL_WIDTH;
45868
- if (run.bg !== null) backgrounds.push(`<rect x="${coordinate(x)}" y="${coordinate(top)}" width="${coordinate(width)}" height="${String(TERMINAL_ROW_HEIGHT)}" fill="${run.bg}"/>`);
45869
- if (clipped.trim() !== "") {
45870
- const fill = run.fg === null ? "" : ` fill="${run.fg}"`;
45871
- const weight = run.bold ? " font-weight=\"bold\"" : "";
45872
- texts.push(`<text xml:space="preserve" x="${coordinate(x)}" y="${coordinate(baseline)}" textLength="${coordinate(width)}" lengthAdjust="spacingAndGlyphs"${fill}${weight}>${escapeXml(clipped)}</text>`);
45873
- }
45874
- column += columns;
45875
- }
45876
- });
45877
- return `<svg xmlns="http://www.w3.org/2000/svg" width="${String(960)}" height="${String(640)}"><rect width="100%" height="100%" fill="${TERMINAL_DEFAULT_BG}"/>${backgrounds.join("")}<g fill="${TERMINAL_DEFAULT_FG}" font-family="${TERMINAL_FONT_STACK}" font-size="${String(TERMINAL_FONT_SIZE)}">${texts.join("")}</g></svg>`;
45878
- }
45879
- /** Cut a run to the columns still left in the row, by code point not unit. */
45880
- function clipRun(run, remaining) {
45881
- const points = [...run.text];
45882
- return points.length <= remaining ? run.text : points.slice(0, remaining).join("");
45883
- }
45884
- async function renderTerminalJpeg(rows) {
45885
- return sharp(Buffer.from(renderTerminalSvg(rows))).jpeg({
45886
- quality: 82,
45887
- chromaSubsampling: "4:2:0"
45888
- }).toBuffer();
45889
- }
45890
- //#endregion
45891
- //#region src/terminal-camera-device.ts
45892
- var terminalCameraSchema = object({
45893
- instanceId: string().min(1).optional(),
45894
- nodeId: string().min(1),
45895
- profileId: string().min(1).default("monitor"),
45896
- profileLabel: string().min(1).default("BTM")
45897
- });
45898
- var relay = null;
45899
- function installTerminalCameraRelay(next) {
45900
- relay = next;
45901
- }
45902
- var TerminalCameraDevice = class extends BaseDevice {
45903
- features = [DeviceFeature.NativeSnapshot];
45904
- constructor(ctx) {
45905
- super(ctx, terminalCameraSchema, { type: ctx.deviceMeta.type });
45906
- this.ctx.registerNativeCap(streamCatalogCapability, { getCatalog: async ({ deviceId }) => {
45907
- if (deviceId !== this.id) return [];
45908
- return this.catalog();
45909
- } });
45910
- this.ctx.registerNativeCap(snapshotCapability, {
45911
- getSnapshot: async ({ deviceId }) => {
45912
- if (deviceId !== this.id) throw new Error(`TerminalCameraDevice: deviceId mismatch, expected ${String(this.id)}, got ${String(deviceId)}`);
45913
- const activeRelay = relay;
45914
- if (!activeRelay) throw new Error("terminal camera relay is unavailable");
45915
- return {
45916
- base64: (await activeRelay.snapshot(this.relayInstanceId(), this.config.get("nodeId"), this.config.get("profileId"))).toString("base64"),
45917
- contentType: "image/jpeg"
45918
- };
45919
- },
45920
- invalidateCache: async () => {}
45921
- });
45922
- this.markOnline(true);
45923
- }
45924
- async catalog() {
45925
- const activeRelay = relay;
45926
- if (!activeRelay) throw new Error("terminal camera relay is unavailable");
45927
- const nodeId = this.config.get("nodeId");
45928
- const profileId = this.config.get("profileId");
45929
- const instanceId = this.relayInstanceId();
45930
- return [{
45931
- camStreamId: profileId,
45932
- kind: "pull-http",
45933
- url: activeRelay.streamUrl(instanceId, nodeId, profileId),
45934
- codec: "h264",
45935
- resolution: {
45936
- width: 960,
45937
- height: 640
45938
- },
45939
- fps: 2,
45940
- label: this.config.get("profileLabel")
45941
- }];
45942
- }
45943
- setNodeOnline(online) {
45944
- this.markOnline(online);
45945
- if (!online) relay?.closeInstance(this.relayInstanceId());
45946
- }
45947
- async removeDevice() {
45948
- await relay?.closeInstance(this.relayInstanceId());
45949
- }
45950
- relayInstanceId() {
45951
- return this.config.get("instanceId") ?? `legacy:${this.stableId}`;
45952
- }
45953
- };
45954
- //#endregion
45955
46460
  //#region src/terminal-camera-relay.ts
45956
46461
  var MJPEG_BOUNDARY = "camstack-terminal-frame";
45957
46462
  var SESSION_IDLE_MS = 3e4;
@@ -46502,13 +47007,34 @@ function newTerminalCameraStableId(instanceId) {
46502
47007
  * Legacy automatic cameras are migration candidates only. A tombstone is
46503
47008
  * durable deletion intent, so a lingering failed device removal must never
46504
47009
  * make that camera adoptable again.
47010
+ *
47011
+ * ## `config` is load-bearing — this read can never be `projection: 'slim'`
47012
+ *
47013
+ * `nodeId`, `profileId` and `profileLabel` all live in the device's `config`,
47014
+ * and a row without `nodeId` is skipped. The slim projection returns
47015
+ * `config: {}` for every row, so a slim answer here is shape-identical to a
47016
+ * fleet that has no legacy cameras — the whole migration section disappears
47017
+ * and nothing says why. `legacy-camera-read-shape.spec.ts` is the arm on that.
47018
+ *
47019
+ * `onSkipped` is why the disappearance would now be visible: a row that LOOKS
47020
+ * like a legacy camera (`terminal-camera-*`, not an instance camera, not
47021
+ * tombstoned, not already adopted) but carries no `nodeId` is reported with
47022
+ * its numeric device id, so the caller can log it per-camera. Rows that are
47023
+ * not candidates at all are silent — a fleet of 1 017 devices must not
47024
+ * produce 1 017 lines to say none of them is a Terminal monitor.
46505
47025
  */
46506
- function listLegacyTerminalCameraCandidates(rows, instanceStableIds, tombstones) {
47026
+ function listLegacyTerminalCameraCandidates(rows, instanceStableIds, tombstones, onSkipped) {
46507
47027
  const legacy = [];
46508
47028
  for (const row of rows) {
46509
47029
  if (tombstones.has(row.stableId) || instanceStableIds.has(row.stableId) || row.stableId.startsWith("terminal-camera-instance-") || !row.stableId.startsWith("terminal-camera-")) continue;
46510
47030
  const nodeId = typeof row.config.nodeId === "string" ? row.config.nodeId : null;
46511
- if (!nodeId) continue;
47031
+ if (!nodeId) {
47032
+ onSkipped?.({
47033
+ deviceId: row.id,
47034
+ stableId: row.stableId
47035
+ });
47036
+ continue;
47037
+ }
46512
47038
  const profileId = typeof row.config.profileId === "string" ? row.config.profileId : "monitor";
46513
47039
  const profileLabel = typeof row.config.profileLabel === "string" ? row.config.profileLabel : profileId === "monitor" ? "BTM" : profileId;
46514
47040
  legacy.push({
@@ -46669,111 +47195,6 @@ function findProfile(profiles, profileId) {
46669
47195
  return profiles.find((p) => p.profileId === profileId);
46670
47196
  }
46671
47197
  //#endregion
46672
- //#region src/profile-settings.ts
46673
- var GLANCES_PLUGINS = [
46674
- {
46675
- key: "showCpu",
46676
- plugin: "cpu",
46677
- label: "CPU"
46678
- },
46679
- {
46680
- key: "showMem",
46681
- plugin: "mem",
46682
- label: "Memory"
46683
- },
46684
- {
46685
- key: "showLoad",
46686
- plugin: "load",
46687
- label: "Load"
46688
- },
46689
- {
46690
- key: "showNetwork",
46691
- plugin: "network",
46692
- label: "Network"
46693
- },
46694
- {
46695
- key: "showDiskIo",
46696
- plugin: "diskio",
46697
- label: "Disk I/O"
46698
- },
46699
- {
46700
- key: "showFs",
46701
- plugin: "fs",
46702
- label: "Filesystems"
46703
- },
46704
- {
46705
- key: "showProcessList",
46706
- plugin: "processlist",
46707
- label: "Process list"
46708
- },
46709
- {
46710
- key: "showContainers",
46711
- plugin: "containers",
46712
- label: "Containers"
46713
- },
46714
- {
46715
- key: "showSensors",
46716
- plugin: "sensors",
46717
- label: "Sensors"
46718
- }
46719
- ];
46720
- function glancesBooleanField(key, label) {
46721
- return {
46722
- type: "boolean",
46723
- key,
46724
- label,
46725
- default: true,
46726
- style: "switch"
46727
- };
46728
- }
46729
- function glancesSettingsSchema() {
46730
- return { sections: [{
46731
- id: "glances-panels",
46732
- title: "Glances panels",
46733
- description: "Turn off a panel to pass --disable-plugin to this Terminal only. All on is the measured default (~1% of one core at the camera grid).",
46734
- columns: 2,
46735
- fields: GLANCES_PLUGINS.map((plugin) => glancesBooleanField(plugin.key, plugin.label))
46736
- }] };
46737
- }
46738
- function settingsSchemaForProfile(profileId) {
46739
- if (profileId === "glances") return glancesSettingsSchema();
46740
- return null;
46741
- }
46742
- function glancesSettingsToArgs(settings) {
46743
- const disabled = GLANCES_PLUGINS.filter((plugin) => settings[plugin.key] === false).map((plugin) => plugin.plugin);
46744
- if (disabled.length === 0) return [];
46745
- return ["--disable-plugin", disabled.join(",")];
46746
- }
46747
- function sanitizeProfileSettings(profileId, raw) {
46748
- if (profileId !== "glances") return {};
46749
- const bag = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
46750
- const out = {
46751
- showCpu: true,
46752
- showMem: true,
46753
- showLoad: true,
46754
- showNetwork: true,
46755
- showDiskIo: true,
46756
- showFs: true,
46757
- showProcessList: true,
46758
- showContainers: true,
46759
- showSensors: true
46760
- };
46761
- for (const plugin of GLANCES_PLUGINS) {
46762
- const value = bag[plugin.key];
46763
- if (typeof value === "boolean") out[plugin.key] = value;
46764
- }
46765
- return out;
46766
- }
46767
- function profileSettingsToArgs(profileId, settings) {
46768
- if (profileId !== "glances") return [];
46769
- return glancesSettingsToArgs(sanitizeProfileSettings(profileId, settings));
46770
- }
46771
- function spawnArgsForInstance(input) {
46772
- const extra = profileSettingsToArgs(input.profileId, input.profileSettings);
46773
- if (input.instanceArgs.length > 0) return [...input.instanceArgs, ...extra];
46774
- if (extra.length > 0) return [...input.profileArgs, ...extra];
46775
- }
46776
- //#endregion
46777
47198
  //#region src/terminal-session-manager.ts
46778
47199
  var MIN_GRID = 1;
46779
47200
  var MAX_COLS = 1e3;
@@ -47595,7 +48016,12 @@ var TerminalAddon = class extends BaseAddon {
47595
48016
  async listLegacyTerminalCameras() {
47596
48017
  const instances = this.terminalInstances();
47597
48018
  const instanceStableIds = new Set(instances.map((instance) => instance.cameraStableId));
47598
- return listLegacyTerminalCameraCandidates(await this.ctx.api.deviceManager.listAll.query({ addonId: this.ctx.id }), instanceStableIds, this.terminalCameraTombstones);
48019
+ return listLegacyTerminalCameraCandidates(await this.ctx.api.deviceManager.listAll.query({ addonId: this.ctx.id }), instanceStableIds, this.terminalCameraTombstones, ({ deviceId, stableId }) => {
48020
+ this.ctx.logger.warn("legacy Terminal camera skipped — its device config carries no nodeId, so it cannot be offered for adoption", {
48021
+ tags: { deviceId },
48022
+ meta: { stableId }
48023
+ });
48024
+ });
47599
48025
  }
47600
48026
  async adoptLegacyMonitor(stableId, requestedName) {
47601
48027
  const instance = await this.instanceMutationQueue.run(() => this.adoptLegacyMonitorUnlocked(stableId, requestedName));