@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.js CHANGED
@@ -8117,6 +8117,20 @@ var RelocateJobSchema = object({
8117
8117
  * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8118
8118
  */
8119
8119
  rowsReconciled: number().int().nonnegative().optional(),
8120
+ /**
8121
+ * Rows this run FORGOT because the file they name is not on disk.
8122
+ *
8123
+ * The mover derived the path from the row's own fields and `stat`ed it; an
8124
+ * ENOENT there is a per-path confirmation that the segment is gone (D296),
8125
+ * and the durable row is dropped through the same channel eviction uses. It
8126
+ * is reported for the same reason `rowsReconciled` is: this is a durable
8127
+ * mutation nobody asked for, and a migration that quietly erases hour rows is
8128
+ * the same failure as one that quietly skips them (D295).
8129
+ *
8130
+ * The production drain of 2026-08-30 would have reported 11 074 here — the
8131
+ * ledger claimed 5.65 GB of footage that no longer existed.
8132
+ */
8133
+ rowsForgotten: number().int().nonnegative().optional(),
8120
8134
  startedAt: number(),
8121
8135
  finishedAt: number().nullable(),
8122
8136
  error: string().nullable()
@@ -8181,6 +8195,13 @@ var MediaRelocateModeSchema = _enum([
8181
8195
  ]);
8182
8196
  var RelocateMediaInputSchema = object({
8183
8197
  toLocationId: string(),
8198
+ /**
8199
+ * Restrict the pass to rows currently on this location. Omitted / `'*'` =
8200
+ * every row that is not already on `toLocationId` (the historical
8201
+ * behaviour). A named source is what a from→to migration needs: without it
8202
+ * "move events off disk 2" also emptied disk 1.
8203
+ */
8204
+ fromLocationId: string().optional(),
8184
8205
  throttleMbps: number().min(1).max(1e3).optional(),
8185
8206
  /** Omitted = `move`, the pre-existing behaviour. */
8186
8207
  mode: MediaRelocateModeSchema.optional()
@@ -8248,6 +8269,19 @@ var StorageMigrationDestinationsSchema = object({
8248
8269
  galleryMedia: string().min(1).optional()
8249
8270
  }).refine((value) => Object.keys(value).length > 0, { message: "select at least one storage class" });
8250
8271
  /**
8272
+ * Optional named source per class. Omitted = the class's current default
8273
+ * (the historical behaviour). A named source that is NOT the default is a
8274
+ * drain of that disk: bytes move, the default stays, and the source is
8275
+ * disabled when the move finishes.
8276
+ */
8277
+ var StorageMigrationSourcesSchema = object({
8278
+ recordings: string().min(1).optional(),
8279
+ recordingsLow: string().min(1).optional(),
8280
+ eventMedia: string().min(1).optional(),
8281
+ backups: string().min(1).optional(),
8282
+ galleryMedia: string().min(1).optional()
8283
+ }).optional();
8284
+ /**
8251
8285
  * How a migration sequences the cutover against the byte move.
8252
8286
  *
8253
8287
  * - `blocking` — the historical order: pause, move every byte, repoint,
@@ -8269,6 +8303,8 @@ var StorageMigrationModeSchema = _enum(["blocking", "nonBlocking"]);
8269
8303
  /** Shared input for planning and starting an orchestrated storage migration. */
8270
8304
  var StorageMigrationInputSchema = object({
8271
8305
  destinations: StorageMigrationDestinationsSchema,
8306
+ /** Omitted = each class's current default. */
8307
+ sources: StorageMigrationSourcesSchema,
8272
8308
  throttleMbps: number().min(1).max(1e3).optional(),
8273
8309
  /** Omitted = `blocking`, which stays the default. */
8274
8310
  mode: StorageMigrationModeSchema.optional()
@@ -8348,6 +8384,13 @@ var StorageMigrationMoveSchema = object({
8348
8384
  storageClass: StorageMigrationClassSchema,
8349
8385
  fromLocationId: string(),
8350
8386
  toLocationId: string(),
8387
+ /**
8388
+ * True when `from` was NOT the class default at plan time. The move still
8389
+ * copies bytes, but the default is left alone and the source is disabled
8390
+ * once the copy verifies. Absent on jobs planned before this field existed
8391
+ * — those jobs always repointed, which is `false`.
8392
+ */
8393
+ freezeSource: boolean().optional(),
8351
8394
  moverJobId: string().nullable(),
8352
8395
  state: RelocateJobStateSchema.nullable(),
8353
8396
  error: string().nullable(),
@@ -8361,6 +8404,7 @@ var StorageMigrationJobSchema = object({
8361
8404
  * can tell a seconds-long cutover from a thirty-hour one. */
8362
8405
  mode: StorageMigrationModeSchema,
8363
8406
  destinations: StorageMigrationDestinationsSchema,
8407
+ sources: StorageMigrationSourcesSchema,
8364
8408
  throttleMbps: number(),
8365
8409
  moves: array(StorageMigrationMoveSchema),
8366
8410
  pauseLeaseId: string().nullable(),
@@ -8386,6 +8430,7 @@ var StorageMigrationFindingSchema = object({
8386
8430
  });
8387
8431
  var StorageMigrationPlanSchema = object({
8388
8432
  destinations: StorageMigrationDestinationsSchema,
8433
+ sources: StorageMigrationSourcesSchema,
8389
8434
  /** The mode this plan was built for. A plan is only valid for its mode: the
8390
8435
  * `eventMedia` seal gate and the single-cardinality refusal both depend on
8391
8436
  * it. */
@@ -8393,7 +8438,8 @@ var StorageMigrationPlanSchema = object({
8393
8438
  moves: array(object({
8394
8439
  storageClass: StorageMigrationClassSchema,
8395
8440
  fromLocationId: string(),
8396
- toLocationId: string()
8441
+ toLocationId: string(),
8442
+ freezeSource: boolean().optional()
8397
8443
  })),
8398
8444
  findings: array(StorageMigrationFindingSchema)
8399
8445
  });
@@ -8480,16 +8526,142 @@ var RelocateResidueSchema = object({
8480
8526
  segments: number().int().nonnegative(),
8481
8527
  bytes: number().int().nonnegative()
8482
8528
  }).nullable();
8529
+ /**
8530
+ * Ask one location whether its durable hour rows describe the disk — the walk
8531
+ * (D319).
8532
+ *
8533
+ * `apply` DEFAULTS TO FALSE and that default is the product: the operator's
8534
+ * missing tool is the question, and the dry run is how they sanity-check the
8535
+ * destructive run before authorising it.
8536
+ */
8537
+ var LedgerWalkInputSchema = object({
8538
+ locationId: string().min(1),
8539
+ /** Forget the confirmed-absent rows, rather than only counting them. */
8540
+ apply: boolean().optional(),
8541
+ /** Narrow to one camera. */
8542
+ deviceId: number().int().positive().optional(),
8543
+ /** Narrow to these recording profiles; empty/absent = every profile. */
8544
+ profiles: array(string().min(1)).optional()
8545
+ });
8546
+ /** Why a whole walk did nothing. Every one leaves the ledger untouched. */
8547
+ var LedgerWalkRefusalSchema = _enum([
8548
+ "location-unknown",
8549
+ "source-writable",
8550
+ "no-ledger",
8551
+ "archive-unreadable",
8552
+ "anchor-absent",
8553
+ "anchor-unreadable",
8554
+ "anchor-moved"
8555
+ ]);
8556
+ _enum([
8557
+ "live-tail",
8558
+ "listing-error",
8559
+ "path-mismatch",
8560
+ "durable-refused"
8561
+ ]);
8562
+ /** Every skip reason, always present, always a number — so a reason that never
8563
+ * fired reports as zero rather than absent and the report shape is constant
8564
+ * between passes. Spelled out rather than `z.record` for exactly that. */
8565
+ var LedgerWalkSkipCountsSchema = object({
8566
+ "live-tail": number().int().nonnegative(),
8567
+ "listing-error": number().int().nonnegative(),
8568
+ "path-mismatch": number().int().nonnegative(),
8569
+ "durable-refused": number().int().nonnegative()
8570
+ });
8571
+ /** One camera's share of a walk, so a report names cameras and not rows. */
8572
+ var LedgerWalkDeviceReportSchema = object({
8573
+ deviceId: number().int(),
8574
+ hoursWalked: number().int().nonnegative(),
8575
+ hoursMissing: number().int().nonnegative(),
8576
+ ghostSegments: number().int().nonnegative(),
8577
+ ghostBytes: number().int().nonnegative(),
8578
+ forgottenSegments: number().int().nonnegative(),
8579
+ orphanFiles: number().int().nonnegative()
8580
+ });
8581
+ /**
8582
+ * What one walk claimed, listed, found and (only when armed) forgot.
8583
+ *
8584
+ * `archiveSegments` is the **M** and `segmentsClaimed` the **N** (D295), so a
8585
+ * walk that saw a fraction of the location is visible in its own report rather
8586
+ * than in the absence of one.
8587
+ */
8588
+ var LedgerWalkReportSchema = object({
8589
+ locationId: string(),
8590
+ applied: boolean(),
8591
+ refused: LedgerWalkRefusalSchema.nullable(),
8592
+ archiveSegments: number().int().nonnegative().nullable(),
8593
+ archiveBytes: number().int().nonnegative().nullable(),
8594
+ hoursClaimed: number().int().nonnegative(),
8595
+ hoursWalked: number().int().nonnegative(),
8596
+ hoursMissing: number().int().nonnegative(),
8597
+ /** `readdir` calls issued — the cost, stated in the unit that is paid. */
8598
+ listings: number().int().nonnegative(),
8599
+ segmentsClaimed: number().int().nonnegative(),
8600
+ ghostSegments: number().int().nonnegative(),
8601
+ ghostBytes: number().int().nonnegative(),
8602
+ ghostHoursWhole: number().int().nonnegative(),
8603
+ forgottenSegments: number().int().nonnegative(),
8604
+ forgottenBytes: number().int().nonnegative(),
8605
+ /** Files under a claimed hour that no durable row names. Never deleted. */
8606
+ orphanFiles: number().int().nonnegative(),
8607
+ orphanSample: array(string()).readonly(),
8608
+ hoursSkipped: number().int().nonnegative(),
8609
+ skippedByReason: LedgerWalkSkipCountsSchema,
8610
+ /** The walk stopped at its per-pass hour bound with claims unwalked. */
8611
+ bounded: boolean(),
8612
+ byDevice: array(LedgerWalkDeviceReportSchema).readonly()
8613
+ });
8483
8614
  /** How many rows a media pass would still act on against a given target — the
8484
8615
  * media lane's denominator AND its residue, from ONE derivation so the two can
8485
8616
  * never disagree. `null` = the count could not be taken. */
8486
8617
  var RelocatableMediaCountSchema = object({ rows: number().int().nonnegative() }).nullable();
8487
8618
  var RelocatableMediaCountInputSchema = object({
8488
8619
  toLocationId: string().min(1),
8620
+ /** Restrict the count to one source; omitted / `'*'` = every non-target row. */
8621
+ fromLocationId: string().optional(),
8489
8622
  /** Omitted = `move`. */
8490
8623
  mode: MediaRelocateModeSchema.optional()
8491
8624
  });
8492
8625
  /**
8626
+ * Operator cleanup of leftover analytics rows, optional debug media, and
8627
+ * ghost ledger entries on frozen footage locations.
8628
+ *
8629
+ * A pass is tens of minutes on a standing backlog. The hub method RETURNS
8630
+ * `{ jobId }` immediately; progress is `cleanupStatus`. Awaiting the work
8631
+ * is how `addons.custom` hit the 60 s UDS deadline while reclaim continued
8632
+ * with no operator-visible status.
8633
+ */
8634
+ var StorageCleanupPhaseSchema = _enum([
8635
+ "orphans",
8636
+ "debug-media",
8637
+ "ghost-ledger",
8638
+ "done",
8639
+ "failed",
8640
+ "cancelled"
8641
+ ]);
8642
+ var StorageCleanupInputSchema = object({
8643
+ /** Also walk motion stills / track filmstrips. Off by default. */
8644
+ includeDebugMedia: boolean().optional() });
8645
+ var StorageCleanupJobSchema = object({
8646
+ jobId: string(),
8647
+ phase: StorageCleanupPhaseSchema,
8648
+ includeDebugMedia: boolean(),
8649
+ orphansReclaimed: number().int().nonnegative(),
8650
+ orphanBytesReclaimed: number().int().nonnegative(),
8651
+ debugMediaReclaimed: number().int().nonnegative(),
8652
+ debugMediaBytesReclaimed: number().int().nonnegative(),
8653
+ ghostsForgotten: number().int().nonnegative(),
8654
+ ghostBytesForgotten: number().int().nonnegative(),
8655
+ /** Short operator-facing line: current collection, pass, or location. */
8656
+ detail: string().nullable(),
8657
+ cancelRequested: boolean(),
8658
+ startedAt: number(),
8659
+ updatedAt: number(),
8660
+ finishedAt: number().nullable(),
8661
+ error: string().nullable()
8662
+ });
8663
+ var StorageCleanupStatusInputSchema = object({ jobId: string().optional() });
8664
+ /**
8493
8665
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
8494
8666
  * storage a location serves. Defined here (not in `capabilities/storage.cap.ts`)
8495
8667
  * so the persisted record schema and the consumer-facing cap can both consume it
@@ -8517,11 +8689,10 @@ var StorageLocationTypeSchema = string().regex(/^[a-z][a-zA-Z0-9-]*$/);
8517
8689
  * The default location for a type uses `id === <type>:default` by
8518
8690
  * convention (the bare type ref like `'backups'` resolves to it).
8519
8691
  *
8520
- * `isSystem: true` marks a location as orchestrator-seeded and
8521
- * undeletable. The bootstrap-installed defaults (one per type) carry
8522
- * this flag; operator-added locations don't. Editing the config of
8523
- * a system location is allowed (path migration, provider swap) but
8524
- * deleting it is rejected at the cap level.
8692
+ * `isSystem` is a legacy persisted flag. Seed still creates the initial
8693
+ * `<type>:default` locations; the flag is no longer a lock, a badge, or a
8694
+ * prune selector. New writes leave it false. Deletion is gated on uniqueness
8695
+ * / last-enabled, not on this bit.
8525
8696
  */
8526
8697
  var StorageLocationSchema = object({
8527
8698
  id: string().regex(/^[a-z][a-zA-Z0-9-]*:[a-zA-Z0-9-]+$/),
@@ -12992,6 +13163,12 @@ method(object({
12992
13163
  }), _void(), {
12993
13164
  kind: "mutation",
12994
13165
  auth: "admin"
13166
+ }), method(object({
13167
+ from: string(),
13168
+ to: string()
13169
+ }), object({ moved: number() }), {
13170
+ kind: "mutation",
13171
+ auth: "admin"
12995
13172
  }), method(object({
12996
13173
  deviceId: number(),
12997
13174
  disabled: boolean()
@@ -19000,53 +19177,15 @@ var RecentTracksPageSchema = object({
19000
19177
  /** Cursor for the next page, or null when this page is the last. */
19001
19178
  nextCursor: string().nullable()
19002
19179
  });
19003
- var LIST_GROUPS_DEFAULT_LIMIT = 40;
19004
- var LIST_GROUPS_MAX_LIMIT = 100;
19005
- var AnalyticsGroupRecordSchema = object({
19006
- id: string(),
19007
- deviceId: number().int(),
19008
- openedAt: number().int(),
19009
- closedAt: number().int(),
19010
- timestamp: number().int(),
19011
- memberCount: number().int(),
19012
- memberTrackIds: array(string()).readonly(),
19013
- className: string(),
19014
- classes: array(string()).readonly(),
19015
- /** Relative event-media path, or null when the group has no picture yet. */
19016
- mediaUrl: string().nullable(),
19017
- singleton: boolean()
19018
- });
19019
- var AnalyticsGroupMemberSchema = object({
19020
- trackId: string(),
19021
- deviceId: number().int(),
19022
- className: string(),
19023
- firstSeen: number().int(),
19024
- lastSeen: number().int(),
19025
- mediaUrl: string().nullable()
19026
- });
19027
- var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
19028
- var ListGroupsQueryInput = object({
19029
- /** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
19030
- deviceIds: array(number()),
19031
- /** Window lower bound on `closedAt` (inclusive). */
19032
- since: number().optional(),
19033
- /** Window upper bound on `openedAt` (inclusive). */
19034
- until: number().optional(),
19035
- limit: number().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
19036
- /** Opaque continuation cursor from a previous page's `nextCursor`. */
19037
- cursor: string().optional()
19038
- });
19039
- var ListGroupsPageSchema = object({
19040
- groups: array(AnalyticsGroupRecordSchema).readonly(),
19041
- nextCursor: string().nullable()
19042
- });
19180
+ var KEY_EVENTS_DEFAULT_LIMIT = 50;
19181
+ var KEY_EVENTS_MAX_LIMIT = 200;
19043
19182
  var KeyEventQueryInput = object({
19044
19183
  deviceId: number(),
19045
19184
  /** Window lower bound (track firstSeen ≥ since). */
19046
19185
  since: number(),
19047
19186
  /** Window upper bound (track firstSeen ≤ until). */
19048
19187
  until: number(),
19049
- limit: number().int().min(1).max(200).default(50),
19188
+ limit: number().int().min(1).max(KEY_EVENTS_MAX_LIMIT).default(KEY_EVENTS_DEFAULT_LIMIT),
19050
19189
  /** Drop tracks scoring below this importance. */
19051
19190
  minImportance: number().min(0).max(1).optional(),
19052
19191
  /** Restrict to a single class (e.g. 'person'). */
@@ -19068,6 +19207,32 @@ var KeyEventSchema = object({
19068
19207
  ...TrackFlagFields,
19069
19208
  ...TrackRetrainFields
19070
19209
  });
19210
+ /** `getKeyEvents`' window and filters, asked of a SET of cameras at once. */
19211
+ var KeyEventBatchQueryInput = object({
19212
+ deviceIds: array(number()).min(1).max(200),
19213
+ since: number(),
19214
+ until: number(),
19215
+ /** Applied PER CAMERA, exactly as `getKeyEvents.limit` is — never a total
19216
+ * across the set, which would let a busy camera starve a quiet one of its
19217
+ * rows and change what the merged feed contains. */
19218
+ limit: number().int().min(1).max(KEY_EVENTS_MAX_LIMIT).default(KEY_EVENTS_DEFAULT_LIMIT),
19219
+ minImportance: number().min(0).max(1).optional(),
19220
+ classFilter: string().optional()
19221
+ });
19222
+ /**
19223
+ * One camera's key events in a batch answer.
19224
+ *
19225
+ * The row exists for every requested id. `getKeyEvents` degrades to `[]` on
19226
+ * error rather than throwing, so a camera whose store read failed and one with
19227
+ * no events in the window were ALREADY indistinguishable per camera — the
19228
+ * batch does not make that worse, and the row keeps the deviceId the single
19229
+ * method's output never carried (the caller used to stamp it from the fan-out
19230
+ * key, which only worked because there was one query per camera).
19231
+ */
19232
+ var KeyEventsForDeviceSchema = object({
19233
+ deviceId: number(),
19234
+ events: array(KeyEventSchema).readonly()
19235
+ });
19071
19236
  object({
19072
19237
  trackId: string(),
19073
19238
  className: string(),
@@ -19115,9 +19280,7 @@ var TrackCascadeCountsSchema = object({
19115
19280
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
19116
19281
  plates: number().int(),
19117
19282
  /** Per-track CLIP search vectors removed (best-effort). */
19118
- embeddings: number().int(),
19119
- /** Group membership + group rows removed with their last member (best-effort). */
19120
- groups: number().int()
19283
+ embeddings: number().int()
19121
19284
  });
19122
19285
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
19123
19286
  var DiskReconcileCountsSchema = object({
@@ -19287,6 +19450,47 @@ var RebuildStatusSchema = object({
19287
19450
  /** Present when the pass ended by throwing. */
19288
19451
  error: string().nullable()
19289
19452
  });
19453
+ /**
19454
+ * Acknowledgement that a debug-media reclaim STARTED.
19455
+ *
19456
+ * The pass is paced at ~2 MB/s over a standing 100 GB — tens of minutes — so
19457
+ * it runs detached and this returns immediately. Awaiting it is how the
19458
+ * `addons.custom` door hit the 60 s UDS deadline while the walk carried on
19459
+ * with no status. Poll {@link pipelineAnalyticsCapability.methods.getMediaReclaimStatus}.
19460
+ */
19461
+ var MediaReclaimStartResultSchema = object({
19462
+ started: boolean(),
19463
+ /** True when a pass was already running; the new request is ignored. */
19464
+ alreadyRunning: boolean()
19465
+ });
19466
+ var MediaReclaimInputSchema = object({
19467
+ mode: _enum(["report", "reclaim"]).default("report"),
19468
+ scopes: array(_enum(["motion-still", "track-filmstrip"])).min(1).optional(),
19469
+ deviceIds: array(number().int()).min(1).optional(),
19470
+ restart: boolean().optional(),
19471
+ pageSize: number().int().min(50).max(5e3).optional(),
19472
+ maxRowsPerDevice: number().int().min(1).max(5e5).optional(),
19473
+ maxReclaimPerDevice: number().int().min(1).max(5e5).optional(),
19474
+ maxBytesPerRun: number().int().min(1).optional(),
19475
+ budgetMinutes: number().int().min(1).max(720).optional(),
19476
+ throttleBytesPerSec: number().int().min(64 * 1024).optional(),
19477
+ graceMinutes: number().int().min(1).max(10080).optional()
19478
+ });
19479
+ var MediaReclaimStatusSchema = object({
19480
+ running: boolean(),
19481
+ mode: _enum(["report", "reclaim"]).nullable(),
19482
+ totalExamined: number(),
19483
+ totalEligible: number(),
19484
+ totalReclaimed: number(),
19485
+ totalBytesReclaimed: number(),
19486
+ totalRefused: number(),
19487
+ /** Device+scope windows finished in this pass. */
19488
+ devicesDone: number(),
19489
+ complete: boolean().nullable(),
19490
+ startedAtMs: number().nullable(),
19491
+ finishedAtMs: number().nullable(),
19492
+ error: string().nullable()
19493
+ });
19290
19494
  var ReplayFrameInputSchema = object({
19291
19495
  timestamp: number(),
19292
19496
  frame: PipelineRunResultBridge
@@ -19325,10 +19529,7 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19325
19529
  * stationary registry). Default false: the timeline lists passages,
19326
19530
  * not parking records (operator decision, 2026-08-15). */
19327
19531
  includeStationary: boolean().optional()
19328
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(ListGroupsQueryInput, ListGroupsPageSchema), method(object({
19329
- deviceId: number(),
19330
- groupId: string().min(1)
19331
- }), AnalyticsGroupDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
19532
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
19332
19533
  kind: "mutation",
19333
19534
  auth: "admin"
19334
19535
  }), 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({
@@ -19337,7 +19538,7 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19337
19538
  until: number().optional(),
19338
19539
  kinds: array(string()).optional(),
19339
19540
  limit: number().int().min(1).max(MAX_EVENT_QUERY_LIMIT).default(DEFAULT_EVENT_QUERY_LIMIT)
19340
- }), array(SensorEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(object({
19541
+ }), array(SensorEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(KeyEventBatchQueryInput, array(KeyEventsForDeviceSchema).readonly()), method(object({
19341
19542
  deviceId: number(),
19342
19543
  since: number(),
19343
19544
  until: number(),
@@ -19428,6 +19629,12 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19428
19629
  }), method(OpsLogQueryInputSchema, array(OpsLogEntrySchema).readonly(), {
19429
19630
  kind: "query",
19430
19631
  auth: "admin"
19632
+ }), method(MediaReclaimInputSchema, MediaReclaimStartResultSchema, {
19633
+ kind: "mutation",
19634
+ auth: "admin"
19635
+ }), method(object({}), MediaReclaimStatusSchema, {
19636
+ kind: "query",
19637
+ auth: "admin"
19431
19638
  }), method(object({ deviceIds: array(number()).optional() }), TrainingExportSummarySchema, {
19432
19639
  kind: "query",
19433
19640
  auth: "admin"
@@ -21502,7 +21709,13 @@ method(StorageMigrationInputSchema, StorageMigrationPlanSchema, { auth: "admin"
21502
21709
  }), method(object({}), array(StorageMigrationMoverSchema).readonly(), { auth: "admin" }), method(object({}), array(StorageMigrationResidueSchema).readonly(), { auth: "admin" }), method(StorageMigrationDrainInputSchema, object({ jobId: string() }), {
21503
21710
  kind: "mutation",
21504
21711
  auth: "admin"
21505
- });
21712
+ }), method(StorageCleanupInputSchema, object({ jobId: string() }), {
21713
+ kind: "mutation",
21714
+ auth: "admin"
21715
+ }), method(StorageCleanupStatusInputSchema, StorageCleanupJobSchema.nullable(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
21716
+ kind: "mutation",
21717
+ auth: "admin"
21718
+ }), method(object({}), array(StorageMigrationJobSchema).readonly(), { auth: "admin" });
21506
21719
  var ProviderInfoSchema = discriminatedUnion("shouldSaveDiskSpace", [object({
21507
21720
  providerId: string().min(1),
21508
21721
  displayName: string().min(1),
@@ -28639,6 +28852,33 @@ var RecordingRebalanceInputSchema = object({
28639
28852
  minMoveGb: number().min(0).optional()
28640
28853
  });
28641
28854
  /**
28855
+ * Operator-facing placement of one camera onto a recordings location.
28856
+ *
28857
+ * `pinLocationId` is the operator instruction: a location id, or `null` for
28858
+ * Auto (the planner may move this camera). `locationId` is where high/mid
28859
+ * currently write — the plan, which may disagree with the pin when Auto.
28860
+ */
28861
+ var RecordingDevicePlacementSchema = object({
28862
+ deviceId: number().int(),
28863
+ profile: string(),
28864
+ locationId: string()
28865
+ });
28866
+ var RecordingDevicePinSchema = object({
28867
+ deviceId: number().int(),
28868
+ /** Recordings-class location this camera is pinned to. */
28869
+ locationId: string()
28870
+ });
28871
+ var RecordingPlacementViewSchema = object({
28872
+ assignments: array(RecordingDevicePlacementSchema),
28873
+ pins: array(RecordingDevicePinSchema),
28874
+ defaultLocations: record(string(), string())
28875
+ });
28876
+ var RecordingSetDevicePlacementInputSchema = object({
28877
+ deviceId: number().int(),
28878
+ /** `null` = Auto. Otherwise a `recordings:*` location id. */
28879
+ locationId: string().nullable()
28880
+ });
28881
+ /**
28642
28882
  * Result of locating footage at a wall-clock instant for one device/profile.
28643
28883
  * `segment` carries the covering segment's window; `gap` reports the forward
28644
28884
  * nearest covered edge (`null` past the end of footage / when none exists)
@@ -28852,6 +29092,9 @@ method(object({
28852
29092
  }), method(RelocateResidueInputSchema, RelocateResidueSchema, {
28853
29093
  kind: "query",
28854
29094
  auth: "admin"
29095
+ }), method(LedgerWalkInputSchema, LedgerWalkReportSchema, {
29096
+ kind: "mutation",
29097
+ auth: "admin"
28855
29098
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
28856
29099
  kind: "mutation",
28857
29100
  auth: "admin"
@@ -28861,6 +29104,12 @@ method(object({
28861
29104
  }), method(RecordingRebalanceInputSchema, RecordingRebalancePlanSchema, {
28862
29105
  kind: "mutation",
28863
29106
  auth: "admin"
29107
+ }), method(object({}), RecordingPlacementViewSchema, {
29108
+ kind: "query",
29109
+ auth: "admin"
29110
+ }), method(RecordingSetDevicePlacementInputSchema, object({ ok: literal(true) }), {
29111
+ kind: "mutation",
29112
+ auth: "admin"
28864
29113
  });
28865
29114
  /**
28866
29115
  * `recording-export` cap — render a footage time range into a single downloadable
@@ -29243,6 +29492,25 @@ var SceneMonitorStatusSchema = object({
29243
29492
  monitors: array(SceneMonitorSchema),
29244
29493
  lastFetchedAt: number()
29245
29494
  });
29495
+ /**
29496
+ * One camera's row in a `listScenesBatch` answer.
29497
+ *
29498
+ * `status` is NULLABLE and the nullability is the whole point. `scene-monitor`
29499
+ * is a `defaultActive` wrapper, so every camera is asked; a camera whose
29500
+ * provider is absent (pipeline-analytics not deployed, the post-processing node
29501
+ * down) cannot answer, and that is not the same fact as a camera with no scenes
29502
+ * configured. Fanned out per camera the difference was visible — one query
29503
+ * errored while the others resolved — and a batch that returned only the rows
29504
+ * it managed would have destroyed it, silently, by making an unreachable camera
29505
+ * indistinguishable from one that answered `monitors: []`.
29506
+ *
29507
+ * So: EVERY requested deviceId gets a row. `status: null` means "this camera
29508
+ * could not be read"; `status.monitors: []` means "read, and it has none".
29509
+ */
29510
+ var SceneMonitorStatusForDeviceSchema = object({
29511
+ deviceId: number(),
29512
+ status: SceneMonitorStatusSchema.nullable()
29513
+ });
29246
29514
  var sceneMonitorCapability = {
29247
29515
  name: "scene-monitor",
29248
29516
  scope: "device",
@@ -29252,6 +29520,22 @@ var sceneMonitorCapability = {
29252
29520
  deviceTypes: [DeviceType.Camera],
29253
29521
  methods: {
29254
29522
  listScenes: method(object({ deviceId: number() }), SceneMonitorStatusSchema),
29523
+ /**
29524
+ * The same answer, for a SET of cameras, in one round trip.
29525
+ *
29526
+ * `/scenes` renders every camera and re-reads them on a 30s safety-net
29527
+ * poll behind the push slice. Fanned out client-side that was one query
29528
+ * per camera — 29 round trips through the browser, the hub and the
29529
+ * post-analysis runner every 30 seconds to read an in-memory map the
29530
+ * owner had already merged. The work is unchanged (`statusFor` per
29531
+ * device, all in-process at the owner); what collapses is the transport.
29532
+ *
29533
+ * A camera that cannot answer still gets a row, with `status: null` —
29534
+ * see {@link SceneMonitorStatusForDeviceSchema}. Never fewer rows than
29535
+ * ids: a caller that asked for twenty-nine and got twenty-seven cannot
29536
+ * tell which two are missing, or that any are.
29537
+ */
29538
+ listScenesBatch: method(object({ deviceIds: array(number()).min(1).max(200) }), array(SceneMonitorStatusForDeviceSchema).readonly()),
29255
29539
  createScene: method(object({
29256
29540
  deviceId: number(),
29257
29541
  label: string(),
@@ -31087,6 +31371,27 @@ var CameraOccupancySnapshotSchema = object({
31087
31371
  stationaryObjects: array(StationaryObjectSchema).readonly().optional()
31088
31372
  });
31089
31373
  /**
31374
+ * One camera's row in a `getCurrentSnapshotBatch` answer.
31375
+ *
31376
+ * THREE outcomes, and the single-camera method could only express two of them
31377
+ * because `snapshot: null` was already spoken for:
31378
+ *
31379
+ * - `read: 'read'`, `snapshot` present — the live occupancy reading.
31380
+ * - `read: 'read'`, `snapshot: null` — read, and this camera has no reading
31381
+ * yet: no frame since boot, and no parked-object registry to hydrate from.
31382
+ * - `read: 'unreadable'` — the owner could not answer for this
31383
+ * camera. `snapshot` is null, and it does NOT mean "nothing parked here".
31384
+ *
31385
+ * Collapsing the last two is the failure this field exists to prevent: a
31386
+ * hydration that threw would otherwise render as an empty Stationary section,
31387
+ * which is a definite claim about a camera nobody could read.
31388
+ */
31389
+ var CameraOccupancySnapshotForDeviceSchema = object({
31390
+ deviceId: number(),
31391
+ read: _enum(["read", "unreadable"]),
31392
+ snapshot: CameraOccupancySnapshotSchema.nullable()
31393
+ });
31394
+ /**
31090
31395
  * Time-series resolution. The history methods return one bucket per
31091
31396
  * step over the requested range. Smaller resolutions cost more
31092
31397
  * memory + bandwidth; bound to discrete steps so caller cannot ask
@@ -31143,6 +31448,20 @@ var zoneAnalyticsCapability = {
31143
31448
  * (no inference result emitted since boot or since binding was
31144
31449
  * activated). */
31145
31450
  getCurrentSnapshot: method(object({ deviceId: number() }), CameraOccupancySnapshotSchema.nullable()),
31451
+ /**
31452
+ * The same snapshot, for a SET of cameras, in one round trip.
31453
+ *
31454
+ * The Events page's Stationary section polls this every 15s for every
31455
+ * selected camera. Fanned out client-side that is one query per camera to
31456
+ * read a `Map.get` at the owner — the answer costs nothing, the round trip
31457
+ * costs everything. Batched, N transports become one and the per-device
31458
+ * work is unchanged.
31459
+ *
31460
+ * Every requested deviceId gets a row, tagged `read` — see
31461
+ * {@link CameraOccupancySnapshotForDeviceSchema}. A camera the owner could
31462
+ * not answer for is `'unreadable'`, never an empty reading.
31463
+ */
31464
+ getCurrentSnapshotBatch: method(object({ deviceIds: array(number()).min(1).max(200) }), array(CameraOccupancySnapshotForDeviceSchema).readonly()),
31146
31465
  /** Time-series object count inside one zone. `className` optional —
31147
31466
  * omit to count every class in the zone. */
31148
31467
  getZoneHistory: method(object({
@@ -33858,6 +34177,12 @@ Object.freeze({
33858
34177
  addonId: null,
33859
34178
  access: "delete"
33860
34179
  },
34180
+ "deviceManager.renameLocation": {
34181
+ capName: "device-manager",
34182
+ capScope: "system",
34183
+ addonId: null,
34184
+ access: "create"
34185
+ },
33861
34186
  "deviceManager.runDeviceAction": {
33862
34187
  capName: "device-manager",
33863
34188
  capScope: "system",
@@ -35550,13 +35875,19 @@ Object.freeze({
35550
35875
  addonId: null,
35551
35876
  access: "view"
35552
35877
  },
35553
- "pipelineAnalytics.getGroup": {
35878
+ "pipelineAnalytics.getKeyEvents": {
35554
35879
  capName: "pipeline-analytics",
35555
35880
  capScope: "device",
35556
35881
  addonId: null,
35557
35882
  access: "view"
35558
35883
  },
35559
- "pipelineAnalytics.getKeyEvents": {
35884
+ "pipelineAnalytics.getKeyEventsBatch": {
35885
+ capName: "pipeline-analytics",
35886
+ capScope: "device",
35887
+ addonId: null,
35888
+ access: "view"
35889
+ },
35890
+ "pipelineAnalytics.getMediaReclaimStatus": {
35560
35891
  capName: "pipeline-analytics",
35561
35892
  capScope: "device",
35562
35893
  addonId: null,
@@ -35646,12 +35977,6 @@ Object.freeze({
35646
35977
  addonId: null,
35647
35978
  access: "view"
35648
35979
  },
35649
- "pipelineAnalytics.listGroups": {
35650
- capName: "pipeline-analytics",
35651
- capScope: "device",
35652
- addonId: null,
35653
- access: "view"
35654
- },
35655
35980
  "pipelineAnalytics.listOpsLog": {
35656
35981
  capName: "pipeline-analytics",
35657
35982
  capScope: "device",
@@ -35736,6 +36061,12 @@ Object.freeze({
35736
36061
  addonId: null,
35737
36062
  access: "create"
35738
36063
  },
36064
+ "pipelineAnalytics.reclaimDebugMedia": {
36065
+ capName: "pipeline-analytics",
36066
+ capScope: "device",
36067
+ addonId: null,
36068
+ access: "create"
36069
+ },
35739
36070
  "pipelineAnalytics.reconcileFromDisk": {
35740
36071
  capName: "pipeline-analytics",
35741
36072
  capScope: "device",
@@ -36660,6 +36991,12 @@ Object.freeze({
36660
36991
  addonId: null,
36661
36992
  access: "view"
36662
36993
  },
36994
+ "recording.getPlacement": {
36995
+ capName: "recording",
36996
+ capScope: "system",
36997
+ addonId: null,
36998
+ access: "view"
36999
+ },
36663
37000
  "recording.getPlaybackManifest": {
36664
37001
  capName: "recording",
36665
37002
  capScope: "system",
@@ -36738,6 +37075,12 @@ Object.freeze({
36738
37075
  addonId: null,
36739
37076
  access: "view"
36740
37077
  },
37078
+ "recording.reconcileLedgerAgainstDisk": {
37079
+ capName: "recording",
37080
+ capScope: "system",
37081
+ addonId: null,
37082
+ access: "create"
37083
+ },
36741
37084
  "recording.refreshStorageLocationsForMigration": {
36742
37085
  capName: "recording",
36743
37086
  capScope: "system",
@@ -36780,6 +37123,12 @@ Object.freeze({
36780
37123
  addonId: null,
36781
37124
  access: "create"
36782
37125
  },
37126
+ "recording.setDevicePlacement": {
37127
+ capName: "recording",
37128
+ capScope: "system",
37129
+ addonId: null,
37130
+ access: "create"
37131
+ },
36783
37132
  "recording.startStorageMigrationMove": {
36784
37133
  capName: "recording",
36785
37134
  capScope: "system",
@@ -36864,6 +37213,12 @@ Object.freeze({
36864
37213
  addonId: null,
36865
37214
  access: "view"
36866
37215
  },
37216
+ "sceneMonitor.listScenesBatch": {
37217
+ capName: "scene-monitor",
37218
+ capScope: "device",
37219
+ addonId: null,
37220
+ access: "view"
37221
+ },
36867
37222
  "sceneMonitor.recheckNow": {
36868
37223
  capName: "scene-monitor",
36869
37224
  capScope: "device",
@@ -37218,12 +37573,36 @@ Object.freeze({
37218
37573
  addonId: null,
37219
37574
  access: "create"
37220
37575
  },
37576
+ "storageMigration.cleanupCancel": {
37577
+ capName: "storage-migration",
37578
+ capScope: "system",
37579
+ addonId: null,
37580
+ access: "create"
37581
+ },
37582
+ "storageMigration.cleanupStart": {
37583
+ capName: "storage-migration",
37584
+ capScope: "system",
37585
+ addonId: null,
37586
+ access: "create"
37587
+ },
37588
+ "storageMigration.cleanupStatus": {
37589
+ capName: "storage-migration",
37590
+ capScope: "system",
37591
+ addonId: null,
37592
+ access: "view"
37593
+ },
37221
37594
  "storageMigration.drain": {
37222
37595
  capName: "storage-migration",
37223
37596
  capScope: "system",
37224
37597
  addonId: null,
37225
37598
  access: "create"
37226
37599
  },
37600
+ "storageMigration.history": {
37601
+ capName: "storage-migration",
37602
+ capScope: "system",
37603
+ addonId: null,
37604
+ access: "view"
37605
+ },
37227
37606
  "storageMigration.movers": {
37228
37607
  capName: "storage-migration",
37229
37608
  capScope: "system",
@@ -38220,6 +38599,12 @@ Object.freeze({
38220
38599
  addonId: null,
38221
38600
  access: "view"
38222
38601
  },
38602
+ "zoneAnalytics.getCurrentSnapshotBatch": {
38603
+ capName: "zone-analytics",
38604
+ capScope: "device",
38605
+ addonId: null,
38606
+ access: "view"
38607
+ },
38223
38608
  "zoneAnalytics.getUnzonedHistory": {
38224
38609
  capName: "zone-analytics",
38225
38610
  capScope: "device",
@@ -39214,14 +39599,14 @@ Object.freeze({
39214
39599
  form: "single",
39215
39600
  optional: true
39216
39601
  }],
39217
- "pipelineAnalytics.getGroup": [{
39602
+ "pipelineAnalytics.getKeyEvents": [{
39218
39603
  name: "deviceId",
39219
39604
  form: "single",
39220
39605
  optional: false
39221
39606
  }],
39222
- "pipelineAnalytics.getKeyEvents": [{
39223
- name: "deviceId",
39224
- form: "single",
39607
+ "pipelineAnalytics.getKeyEventsBatch": [{
39608
+ name: "deviceIds",
39609
+ form: "array",
39225
39610
  optional: false
39226
39611
  }],
39227
39612
  "pipelineAnalytics.getMotionEvents": [{
@@ -39279,11 +39664,6 @@ Object.freeze({
39279
39664
  form: "single",
39280
39665
  optional: false
39281
39666
  }],
39282
- "pipelineAnalytics.listGroups": [{
39283
- name: "deviceIds",
39284
- form: "array",
39285
- optional: false
39286
- }],
39287
39667
  "pipelineAnalytics.listOpsLog": [{
39288
39668
  name: "deviceId",
39289
39669
  form: "single",
@@ -39329,6 +39709,11 @@ Object.freeze({
39329
39709
  form: "single",
39330
39710
  optional: true
39331
39711
  }],
39712
+ "pipelineAnalytics.reclaimDebugMedia": [{
39713
+ name: "deviceIds",
39714
+ form: "array",
39715
+ optional: true
39716
+ }],
39332
39717
  "pipelineAnalytics.reconcileFromDisk": [{
39333
39718
  name: "deviceId",
39334
39719
  form: "single",
@@ -39664,6 +40049,11 @@ Object.freeze({
39664
40049
  form: "single",
39665
40050
  optional: false
39666
40051
  }],
40052
+ "recording.reconcileLedgerAgainstDisk": [{
40053
+ name: "deviceId",
40054
+ form: "single",
40055
+ optional: true
40056
+ }],
39667
40057
  "recording.relocateFootage": [{
39668
40058
  name: "deviceId",
39669
40059
  form: "single",
@@ -39689,6 +40079,11 @@ Object.freeze({
39689
40079
  form: "single",
39690
40080
  optional: false
39691
40081
  }],
40082
+ "recording.setDevicePlacement": [{
40083
+ name: "deviceId",
40084
+ form: "single",
40085
+ optional: false
40086
+ }],
39692
40087
  "recording.startStorageMigrationMove": [{
39693
40088
  name: "deviceId",
39694
40089
  form: "single",
@@ -39729,6 +40124,11 @@ Object.freeze({
39729
40124
  form: "single",
39730
40125
  optional: false
39731
40126
  }],
40127
+ "sceneMonitor.listScenesBatch": [{
40128
+ name: "deviceIds",
40129
+ form: "array",
40130
+ optional: false
40131
+ }],
39732
40132
  "sceneMonitor.recheckNow": [{
39733
40133
  name: "deviceId",
39734
40134
  form: "single",
@@ -39990,6 +40390,11 @@ Object.freeze({
39990
40390
  form: "single",
39991
40391
  optional: false
39992
40392
  }],
40393
+ "zoneAnalytics.getCurrentSnapshotBatch": [{
40394
+ name: "deviceIds",
40395
+ form: "array",
40396
+ optional: false
40397
+ }],
39993
40398
  "zoneAnalytics.getUnzonedHistory": [{
39994
40399
  name: "deviceId",
39995
40400
  form: "single",
@@ -40563,6 +40968,111 @@ function resolveGlancesCursesShimEnv(options) {
40563
40968
  return { PYTHONPATH: existing ? `${options.shimDir}:${existing}` : options.shimDir };
40564
40969
  }
40565
40970
  //#endregion
40971
+ //#region src/profile-settings.ts
40972
+ var GLANCES_PLUGINS = [
40973
+ {
40974
+ key: "showCpu",
40975
+ plugin: "cpu",
40976
+ label: "CPU"
40977
+ },
40978
+ {
40979
+ key: "showMem",
40980
+ plugin: "mem",
40981
+ label: "Memory"
40982
+ },
40983
+ {
40984
+ key: "showLoad",
40985
+ plugin: "load",
40986
+ label: "Load"
40987
+ },
40988
+ {
40989
+ key: "showNetwork",
40990
+ plugin: "network",
40991
+ label: "Network"
40992
+ },
40993
+ {
40994
+ key: "showDiskIo",
40995
+ plugin: "diskio",
40996
+ label: "Disk I/O"
40997
+ },
40998
+ {
40999
+ key: "showFs",
41000
+ plugin: "fs",
41001
+ label: "Filesystems"
41002
+ },
41003
+ {
41004
+ key: "showProcessList",
41005
+ plugin: "processlist",
41006
+ label: "Process list"
41007
+ },
41008
+ {
41009
+ key: "showContainers",
41010
+ plugin: "containers",
41011
+ label: "Containers"
41012
+ },
41013
+ {
41014
+ key: "showSensors",
41015
+ plugin: "sensors",
41016
+ label: "Sensors"
41017
+ }
41018
+ ];
41019
+ function glancesBooleanField(key, label) {
41020
+ return {
41021
+ type: "boolean",
41022
+ key,
41023
+ label,
41024
+ default: true,
41025
+ style: "switch"
41026
+ };
41027
+ }
41028
+ function glancesSettingsSchema() {
41029
+ return { sections: [{
41030
+ id: "glances-panels",
41031
+ title: "Glances panels",
41032
+ 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).",
41033
+ columns: 2,
41034
+ fields: GLANCES_PLUGINS.map((plugin) => glancesBooleanField(plugin.key, plugin.label))
41035
+ }] };
41036
+ }
41037
+ function settingsSchemaForProfile(profileId) {
41038
+ if (profileId === "glances") return glancesSettingsSchema();
41039
+ return null;
41040
+ }
41041
+ function glancesSettingsToArgs(settings) {
41042
+ const disabled = GLANCES_PLUGINS.filter((plugin) => settings[plugin.key] === false).map((plugin) => plugin.plugin);
41043
+ if (disabled.length === 0) return [];
41044
+ return ["--disable-plugin", disabled.join(",")];
41045
+ }
41046
+ function sanitizeProfileSettings(profileId, raw) {
41047
+ if (profileId !== "glances") return {};
41048
+ const bag = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
41049
+ const out = {
41050
+ showCpu: true,
41051
+ showMem: true,
41052
+ showLoad: true,
41053
+ showNetwork: true,
41054
+ showDiskIo: true,
41055
+ showFs: true,
41056
+ showProcessList: true,
41057
+ showContainers: true,
41058
+ showSensors: true
41059
+ };
41060
+ for (const plugin of GLANCES_PLUGINS) {
41061
+ const value = bag[plugin.key];
41062
+ if (typeof value === "boolean") out[plugin.key] = value;
41063
+ }
41064
+ return out;
41065
+ }
41066
+ function profileSettingsToArgs(profileId, settings) {
41067
+ if (profileId !== "glances") return [];
41068
+ return glancesSettingsToArgs(sanitizeProfileSettings(profileId, settings));
41069
+ }
41070
+ function spawnArgsForInstance(input) {
41071
+ const extra = profileSettingsToArgs(input.profileId, input.profileSettings);
41072
+ if (input.instanceArgs.length > 0) return [...input.instanceArgs, ...extra];
41073
+ if (extra.length > 0) return [...input.profileArgs, ...extra];
41074
+ }
41075
+ //#endregion
40566
41076
  //#region src/pty.ts
40567
41077
  /**
40568
41078
  * Minimal pty abstraction. The manager depends on this interface, never on
@@ -40744,6 +41254,356 @@ async function silenceAnalysisFor(deps, deviceId) {
40744
41254
  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("; ")})`);
40745
41255
  }
40746
41256
  //#endregion
41257
+ //#region src/terminal-camera-declarations.ts
41258
+ /**
41259
+ * Feed DeclaredDevices every live declaration plus one deterministic orphan
41260
+ * batch. The generic sweep intentionally refuses an over-limit set; selecting
41261
+ * a batch here drains large historical Terminal orphan sets across convergence
41262
+ * passes without weakening that global safety guard.
41263
+ */
41264
+ function terminalCameraReconciliationIndex(rows, integrationId, declarations, managedStableIds = /* @__PURE__ */ new Set()) {
41265
+ if (!integrationId) return [];
41266
+ const declared = new Set(declarations.map((camera) => camera.stableId));
41267
+ const owned = rows.filter((row) => row.integrationId === integrationId && (declared.has(row.stableId) || managedStableIds.has(row.stableId) || row.stableId.startsWith("terminal-camera-instance-")));
41268
+ 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)];
41269
+ }
41270
+ /** Explicit persisted instances, never the node × profile template matrix. */
41271
+ function buildTerminalInstanceCameraDeclarations(instances) {
41272
+ return instances.filter((instance) => instance.enabled).map((instance) => ({
41273
+ stableId: instance.cameraStableId,
41274
+ name: instance.name,
41275
+ config: {
41276
+ instanceId: instance.id,
41277
+ nodeId: instance.nodeId,
41278
+ profileId: instance.profileId,
41279
+ profileLabel: instance.profileLabel
41280
+ }
41281
+ }));
41282
+ }
41283
+ /**
41284
+ * `DeviceConfig` materializes schema defaults in memory, so comparing
41285
+ * `config.get()` cannot detect a legacy `{ nodeId }` row. Reconciliation must
41286
+ * inspect the raw persisted blob to make the profile migration durable.
41287
+ */
41288
+ function needsTerminalCameraConfigMigration(persistedConfig, declaration) {
41289
+ return persistedConfig.instanceId !== declaration.config.instanceId || persistedConfig.nodeId !== declaration.config.nodeId || persistedConfig.profileId !== declaration.config.profileId || persistedConfig.profileLabel !== declaration.config.profileLabel;
41290
+ }
41291
+ //#endregion
41292
+ //#region src/terminal-cell-runs.ts
41293
+ var TERMINAL_DEFAULT_FG = "#d7dce2";
41294
+ var TERMINAL_DEFAULT_BG = "#0b0d10";
41295
+ /**
41296
+ * The 16 ANSI colours, in a dark-theme variant chosen to sit with the existing
41297
+ * frame (`#0b0d10` ground, `#d7dce2` text) rather than the raw xterm defaults,
41298
+ * whose pure `#0000ff` blue and `#008000` green are close to unreadable on a
41299
+ * near-black background at 13px. Index 7 IS the default foreground, so plain
41300
+ * `CSI 37m` text renders identically to unstyled text.
41301
+ */
41302
+ var TERMINAL_ANSI_PALETTE = [
41303
+ "#282c34",
41304
+ "#e06c75",
41305
+ "#98c379",
41306
+ "#e5c07b",
41307
+ "#61afef",
41308
+ "#c678dd",
41309
+ "#56b6c2",
41310
+ TERMINAL_DEFAULT_FG,
41311
+ "#5c6370",
41312
+ "#ef596f",
41313
+ "#89ca78",
41314
+ "#f0c674",
41315
+ "#6cb6ff",
41316
+ "#d55fde",
41317
+ "#2bbac5",
41318
+ "#ffffff"
41319
+ ];
41320
+ /** The six levels of the xterm 6×6×6 colour cube (indices 16-231). */
41321
+ var TERMINAL_CUBE_LEVELS = [
41322
+ 0,
41323
+ 95,
41324
+ 135,
41325
+ 175,
41326
+ 215,
41327
+ 255
41328
+ ];
41329
+ var TERMINAL_CUBE_FIRST = 16;
41330
+ var TERMINAL_GRAYSCALE_FIRST = 232;
41331
+ var TERMINAL_GRAYSCALE_BASE = 8;
41332
+ var TERMINAL_GRAYSCALE_STEP = 10;
41333
+ /** SGR 2 keeps the foreground legible; it must not become the background. */
41334
+ var TERMINAL_DIM_WEIGHT = .6;
41335
+ function channel(value) {
41336
+ return Math.max(0, Math.min(255, Math.round(value))).toString(16).padStart(2, "0");
41337
+ }
41338
+ function hex(red, green, blue) {
41339
+ return `#${channel(red)}${channel(green)}${channel(blue)}`;
41340
+ }
41341
+ function parseHex(color) {
41342
+ return [
41343
+ Number.parseInt(color.slice(1, 3), 16),
41344
+ Number.parseInt(color.slice(3, 5), 16),
41345
+ Number.parseInt(color.slice(5, 7), 16)
41346
+ ];
41347
+ }
41348
+ /** Resolve an xterm palette index (0-255) to a hex colour. */
41349
+ function terminalPaletteColor(index) {
41350
+ const ansi = TERMINAL_ANSI_PALETTE[index];
41351
+ if (ansi !== void 0) return ansi;
41352
+ if (index >= TERMINAL_GRAYSCALE_FIRST) {
41353
+ const level = TERMINAL_GRAYSCALE_BASE + (index - TERMINAL_GRAYSCALE_FIRST) * TERMINAL_GRAYSCALE_STEP;
41354
+ return hex(level, level, level);
41355
+ }
41356
+ if (index >= TERMINAL_CUBE_FIRST) {
41357
+ const offset = index - TERMINAL_CUBE_FIRST;
41358
+ 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);
41359
+ }
41360
+ return TERMINAL_DEFAULT_FG;
41361
+ }
41362
+ /** Resolve a 0xRRGGBB truecolor value to a hex colour. */
41363
+ function terminalRgbColor(value) {
41364
+ return hex(value >> 16 & 255, value >> 8 & 255, value & 255);
41365
+ }
41366
+ function blend(color, toward, weight) {
41367
+ const [red, green, blue] = parseHex(color);
41368
+ const [targetRed, targetGreen, targetBlue] = parseHex(toward);
41369
+ return hex(red * weight + targetRed * (1 - weight), green * weight + targetGreen * (1 - weight), blue * weight + targetBlue * (1 - weight));
41370
+ }
41371
+ function resolveForeground(cell) {
41372
+ if (cell.isFgRGB()) return terminalRgbColor(cell.getFgColor());
41373
+ if (cell.isFgPalette()) return terminalPaletteColor(cell.getFgColor());
41374
+ return TERMINAL_DEFAULT_FG;
41375
+ }
41376
+ function resolveBackground(cell) {
41377
+ if (cell.isBgRGB()) return terminalRgbColor(cell.getBgColor());
41378
+ if (cell.isBgPalette()) return terminalPaletteColor(cell.getBgColor());
41379
+ return TERMINAL_DEFAULT_BG;
41380
+ }
41381
+ /**
41382
+ * Resolve one cell's attributes into concrete colours.
41383
+ *
41384
+ * Inverse is applied by SWAPPING the already-resolved pair, so inverse over two
41385
+ * defaults is still a visible swap rather than a no-op — that is how a selected
41386
+ * or highlighted row in Glances reads. Invisible is then conceal-by-equality
41387
+ * (foreground painted in its own background): the cell keeps its columns, which
41388
+ * a dropped cell would not, and dropping it would shift the whole rest of the
41389
+ * row left.
41390
+ */
41391
+ function resolveCellStyle(cell) {
41392
+ const inverse = cell.isInverse() !== 0;
41393
+ const plainFg = resolveForeground(cell);
41394
+ const plainBg = resolveBackground(cell);
41395
+ const background = inverse ? plainFg : plainBg;
41396
+ let foreground = inverse ? plainBg : plainFg;
41397
+ if (cell.isDim() !== 0) foreground = blend(foreground, background, TERMINAL_DIM_WEIGHT);
41398
+ if (cell.isInvisible() !== 0) foreground = background;
41399
+ return {
41400
+ fg: foreground === "#d7dce2" ? null : foreground,
41401
+ bg: background === "#0b0d10" ? null : background,
41402
+ bold: cell.isBold() !== 0
41403
+ };
41404
+ }
41405
+ function sameStyle(left, right) {
41406
+ return left.fg === right.fg && left.bg === right.bg && left.bold === right.bold;
41407
+ }
41408
+ /**
41409
+ * Merge adjacent same-style cells into runs, then drop the trailing run of
41410
+ * default-styled whitespace so a row costs what it draws — the same trim
41411
+ * `translateToString(true)` performs. Whitespace carrying a BACKGROUND is kept:
41412
+ * a green bar of spaces out to the right margin is a pixel Glances drew.
41413
+ */
41414
+ function buildCellRuns(cells) {
41415
+ const runs = [];
41416
+ let text = "";
41417
+ let style = null;
41418
+ for (const cell of cells) {
41419
+ if (style !== null && sameStyle(style, cell.style)) {
41420
+ text += cell.text;
41421
+ continue;
41422
+ }
41423
+ if (style !== null) runs.push({
41424
+ text,
41425
+ ...style
41426
+ });
41427
+ text = cell.text;
41428
+ style = cell.style;
41429
+ }
41430
+ if (style !== null) runs.push({
41431
+ text,
41432
+ ...style
41433
+ });
41434
+ while (runs.length > 0) {
41435
+ const last = runs[runs.length - 1];
41436
+ if (last === void 0 || last.bg !== null) break;
41437
+ const trimmed = last.text.replace(/\s+$/u, "");
41438
+ if (trimmed === last.text) break;
41439
+ if (trimmed === "") {
41440
+ runs.pop();
41441
+ continue;
41442
+ }
41443
+ runs[runs.length - 1] = {
41444
+ ...last,
41445
+ text: trimmed
41446
+ };
41447
+ break;
41448
+ }
41449
+ return runs;
41450
+ }
41451
+ /**
41452
+ * Monospace families to try, in order — NOT one family and a generic.
41453
+ *
41454
+ * A terminal screen is mostly box-drawing and block characters, and a font
41455
+ * without them renders the frame as noise rather than as missing detail.
41456
+ * `DejaVu Sans Mono` covers them and ships in the hub image; macOS has neither
41457
+ * DejaVu nor fontconfig, so the Mac agent fell through to a generic whose
41458
+ * coverage is not, and its Glances camera came out unreadable while the hub's
41459
+ * was pixel-perfect (measured 2026-08-11, same Glances, two nodes).
41460
+ *
41461
+ * `Menlo` is the fix rather than a guess: it is macOS's default terminal face,
41462
+ * present on every install, and derived from DejaVu Sans Mono — the same glyph
41463
+ * coverage by ancestry. Monaco and Courier New follow as older fallbacks; the
41464
+ * generic stays last so a host with none of them still draws something.
41465
+ */
41466
+ var TERMINAL_FONT_STACK = "DejaVu Sans Mono,Menlo,Monaco,Courier New,monospace";
41467
+ var TERMINAL_FONT_SIZE = 13;
41468
+ var TERMINAL_TEXT_MARGIN_X = 8;
41469
+ var TERMINAL_ROW_HEIGHT = 15;
41470
+ var TERMINAL_BASELINE_Y = 18;
41471
+ /**
41472
+ * Distance from a row's baseline up to the top of its cell box. Chosen so
41473
+ * consecutive rows tile exactly: row N's box runs from `baseline - this` for
41474
+ * `TERMINAL_ROW_HEIGHT`, and row N+1's box starts where it ends. A background
41475
+ * bar that stopped short would draw as stripes across a `CSI 42m` panel.
41476
+ */
41477
+ var TERMINAL_CELL_ASCENT = 11.5;
41478
+ var TERMINAL_CELL_WIDTH = TERMINAL_FONT_SIZE * (1233 / 2048);
41479
+ /** Two decimals is under a tenth of a pixel and keeps the SVG small. */
41480
+ function coordinate(value) {
41481
+ return String(Number(value.toFixed(2)));
41482
+ }
41483
+ function escapeXml(value) {
41484
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;");
41485
+ }
41486
+ /**
41487
+ * Render already-interpreted terminal rows into a compact MJPEG frame.
41488
+ *
41489
+ * `xml:space="preserve"` is load-bearing, not tidiness. SVG `<text>` collapses
41490
+ * runs of whitespace by default, and a terminal's entire column alignment IS
41491
+ * runs of whitespace — Glances pads every field with spaces. Without it the
41492
+ * frame drew each line at roughly half its true width, crammed into the
41493
+ * top-left of a mostly-black image, while the SAME session over `attach`
41494
+ * looked perfect — which is exactly how the operator reported it. Measured in
41495
+ * the hub's own rasterizer 2026-08-11: `AAA<10 spaces>BBB<3>CCC` drew 92 px
41496
+ * collapsed against 178 px preserved.
41497
+ *
41498
+ * Every run is anchored at its OWN column (`x = margin + startCol * cellW`),
41499
+ * never appended to the one before it, so the background rects and the glyphs
41500
+ * are placed off the same grid and cannot drift apart. `textLength` is emitted
41501
+ * with it because it is the correct declaration and renderers that honour it
41502
+ * get an exact grid — but it is not what makes this work: librsvg, which sharp
41503
+ * uses, ignores it outright (measured 2026-08-12, a 120-glyph run pinned to
41504
+ * 600 px still drew its natural 937 px). The anchoring is the guarantee.
41505
+ */
41506
+ function renderTerminalSvg(rows) {
41507
+ const backgrounds = [];
41508
+ const texts = [];
41509
+ rows.slice(0, 40).forEach((row, index) => {
41510
+ const baseline = TERMINAL_BASELINE_Y + index * TERMINAL_ROW_HEIGHT;
41511
+ const top = baseline - TERMINAL_CELL_ASCENT;
41512
+ let column = 0;
41513
+ for (const run of row) {
41514
+ if (column >= 120) break;
41515
+ const clipped = clipRun(run, 120 - column);
41516
+ const columns = [...clipped].length;
41517
+ if (columns === 0) continue;
41518
+ const x = TERMINAL_TEXT_MARGIN_X + column * TERMINAL_CELL_WIDTH;
41519
+ const width = columns * TERMINAL_CELL_WIDTH;
41520
+ if (run.bg !== null) backgrounds.push(`<rect x="${coordinate(x)}" y="${coordinate(top)}" width="${coordinate(width)}" height="${String(TERMINAL_ROW_HEIGHT)}" fill="${run.bg}"/>`);
41521
+ if (clipped.trim() !== "") {
41522
+ const fill = run.fg === null ? "" : ` fill="${run.fg}"`;
41523
+ const weight = run.bold ? " font-weight=\"bold\"" : "";
41524
+ texts.push(`<text xml:space="preserve" x="${coordinate(x)}" y="${coordinate(baseline)}" textLength="${coordinate(width)}" lengthAdjust="spacingAndGlyphs"${fill}${weight}>${escapeXml(clipped)}</text>`);
41525
+ }
41526
+ column += columns;
41527
+ }
41528
+ });
41529
+ 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>`;
41530
+ }
41531
+ /** Cut a run to the columns still left in the row, by code point not unit. */
41532
+ function clipRun(run, remaining) {
41533
+ const points = [...run.text];
41534
+ return points.length <= remaining ? run.text : points.slice(0, remaining).join("");
41535
+ }
41536
+ async function renderTerminalJpeg(rows) {
41537
+ return (0, sharp.default)(Buffer.from(renderTerminalSvg(rows))).jpeg({
41538
+ quality: 82,
41539
+ chromaSubsampling: "4:2:0"
41540
+ }).toBuffer();
41541
+ }
41542
+ //#endregion
41543
+ //#region src/terminal-camera-device.ts
41544
+ var terminalCameraSchema = object({
41545
+ instanceId: string().min(1).optional(),
41546
+ nodeId: string().min(1),
41547
+ profileId: string().min(1).default("monitor"),
41548
+ profileLabel: string().min(1).default("BTM")
41549
+ });
41550
+ var relay = null;
41551
+ function installTerminalCameraRelay(next) {
41552
+ relay = next;
41553
+ }
41554
+ var TerminalCameraDevice = class extends BaseDevice {
41555
+ features = [DeviceFeature.NativeSnapshot];
41556
+ constructor(ctx) {
41557
+ super(ctx, terminalCameraSchema, { type: ctx.deviceMeta.type });
41558
+ this.ctx.registerNativeCap(streamCatalogCapability, { getCatalog: async ({ deviceId }) => {
41559
+ if (deviceId !== this.id) return [];
41560
+ return this.catalog();
41561
+ } });
41562
+ this.ctx.registerNativeCap(snapshotCapability, {
41563
+ getSnapshot: async ({ deviceId }) => {
41564
+ if (deviceId !== this.id) throw new Error(`TerminalCameraDevice: deviceId mismatch, expected ${String(this.id)}, got ${String(deviceId)}`);
41565
+ const activeRelay = relay;
41566
+ if (!activeRelay) throw new Error("terminal camera relay is unavailable");
41567
+ return {
41568
+ base64: (await activeRelay.snapshot(this.relayInstanceId(), this.config.get("nodeId"), this.config.get("profileId"))).toString("base64"),
41569
+ contentType: "image/jpeg"
41570
+ };
41571
+ },
41572
+ invalidateCache: async () => {}
41573
+ });
41574
+ this.markOnline(true);
41575
+ }
41576
+ async catalog() {
41577
+ const activeRelay = relay;
41578
+ if (!activeRelay) throw new Error("terminal camera relay is unavailable");
41579
+ const nodeId = this.config.get("nodeId");
41580
+ const profileId = this.config.get("profileId");
41581
+ const instanceId = this.relayInstanceId();
41582
+ return [{
41583
+ camStreamId: profileId,
41584
+ kind: "pull-http",
41585
+ url: activeRelay.streamUrl(instanceId, nodeId, profileId),
41586
+ codec: "h264",
41587
+ resolution: {
41588
+ width: 960,
41589
+ height: 640
41590
+ },
41591
+ fps: 2,
41592
+ label: this.config.get("profileLabel")
41593
+ }];
41594
+ }
41595
+ setNodeOnline(online) {
41596
+ this.markOnline(online);
41597
+ if (!online) relay?.closeInstance(this.relayInstanceId());
41598
+ }
41599
+ async removeDevice() {
41600
+ await relay?.closeInstance(this.relayInstanceId());
41601
+ }
41602
+ relayInstanceId() {
41603
+ return this.config.get("instanceId") ?? `legacy:${this.stableId}`;
41604
+ }
41605
+ };
41606
+ //#endregion
40747
41607
  //#region ../../node_modules/@xterm/addon-serialize/lib/addon-serialize.js
40748
41608
  var require_addon_serialize = /* @__PURE__ */ __commonJSMin(((exports, module) => {
40749
41609
  (function(e, t) {
@@ -45551,174 +46411,9 @@ var require_xterm_headless = /* @__PURE__ */ __commonJSMin(((exports) => {
45551
46411
  })();
45552
46412
  }));
45553
46413
  //#endregion
45554
- //#region src/terminal-cell-runs.ts
46414
+ //#region src/xterm-screen.ts
45555
46415
  var import_addon_serialize = require_addon_serialize();
45556
46416
  var import_xterm_headless = require_xterm_headless();
45557
- var TERMINAL_DEFAULT_FG = "#d7dce2";
45558
- var TERMINAL_DEFAULT_BG = "#0b0d10";
45559
- /**
45560
- * The 16 ANSI colours, in a dark-theme variant chosen to sit with the existing
45561
- * frame (`#0b0d10` ground, `#d7dce2` text) rather than the raw xterm defaults,
45562
- * whose pure `#0000ff` blue and `#008000` green are close to unreadable on a
45563
- * near-black background at 13px. Index 7 IS the default foreground, so plain
45564
- * `CSI 37m` text renders identically to unstyled text.
45565
- */
45566
- var TERMINAL_ANSI_PALETTE = [
45567
- "#282c34",
45568
- "#e06c75",
45569
- "#98c379",
45570
- "#e5c07b",
45571
- "#61afef",
45572
- "#c678dd",
45573
- "#56b6c2",
45574
- TERMINAL_DEFAULT_FG,
45575
- "#5c6370",
45576
- "#ef596f",
45577
- "#89ca78",
45578
- "#f0c674",
45579
- "#6cb6ff",
45580
- "#d55fde",
45581
- "#2bbac5",
45582
- "#ffffff"
45583
- ];
45584
- /** The six levels of the xterm 6×6×6 colour cube (indices 16-231). */
45585
- var TERMINAL_CUBE_LEVELS = [
45586
- 0,
45587
- 95,
45588
- 135,
45589
- 175,
45590
- 215,
45591
- 255
45592
- ];
45593
- var TERMINAL_CUBE_FIRST = 16;
45594
- var TERMINAL_GRAYSCALE_FIRST = 232;
45595
- var TERMINAL_GRAYSCALE_BASE = 8;
45596
- var TERMINAL_GRAYSCALE_STEP = 10;
45597
- /** SGR 2 keeps the foreground legible; it must not become the background. */
45598
- var TERMINAL_DIM_WEIGHT = .6;
45599
- function channel(value) {
45600
- return Math.max(0, Math.min(255, Math.round(value))).toString(16).padStart(2, "0");
45601
- }
45602
- function hex(red, green, blue) {
45603
- return `#${channel(red)}${channel(green)}${channel(blue)}`;
45604
- }
45605
- function parseHex(color) {
45606
- return [
45607
- Number.parseInt(color.slice(1, 3), 16),
45608
- Number.parseInt(color.slice(3, 5), 16),
45609
- Number.parseInt(color.slice(5, 7), 16)
45610
- ];
45611
- }
45612
- /** Resolve an xterm palette index (0-255) to a hex colour. */
45613
- function terminalPaletteColor(index) {
45614
- const ansi = TERMINAL_ANSI_PALETTE[index];
45615
- if (ansi !== void 0) return ansi;
45616
- if (index >= TERMINAL_GRAYSCALE_FIRST) {
45617
- const level = TERMINAL_GRAYSCALE_BASE + (index - TERMINAL_GRAYSCALE_FIRST) * TERMINAL_GRAYSCALE_STEP;
45618
- return hex(level, level, level);
45619
- }
45620
- if (index >= TERMINAL_CUBE_FIRST) {
45621
- const offset = index - TERMINAL_CUBE_FIRST;
45622
- 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);
45623
- }
45624
- return TERMINAL_DEFAULT_FG;
45625
- }
45626
- /** Resolve a 0xRRGGBB truecolor value to a hex colour. */
45627
- function terminalRgbColor(value) {
45628
- return hex(value >> 16 & 255, value >> 8 & 255, value & 255);
45629
- }
45630
- function blend(color, toward, weight) {
45631
- const [red, green, blue] = parseHex(color);
45632
- const [targetRed, targetGreen, targetBlue] = parseHex(toward);
45633
- return hex(red * weight + targetRed * (1 - weight), green * weight + targetGreen * (1 - weight), blue * weight + targetBlue * (1 - weight));
45634
- }
45635
- function resolveForeground(cell) {
45636
- if (cell.isFgRGB()) return terminalRgbColor(cell.getFgColor());
45637
- if (cell.isFgPalette()) return terminalPaletteColor(cell.getFgColor());
45638
- return TERMINAL_DEFAULT_FG;
45639
- }
45640
- function resolveBackground(cell) {
45641
- if (cell.isBgRGB()) return terminalRgbColor(cell.getBgColor());
45642
- if (cell.isBgPalette()) return terminalPaletteColor(cell.getBgColor());
45643
- return TERMINAL_DEFAULT_BG;
45644
- }
45645
- /**
45646
- * Resolve one cell's attributes into concrete colours.
45647
- *
45648
- * Inverse is applied by SWAPPING the already-resolved pair, so inverse over two
45649
- * defaults is still a visible swap rather than a no-op — that is how a selected
45650
- * or highlighted row in Glances reads. Invisible is then conceal-by-equality
45651
- * (foreground painted in its own background): the cell keeps its columns, which
45652
- * a dropped cell would not, and dropping it would shift the whole rest of the
45653
- * row left.
45654
- */
45655
- function resolveCellStyle(cell) {
45656
- const inverse = cell.isInverse() !== 0;
45657
- const plainFg = resolveForeground(cell);
45658
- const plainBg = resolveBackground(cell);
45659
- const background = inverse ? plainFg : plainBg;
45660
- let foreground = inverse ? plainBg : plainFg;
45661
- if (cell.isDim() !== 0) foreground = blend(foreground, background, TERMINAL_DIM_WEIGHT);
45662
- if (cell.isInvisible() !== 0) foreground = background;
45663
- return {
45664
- fg: foreground === "#d7dce2" ? null : foreground,
45665
- bg: background === "#0b0d10" ? null : background,
45666
- bold: cell.isBold() !== 0
45667
- };
45668
- }
45669
- function sameStyle(left, right) {
45670
- return left.fg === right.fg && left.bg === right.bg && left.bold === right.bold;
45671
- }
45672
- /**
45673
- * Merge adjacent same-style cells into runs, then drop the trailing run of
45674
- * default-styled whitespace so a row costs what it draws — the same trim
45675
- * `translateToString(true)` performs. Whitespace carrying a BACKGROUND is kept:
45676
- * a green bar of spaces out to the right margin is a pixel Glances drew.
45677
- */
45678
- function buildCellRuns(cells) {
45679
- const runs = [];
45680
- let text = "";
45681
- let style = null;
45682
- for (const cell of cells) {
45683
- if (style !== null && sameStyle(style, cell.style)) {
45684
- text += cell.text;
45685
- continue;
45686
- }
45687
- if (style !== null) runs.push({
45688
- text,
45689
- ...style
45690
- });
45691
- text = cell.text;
45692
- style = cell.style;
45693
- }
45694
- if (style !== null) runs.push({
45695
- text,
45696
- ...style
45697
- });
45698
- while (runs.length > 0) {
45699
- const last = runs[runs.length - 1];
45700
- if (last === void 0 || last.bg !== null) break;
45701
- const trimmed = last.text.replace(/\s+$/u, "");
45702
- if (trimmed === last.text) break;
45703
- if (trimmed === "") {
45704
- runs.pop();
45705
- continue;
45706
- }
45707
- runs[runs.length - 1] = {
45708
- ...last,
45709
- text: trimmed
45710
- };
45711
- break;
45712
- }
45713
- return runs;
45714
- }
45715
- //#endregion
45716
- //#region src/xterm-screen.ts
45717
- /**
45718
- * Real {@link ScreenBuffer} backed by `@xterm/headless` — the DOM-less xterm
45719
- * build — plus the serialize addon, which turns the current buffer into a
45720
- * self-contained repaint escape sequence for reconnecting clients.
45721
- */
45722
46417
  var SCROLLBACK_LINES = 2e3;
45723
46418
  function createXtermScreen(cols, rows) {
45724
46419
  const term = new import_xterm_headless.Terminal({
@@ -45785,196 +46480,6 @@ function createXtermScreen(cols, rows) {
45785
46480
  };
45786
46481
  }
45787
46482
  //#endregion
45788
- //#region src/terminal-camera-declarations.ts
45789
- /**
45790
- * Feed DeclaredDevices every live declaration plus one deterministic orphan
45791
- * batch. The generic sweep intentionally refuses an over-limit set; selecting
45792
- * a batch here drains large historical Terminal orphan sets across convergence
45793
- * passes without weakening that global safety guard.
45794
- */
45795
- function terminalCameraReconciliationIndex(rows, integrationId, declarations, managedStableIds = /* @__PURE__ */ new Set()) {
45796
- if (!integrationId) return [];
45797
- const declared = new Set(declarations.map((camera) => camera.stableId));
45798
- const owned = rows.filter((row) => row.integrationId === integrationId && (declared.has(row.stableId) || managedStableIds.has(row.stableId) || row.stableId.startsWith("terminal-camera-instance-")));
45799
- 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)];
45800
- }
45801
- /** Explicit persisted instances, never the node × profile template matrix. */
45802
- function buildTerminalInstanceCameraDeclarations(instances) {
45803
- return instances.filter((instance) => instance.enabled).map((instance) => ({
45804
- stableId: instance.cameraStableId,
45805
- name: instance.name,
45806
- config: {
45807
- instanceId: instance.id,
45808
- nodeId: instance.nodeId,
45809
- profileId: instance.profileId,
45810
- profileLabel: instance.profileLabel
45811
- }
45812
- }));
45813
- }
45814
- /**
45815
- * `DeviceConfig` materializes schema defaults in memory, so comparing
45816
- * `config.get()` cannot detect a legacy `{ nodeId }` row. Reconciliation must
45817
- * inspect the raw persisted blob to make the profile migration durable.
45818
- */
45819
- function needsTerminalCameraConfigMigration(persistedConfig, declaration) {
45820
- return persistedConfig.instanceId !== declaration.config.instanceId || persistedConfig.nodeId !== declaration.config.nodeId || persistedConfig.profileId !== declaration.config.profileId || persistedConfig.profileLabel !== declaration.config.profileLabel;
45821
- }
45822
- /**
45823
- * Monospace families to try, in order — NOT one family and a generic.
45824
- *
45825
- * A terminal screen is mostly box-drawing and block characters, and a font
45826
- * without them renders the frame as noise rather than as missing detail.
45827
- * `DejaVu Sans Mono` covers them and ships in the hub image; macOS has neither
45828
- * DejaVu nor fontconfig, so the Mac agent fell through to a generic whose
45829
- * coverage is not, and its Glances camera came out unreadable while the hub's
45830
- * was pixel-perfect (measured 2026-08-11, same Glances, two nodes).
45831
- *
45832
- * `Menlo` is the fix rather than a guess: it is macOS's default terminal face,
45833
- * present on every install, and derived from DejaVu Sans Mono — the same glyph
45834
- * coverage by ancestry. Monaco and Courier New follow as older fallbacks; the
45835
- * generic stays last so a host with none of them still draws something.
45836
- */
45837
- var TERMINAL_FONT_STACK = "DejaVu Sans Mono,Menlo,Monaco,Courier New,monospace";
45838
- var TERMINAL_FONT_SIZE = 13;
45839
- var TERMINAL_TEXT_MARGIN_X = 8;
45840
- var TERMINAL_ROW_HEIGHT = 15;
45841
- var TERMINAL_BASELINE_Y = 18;
45842
- /**
45843
- * Distance from a row's baseline up to the top of its cell box. Chosen so
45844
- * consecutive rows tile exactly: row N's box runs from `baseline - this` for
45845
- * `TERMINAL_ROW_HEIGHT`, and row N+1's box starts where it ends. A background
45846
- * bar that stopped short would draw as stripes across a `CSI 42m` panel.
45847
- */
45848
- var TERMINAL_CELL_ASCENT = 11.5;
45849
- var TERMINAL_CELL_WIDTH = TERMINAL_FONT_SIZE * (1233 / 2048);
45850
- /** Two decimals is under a tenth of a pixel and keeps the SVG small. */
45851
- function coordinate(value) {
45852
- return String(Number(value.toFixed(2)));
45853
- }
45854
- function escapeXml(value) {
45855
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;");
45856
- }
45857
- /**
45858
- * Render already-interpreted terminal rows into a compact MJPEG frame.
45859
- *
45860
- * `xml:space="preserve"` is load-bearing, not tidiness. SVG `<text>` collapses
45861
- * runs of whitespace by default, and a terminal's entire column alignment IS
45862
- * runs of whitespace — Glances pads every field with spaces. Without it the
45863
- * frame drew each line at roughly half its true width, crammed into the
45864
- * top-left of a mostly-black image, while the SAME session over `attach`
45865
- * looked perfect — which is exactly how the operator reported it. Measured in
45866
- * the hub's own rasterizer 2026-08-11: `AAA<10 spaces>BBB<3>CCC` drew 92 px
45867
- * collapsed against 178 px preserved.
45868
- *
45869
- * Every run is anchored at its OWN column (`x = margin + startCol * cellW`),
45870
- * never appended to the one before it, so the background rects and the glyphs
45871
- * are placed off the same grid and cannot drift apart. `textLength` is emitted
45872
- * with it because it is the correct declaration and renderers that honour it
45873
- * get an exact grid — but it is not what makes this work: librsvg, which sharp
45874
- * uses, ignores it outright (measured 2026-08-12, a 120-glyph run pinned to
45875
- * 600 px still drew its natural 937 px). The anchoring is the guarantee.
45876
- */
45877
- function renderTerminalSvg(rows) {
45878
- const backgrounds = [];
45879
- const texts = [];
45880
- rows.slice(0, 40).forEach((row, index) => {
45881
- const baseline = TERMINAL_BASELINE_Y + index * TERMINAL_ROW_HEIGHT;
45882
- const top = baseline - TERMINAL_CELL_ASCENT;
45883
- let column = 0;
45884
- for (const run of row) {
45885
- if (column >= 120) break;
45886
- const clipped = clipRun(run, 120 - column);
45887
- const columns = [...clipped].length;
45888
- if (columns === 0) continue;
45889
- const x = TERMINAL_TEXT_MARGIN_X + column * TERMINAL_CELL_WIDTH;
45890
- const width = columns * TERMINAL_CELL_WIDTH;
45891
- if (run.bg !== null) backgrounds.push(`<rect x="${coordinate(x)}" y="${coordinate(top)}" width="${coordinate(width)}" height="${String(TERMINAL_ROW_HEIGHT)}" fill="${run.bg}"/>`);
45892
- if (clipped.trim() !== "") {
45893
- const fill = run.fg === null ? "" : ` fill="${run.fg}"`;
45894
- const weight = run.bold ? " font-weight=\"bold\"" : "";
45895
- texts.push(`<text xml:space="preserve" x="${coordinate(x)}" y="${coordinate(baseline)}" textLength="${coordinate(width)}" lengthAdjust="spacingAndGlyphs"${fill}${weight}>${escapeXml(clipped)}</text>`);
45896
- }
45897
- column += columns;
45898
- }
45899
- });
45900
- 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>`;
45901
- }
45902
- /** Cut a run to the columns still left in the row, by code point not unit. */
45903
- function clipRun(run, remaining) {
45904
- const points = [...run.text];
45905
- return points.length <= remaining ? run.text : points.slice(0, remaining).join("");
45906
- }
45907
- async function renderTerminalJpeg(rows) {
45908
- return (0, sharp.default)(Buffer.from(renderTerminalSvg(rows))).jpeg({
45909
- quality: 82,
45910
- chromaSubsampling: "4:2:0"
45911
- }).toBuffer();
45912
- }
45913
- //#endregion
45914
- //#region src/terminal-camera-device.ts
45915
- var terminalCameraSchema = object({
45916
- instanceId: string().min(1).optional(),
45917
- nodeId: string().min(1),
45918
- profileId: string().min(1).default("monitor"),
45919
- profileLabel: string().min(1).default("BTM")
45920
- });
45921
- var relay = null;
45922
- function installTerminalCameraRelay(next) {
45923
- relay = next;
45924
- }
45925
- var TerminalCameraDevice = class extends BaseDevice {
45926
- features = [DeviceFeature.NativeSnapshot];
45927
- constructor(ctx) {
45928
- super(ctx, terminalCameraSchema, { type: ctx.deviceMeta.type });
45929
- this.ctx.registerNativeCap(streamCatalogCapability, { getCatalog: async ({ deviceId }) => {
45930
- if (deviceId !== this.id) return [];
45931
- return this.catalog();
45932
- } });
45933
- this.ctx.registerNativeCap(snapshotCapability, {
45934
- getSnapshot: async ({ deviceId }) => {
45935
- if (deviceId !== this.id) throw new Error(`TerminalCameraDevice: deviceId mismatch, expected ${String(this.id)}, got ${String(deviceId)}`);
45936
- const activeRelay = relay;
45937
- if (!activeRelay) throw new Error("terminal camera relay is unavailable");
45938
- return {
45939
- base64: (await activeRelay.snapshot(this.relayInstanceId(), this.config.get("nodeId"), this.config.get("profileId"))).toString("base64"),
45940
- contentType: "image/jpeg"
45941
- };
45942
- },
45943
- invalidateCache: async () => {}
45944
- });
45945
- this.markOnline(true);
45946
- }
45947
- async catalog() {
45948
- const activeRelay = relay;
45949
- if (!activeRelay) throw new Error("terminal camera relay is unavailable");
45950
- const nodeId = this.config.get("nodeId");
45951
- const profileId = this.config.get("profileId");
45952
- const instanceId = this.relayInstanceId();
45953
- return [{
45954
- camStreamId: profileId,
45955
- kind: "pull-http",
45956
- url: activeRelay.streamUrl(instanceId, nodeId, profileId),
45957
- codec: "h264",
45958
- resolution: {
45959
- width: 960,
45960
- height: 640
45961
- },
45962
- fps: 2,
45963
- label: this.config.get("profileLabel")
45964
- }];
45965
- }
45966
- setNodeOnline(online) {
45967
- this.markOnline(online);
45968
- if (!online) relay?.closeInstance(this.relayInstanceId());
45969
- }
45970
- async removeDevice() {
45971
- await relay?.closeInstance(this.relayInstanceId());
45972
- }
45973
- relayInstanceId() {
45974
- return this.config.get("instanceId") ?? `legacy:${this.stableId}`;
45975
- }
45976
- };
45977
- //#endregion
45978
46483
  //#region src/terminal-camera-relay.ts
45979
46484
  var MJPEG_BOUNDARY = "camstack-terminal-frame";
45980
46485
  var SESSION_IDLE_MS = 3e4;
@@ -46525,13 +47030,34 @@ function newTerminalCameraStableId(instanceId) {
46525
47030
  * Legacy automatic cameras are migration candidates only. A tombstone is
46526
47031
  * durable deletion intent, so a lingering failed device removal must never
46527
47032
  * make that camera adoptable again.
47033
+ *
47034
+ * ## `config` is load-bearing — this read can never be `projection: 'slim'`
47035
+ *
47036
+ * `nodeId`, `profileId` and `profileLabel` all live in the device's `config`,
47037
+ * and a row without `nodeId` is skipped. The slim projection returns
47038
+ * `config: {}` for every row, so a slim answer here is shape-identical to a
47039
+ * fleet that has no legacy cameras — the whole migration section disappears
47040
+ * and nothing says why. `legacy-camera-read-shape.spec.ts` is the arm on that.
47041
+ *
47042
+ * `onSkipped` is why the disappearance would now be visible: a row that LOOKS
47043
+ * like a legacy camera (`terminal-camera-*`, not an instance camera, not
47044
+ * tombstoned, not already adopted) but carries no `nodeId` is reported with
47045
+ * its numeric device id, so the caller can log it per-camera. Rows that are
47046
+ * not candidates at all are silent — a fleet of 1 017 devices must not
47047
+ * produce 1 017 lines to say none of them is a Terminal monitor.
46528
47048
  */
46529
- function listLegacyTerminalCameraCandidates(rows, instanceStableIds, tombstones) {
47049
+ function listLegacyTerminalCameraCandidates(rows, instanceStableIds, tombstones, onSkipped) {
46530
47050
  const legacy = [];
46531
47051
  for (const row of rows) {
46532
47052
  if (tombstones.has(row.stableId) || instanceStableIds.has(row.stableId) || row.stableId.startsWith("terminal-camera-instance-") || !row.stableId.startsWith("terminal-camera-")) continue;
46533
47053
  const nodeId = typeof row.config.nodeId === "string" ? row.config.nodeId : null;
46534
- if (!nodeId) continue;
47054
+ if (!nodeId) {
47055
+ onSkipped?.({
47056
+ deviceId: row.id,
47057
+ stableId: row.stableId
47058
+ });
47059
+ continue;
47060
+ }
46535
47061
  const profileId = typeof row.config.profileId === "string" ? row.config.profileId : "monitor";
46536
47062
  const profileLabel = typeof row.config.profileLabel === "string" ? row.config.profileLabel : profileId === "monitor" ? "BTM" : profileId;
46537
47063
  legacy.push({
@@ -46692,111 +47218,6 @@ function findProfile(profiles, profileId) {
46692
47218
  return profiles.find((p) => p.profileId === profileId);
46693
47219
  }
46694
47220
  //#endregion
46695
- //#region src/profile-settings.ts
46696
- var GLANCES_PLUGINS = [
46697
- {
46698
- key: "showCpu",
46699
- plugin: "cpu",
46700
- label: "CPU"
46701
- },
46702
- {
46703
- key: "showMem",
46704
- plugin: "mem",
46705
- label: "Memory"
46706
- },
46707
- {
46708
- key: "showLoad",
46709
- plugin: "load",
46710
- label: "Load"
46711
- },
46712
- {
46713
- key: "showNetwork",
46714
- plugin: "network",
46715
- label: "Network"
46716
- },
46717
- {
46718
- key: "showDiskIo",
46719
- plugin: "diskio",
46720
- label: "Disk I/O"
46721
- },
46722
- {
46723
- key: "showFs",
46724
- plugin: "fs",
46725
- label: "Filesystems"
46726
- },
46727
- {
46728
- key: "showProcessList",
46729
- plugin: "processlist",
46730
- label: "Process list"
46731
- },
46732
- {
46733
- key: "showContainers",
46734
- plugin: "containers",
46735
- label: "Containers"
46736
- },
46737
- {
46738
- key: "showSensors",
46739
- plugin: "sensors",
46740
- label: "Sensors"
46741
- }
46742
- ];
46743
- function glancesBooleanField(key, label) {
46744
- return {
46745
- type: "boolean",
46746
- key,
46747
- label,
46748
- default: true,
46749
- style: "switch"
46750
- };
46751
- }
46752
- function glancesSettingsSchema() {
46753
- return { sections: [{
46754
- id: "glances-panels",
46755
- title: "Glances panels",
46756
- 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).",
46757
- columns: 2,
46758
- fields: GLANCES_PLUGINS.map((plugin) => glancesBooleanField(plugin.key, plugin.label))
46759
- }] };
46760
- }
46761
- function settingsSchemaForProfile(profileId) {
46762
- if (profileId === "glances") return glancesSettingsSchema();
46763
- return null;
46764
- }
46765
- function glancesSettingsToArgs(settings) {
46766
- const disabled = GLANCES_PLUGINS.filter((plugin) => settings[plugin.key] === false).map((plugin) => plugin.plugin);
46767
- if (disabled.length === 0) return [];
46768
- return ["--disable-plugin", disabled.join(",")];
46769
- }
46770
- function sanitizeProfileSettings(profileId, raw) {
46771
- if (profileId !== "glances") return {};
46772
- const bag = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
46773
- const out = {
46774
- showCpu: true,
46775
- showMem: true,
46776
- showLoad: true,
46777
- showNetwork: true,
46778
- showDiskIo: true,
46779
- showFs: true,
46780
- showProcessList: true,
46781
- showContainers: true,
46782
- showSensors: true
46783
- };
46784
- for (const plugin of GLANCES_PLUGINS) {
46785
- const value = bag[plugin.key];
46786
- if (typeof value === "boolean") out[plugin.key] = value;
46787
- }
46788
- return out;
46789
- }
46790
- function profileSettingsToArgs(profileId, settings) {
46791
- if (profileId !== "glances") return [];
46792
- return glancesSettingsToArgs(sanitizeProfileSettings(profileId, settings));
46793
- }
46794
- function spawnArgsForInstance(input) {
46795
- const extra = profileSettingsToArgs(input.profileId, input.profileSettings);
46796
- if (input.instanceArgs.length > 0) return [...input.instanceArgs, ...extra];
46797
- if (extra.length > 0) return [...input.profileArgs, ...extra];
46798
- }
46799
- //#endregion
46800
47221
  //#region src/terminal-session-manager.ts
46801
47222
  var MIN_GRID = 1;
46802
47223
  var MAX_COLS = 1e3;
@@ -47618,7 +48039,12 @@ var TerminalAddon = class extends BaseAddon {
47618
48039
  async listLegacyTerminalCameras() {
47619
48040
  const instances = this.terminalInstances();
47620
48041
  const instanceStableIds = new Set(instances.map((instance) => instance.cameraStableId));
47621
- return listLegacyTerminalCameraCandidates(await this.ctx.api.deviceManager.listAll.query({ addonId: this.ctx.id }), instanceStableIds, this.terminalCameraTombstones);
48042
+ return listLegacyTerminalCameraCandidates(await this.ctx.api.deviceManager.listAll.query({ addonId: this.ctx.id }), instanceStableIds, this.terminalCameraTombstones, ({ deviceId, stableId }) => {
48043
+ this.ctx.logger.warn("legacy Terminal camera skipped — its device config carries no nodeId, so it cannot be offered for adoption", {
48044
+ tags: { deviceId },
48045
+ meta: { stableId }
48046
+ });
48047
+ });
47622
48048
  }
47623
48049
  async adoptLegacyMonitor(stableId, requestedName) {
47624
48050
  const instance = await this.instanceMutationQueue.run(() => this.adoptLegacyMonitorUnlocked(stableId, requestedName));