@camstack/addon-terminal 0.1.52 → 0.1.54

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 +803 -466
  2. package/dist/addon.mjs +803 -466
  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()
@@ -8457,6 +8471,91 @@ var RelocateResidueSchema = object({
8457
8471
  segments: number().int().nonnegative(),
8458
8472
  bytes: number().int().nonnegative()
8459
8473
  }).nullable();
8474
+ /**
8475
+ * Ask one location whether its durable hour rows describe the disk — the walk
8476
+ * (D319).
8477
+ *
8478
+ * `apply` DEFAULTS TO FALSE and that default is the product: the operator's
8479
+ * missing tool is the question, and the dry run is how they sanity-check the
8480
+ * destructive run before authorising it.
8481
+ */
8482
+ var LedgerWalkInputSchema = object({
8483
+ locationId: string().min(1),
8484
+ /** Forget the confirmed-absent rows, rather than only counting them. */
8485
+ apply: boolean().optional(),
8486
+ /** Narrow to one camera. */
8487
+ deviceId: number().int().positive().optional(),
8488
+ /** Narrow to these recording profiles; empty/absent = every profile. */
8489
+ profiles: array(string().min(1)).optional()
8490
+ });
8491
+ /** Why a whole walk did nothing. Every one leaves the ledger untouched. */
8492
+ var LedgerWalkRefusalSchema = _enum([
8493
+ "location-unknown",
8494
+ "source-writable",
8495
+ "no-ledger",
8496
+ "archive-unreadable",
8497
+ "anchor-absent",
8498
+ "anchor-unreadable",
8499
+ "anchor-moved"
8500
+ ]);
8501
+ _enum([
8502
+ "live-tail",
8503
+ "listing-error",
8504
+ "path-mismatch",
8505
+ "durable-refused"
8506
+ ]);
8507
+ /** Every skip reason, always present, always a number — so a reason that never
8508
+ * fired reports as zero rather than absent and the report shape is constant
8509
+ * between passes. Spelled out rather than `z.record` for exactly that. */
8510
+ var LedgerWalkSkipCountsSchema = object({
8511
+ "live-tail": number().int().nonnegative(),
8512
+ "listing-error": number().int().nonnegative(),
8513
+ "path-mismatch": number().int().nonnegative(),
8514
+ "durable-refused": number().int().nonnegative()
8515
+ });
8516
+ /** One camera's share of a walk, so a report names cameras and not rows. */
8517
+ var LedgerWalkDeviceReportSchema = object({
8518
+ deviceId: number().int(),
8519
+ hoursWalked: number().int().nonnegative(),
8520
+ hoursMissing: number().int().nonnegative(),
8521
+ ghostSegments: number().int().nonnegative(),
8522
+ ghostBytes: number().int().nonnegative(),
8523
+ forgottenSegments: number().int().nonnegative(),
8524
+ orphanFiles: number().int().nonnegative()
8525
+ });
8526
+ /**
8527
+ * What one walk claimed, listed, found and (only when armed) forgot.
8528
+ *
8529
+ * `archiveSegments` is the **M** and `segmentsClaimed` the **N** (D295), so a
8530
+ * walk that saw a fraction of the location is visible in its own report rather
8531
+ * than in the absence of one.
8532
+ */
8533
+ var LedgerWalkReportSchema = object({
8534
+ locationId: string(),
8535
+ applied: boolean(),
8536
+ refused: LedgerWalkRefusalSchema.nullable(),
8537
+ archiveSegments: number().int().nonnegative().nullable(),
8538
+ archiveBytes: number().int().nonnegative().nullable(),
8539
+ hoursClaimed: number().int().nonnegative(),
8540
+ hoursWalked: number().int().nonnegative(),
8541
+ hoursMissing: number().int().nonnegative(),
8542
+ /** `readdir` calls issued — the cost, stated in the unit that is paid. */
8543
+ listings: number().int().nonnegative(),
8544
+ segmentsClaimed: number().int().nonnegative(),
8545
+ ghostSegments: number().int().nonnegative(),
8546
+ ghostBytes: number().int().nonnegative(),
8547
+ ghostHoursWhole: number().int().nonnegative(),
8548
+ forgottenSegments: number().int().nonnegative(),
8549
+ forgottenBytes: number().int().nonnegative(),
8550
+ /** Files under a claimed hour that no durable row names. Never deleted. */
8551
+ orphanFiles: number().int().nonnegative(),
8552
+ orphanSample: array(string()).readonly(),
8553
+ hoursSkipped: number().int().nonnegative(),
8554
+ skippedByReason: LedgerWalkSkipCountsSchema,
8555
+ /** The walk stopped at its per-pass hour bound with claims unwalked. */
8556
+ bounded: boolean(),
8557
+ byDevice: array(LedgerWalkDeviceReportSchema).readonly()
8558
+ });
8460
8559
  /** How many rows a media pass would still act on against a given target — the
8461
8560
  * media lane's denominator AND its residue, from ONE derivation so the two can
8462
8561
  * never disagree. `null` = the count could not be taken. */
@@ -19017,13 +19116,15 @@ var ListGroupsPageSchema = object({
19017
19116
  groups: array(AnalyticsGroupRecordSchema).readonly(),
19018
19117
  nextCursor: string().nullable()
19019
19118
  });
19119
+ var KEY_EVENTS_DEFAULT_LIMIT = 50;
19120
+ var KEY_EVENTS_MAX_LIMIT = 200;
19020
19121
  var KeyEventQueryInput = object({
19021
19122
  deviceId: number(),
19022
19123
  /** Window lower bound (track firstSeen ≥ since). */
19023
19124
  since: number(),
19024
19125
  /** Window upper bound (track firstSeen ≤ until). */
19025
19126
  until: number(),
19026
- limit: number().int().min(1).max(200).default(50),
19127
+ limit: number().int().min(1).max(KEY_EVENTS_MAX_LIMIT).default(KEY_EVENTS_DEFAULT_LIMIT),
19027
19128
  /** Drop tracks scoring below this importance. */
19028
19129
  minImportance: number().min(0).max(1).optional(),
19029
19130
  /** Restrict to a single class (e.g. 'person'). */
@@ -19045,6 +19146,32 @@ var KeyEventSchema = object({
19045
19146
  ...TrackFlagFields,
19046
19147
  ...TrackRetrainFields
19047
19148
  });
19149
+ /** `getKeyEvents`' window and filters, asked of a SET of cameras at once. */
19150
+ var KeyEventBatchQueryInput = object({
19151
+ deviceIds: array(number()).min(1).max(200),
19152
+ since: number(),
19153
+ until: number(),
19154
+ /** Applied PER CAMERA, exactly as `getKeyEvents.limit` is — never a total
19155
+ * across the set, which would let a busy camera starve a quiet one of its
19156
+ * rows and change what the merged feed contains. */
19157
+ limit: number().int().min(1).max(KEY_EVENTS_MAX_LIMIT).default(KEY_EVENTS_DEFAULT_LIMIT),
19158
+ minImportance: number().min(0).max(1).optional(),
19159
+ classFilter: string().optional()
19160
+ });
19161
+ /**
19162
+ * One camera's key events in a batch answer.
19163
+ *
19164
+ * The row exists for every requested id. `getKeyEvents` degrades to `[]` on
19165
+ * error rather than throwing, so a camera whose store read failed and one with
19166
+ * no events in the window were ALREADY indistinguishable per camera — the
19167
+ * batch does not make that worse, and the row keeps the deviceId the single
19168
+ * method's output never carried (the caller used to stamp it from the fan-out
19169
+ * key, which only worked because there was one query per camera).
19170
+ */
19171
+ var KeyEventsForDeviceSchema = object({
19172
+ deviceId: number(),
19173
+ events: array(KeyEventSchema).readonly()
19174
+ });
19048
19175
  object({
19049
19176
  trackId: string(),
19050
19177
  className: string(),
@@ -19116,6 +19243,50 @@ var EventStoreFootprintSchema = object({
19116
19243
  totalBytes: number().int(),
19117
19244
  devices: array(EventStoreDeviceFootprintSchema).readonly()
19118
19245
  });
19246
+ /** Event-media footprint for one {@link MediaFileKind}. */
19247
+ var EventMediaKindFootprintSchema = object({
19248
+ kind: MediaFileKindEnum,
19249
+ /** Media rows of this kind. */
19250
+ rows: number().int(),
19251
+ /** Bytes on disk held by those rows. */
19252
+ bytes: number().int()
19253
+ });
19254
+ /**
19255
+ * The media footprint broken down by KIND — the axis a deletion decision
19256
+ * actually turns on.
19257
+ *
19258
+ * A byte total says how much there is; it cannot say what is safe to remove.
19259
+ * The deletable set (the periodic `snapshot` filmstrip, the surplus per-edge
19260
+ * motion stills) and the keep set (`firstFrame`, rolling `lastFrame`,
19261
+ * `thumbnail`/`thumbnailSmall`, `keyFrame`/`keyFrameSmall`, the face/plate
19262
+ * buffers, gallery media, the CLIP `crop`) are distinguished by `kind` and by
19263
+ * nothing else, so sizing a deletion means summing per kind.
19264
+ *
19265
+ * ## Why `unaccounted*` exists
19266
+ *
19267
+ * `kinds` is enumerated from {@link MediaFileKindEnum} — the closed set the
19268
+ * writers use — and summed one kind at a time. `totalRows` / `totalBytes` come
19269
+ * from a SEPARATE unfiltered aggregate over the same rows, never from adding
19270
+ * `kinds` up. A row whose stored `kind` is not in the enum (written by a
19271
+ * retired code path, or by a version that knew a kind this one does not) would
19272
+ * otherwise vanish from the total silently, and an operator would delete
19273
+ * against a denominator smaller than the disk.
19274
+ *
19275
+ * `unaccountedRows` / `unaccountedBytes` are the difference. They are normally
19276
+ * zero; a non-zero value is a real finding and must be shown, not rounded away.
19277
+ */
19278
+ var EventMediaKindBreakdownSchema = object({
19279
+ /** Every media row in scope, from one unfiltered aggregate. */
19280
+ totalRows: number().int(),
19281
+ /** Every media byte in scope, from that same aggregate. */
19282
+ totalBytes: number().int(),
19283
+ /** Per-kind footprint, ordered by bytes descending. */
19284
+ kinds: array(EventMediaKindFootprintSchema).readonly(),
19285
+ /** `totalRows` minus the summed `kinds` rows — see the schema note. */
19286
+ unaccountedRows: number().int(),
19287
+ /** `totalBytes` minus the summed `kinds` bytes — see the schema note. */
19288
+ unaccountedBytes: number().int()
19289
+ });
19119
19290
  /** Per-kind counts returned by the event-prune / device-delete mutations. */
19120
19291
  var EventPruneCountsSchema = object({
19121
19292
  motion: number().int(),
@@ -19270,7 +19441,7 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19270
19441
  until: number().optional(),
19271
19442
  kinds: array(string()).optional(),
19272
19443
  limit: number().int().min(1).max(MAX_EVENT_QUERY_LIMIT).default(DEFAULT_EVENT_QUERY_LIMIT)
19273
- }), array(SensorEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(object({
19444
+ }), array(SensorEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(KeyEventBatchQueryInput, array(KeyEventsForDeviceSchema).readonly()), method(object({
19274
19445
  deviceId: number(),
19275
19446
  since: number(),
19276
19447
  until: number(),
@@ -19319,6 +19490,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19319
19490
  }), TrackFlagsSchema, { kind: "mutation" }), method(object({}), EventStoreFootprintSchema, {
19320
19491
  kind: "query",
19321
19492
  auth: "admin"
19493
+ }), method(object({ deviceId: number().int().optional() }), EventMediaKindBreakdownSchema, {
19494
+ kind: "query",
19495
+ auth: "admin"
19322
19496
  }), method(object({
19323
19497
  olderThanMs: number(),
19324
19498
  reason: OpsLogReasonSchema.optional()
@@ -21374,6 +21548,20 @@ method(object({
21374
21548
  error: string().optional()
21375
21549
  }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
21376
21550
  providerId: string(),
21551
+ /**
21552
+ * The location this config is an UNSAVED edit of, when there is one.
21553
+ *
21554
+ * `listLocations` replaces every declared secret with the redaction
21555
+ * sentinel, so the edit modal's form state holds the sentinel for any
21556
+ * credential the operator did not retype — and posting that here
21557
+ * without a way to resolve it makes the provider try to authenticate
21558
+ * as `__camstack_redacted__` and report the operator's own working
21559
+ * password as wrong. Given this id, the orchestrator restores each
21560
+ * sentinel from the stored config (same rule as `upsertLocation`)
21561
+ * before dispatching. Omitted by the "Add location" wizard, where
21562
+ * every value was typed just now and nothing is stored yet.
21563
+ */
21564
+ locationId: string().optional(),
21377
21565
  config: record(string(), unknown())
21378
21566
  }), object({
21379
21567
  ok: boolean(),
@@ -28768,6 +28956,9 @@ method(object({
28768
28956
  }), method(RelocateResidueInputSchema, RelocateResidueSchema, {
28769
28957
  kind: "query",
28770
28958
  auth: "admin"
28959
+ }), method(LedgerWalkInputSchema, LedgerWalkReportSchema, {
28960
+ kind: "mutation",
28961
+ auth: "admin"
28771
28962
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
28772
28963
  kind: "mutation",
28773
28964
  auth: "admin"
@@ -29159,6 +29350,25 @@ var SceneMonitorStatusSchema = object({
29159
29350
  monitors: array(SceneMonitorSchema),
29160
29351
  lastFetchedAt: number()
29161
29352
  });
29353
+ /**
29354
+ * One camera's row in a `listScenesBatch` answer.
29355
+ *
29356
+ * `status` is NULLABLE and the nullability is the whole point. `scene-monitor`
29357
+ * is a `defaultActive` wrapper, so every camera is asked; a camera whose
29358
+ * provider is absent (pipeline-analytics not deployed, the post-processing node
29359
+ * down) cannot answer, and that is not the same fact as a camera with no scenes
29360
+ * configured. Fanned out per camera the difference was visible — one query
29361
+ * errored while the others resolved — and a batch that returned only the rows
29362
+ * it managed would have destroyed it, silently, by making an unreachable camera
29363
+ * indistinguishable from one that answered `monitors: []`.
29364
+ *
29365
+ * So: EVERY requested deviceId gets a row. `status: null` means "this camera
29366
+ * could not be read"; `status.monitors: []` means "read, and it has none".
29367
+ */
29368
+ var SceneMonitorStatusForDeviceSchema = object({
29369
+ deviceId: number(),
29370
+ status: SceneMonitorStatusSchema.nullable()
29371
+ });
29162
29372
  var sceneMonitorCapability = {
29163
29373
  name: "scene-monitor",
29164
29374
  scope: "device",
@@ -29168,6 +29378,22 @@ var sceneMonitorCapability = {
29168
29378
  deviceTypes: [DeviceType.Camera],
29169
29379
  methods: {
29170
29380
  listScenes: method(object({ deviceId: number() }), SceneMonitorStatusSchema),
29381
+ /**
29382
+ * The same answer, for a SET of cameras, in one round trip.
29383
+ *
29384
+ * `/scenes` renders every camera and re-reads them on a 30s safety-net
29385
+ * poll behind the push slice. Fanned out client-side that was one query
29386
+ * per camera — 29 round trips through the browser, the hub and the
29387
+ * post-analysis runner every 30 seconds to read an in-memory map the
29388
+ * owner had already merged. The work is unchanged (`statusFor` per
29389
+ * device, all in-process at the owner); what collapses is the transport.
29390
+ *
29391
+ * A camera that cannot answer still gets a row, with `status: null` —
29392
+ * see {@link SceneMonitorStatusForDeviceSchema}. Never fewer rows than
29393
+ * ids: a caller that asked for twenty-nine and got twenty-seven cannot
29394
+ * tell which two are missing, or that any are.
29395
+ */
29396
+ listScenesBatch: method(object({ deviceIds: array(number()).min(1).max(200) }), array(SceneMonitorStatusForDeviceSchema).readonly()),
29171
29397
  createScene: method(object({
29172
29398
  deviceId: number(),
29173
29399
  label: string(),
@@ -31003,6 +31229,27 @@ var CameraOccupancySnapshotSchema = object({
31003
31229
  stationaryObjects: array(StationaryObjectSchema).readonly().optional()
31004
31230
  });
31005
31231
  /**
31232
+ * One camera's row in a `getCurrentSnapshotBatch` answer.
31233
+ *
31234
+ * THREE outcomes, and the single-camera method could only express two of them
31235
+ * because `snapshot: null` was already spoken for:
31236
+ *
31237
+ * - `read: 'read'`, `snapshot` present — the live occupancy reading.
31238
+ * - `read: 'read'`, `snapshot: null` — read, and this camera has no reading
31239
+ * yet: no frame since boot, and no parked-object registry to hydrate from.
31240
+ * - `read: 'unreadable'` — the owner could not answer for this
31241
+ * camera. `snapshot` is null, and it does NOT mean "nothing parked here".
31242
+ *
31243
+ * Collapsing the last two is the failure this field exists to prevent: a
31244
+ * hydration that threw would otherwise render as an empty Stationary section,
31245
+ * which is a definite claim about a camera nobody could read.
31246
+ */
31247
+ var CameraOccupancySnapshotForDeviceSchema = object({
31248
+ deviceId: number(),
31249
+ read: _enum(["read", "unreadable"]),
31250
+ snapshot: CameraOccupancySnapshotSchema.nullable()
31251
+ });
31252
+ /**
31006
31253
  * Time-series resolution. The history methods return one bucket per
31007
31254
  * step over the requested range. Smaller resolutions cost more
31008
31255
  * memory + bandwidth; bound to discrete steps so caller cannot ask
@@ -31059,6 +31306,20 @@ var zoneAnalyticsCapability = {
31059
31306
  * (no inference result emitted since boot or since binding was
31060
31307
  * activated). */
31061
31308
  getCurrentSnapshot: method(object({ deviceId: number() }), CameraOccupancySnapshotSchema.nullable()),
31309
+ /**
31310
+ * The same snapshot, for a SET of cameras, in one round trip.
31311
+ *
31312
+ * The Events page's Stationary section polls this every 15s for every
31313
+ * selected camera. Fanned out client-side that is one query per camera to
31314
+ * read a `Map.get` at the owner — the answer costs nothing, the round trip
31315
+ * costs everything. Batched, N transports become one and the per-device
31316
+ * work is unchanged.
31317
+ *
31318
+ * Every requested deviceId gets a row, tagged `read` — see
31319
+ * {@link CameraOccupancySnapshotForDeviceSchema}. A camera the owner could
31320
+ * not answer for is `'unreadable'`, never an empty reading.
31321
+ */
31322
+ getCurrentSnapshotBatch: method(object({ deviceIds: array(number()).min(1).max(200) }), array(CameraOccupancySnapshotForDeviceSchema).readonly()),
31062
31323
  /** Time-series object count inside one zone. `className` optional —
31063
31324
  * omit to count every class in the zone. */
31064
31325
  getZoneHistory: method(object({
@@ -35454,6 +35715,12 @@ Object.freeze({
35454
35715
  addonId: null,
35455
35716
  access: "view"
35456
35717
  },
35718
+ "pipelineAnalytics.getEventMediaFootprintByKind": {
35719
+ capName: "pipeline-analytics",
35720
+ capScope: "device",
35721
+ addonId: null,
35722
+ access: "view"
35723
+ },
35457
35724
  "pipelineAnalytics.getEventStoreFootprint": {
35458
35725
  capName: "pipeline-analytics",
35459
35726
  capScope: "device",
@@ -35472,6 +35739,12 @@ Object.freeze({
35472
35739
  addonId: null,
35473
35740
  access: "view"
35474
35741
  },
35742
+ "pipelineAnalytics.getKeyEventsBatch": {
35743
+ capName: "pipeline-analytics",
35744
+ capScope: "device",
35745
+ addonId: null,
35746
+ access: "view"
35747
+ },
35475
35748
  "pipelineAnalytics.getMotionEvents": {
35476
35749
  capName: "pipeline-analytics",
35477
35750
  capScope: "device",
@@ -36648,6 +36921,12 @@ Object.freeze({
36648
36921
  addonId: null,
36649
36922
  access: "view"
36650
36923
  },
36924
+ "recording.reconcileLedgerAgainstDisk": {
36925
+ capName: "recording",
36926
+ capScope: "system",
36927
+ addonId: null,
36928
+ access: "create"
36929
+ },
36651
36930
  "recording.refreshStorageLocationsForMigration": {
36652
36931
  capName: "recording",
36653
36932
  capScope: "system",
@@ -36774,6 +37053,12 @@ Object.freeze({
36774
37053
  addonId: null,
36775
37054
  access: "view"
36776
37055
  },
37056
+ "sceneMonitor.listScenesBatch": {
37057
+ capName: "scene-monitor",
37058
+ capScope: "device",
37059
+ addonId: null,
37060
+ access: "view"
37061
+ },
36777
37062
  "sceneMonitor.recheckNow": {
36778
37063
  capName: "scene-monitor",
36779
37064
  capScope: "device",
@@ -38130,6 +38415,12 @@ Object.freeze({
38130
38415
  addonId: null,
38131
38416
  access: "view"
38132
38417
  },
38418
+ "zoneAnalytics.getCurrentSnapshotBatch": {
38419
+ capName: "zone-analytics",
38420
+ capScope: "device",
38421
+ addonId: null,
38422
+ access: "view"
38423
+ },
38133
38424
  "zoneAnalytics.getUnzonedHistory": {
38134
38425
  capName: "zone-analytics",
38135
38426
  capScope: "device",
@@ -39119,6 +39410,11 @@ Object.freeze({
39119
39410
  form: "single",
39120
39411
  optional: false
39121
39412
  }],
39413
+ "pipelineAnalytics.getEventMediaFootprintByKind": [{
39414
+ name: "deviceId",
39415
+ form: "single",
39416
+ optional: true
39417
+ }],
39122
39418
  "pipelineAnalytics.getGroup": [{
39123
39419
  name: "deviceId",
39124
39420
  form: "single",
@@ -39129,6 +39425,11 @@ Object.freeze({
39129
39425
  form: "single",
39130
39426
  optional: false
39131
39427
  }],
39428
+ "pipelineAnalytics.getKeyEventsBatch": [{
39429
+ name: "deviceIds",
39430
+ form: "array",
39431
+ optional: false
39432
+ }],
39132
39433
  "pipelineAnalytics.getMotionEvents": [{
39133
39434
  name: "deviceId",
39134
39435
  form: "single",
@@ -39569,6 +39870,11 @@ Object.freeze({
39569
39870
  form: "single",
39570
39871
  optional: false
39571
39872
  }],
39873
+ "recording.reconcileLedgerAgainstDisk": [{
39874
+ name: "deviceId",
39875
+ form: "single",
39876
+ optional: true
39877
+ }],
39572
39878
  "recording.relocateFootage": [{
39573
39879
  name: "deviceId",
39574
39880
  form: "single",
@@ -39634,6 +39940,11 @@ Object.freeze({
39634
39940
  form: "single",
39635
39941
  optional: false
39636
39942
  }],
39943
+ "sceneMonitor.listScenesBatch": [{
39944
+ name: "deviceIds",
39945
+ form: "array",
39946
+ optional: false
39947
+ }],
39637
39948
  "sceneMonitor.recheckNow": [{
39638
39949
  name: "deviceId",
39639
39950
  form: "single",
@@ -39895,6 +40206,11 @@ Object.freeze({
39895
40206
  form: "single",
39896
40207
  optional: false
39897
40208
  }],
40209
+ "zoneAnalytics.getCurrentSnapshotBatch": [{
40210
+ name: "deviceIds",
40211
+ form: "array",
40212
+ optional: false
40213
+ }],
39898
40214
  "zoneAnalytics.getUnzonedHistory": [{
39899
40215
  name: "deviceId",
39900
40216
  form: "single",
@@ -40468,6 +40784,111 @@ function resolveGlancesCursesShimEnv(options) {
40468
40784
  return { PYTHONPATH: existing ? `${options.shimDir}:${existing}` : options.shimDir };
40469
40785
  }
40470
40786
  //#endregion
40787
+ //#region src/profile-settings.ts
40788
+ var GLANCES_PLUGINS = [
40789
+ {
40790
+ key: "showCpu",
40791
+ plugin: "cpu",
40792
+ label: "CPU"
40793
+ },
40794
+ {
40795
+ key: "showMem",
40796
+ plugin: "mem",
40797
+ label: "Memory"
40798
+ },
40799
+ {
40800
+ key: "showLoad",
40801
+ plugin: "load",
40802
+ label: "Load"
40803
+ },
40804
+ {
40805
+ key: "showNetwork",
40806
+ plugin: "network",
40807
+ label: "Network"
40808
+ },
40809
+ {
40810
+ key: "showDiskIo",
40811
+ plugin: "diskio",
40812
+ label: "Disk I/O"
40813
+ },
40814
+ {
40815
+ key: "showFs",
40816
+ plugin: "fs",
40817
+ label: "Filesystems"
40818
+ },
40819
+ {
40820
+ key: "showProcessList",
40821
+ plugin: "processlist",
40822
+ label: "Process list"
40823
+ },
40824
+ {
40825
+ key: "showContainers",
40826
+ plugin: "containers",
40827
+ label: "Containers"
40828
+ },
40829
+ {
40830
+ key: "showSensors",
40831
+ plugin: "sensors",
40832
+ label: "Sensors"
40833
+ }
40834
+ ];
40835
+ function glancesBooleanField(key, label) {
40836
+ return {
40837
+ type: "boolean",
40838
+ key,
40839
+ label,
40840
+ default: true,
40841
+ style: "switch"
40842
+ };
40843
+ }
40844
+ function glancesSettingsSchema() {
40845
+ return { sections: [{
40846
+ id: "glances-panels",
40847
+ title: "Glances panels",
40848
+ 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).",
40849
+ columns: 2,
40850
+ fields: GLANCES_PLUGINS.map((plugin) => glancesBooleanField(plugin.key, plugin.label))
40851
+ }] };
40852
+ }
40853
+ function settingsSchemaForProfile(profileId) {
40854
+ if (profileId === "glances") return glancesSettingsSchema();
40855
+ return null;
40856
+ }
40857
+ function glancesSettingsToArgs(settings) {
40858
+ const disabled = GLANCES_PLUGINS.filter((plugin) => settings[plugin.key] === false).map((plugin) => plugin.plugin);
40859
+ if (disabled.length === 0) return [];
40860
+ return ["--disable-plugin", disabled.join(",")];
40861
+ }
40862
+ function sanitizeProfileSettings(profileId, raw) {
40863
+ if (profileId !== "glances") return {};
40864
+ const bag = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
40865
+ const out = {
40866
+ showCpu: true,
40867
+ showMem: true,
40868
+ showLoad: true,
40869
+ showNetwork: true,
40870
+ showDiskIo: true,
40871
+ showFs: true,
40872
+ showProcessList: true,
40873
+ showContainers: true,
40874
+ showSensors: true
40875
+ };
40876
+ for (const plugin of GLANCES_PLUGINS) {
40877
+ const value = bag[plugin.key];
40878
+ if (typeof value === "boolean") out[plugin.key] = value;
40879
+ }
40880
+ return out;
40881
+ }
40882
+ function profileSettingsToArgs(profileId, settings) {
40883
+ if (profileId !== "glances") return [];
40884
+ return glancesSettingsToArgs(sanitizeProfileSettings(profileId, settings));
40885
+ }
40886
+ function spawnArgsForInstance(input) {
40887
+ const extra = profileSettingsToArgs(input.profileId, input.profileSettings);
40888
+ if (input.instanceArgs.length > 0) return [...input.instanceArgs, ...extra];
40889
+ if (extra.length > 0) return [...input.profileArgs, ...extra];
40890
+ }
40891
+ //#endregion
40471
40892
  //#region src/pty.ts
40472
40893
  /**
40473
40894
  * Minimal pty abstraction. The manager depends on this interface, never on
@@ -40649,6 +41070,356 @@ async function silenceAnalysisFor(deps, deviceId) {
40649
41070
  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("; ")})`);
40650
41071
  }
40651
41072
  //#endregion
41073
+ //#region src/terminal-camera-declarations.ts
41074
+ /**
41075
+ * Feed DeclaredDevices every live declaration plus one deterministic orphan
41076
+ * batch. The generic sweep intentionally refuses an over-limit set; selecting
41077
+ * a batch here drains large historical Terminal orphan sets across convergence
41078
+ * passes without weakening that global safety guard.
41079
+ */
41080
+ function terminalCameraReconciliationIndex(rows, integrationId, declarations, managedStableIds = /* @__PURE__ */ new Set()) {
41081
+ if (!integrationId) return [];
41082
+ const declared = new Set(declarations.map((camera) => camera.stableId));
41083
+ const owned = rows.filter((row) => row.integrationId === integrationId && (declared.has(row.stableId) || managedStableIds.has(row.stableId) || row.stableId.startsWith("terminal-camera-instance-")));
41084
+ 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)];
41085
+ }
41086
+ /** Explicit persisted instances, never the node × profile template matrix. */
41087
+ function buildTerminalInstanceCameraDeclarations(instances) {
41088
+ return instances.filter((instance) => instance.enabled).map((instance) => ({
41089
+ stableId: instance.cameraStableId,
41090
+ name: instance.name,
41091
+ config: {
41092
+ instanceId: instance.id,
41093
+ nodeId: instance.nodeId,
41094
+ profileId: instance.profileId,
41095
+ profileLabel: instance.profileLabel
41096
+ }
41097
+ }));
41098
+ }
41099
+ /**
41100
+ * `DeviceConfig` materializes schema defaults in memory, so comparing
41101
+ * `config.get()` cannot detect a legacy `{ nodeId }` row. Reconciliation must
41102
+ * inspect the raw persisted blob to make the profile migration durable.
41103
+ */
41104
+ function needsTerminalCameraConfigMigration(persistedConfig, declaration) {
41105
+ return persistedConfig.instanceId !== declaration.config.instanceId || persistedConfig.nodeId !== declaration.config.nodeId || persistedConfig.profileId !== declaration.config.profileId || persistedConfig.profileLabel !== declaration.config.profileLabel;
41106
+ }
41107
+ //#endregion
41108
+ //#region src/terminal-cell-runs.ts
41109
+ var TERMINAL_DEFAULT_FG = "#d7dce2";
41110
+ var TERMINAL_DEFAULT_BG = "#0b0d10";
41111
+ /**
41112
+ * The 16 ANSI colours, in a dark-theme variant chosen to sit with the existing
41113
+ * frame (`#0b0d10` ground, `#d7dce2` text) rather than the raw xterm defaults,
41114
+ * whose pure `#0000ff` blue and `#008000` green are close to unreadable on a
41115
+ * near-black background at 13px. Index 7 IS the default foreground, so plain
41116
+ * `CSI 37m` text renders identically to unstyled text.
41117
+ */
41118
+ var TERMINAL_ANSI_PALETTE = [
41119
+ "#282c34",
41120
+ "#e06c75",
41121
+ "#98c379",
41122
+ "#e5c07b",
41123
+ "#61afef",
41124
+ "#c678dd",
41125
+ "#56b6c2",
41126
+ TERMINAL_DEFAULT_FG,
41127
+ "#5c6370",
41128
+ "#ef596f",
41129
+ "#89ca78",
41130
+ "#f0c674",
41131
+ "#6cb6ff",
41132
+ "#d55fde",
41133
+ "#2bbac5",
41134
+ "#ffffff"
41135
+ ];
41136
+ /** The six levels of the xterm 6×6×6 colour cube (indices 16-231). */
41137
+ var TERMINAL_CUBE_LEVELS = [
41138
+ 0,
41139
+ 95,
41140
+ 135,
41141
+ 175,
41142
+ 215,
41143
+ 255
41144
+ ];
41145
+ var TERMINAL_CUBE_FIRST = 16;
41146
+ var TERMINAL_GRAYSCALE_FIRST = 232;
41147
+ var TERMINAL_GRAYSCALE_BASE = 8;
41148
+ var TERMINAL_GRAYSCALE_STEP = 10;
41149
+ /** SGR 2 keeps the foreground legible; it must not become the background. */
41150
+ var TERMINAL_DIM_WEIGHT = .6;
41151
+ function channel(value) {
41152
+ return Math.max(0, Math.min(255, Math.round(value))).toString(16).padStart(2, "0");
41153
+ }
41154
+ function hex(red, green, blue) {
41155
+ return `#${channel(red)}${channel(green)}${channel(blue)}`;
41156
+ }
41157
+ function parseHex(color) {
41158
+ return [
41159
+ Number.parseInt(color.slice(1, 3), 16),
41160
+ Number.parseInt(color.slice(3, 5), 16),
41161
+ Number.parseInt(color.slice(5, 7), 16)
41162
+ ];
41163
+ }
41164
+ /** Resolve an xterm palette index (0-255) to a hex colour. */
41165
+ function terminalPaletteColor(index) {
41166
+ const ansi = TERMINAL_ANSI_PALETTE[index];
41167
+ if (ansi !== void 0) return ansi;
41168
+ if (index >= TERMINAL_GRAYSCALE_FIRST) {
41169
+ const level = TERMINAL_GRAYSCALE_BASE + (index - TERMINAL_GRAYSCALE_FIRST) * TERMINAL_GRAYSCALE_STEP;
41170
+ return hex(level, level, level);
41171
+ }
41172
+ if (index >= TERMINAL_CUBE_FIRST) {
41173
+ const offset = index - TERMINAL_CUBE_FIRST;
41174
+ 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);
41175
+ }
41176
+ return TERMINAL_DEFAULT_FG;
41177
+ }
41178
+ /** Resolve a 0xRRGGBB truecolor value to a hex colour. */
41179
+ function terminalRgbColor(value) {
41180
+ return hex(value >> 16 & 255, value >> 8 & 255, value & 255);
41181
+ }
41182
+ function blend(color, toward, weight) {
41183
+ const [red, green, blue] = parseHex(color);
41184
+ const [targetRed, targetGreen, targetBlue] = parseHex(toward);
41185
+ return hex(red * weight + targetRed * (1 - weight), green * weight + targetGreen * (1 - weight), blue * weight + targetBlue * (1 - weight));
41186
+ }
41187
+ function resolveForeground(cell) {
41188
+ if (cell.isFgRGB()) return terminalRgbColor(cell.getFgColor());
41189
+ if (cell.isFgPalette()) return terminalPaletteColor(cell.getFgColor());
41190
+ return TERMINAL_DEFAULT_FG;
41191
+ }
41192
+ function resolveBackground(cell) {
41193
+ if (cell.isBgRGB()) return terminalRgbColor(cell.getBgColor());
41194
+ if (cell.isBgPalette()) return terminalPaletteColor(cell.getBgColor());
41195
+ return TERMINAL_DEFAULT_BG;
41196
+ }
41197
+ /**
41198
+ * Resolve one cell's attributes into concrete colours.
41199
+ *
41200
+ * Inverse is applied by SWAPPING the already-resolved pair, so inverse over two
41201
+ * defaults is still a visible swap rather than a no-op — that is how a selected
41202
+ * or highlighted row in Glances reads. Invisible is then conceal-by-equality
41203
+ * (foreground painted in its own background): the cell keeps its columns, which
41204
+ * a dropped cell would not, and dropping it would shift the whole rest of the
41205
+ * row left.
41206
+ */
41207
+ function resolveCellStyle(cell) {
41208
+ const inverse = cell.isInverse() !== 0;
41209
+ const plainFg = resolveForeground(cell);
41210
+ const plainBg = resolveBackground(cell);
41211
+ const background = inverse ? plainFg : plainBg;
41212
+ let foreground = inverse ? plainBg : plainFg;
41213
+ if (cell.isDim() !== 0) foreground = blend(foreground, background, TERMINAL_DIM_WEIGHT);
41214
+ if (cell.isInvisible() !== 0) foreground = background;
41215
+ return {
41216
+ fg: foreground === "#d7dce2" ? null : foreground,
41217
+ bg: background === "#0b0d10" ? null : background,
41218
+ bold: cell.isBold() !== 0
41219
+ };
41220
+ }
41221
+ function sameStyle(left, right) {
41222
+ return left.fg === right.fg && left.bg === right.bg && left.bold === right.bold;
41223
+ }
41224
+ /**
41225
+ * Merge adjacent same-style cells into runs, then drop the trailing run of
41226
+ * default-styled whitespace so a row costs what it draws — the same trim
41227
+ * `translateToString(true)` performs. Whitespace carrying a BACKGROUND is kept:
41228
+ * a green bar of spaces out to the right margin is a pixel Glances drew.
41229
+ */
41230
+ function buildCellRuns(cells) {
41231
+ const runs = [];
41232
+ let text = "";
41233
+ let style = null;
41234
+ for (const cell of cells) {
41235
+ if (style !== null && sameStyle(style, cell.style)) {
41236
+ text += cell.text;
41237
+ continue;
41238
+ }
41239
+ if (style !== null) runs.push({
41240
+ text,
41241
+ ...style
41242
+ });
41243
+ text = cell.text;
41244
+ style = cell.style;
41245
+ }
41246
+ if (style !== null) runs.push({
41247
+ text,
41248
+ ...style
41249
+ });
41250
+ while (runs.length > 0) {
41251
+ const last = runs[runs.length - 1];
41252
+ if (last === void 0 || last.bg !== null) break;
41253
+ const trimmed = last.text.replace(/\s+$/u, "");
41254
+ if (trimmed === last.text) break;
41255
+ if (trimmed === "") {
41256
+ runs.pop();
41257
+ continue;
41258
+ }
41259
+ runs[runs.length - 1] = {
41260
+ ...last,
41261
+ text: trimmed
41262
+ };
41263
+ break;
41264
+ }
41265
+ return runs;
41266
+ }
41267
+ /**
41268
+ * Monospace families to try, in order — NOT one family and a generic.
41269
+ *
41270
+ * A terminal screen is mostly box-drawing and block characters, and a font
41271
+ * without them renders the frame as noise rather than as missing detail.
41272
+ * `DejaVu Sans Mono` covers them and ships in the hub image; macOS has neither
41273
+ * DejaVu nor fontconfig, so the Mac agent fell through to a generic whose
41274
+ * coverage is not, and its Glances camera came out unreadable while the hub's
41275
+ * was pixel-perfect (measured 2026-08-11, same Glances, two nodes).
41276
+ *
41277
+ * `Menlo` is the fix rather than a guess: it is macOS's default terminal face,
41278
+ * present on every install, and derived from DejaVu Sans Mono — the same glyph
41279
+ * coverage by ancestry. Monaco and Courier New follow as older fallbacks; the
41280
+ * generic stays last so a host with none of them still draws something.
41281
+ */
41282
+ var TERMINAL_FONT_STACK = "DejaVu Sans Mono,Menlo,Monaco,Courier New,monospace";
41283
+ var TERMINAL_FONT_SIZE = 13;
41284
+ var TERMINAL_TEXT_MARGIN_X = 8;
41285
+ var TERMINAL_ROW_HEIGHT = 15;
41286
+ var TERMINAL_BASELINE_Y = 18;
41287
+ /**
41288
+ * Distance from a row's baseline up to the top of its cell box. Chosen so
41289
+ * consecutive rows tile exactly: row N's box runs from `baseline - this` for
41290
+ * `TERMINAL_ROW_HEIGHT`, and row N+1's box starts where it ends. A background
41291
+ * bar that stopped short would draw as stripes across a `CSI 42m` panel.
41292
+ */
41293
+ var TERMINAL_CELL_ASCENT = 11.5;
41294
+ var TERMINAL_CELL_WIDTH = TERMINAL_FONT_SIZE * (1233 / 2048);
41295
+ /** Two decimals is under a tenth of a pixel and keeps the SVG small. */
41296
+ function coordinate(value) {
41297
+ return String(Number(value.toFixed(2)));
41298
+ }
41299
+ function escapeXml(value) {
41300
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;");
41301
+ }
41302
+ /**
41303
+ * Render already-interpreted terminal rows into a compact MJPEG frame.
41304
+ *
41305
+ * `xml:space="preserve"` is load-bearing, not tidiness. SVG `<text>` collapses
41306
+ * runs of whitespace by default, and a terminal's entire column alignment IS
41307
+ * runs of whitespace — Glances pads every field with spaces. Without it the
41308
+ * frame drew each line at roughly half its true width, crammed into the
41309
+ * top-left of a mostly-black image, while the SAME session over `attach`
41310
+ * looked perfect — which is exactly how the operator reported it. Measured in
41311
+ * the hub's own rasterizer 2026-08-11: `AAA<10 spaces>BBB<3>CCC` drew 92 px
41312
+ * collapsed against 178 px preserved.
41313
+ *
41314
+ * Every run is anchored at its OWN column (`x = margin + startCol * cellW`),
41315
+ * never appended to the one before it, so the background rects and the glyphs
41316
+ * are placed off the same grid and cannot drift apart. `textLength` is emitted
41317
+ * with it because it is the correct declaration and renderers that honour it
41318
+ * get an exact grid — but it is not what makes this work: librsvg, which sharp
41319
+ * uses, ignores it outright (measured 2026-08-12, a 120-glyph run pinned to
41320
+ * 600 px still drew its natural 937 px). The anchoring is the guarantee.
41321
+ */
41322
+ function renderTerminalSvg(rows) {
41323
+ const backgrounds = [];
41324
+ const texts = [];
41325
+ rows.slice(0, 40).forEach((row, index) => {
41326
+ const baseline = TERMINAL_BASELINE_Y + index * TERMINAL_ROW_HEIGHT;
41327
+ const top = baseline - TERMINAL_CELL_ASCENT;
41328
+ let column = 0;
41329
+ for (const run of row) {
41330
+ if (column >= 120) break;
41331
+ const clipped = clipRun(run, 120 - column);
41332
+ const columns = [...clipped].length;
41333
+ if (columns === 0) continue;
41334
+ const x = TERMINAL_TEXT_MARGIN_X + column * TERMINAL_CELL_WIDTH;
41335
+ const width = columns * TERMINAL_CELL_WIDTH;
41336
+ if (run.bg !== null) backgrounds.push(`<rect x="${coordinate(x)}" y="${coordinate(top)}" width="${coordinate(width)}" height="${String(TERMINAL_ROW_HEIGHT)}" fill="${run.bg}"/>`);
41337
+ if (clipped.trim() !== "") {
41338
+ const fill = run.fg === null ? "" : ` fill="${run.fg}"`;
41339
+ const weight = run.bold ? " font-weight=\"bold\"" : "";
41340
+ texts.push(`<text xml:space="preserve" x="${coordinate(x)}" y="${coordinate(baseline)}" textLength="${coordinate(width)}" lengthAdjust="spacingAndGlyphs"${fill}${weight}>${escapeXml(clipped)}</text>`);
41341
+ }
41342
+ column += columns;
41343
+ }
41344
+ });
41345
+ 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>`;
41346
+ }
41347
+ /** Cut a run to the columns still left in the row, by code point not unit. */
41348
+ function clipRun(run, remaining) {
41349
+ const points = [...run.text];
41350
+ return points.length <= remaining ? run.text : points.slice(0, remaining).join("");
41351
+ }
41352
+ async function renderTerminalJpeg(rows) {
41353
+ return sharp(Buffer.from(renderTerminalSvg(rows))).jpeg({
41354
+ quality: 82,
41355
+ chromaSubsampling: "4:2:0"
41356
+ }).toBuffer();
41357
+ }
41358
+ //#endregion
41359
+ //#region src/terminal-camera-device.ts
41360
+ var terminalCameraSchema = object({
41361
+ instanceId: string().min(1).optional(),
41362
+ nodeId: string().min(1),
41363
+ profileId: string().min(1).default("monitor"),
41364
+ profileLabel: string().min(1).default("BTM")
41365
+ });
41366
+ var relay = null;
41367
+ function installTerminalCameraRelay(next) {
41368
+ relay = next;
41369
+ }
41370
+ var TerminalCameraDevice = class extends BaseDevice {
41371
+ features = [DeviceFeature.NativeSnapshot];
41372
+ constructor(ctx) {
41373
+ super(ctx, terminalCameraSchema, { type: ctx.deviceMeta.type });
41374
+ this.ctx.registerNativeCap(streamCatalogCapability, { getCatalog: async ({ deviceId }) => {
41375
+ if (deviceId !== this.id) return [];
41376
+ return this.catalog();
41377
+ } });
41378
+ this.ctx.registerNativeCap(snapshotCapability, {
41379
+ getSnapshot: async ({ deviceId }) => {
41380
+ if (deviceId !== this.id) throw new Error(`TerminalCameraDevice: deviceId mismatch, expected ${String(this.id)}, got ${String(deviceId)}`);
41381
+ const activeRelay = relay;
41382
+ if (!activeRelay) throw new Error("terminal camera relay is unavailable");
41383
+ return {
41384
+ base64: (await activeRelay.snapshot(this.relayInstanceId(), this.config.get("nodeId"), this.config.get("profileId"))).toString("base64"),
41385
+ contentType: "image/jpeg"
41386
+ };
41387
+ },
41388
+ invalidateCache: async () => {}
41389
+ });
41390
+ this.markOnline(true);
41391
+ }
41392
+ async catalog() {
41393
+ const activeRelay = relay;
41394
+ if (!activeRelay) throw new Error("terminal camera relay is unavailable");
41395
+ const nodeId = this.config.get("nodeId");
41396
+ const profileId = this.config.get("profileId");
41397
+ const instanceId = this.relayInstanceId();
41398
+ return [{
41399
+ camStreamId: profileId,
41400
+ kind: "pull-http",
41401
+ url: activeRelay.streamUrl(instanceId, nodeId, profileId),
41402
+ codec: "h264",
41403
+ resolution: {
41404
+ width: 960,
41405
+ height: 640
41406
+ },
41407
+ fps: 2,
41408
+ label: this.config.get("profileLabel")
41409
+ }];
41410
+ }
41411
+ setNodeOnline(online) {
41412
+ this.markOnline(online);
41413
+ if (!online) relay?.closeInstance(this.relayInstanceId());
41414
+ }
41415
+ async removeDevice() {
41416
+ await relay?.closeInstance(this.relayInstanceId());
41417
+ }
41418
+ relayInstanceId() {
41419
+ return this.config.get("instanceId") ?? `legacy:${this.stableId}`;
41420
+ }
41421
+ };
41422
+ //#endregion
40652
41423
  //#region ../../node_modules/@xterm/addon-serialize/lib/addon-serialize.js
40653
41424
  var require_addon_serialize = /* @__PURE__ */ __commonJSMin(((exports, module) => {
40654
41425
  (function(e, t) {
@@ -45456,174 +46227,9 @@ var require_xterm_headless = /* @__PURE__ */ __commonJSMin(((exports) => {
45456
46227
  })();
45457
46228
  }));
45458
46229
  //#endregion
45459
- //#region src/terminal-cell-runs.ts
46230
+ //#region src/xterm-screen.ts
45460
46231
  var import_addon_serialize = require_addon_serialize();
45461
46232
  var import_xterm_headless = require_xterm_headless();
45462
- var TERMINAL_DEFAULT_FG = "#d7dce2";
45463
- var TERMINAL_DEFAULT_BG = "#0b0d10";
45464
- /**
45465
- * The 16 ANSI colours, in a dark-theme variant chosen to sit with the existing
45466
- * frame (`#0b0d10` ground, `#d7dce2` text) rather than the raw xterm defaults,
45467
- * whose pure `#0000ff` blue and `#008000` green are close to unreadable on a
45468
- * near-black background at 13px. Index 7 IS the default foreground, so plain
45469
- * `CSI 37m` text renders identically to unstyled text.
45470
- */
45471
- var TERMINAL_ANSI_PALETTE = [
45472
- "#282c34",
45473
- "#e06c75",
45474
- "#98c379",
45475
- "#e5c07b",
45476
- "#61afef",
45477
- "#c678dd",
45478
- "#56b6c2",
45479
- TERMINAL_DEFAULT_FG,
45480
- "#5c6370",
45481
- "#ef596f",
45482
- "#89ca78",
45483
- "#f0c674",
45484
- "#6cb6ff",
45485
- "#d55fde",
45486
- "#2bbac5",
45487
- "#ffffff"
45488
- ];
45489
- /** The six levels of the xterm 6×6×6 colour cube (indices 16-231). */
45490
- var TERMINAL_CUBE_LEVELS = [
45491
- 0,
45492
- 95,
45493
- 135,
45494
- 175,
45495
- 215,
45496
- 255
45497
- ];
45498
- var TERMINAL_CUBE_FIRST = 16;
45499
- var TERMINAL_GRAYSCALE_FIRST = 232;
45500
- var TERMINAL_GRAYSCALE_BASE = 8;
45501
- var TERMINAL_GRAYSCALE_STEP = 10;
45502
- /** SGR 2 keeps the foreground legible; it must not become the background. */
45503
- var TERMINAL_DIM_WEIGHT = .6;
45504
- function channel(value) {
45505
- return Math.max(0, Math.min(255, Math.round(value))).toString(16).padStart(2, "0");
45506
- }
45507
- function hex(red, green, blue) {
45508
- return `#${channel(red)}${channel(green)}${channel(blue)}`;
45509
- }
45510
- function parseHex(color) {
45511
- return [
45512
- Number.parseInt(color.slice(1, 3), 16),
45513
- Number.parseInt(color.slice(3, 5), 16),
45514
- Number.parseInt(color.slice(5, 7), 16)
45515
- ];
45516
- }
45517
- /** Resolve an xterm palette index (0-255) to a hex colour. */
45518
- function terminalPaletteColor(index) {
45519
- const ansi = TERMINAL_ANSI_PALETTE[index];
45520
- if (ansi !== void 0) return ansi;
45521
- if (index >= TERMINAL_GRAYSCALE_FIRST) {
45522
- const level = TERMINAL_GRAYSCALE_BASE + (index - TERMINAL_GRAYSCALE_FIRST) * TERMINAL_GRAYSCALE_STEP;
45523
- return hex(level, level, level);
45524
- }
45525
- if (index >= TERMINAL_CUBE_FIRST) {
45526
- const offset = index - TERMINAL_CUBE_FIRST;
45527
- 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);
45528
- }
45529
- return TERMINAL_DEFAULT_FG;
45530
- }
45531
- /** Resolve a 0xRRGGBB truecolor value to a hex colour. */
45532
- function terminalRgbColor(value) {
45533
- return hex(value >> 16 & 255, value >> 8 & 255, value & 255);
45534
- }
45535
- function blend(color, toward, weight) {
45536
- const [red, green, blue] = parseHex(color);
45537
- const [targetRed, targetGreen, targetBlue] = parseHex(toward);
45538
- return hex(red * weight + targetRed * (1 - weight), green * weight + targetGreen * (1 - weight), blue * weight + targetBlue * (1 - weight));
45539
- }
45540
- function resolveForeground(cell) {
45541
- if (cell.isFgRGB()) return terminalRgbColor(cell.getFgColor());
45542
- if (cell.isFgPalette()) return terminalPaletteColor(cell.getFgColor());
45543
- return TERMINAL_DEFAULT_FG;
45544
- }
45545
- function resolveBackground(cell) {
45546
- if (cell.isBgRGB()) return terminalRgbColor(cell.getBgColor());
45547
- if (cell.isBgPalette()) return terminalPaletteColor(cell.getBgColor());
45548
- return TERMINAL_DEFAULT_BG;
45549
- }
45550
- /**
45551
- * Resolve one cell's attributes into concrete colours.
45552
- *
45553
- * Inverse is applied by SWAPPING the already-resolved pair, so inverse over two
45554
- * defaults is still a visible swap rather than a no-op — that is how a selected
45555
- * or highlighted row in Glances reads. Invisible is then conceal-by-equality
45556
- * (foreground painted in its own background): the cell keeps its columns, which
45557
- * a dropped cell would not, and dropping it would shift the whole rest of the
45558
- * row left.
45559
- */
45560
- function resolveCellStyle(cell) {
45561
- const inverse = cell.isInverse() !== 0;
45562
- const plainFg = resolveForeground(cell);
45563
- const plainBg = resolveBackground(cell);
45564
- const background = inverse ? plainFg : plainBg;
45565
- let foreground = inverse ? plainBg : plainFg;
45566
- if (cell.isDim() !== 0) foreground = blend(foreground, background, TERMINAL_DIM_WEIGHT);
45567
- if (cell.isInvisible() !== 0) foreground = background;
45568
- return {
45569
- fg: foreground === "#d7dce2" ? null : foreground,
45570
- bg: background === "#0b0d10" ? null : background,
45571
- bold: cell.isBold() !== 0
45572
- };
45573
- }
45574
- function sameStyle(left, right) {
45575
- return left.fg === right.fg && left.bg === right.bg && left.bold === right.bold;
45576
- }
45577
- /**
45578
- * Merge adjacent same-style cells into runs, then drop the trailing run of
45579
- * default-styled whitespace so a row costs what it draws — the same trim
45580
- * `translateToString(true)` performs. Whitespace carrying a BACKGROUND is kept:
45581
- * a green bar of spaces out to the right margin is a pixel Glances drew.
45582
- */
45583
- function buildCellRuns(cells) {
45584
- const runs = [];
45585
- let text = "";
45586
- let style = null;
45587
- for (const cell of cells) {
45588
- if (style !== null && sameStyle(style, cell.style)) {
45589
- text += cell.text;
45590
- continue;
45591
- }
45592
- if (style !== null) runs.push({
45593
- text,
45594
- ...style
45595
- });
45596
- text = cell.text;
45597
- style = cell.style;
45598
- }
45599
- if (style !== null) runs.push({
45600
- text,
45601
- ...style
45602
- });
45603
- while (runs.length > 0) {
45604
- const last = runs[runs.length - 1];
45605
- if (last === void 0 || last.bg !== null) break;
45606
- const trimmed = last.text.replace(/\s+$/u, "");
45607
- if (trimmed === last.text) break;
45608
- if (trimmed === "") {
45609
- runs.pop();
45610
- continue;
45611
- }
45612
- runs[runs.length - 1] = {
45613
- ...last,
45614
- text: trimmed
45615
- };
45616
- break;
45617
- }
45618
- return runs;
45619
- }
45620
- //#endregion
45621
- //#region src/xterm-screen.ts
45622
- /**
45623
- * Real {@link ScreenBuffer} backed by `@xterm/headless` — the DOM-less xterm
45624
- * build — plus the serialize addon, which turns the current buffer into a
45625
- * self-contained repaint escape sequence for reconnecting clients.
45626
- */
45627
46233
  var SCROLLBACK_LINES = 2e3;
45628
46234
  function createXtermScreen(cols, rows) {
45629
46235
  const term = new import_xterm_headless.Terminal({
@@ -45690,196 +46296,6 @@ function createXtermScreen(cols, rows) {
45690
46296
  };
45691
46297
  }
45692
46298
  //#endregion
45693
- //#region src/terminal-camera-declarations.ts
45694
- /**
45695
- * Feed DeclaredDevices every live declaration plus one deterministic orphan
45696
- * batch. The generic sweep intentionally refuses an over-limit set; selecting
45697
- * a batch here drains large historical Terminal orphan sets across convergence
45698
- * passes without weakening that global safety guard.
45699
- */
45700
- function terminalCameraReconciliationIndex(rows, integrationId, declarations, managedStableIds = /* @__PURE__ */ new Set()) {
45701
- if (!integrationId) return [];
45702
- const declared = new Set(declarations.map((camera) => camera.stableId));
45703
- const owned = rows.filter((row) => row.integrationId === integrationId && (declared.has(row.stableId) || managedStableIds.has(row.stableId) || row.stableId.startsWith("terminal-camera-instance-")));
45704
- 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)];
45705
- }
45706
- /** Explicit persisted instances, never the node × profile template matrix. */
45707
- function buildTerminalInstanceCameraDeclarations(instances) {
45708
- return instances.filter((instance) => instance.enabled).map((instance) => ({
45709
- stableId: instance.cameraStableId,
45710
- name: instance.name,
45711
- config: {
45712
- instanceId: instance.id,
45713
- nodeId: instance.nodeId,
45714
- profileId: instance.profileId,
45715
- profileLabel: instance.profileLabel
45716
- }
45717
- }));
45718
- }
45719
- /**
45720
- * `DeviceConfig` materializes schema defaults in memory, so comparing
45721
- * `config.get()` cannot detect a legacy `{ nodeId }` row. Reconciliation must
45722
- * inspect the raw persisted blob to make the profile migration durable.
45723
- */
45724
- function needsTerminalCameraConfigMigration(persistedConfig, declaration) {
45725
- return persistedConfig.instanceId !== declaration.config.instanceId || persistedConfig.nodeId !== declaration.config.nodeId || persistedConfig.profileId !== declaration.config.profileId || persistedConfig.profileLabel !== declaration.config.profileLabel;
45726
- }
45727
- /**
45728
- * Monospace families to try, in order — NOT one family and a generic.
45729
- *
45730
- * A terminal screen is mostly box-drawing and block characters, and a font
45731
- * without them renders the frame as noise rather than as missing detail.
45732
- * `DejaVu Sans Mono` covers them and ships in the hub image; macOS has neither
45733
- * DejaVu nor fontconfig, so the Mac agent fell through to a generic whose
45734
- * coverage is not, and its Glances camera came out unreadable while the hub's
45735
- * was pixel-perfect (measured 2026-08-11, same Glances, two nodes).
45736
- *
45737
- * `Menlo` is the fix rather than a guess: it is macOS's default terminal face,
45738
- * present on every install, and derived from DejaVu Sans Mono — the same glyph
45739
- * coverage by ancestry. Monaco and Courier New follow as older fallbacks; the
45740
- * generic stays last so a host with none of them still draws something.
45741
- */
45742
- var TERMINAL_FONT_STACK = "DejaVu Sans Mono,Menlo,Monaco,Courier New,monospace";
45743
- var TERMINAL_FONT_SIZE = 13;
45744
- var TERMINAL_TEXT_MARGIN_X = 8;
45745
- var TERMINAL_ROW_HEIGHT = 15;
45746
- var TERMINAL_BASELINE_Y = 18;
45747
- /**
45748
- * Distance from a row's baseline up to the top of its cell box. Chosen so
45749
- * consecutive rows tile exactly: row N's box runs from `baseline - this` for
45750
- * `TERMINAL_ROW_HEIGHT`, and row N+1's box starts where it ends. A background
45751
- * bar that stopped short would draw as stripes across a `CSI 42m` panel.
45752
- */
45753
- var TERMINAL_CELL_ASCENT = 11.5;
45754
- var TERMINAL_CELL_WIDTH = TERMINAL_FONT_SIZE * (1233 / 2048);
45755
- /** Two decimals is under a tenth of a pixel and keeps the SVG small. */
45756
- function coordinate(value) {
45757
- return String(Number(value.toFixed(2)));
45758
- }
45759
- function escapeXml(value) {
45760
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;");
45761
- }
45762
- /**
45763
- * Render already-interpreted terminal rows into a compact MJPEG frame.
45764
- *
45765
- * `xml:space="preserve"` is load-bearing, not tidiness. SVG `<text>` collapses
45766
- * runs of whitespace by default, and a terminal's entire column alignment IS
45767
- * runs of whitespace — Glances pads every field with spaces. Without it the
45768
- * frame drew each line at roughly half its true width, crammed into the
45769
- * top-left of a mostly-black image, while the SAME session over `attach`
45770
- * looked perfect — which is exactly how the operator reported it. Measured in
45771
- * the hub's own rasterizer 2026-08-11: `AAA<10 spaces>BBB<3>CCC` drew 92 px
45772
- * collapsed against 178 px preserved.
45773
- *
45774
- * Every run is anchored at its OWN column (`x = margin + startCol * cellW`),
45775
- * never appended to the one before it, so the background rects and the glyphs
45776
- * are placed off the same grid and cannot drift apart. `textLength` is emitted
45777
- * with it because it is the correct declaration and renderers that honour it
45778
- * get an exact grid — but it is not what makes this work: librsvg, which sharp
45779
- * uses, ignores it outright (measured 2026-08-12, a 120-glyph run pinned to
45780
- * 600 px still drew its natural 937 px). The anchoring is the guarantee.
45781
- */
45782
- function renderTerminalSvg(rows) {
45783
- const backgrounds = [];
45784
- const texts = [];
45785
- rows.slice(0, 40).forEach((row, index) => {
45786
- const baseline = TERMINAL_BASELINE_Y + index * TERMINAL_ROW_HEIGHT;
45787
- const top = baseline - TERMINAL_CELL_ASCENT;
45788
- let column = 0;
45789
- for (const run of row) {
45790
- if (column >= 120) break;
45791
- const clipped = clipRun(run, 120 - column);
45792
- const columns = [...clipped].length;
45793
- if (columns === 0) continue;
45794
- const x = TERMINAL_TEXT_MARGIN_X + column * TERMINAL_CELL_WIDTH;
45795
- const width = columns * TERMINAL_CELL_WIDTH;
45796
- if (run.bg !== null) backgrounds.push(`<rect x="${coordinate(x)}" y="${coordinate(top)}" width="${coordinate(width)}" height="${String(TERMINAL_ROW_HEIGHT)}" fill="${run.bg}"/>`);
45797
- if (clipped.trim() !== "") {
45798
- const fill = run.fg === null ? "" : ` fill="${run.fg}"`;
45799
- const weight = run.bold ? " font-weight=\"bold\"" : "";
45800
- texts.push(`<text xml:space="preserve" x="${coordinate(x)}" y="${coordinate(baseline)}" textLength="${coordinate(width)}" lengthAdjust="spacingAndGlyphs"${fill}${weight}>${escapeXml(clipped)}</text>`);
45801
- }
45802
- column += columns;
45803
- }
45804
- });
45805
- 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>`;
45806
- }
45807
- /** Cut a run to the columns still left in the row, by code point not unit. */
45808
- function clipRun(run, remaining) {
45809
- const points = [...run.text];
45810
- return points.length <= remaining ? run.text : points.slice(0, remaining).join("");
45811
- }
45812
- async function renderTerminalJpeg(rows) {
45813
- return sharp(Buffer.from(renderTerminalSvg(rows))).jpeg({
45814
- quality: 82,
45815
- chromaSubsampling: "4:2:0"
45816
- }).toBuffer();
45817
- }
45818
- //#endregion
45819
- //#region src/terminal-camera-device.ts
45820
- var terminalCameraSchema = object({
45821
- instanceId: string().min(1).optional(),
45822
- nodeId: string().min(1),
45823
- profileId: string().min(1).default("monitor"),
45824
- profileLabel: string().min(1).default("BTM")
45825
- });
45826
- var relay = null;
45827
- function installTerminalCameraRelay(next) {
45828
- relay = next;
45829
- }
45830
- var TerminalCameraDevice = class extends BaseDevice {
45831
- features = [DeviceFeature.NativeSnapshot];
45832
- constructor(ctx) {
45833
- super(ctx, terminalCameraSchema, { type: ctx.deviceMeta.type });
45834
- this.ctx.registerNativeCap(streamCatalogCapability, { getCatalog: async ({ deviceId }) => {
45835
- if (deviceId !== this.id) return [];
45836
- return this.catalog();
45837
- } });
45838
- this.ctx.registerNativeCap(snapshotCapability, {
45839
- getSnapshot: async ({ deviceId }) => {
45840
- if (deviceId !== this.id) throw new Error(`TerminalCameraDevice: deviceId mismatch, expected ${String(this.id)}, got ${String(deviceId)}`);
45841
- const activeRelay = relay;
45842
- if (!activeRelay) throw new Error("terminal camera relay is unavailable");
45843
- return {
45844
- base64: (await activeRelay.snapshot(this.relayInstanceId(), this.config.get("nodeId"), this.config.get("profileId"))).toString("base64"),
45845
- contentType: "image/jpeg"
45846
- };
45847
- },
45848
- invalidateCache: async () => {}
45849
- });
45850
- this.markOnline(true);
45851
- }
45852
- async catalog() {
45853
- const activeRelay = relay;
45854
- if (!activeRelay) throw new Error("terminal camera relay is unavailable");
45855
- const nodeId = this.config.get("nodeId");
45856
- const profileId = this.config.get("profileId");
45857
- const instanceId = this.relayInstanceId();
45858
- return [{
45859
- camStreamId: profileId,
45860
- kind: "pull-http",
45861
- url: activeRelay.streamUrl(instanceId, nodeId, profileId),
45862
- codec: "h264",
45863
- resolution: {
45864
- width: 960,
45865
- height: 640
45866
- },
45867
- fps: 2,
45868
- label: this.config.get("profileLabel")
45869
- }];
45870
- }
45871
- setNodeOnline(online) {
45872
- this.markOnline(online);
45873
- if (!online) relay?.closeInstance(this.relayInstanceId());
45874
- }
45875
- async removeDevice() {
45876
- await relay?.closeInstance(this.relayInstanceId());
45877
- }
45878
- relayInstanceId() {
45879
- return this.config.get("instanceId") ?? `legacy:${this.stableId}`;
45880
- }
45881
- };
45882
- //#endregion
45883
46299
  //#region src/terminal-camera-relay.ts
45884
46300
  var MJPEG_BOUNDARY = "camstack-terminal-frame";
45885
46301
  var SESSION_IDLE_MS = 3e4;
@@ -46430,13 +46846,34 @@ function newTerminalCameraStableId(instanceId) {
46430
46846
  * Legacy automatic cameras are migration candidates only. A tombstone is
46431
46847
  * durable deletion intent, so a lingering failed device removal must never
46432
46848
  * make that camera adoptable again.
46849
+ *
46850
+ * ## `config` is load-bearing — this read can never be `projection: 'slim'`
46851
+ *
46852
+ * `nodeId`, `profileId` and `profileLabel` all live in the device's `config`,
46853
+ * and a row without `nodeId` is skipped. The slim projection returns
46854
+ * `config: {}` for every row, so a slim answer here is shape-identical to a
46855
+ * fleet that has no legacy cameras — the whole migration section disappears
46856
+ * and nothing says why. `legacy-camera-read-shape.spec.ts` is the arm on that.
46857
+ *
46858
+ * `onSkipped` is why the disappearance would now be visible: a row that LOOKS
46859
+ * like a legacy camera (`terminal-camera-*`, not an instance camera, not
46860
+ * tombstoned, not already adopted) but carries no `nodeId` is reported with
46861
+ * its numeric device id, so the caller can log it per-camera. Rows that are
46862
+ * not candidates at all are silent — a fleet of 1 017 devices must not
46863
+ * produce 1 017 lines to say none of them is a Terminal monitor.
46433
46864
  */
46434
- function listLegacyTerminalCameraCandidates(rows, instanceStableIds, tombstones) {
46865
+ function listLegacyTerminalCameraCandidates(rows, instanceStableIds, tombstones, onSkipped) {
46435
46866
  const legacy = [];
46436
46867
  for (const row of rows) {
46437
46868
  if (tombstones.has(row.stableId) || instanceStableIds.has(row.stableId) || row.stableId.startsWith("terminal-camera-instance-") || !row.stableId.startsWith("terminal-camera-")) continue;
46438
46869
  const nodeId = typeof row.config.nodeId === "string" ? row.config.nodeId : null;
46439
- if (!nodeId) continue;
46870
+ if (!nodeId) {
46871
+ onSkipped?.({
46872
+ deviceId: row.id,
46873
+ stableId: row.stableId
46874
+ });
46875
+ continue;
46876
+ }
46440
46877
  const profileId = typeof row.config.profileId === "string" ? row.config.profileId : "monitor";
46441
46878
  const profileLabel = typeof row.config.profileLabel === "string" ? row.config.profileLabel : profileId === "monitor" ? "BTM" : profileId;
46442
46879
  legacy.push({
@@ -46597,111 +47034,6 @@ function findProfile(profiles, profileId) {
46597
47034
  return profiles.find((p) => p.profileId === profileId);
46598
47035
  }
46599
47036
  //#endregion
46600
- //#region src/profile-settings.ts
46601
- var GLANCES_PLUGINS = [
46602
- {
46603
- key: "showCpu",
46604
- plugin: "cpu",
46605
- label: "CPU"
46606
- },
46607
- {
46608
- key: "showMem",
46609
- plugin: "mem",
46610
- label: "Memory"
46611
- },
46612
- {
46613
- key: "showLoad",
46614
- plugin: "load",
46615
- label: "Load"
46616
- },
46617
- {
46618
- key: "showNetwork",
46619
- plugin: "network",
46620
- label: "Network"
46621
- },
46622
- {
46623
- key: "showDiskIo",
46624
- plugin: "diskio",
46625
- label: "Disk I/O"
46626
- },
46627
- {
46628
- key: "showFs",
46629
- plugin: "fs",
46630
- label: "Filesystems"
46631
- },
46632
- {
46633
- key: "showProcessList",
46634
- plugin: "processlist",
46635
- label: "Process list"
46636
- },
46637
- {
46638
- key: "showContainers",
46639
- plugin: "containers",
46640
- label: "Containers"
46641
- },
46642
- {
46643
- key: "showSensors",
46644
- plugin: "sensors",
46645
- label: "Sensors"
46646
- }
46647
- ];
46648
- function glancesBooleanField(key, label) {
46649
- return {
46650
- type: "boolean",
46651
- key,
46652
- label,
46653
- default: true,
46654
- style: "switch"
46655
- };
46656
- }
46657
- function glancesSettingsSchema() {
46658
- return { sections: [{
46659
- id: "glances-panels",
46660
- title: "Glances panels",
46661
- 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).",
46662
- columns: 2,
46663
- fields: GLANCES_PLUGINS.map((plugin) => glancesBooleanField(plugin.key, plugin.label))
46664
- }] };
46665
- }
46666
- function settingsSchemaForProfile(profileId) {
46667
- if (profileId === "glances") return glancesSettingsSchema();
46668
- return null;
46669
- }
46670
- function glancesSettingsToArgs(settings) {
46671
- const disabled = GLANCES_PLUGINS.filter((plugin) => settings[plugin.key] === false).map((plugin) => plugin.plugin);
46672
- if (disabled.length === 0) return [];
46673
- return ["--disable-plugin", disabled.join(",")];
46674
- }
46675
- function sanitizeProfileSettings(profileId, raw) {
46676
- if (profileId !== "glances") return {};
46677
- const bag = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
46678
- const out = {
46679
- showCpu: true,
46680
- showMem: true,
46681
- showLoad: true,
46682
- showNetwork: true,
46683
- showDiskIo: true,
46684
- showFs: true,
46685
- showProcessList: true,
46686
- showContainers: true,
46687
- showSensors: true
46688
- };
46689
- for (const plugin of GLANCES_PLUGINS) {
46690
- const value = bag[plugin.key];
46691
- if (typeof value === "boolean") out[plugin.key] = value;
46692
- }
46693
- return out;
46694
- }
46695
- function profileSettingsToArgs(profileId, settings) {
46696
- if (profileId !== "glances") return [];
46697
- return glancesSettingsToArgs(sanitizeProfileSettings(profileId, settings));
46698
- }
46699
- function spawnArgsForInstance(input) {
46700
- const extra = profileSettingsToArgs(input.profileId, input.profileSettings);
46701
- if (input.instanceArgs.length > 0) return [...input.instanceArgs, ...extra];
46702
- if (extra.length > 0) return [...input.profileArgs, ...extra];
46703
- }
46704
- //#endregion
46705
47037
  //#region src/terminal-session-manager.ts
46706
47038
  var MIN_GRID = 1;
46707
47039
  var MAX_COLS = 1e3;
@@ -47523,7 +47855,12 @@ var TerminalAddon = class extends BaseAddon {
47523
47855
  async listLegacyTerminalCameras() {
47524
47856
  const instances = this.terminalInstances();
47525
47857
  const instanceStableIds = new Set(instances.map((instance) => instance.cameraStableId));
47526
- return listLegacyTerminalCameraCandidates(await this.ctx.api.deviceManager.listAll.query({ addonId: this.ctx.id }), instanceStableIds, this.terminalCameraTombstones);
47858
+ return listLegacyTerminalCameraCandidates(await this.ctx.api.deviceManager.listAll.query({ addonId: this.ctx.id }), instanceStableIds, this.terminalCameraTombstones, ({ deviceId, stableId }) => {
47859
+ this.ctx.logger.warn("legacy Terminal camera skipped — its device config carries no nodeId, so it cannot be offered for adoption", {
47860
+ tags: { deviceId },
47861
+ meta: { stableId }
47862
+ });
47863
+ });
47527
47864
  }
47528
47865
  async adoptLegacyMonitor(stableId, requestedName) {
47529
47866
  const instance = await this.instanceMutationQueue.run(() => this.adoptLegacyMonitorUnlocked(stableId, requestedName));