@camstack/addon-terminal 0.1.10 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/addon.js +1232 -183
- package/dist/addon.mjs +1232 -183
- package/package.json +7 -3
- package/python/requirements.txt +1 -0
package/dist/addon.mjs
CHANGED
|
@@ -7605,12 +7605,11 @@ var RecordingConfigSchema = object({
|
|
|
7605
7605
|
/**
|
|
7606
7606
|
* Entity-relocation job state (storage entity-routing spec, Phase 4).
|
|
7607
7607
|
*
|
|
7608
|
-
* One shape shared by the recorder
|
|
7609
|
-
*
|
|
7610
|
-
*
|
|
7611
|
-
*
|
|
7612
|
-
*
|
|
7613
|
-
* row on the owning addon's surface.
|
|
7608
|
+
* One shape shared by the recorder and pipeline-analytics internal movers.
|
|
7609
|
+
* The public admin surface is `storage-migration`; child jobs remain in RAM
|
|
7610
|
+
* because copy-if-absent, verify, delete and index/row repoint are resumable.
|
|
7611
|
+
* Each completed/failed run also lands one durable ops-log row on its owning
|
|
7612
|
+
* addon surface.
|
|
7614
7613
|
*/
|
|
7615
7614
|
var RelocateJobStateSchema = _enum([
|
|
7616
7615
|
"running",
|
|
@@ -7637,19 +7636,100 @@ var RelocateJobSchema = object({
|
|
|
7637
7636
|
finishedAt: number().nullable(),
|
|
7638
7637
|
error: string().nullable()
|
|
7639
7638
|
});
|
|
7639
|
+
/** Profile-derived footage selection used only by the migration coordinator:
|
|
7640
|
+
* `recordings` owns high+mid; `recordingsLow` owns low. */
|
|
7641
|
+
var RelocateFootageClassSchema = _enum(["recordings", "recordingsLow"]);
|
|
7640
7642
|
var RelocateFootageInputSchema = object({
|
|
7641
|
-
deviceId: number().optional(),
|
|
7642
7643
|
fromLocationId: string(),
|
|
7643
7644
|
toLocationId: string(),
|
|
7644
7645
|
entities: array(_enum(["segments"])).optional(),
|
|
7646
|
+
/** Limits relocation to the logical profile class. Omit only for the
|
|
7647
|
+
* pre-orchestration compatibility path. */
|
|
7648
|
+
footageClass: RelocateFootageClassSchema.optional(),
|
|
7645
7649
|
/** Copy throttle in MB/s (default 40) — the drain is a background chore,
|
|
7646
7650
|
* never allowed to starve live writers. */
|
|
7647
7651
|
throttleMbps: number().min(1).max(1e3).optional()
|
|
7648
7652
|
});
|
|
7649
|
-
|
|
7650
|
-
|
|
7653
|
+
/** Internal, lease-scoped participant operation. It is intentionally separate
|
|
7654
|
+
* from persistent recording settings: a migration never changes
|
|
7655
|
+
* `RecordingConfig.enabled` or camera wrapper bindings. */
|
|
7656
|
+
var StorageMigrationLeaseInputSchema = object({ leaseId: string().min(1) });
|
|
7657
|
+
var StorageMigrationFootageMoveInputSchema = RelocateFootageInputSchema.extend({ leaseId: string().min(1) });
|
|
7658
|
+
var StorageMigrationMediaMoveInputSchema = object({
|
|
7651
7659
|
toLocationId: string(),
|
|
7652
7660
|
throttleMbps: number().min(1).max(1e3).optional()
|
|
7661
|
+
}).extend({ leaseId: string().min(1) });
|
|
7662
|
+
/** The independently selectable logical storage classes. `recordings`
|
|
7663
|
+
* encompasses the high and mid segment profiles; `recordingsLow` is low
|
|
7664
|
+
* segments; `eventMedia` is post-analysis blobs. */
|
|
7665
|
+
var StorageMigrationClassSchema = _enum([
|
|
7666
|
+
"recordings",
|
|
7667
|
+
"recordingsLow",
|
|
7668
|
+
"eventMedia"
|
|
7669
|
+
]);
|
|
7670
|
+
/** A destination is always an existing, fully-qualified location id. The
|
|
7671
|
+
* migration API intentionally never changes a source location's `basePath`:
|
|
7672
|
+
* callers create a new `<type>:<slug>` location, then select it here. */
|
|
7673
|
+
var StorageMigrationDestinationsSchema = object({
|
|
7674
|
+
recordings: string().min(1).optional(),
|
|
7675
|
+
recordingsLow: string().min(1).optional(),
|
|
7676
|
+
eventMedia: string().min(1).optional()
|
|
7677
|
+
}).refine((value) => Object.keys(value).length > 0, { message: "select at least one storage class" });
|
|
7678
|
+
/** Shared input for planning and starting an orchestrated storage migration. */
|
|
7679
|
+
var StorageMigrationInputSchema = object({
|
|
7680
|
+
destinations: StorageMigrationDestinationsSchema,
|
|
7681
|
+
throttleMbps: number().min(1).max(1e3).optional()
|
|
7682
|
+
});
|
|
7683
|
+
/** The durable coordinator state machine. The only phase that changes default
|
|
7684
|
+
* locations is `repointing`, after every selected mover has completed and been
|
|
7685
|
+
* verified. */
|
|
7686
|
+
var StorageMigrationPhaseSchema = _enum([
|
|
7687
|
+
"planning",
|
|
7688
|
+
"pausing",
|
|
7689
|
+
"moving",
|
|
7690
|
+
"verifying",
|
|
7691
|
+
"repointing",
|
|
7692
|
+
"refreshing",
|
|
7693
|
+
"resuming",
|
|
7694
|
+
"done",
|
|
7695
|
+
"failed",
|
|
7696
|
+
"cancelled"
|
|
7697
|
+
]);
|
|
7698
|
+
var StorageMigrationParticipantSchema = _enum([
|
|
7699
|
+
"pipeline",
|
|
7700
|
+
"recorder",
|
|
7701
|
+
"analytics"
|
|
7702
|
+
]);
|
|
7703
|
+
var StorageMigrationMoveSchema = object({
|
|
7704
|
+
storageClass: StorageMigrationClassSchema,
|
|
7705
|
+
fromLocationId: string(),
|
|
7706
|
+
toLocationId: string(),
|
|
7707
|
+
moverJobId: string().nullable(),
|
|
7708
|
+
state: RelocateJobStateSchema.nullable(),
|
|
7709
|
+
error: string().nullable()
|
|
7710
|
+
});
|
|
7711
|
+
var StorageMigrationJobSchema = object({
|
|
7712
|
+
jobId: string(),
|
|
7713
|
+
phase: StorageMigrationPhaseSchema,
|
|
7714
|
+
destinations: StorageMigrationDestinationsSchema,
|
|
7715
|
+
throttleMbps: number(),
|
|
7716
|
+
moves: array(StorageMigrationMoveSchema),
|
|
7717
|
+
pauseLeaseId: string().nullable(),
|
|
7718
|
+
pausedParticipants: array(StorageMigrationParticipantSchema),
|
|
7719
|
+
repointed: boolean(),
|
|
7720
|
+
cancelRequested: boolean(),
|
|
7721
|
+
startedAt: number(),
|
|
7722
|
+
updatedAt: number(),
|
|
7723
|
+
finishedAt: number().nullable(),
|
|
7724
|
+
error: string().nullable()
|
|
7725
|
+
});
|
|
7726
|
+
var StorageMigrationPlanSchema = object({
|
|
7727
|
+
destinations: StorageMigrationDestinationsSchema,
|
|
7728
|
+
moves: array(object({
|
|
7729
|
+
storageClass: StorageMigrationClassSchema,
|
|
7730
|
+
fromLocationId: string(),
|
|
7731
|
+
toLocationId: string()
|
|
7732
|
+
}))
|
|
7653
7733
|
});
|
|
7654
7734
|
/**
|
|
7655
7735
|
* `StorageLocationType` — an addon-declared id that identifies the *kind* of
|
|
@@ -16248,13 +16328,19 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
16248
16328
|
}), method(object({ deviceId: number() }), EventPruneCountsSchema, {
|
|
16249
16329
|
kind: "mutation",
|
|
16250
16330
|
auth: "admin"
|
|
16251
|
-
}), method(
|
|
16331
|
+
}), method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
|
|
16252
16332
|
kind: "mutation",
|
|
16253
16333
|
auth: "admin"
|
|
16254
|
-
}), method(object({
|
|
16255
|
-
kind: "
|
|
16334
|
+
}), method(StorageMigrationLeaseInputSchema, object({ resumed: literal(true) }), {
|
|
16335
|
+
kind: "mutation",
|
|
16336
|
+
auth: "admin"
|
|
16337
|
+
}), method(StorageMigrationLeaseInputSchema, object({ refreshed: literal(true) }), {
|
|
16338
|
+
kind: "mutation",
|
|
16256
16339
|
auth: "admin"
|
|
16257
|
-
}), method(object({ jobId: string() }),
|
|
16340
|
+
}), method(StorageMigrationMediaMoveInputSchema, object({ jobId: string() }), {
|
|
16341
|
+
kind: "mutation",
|
|
16342
|
+
auth: "admin"
|
|
16343
|
+
}), method(object({ jobId: string() }), RelocateJobSchema.nullable(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
|
|
16258
16344
|
kind: "mutation",
|
|
16259
16345
|
auth: "admin"
|
|
16260
16346
|
}), method(OpsLogQueryInputSchema, array(OpsLogEntrySchema).readonly(), {
|
|
@@ -17784,7 +17870,13 @@ var NodeInferenceDevicesSchema = object({
|
|
|
17784
17870
|
reachable: boolean(),
|
|
17785
17871
|
devices: array(NodeInferenceDeviceSchema).readonly()
|
|
17786
17872
|
});
|
|
17787
|
-
method(object({
|
|
17873
|
+
method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
|
|
17874
|
+
kind: "mutation",
|
|
17875
|
+
auth: "admin"
|
|
17876
|
+
}), method(StorageMigrationLeaseInputSchema, object({ resumed: literal(true) }), {
|
|
17877
|
+
kind: "mutation",
|
|
17878
|
+
auth: "admin"
|
|
17879
|
+
}), method(object({
|
|
17788
17880
|
deviceId: number(),
|
|
17789
17881
|
agentNodeId: string()
|
|
17790
17882
|
}), object({ success: literal(true) }), {
|
|
@@ -18224,7 +18316,33 @@ var SnapshotImageSchema = object({
|
|
|
18224
18316
|
base64: string(),
|
|
18225
18317
|
contentType: string()
|
|
18226
18318
|
});
|
|
18227
|
-
|
|
18319
|
+
/**
|
|
18320
|
+
* snapshot — device-scoped capability for camera image capture.
|
|
18321
|
+
*
|
|
18322
|
+
* Two kinds of providers coexist behind this cap name:
|
|
18323
|
+
*
|
|
18324
|
+
* - **Native** providers (kind:'native'): registered per-device by
|
|
18325
|
+
* device-driver addons (RtspCamera, OnvifCamera, …) via
|
|
18326
|
+
* `DeviceContext.registerNativeCap`. Each knows how to fetch a frame
|
|
18327
|
+
* straight from the camera (HTTP snapshot URL, ONVIF action, etc.).
|
|
18328
|
+
*
|
|
18329
|
+
* - **Wrapper** providers (kind:'wrapper'): register as a system
|
|
18330
|
+
* provider (SnapshotAddon in `@camstack/system/builtins/snapshot`). The
|
|
18331
|
+
* wrapper owns the cache and invokes the native via
|
|
18332
|
+
* `ctx.getNativeProvider(snapshotCapability, deviceId)` on miss.
|
|
18333
|
+
*
|
|
18334
|
+
* Device-scoped routing: callers use `ctx.fetchDevice(id).snapshot.*`;
|
|
18335
|
+
* the DeviceProxy auto-injects `deviceId` + `nodeId` and dispatches to
|
|
18336
|
+
* the provider currently active for that device (wrapper wins when
|
|
18337
|
+
* activated via `setWrapperActive`, otherwise the native).
|
|
18338
|
+
*/
|
|
18339
|
+
/**
|
|
18340
|
+
* Live readable snapshot state — diagnostic info that a consumer can
|
|
18341
|
+
* pull to know when the last image was captured, how stale the cache
|
|
18342
|
+
* is, and which stream was used. Distinct from `getSnapshot` which
|
|
18343
|
+
* returns the JPEG itself.
|
|
18344
|
+
*/
|
|
18345
|
+
var SnapshotStatusSchema = object({
|
|
18228
18346
|
/** Ms epoch of the last successful capture. Null if none yet. */
|
|
18229
18347
|
lastCapturedAt: number().nullable(),
|
|
18230
18348
|
/** Age of the cached image in ms. Null if no cache. */
|
|
@@ -18234,64 +18352,129 @@ object({
|
|
|
18234
18352
|
/** Stream id used for the last capture ('high'|'mid'|'low' or custom). Null if via HTTP endpoint. */
|
|
18235
18353
|
lastStreamId: string().nullable()
|
|
18236
18354
|
});
|
|
18237
|
-
|
|
18238
|
-
|
|
18239
|
-
|
|
18240
|
-
|
|
18241
|
-
|
|
18242
|
-
|
|
18243
|
-
|
|
18244
|
-
|
|
18245
|
-
|
|
18246
|
-
|
|
18247
|
-
|
|
18248
|
-
|
|
18249
|
-
|
|
18250
|
-
|
|
18251
|
-
|
|
18252
|
-
|
|
18253
|
-
|
|
18254
|
-
|
|
18255
|
-
|
|
18256
|
-
|
|
18257
|
-
|
|
18258
|
-
|
|
18259
|
-
|
|
18260
|
-
|
|
18261
|
-
|
|
18262
|
-
|
|
18263
|
-
|
|
18264
|
-
|
|
18265
|
-
|
|
18266
|
-
|
|
18355
|
+
var snapshotCapability = {
|
|
18356
|
+
name: "snapshot",
|
|
18357
|
+
scope: "device",
|
|
18358
|
+
deviceNative: true,
|
|
18359
|
+
mode: "singleton",
|
|
18360
|
+
kind: "wrapper",
|
|
18361
|
+
defaultActive: true,
|
|
18362
|
+
deviceTypes: [DeviceType.Camera],
|
|
18363
|
+
exposesDeviceSettings: true,
|
|
18364
|
+
methods: {
|
|
18365
|
+
getSnapshot: method(object({
|
|
18366
|
+
deviceId: number(),
|
|
18367
|
+
streamId: string().optional(),
|
|
18368
|
+
/**
|
|
18369
|
+
* Bypass the cache freshness check and fetch directly from the
|
|
18370
|
+
* native (or stream-broker fallback). Triggered by the UI's
|
|
18371
|
+
* "refresh" button so an operator can force a fresh frame
|
|
18372
|
+
* even when the cache is well within the device's
|
|
18373
|
+
* `snapshotMaxAgeS` window.
|
|
18374
|
+
*
|
|
18375
|
+
* **`force` is an OPERATOR signal, not a freshness preference.** On a
|
|
18376
|
+
* battery camera it is the one thing that walks past the wrapper's
|
|
18377
|
+
* sleep gate and wakes the camera, so a background caller — a poller,
|
|
18378
|
+
* an event handler, a thumbnail — must NEVER set it. Every such caller
|
|
18379
|
+
* gets the cached frame, which on a sleeping battery camera is the
|
|
18380
|
+
* correct answer: stale but honest beats woken.
|
|
18381
|
+
*/
|
|
18382
|
+
force: boolean().optional()
|
|
18383
|
+
}), SnapshotImageSchema.nullable()),
|
|
18384
|
+
invalidateCache: method(object({ deviceId: number() }), _void(), {
|
|
18385
|
+
kind: "mutation",
|
|
18386
|
+
auth: "admin"
|
|
18387
|
+
}),
|
|
18388
|
+
/**
|
|
18389
|
+
* Cache-only batch overview — answers from the wrapper's in-memory cache in
|
|
18390
|
+
* O(n) and NEVER triggers a capture. Lets a grid skip requesting images for
|
|
18391
|
+
* devices that never produced a frame, and gives it an ETag per device for
|
|
18392
|
+
* conditional (304-able) image fetches. `lastCapturedAt`/`cacheAgeMs`/`etag`
|
|
18393
|
+
* are null for a device with no cached frame.
|
|
18394
|
+
*/
|
|
18395
|
+
getSnapshotOverview: systemMethod(object({ deviceIds: array(number()).min(1).max(200) }), array(object({
|
|
18396
|
+
deviceId: number(),
|
|
18397
|
+
lastCapturedAt: number().nullable(),
|
|
18398
|
+
cacheAgeMs: number().nullable(),
|
|
18399
|
+
etag: string().nullable()
|
|
18400
|
+
}))),
|
|
18401
|
+
/**
|
|
18402
|
+
* Signed, expiring links to a CLIENT-SIZED frame — and the demand signal
|
|
18403
|
+
* that makes those frames current.
|
|
18404
|
+
*
|
|
18405
|
+
* ## The problem it replaces
|
|
18406
|
+
*
|
|
18407
|
+
* `getSnapshotOverview` is cache-only by contract: it answers from whatever
|
|
18408
|
+
* the wrapper happens to hold and never captures. Under D93 the client
|
|
18409
|
+
* versions its image URL on that answer, and an image REQUEST is what enrols
|
|
18410
|
+
* a camera in the keep-warm loop. Both of those are satisfiable by the
|
|
18411
|
+
* client's own image cache — `expo-image` is URL-keyed and never revalidates
|
|
18412
|
+
* — so a URL painted in a previous session comes off disk with no network,
|
|
18413
|
+
* no enrolment, and nothing warming. Measured on the live hub: reopening
|
|
18414
|
+
* after two minutes idle painted 15 of 16 tiles at **168 s old** with zero
|
|
18415
|
+
* HTTP requests, and the fleet only recovered because a later poll happened
|
|
18416
|
+
* to observe a different identity.
|
|
18417
|
+
*
|
|
18418
|
+
* ## The two properties that fix it
|
|
18419
|
+
*
|
|
18420
|
+
* **It is an RPC, so no client cache can answer it.** The demand signal
|
|
18421
|
+
* always reaches the wrapper. This method therefore MAY create keep-warm
|
|
18422
|
+
* subscriptions, where `getSnapshotOverview` must never (D93) — the
|
|
18423
|
+
* distinction is not "one is newer" but that the overview poll is app-wide
|
|
18424
|
+
* (a creating overview would warm every camera on the install) while this is
|
|
18425
|
+
* called by a rendered surface naming the tiles it is actually painting, at
|
|
18426
|
+
* the width it is painting them.
|
|
18427
|
+
*
|
|
18428
|
+
* **It waits, briefly and boundedly, for the capture it triggered.** The
|
|
18429
|
+
* returned `capturedAt` is the frame the link will serve, not the frame the
|
|
18430
|
+
* cache held when the client asked, so a first paint is honest and current
|
|
18431
|
+
* instead of a generation behind. A device that does not settle inside the
|
|
18432
|
+
* bound still gets a link and its real (older) `capturedAt` — the next poll
|
|
18433
|
+
* carries it forward.
|
|
18434
|
+
*
|
|
18435
|
+
* `force` is never set on behalf of a client here. A sleeping battery camera
|
|
18436
|
+
* is reported with `sleeping: true` and the last frame it produced, however
|
|
18437
|
+
* old; the wrapper's existing sleep gate owns that decision and this method
|
|
18438
|
+
* adds no second one.
|
|
18439
|
+
*/
|
|
18440
|
+
getSnapshotLinks: systemMethod(object({
|
|
18441
|
+
/** The tiles a surface is actually rendering. One entry per (device,
|
|
18442
|
+
* width) the caller will paint — the width is snapped to the server's
|
|
18443
|
+
* ladder and becomes part of the link's SIGNED identity. */
|
|
18267
18444
|
targets: array(object({
|
|
18268
|
-
|
|
18269
|
-
|
|
18270
|
-
|
|
18271
|
-
|
|
18272
|
-
})).min(1).max(200) }), array(object({
|
|
18273
|
-
|
|
18274
|
-
|
|
18275
|
-
|
|
18276
|
-
|
|
18277
|
-
|
|
18278
|
-
|
|
18279
|
-
|
|
18280
|
-
|
|
18281
|
-
|
|
18282
|
-
|
|
18283
|
-
|
|
18284
|
-
|
|
18285
|
-
|
|
18286
|
-
|
|
18287
|
-
|
|
18288
|
-
|
|
18289
|
-
|
|
18290
|
-
|
|
18291
|
-
|
|
18292
|
-
|
|
18293
|
-
|
|
18294
|
-
})))
|
|
18445
|
+
deviceId: number(),
|
|
18446
|
+
/** Target width in px. Omit for the frame as captured — correct
|
|
18447
|
+
* for a full-bleed surface, wrong (and expensive) for a grid. */
|
|
18448
|
+
width: number().int().positive().optional()
|
|
18449
|
+
})).min(1).max(200) }), array(object({
|
|
18450
|
+
deviceId: number(),
|
|
18451
|
+
/** Root-relative signed path, or null when the link plane is not
|
|
18452
|
+
* served (no data-plane facility). Present even for a device that has
|
|
18453
|
+
* never captured — the request is what triggers the first one (D94). */
|
|
18454
|
+
url: string().nullable(),
|
|
18455
|
+
/** Epoch ms of the frame this link serves. Null = never captured.
|
|
18456
|
+
* THE honest age: the tRPC path carried none before this. */
|
|
18457
|
+
capturedAt: number().nullable(),
|
|
18458
|
+
/** Age of that frame at the moment the answer was built. */
|
|
18459
|
+
ageMs: number().nullable(),
|
|
18460
|
+
/** Epoch ms after which `url` stops verifying. */
|
|
18461
|
+
expiresAt: number().nullable(),
|
|
18462
|
+
/** Ladder rung the bytes are at; null = the frame as captured. */
|
|
18463
|
+
width: number().nullable(),
|
|
18464
|
+
/** The device has never produced a frame. An empty state, not a
|
|
18465
|
+
* failure — and never a reason to withhold the link (D94). */
|
|
18466
|
+
neverCaptured: boolean(),
|
|
18467
|
+
/** A sleeping battery camera: the frame is deliberately stale and will
|
|
18468
|
+
* NOT refresh in the background. A surface should say so rather than
|
|
18469
|
+
* present it as current. */
|
|
18470
|
+
sleeping: boolean()
|
|
18471
|
+
})))
|
|
18472
|
+
},
|
|
18473
|
+
status: {
|
|
18474
|
+
schema: SnapshotStatusSchema,
|
|
18475
|
+
kind: "poll"
|
|
18476
|
+
}
|
|
18477
|
+
};
|
|
18295
18478
|
/**
|
|
18296
18479
|
* `sso-bridge` — internal hub-only cap that lets SSO-style auth
|
|
18297
18480
|
* providers (OIDC, SAML, magic-link, …) mint an HMAC-signed token
|
|
@@ -18458,6 +18641,13 @@ method(object({ locationId: string() }), EvictableUsageSchema), method(object({
|
|
|
18458
18641
|
locationId: string(),
|
|
18459
18642
|
targetBytes: number().int().positive()
|
|
18460
18643
|
}), EvictResultSchema, { kind: "mutation" });
|
|
18644
|
+
method(StorageMigrationInputSchema, StorageMigrationPlanSchema, { auth: "admin" }), method(StorageMigrationInputSchema, object({ jobId: string() }), {
|
|
18645
|
+
kind: "mutation",
|
|
18646
|
+
auth: "admin"
|
|
18647
|
+
}), method(object({ jobId: string().optional() }), StorageMigrationJobSchema.nullable(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
|
|
18648
|
+
kind: "mutation",
|
|
18649
|
+
auth: "admin"
|
|
18650
|
+
});
|
|
18461
18651
|
var ProviderInfoSchema = discriminatedUnion("shouldSaveDiskSpace", [object({
|
|
18462
18652
|
providerId: string().min(1),
|
|
18463
18653
|
displayName: string().min(1),
|
|
@@ -18561,6 +18751,28 @@ var TerminalProfileInfoSchema = object({
|
|
|
18561
18751
|
label: string(),
|
|
18562
18752
|
description: string().optional()
|
|
18563
18753
|
});
|
|
18754
|
+
/**
|
|
18755
|
+
* A durable operator-created Terminal instance. Profiles are templates; only
|
|
18756
|
+
* an instance declares a camera.
|
|
18757
|
+
*/
|
|
18758
|
+
var TerminalInstanceInfoSchema = object({
|
|
18759
|
+
instanceId: string(),
|
|
18760
|
+
cameraStableId: string(),
|
|
18761
|
+
nodeId: string(),
|
|
18762
|
+
profileId: string(),
|
|
18763
|
+
profileLabel: string(),
|
|
18764
|
+
name: string(),
|
|
18765
|
+
enabled: boolean()
|
|
18766
|
+
});
|
|
18767
|
+
var TerminalLegacyCameraSchema = object({
|
|
18768
|
+
stableId: string(),
|
|
18769
|
+
nodeId: string(),
|
|
18770
|
+
profileId: string(),
|
|
18771
|
+
profileLabel: string(),
|
|
18772
|
+
name: string(),
|
|
18773
|
+
/** Only legacy monitor cameras can retain their historic stable identity. */
|
|
18774
|
+
adoptable: boolean()
|
|
18775
|
+
});
|
|
18564
18776
|
var TerminalOutputEventSchema = discriminatedUnion("kind", [object({
|
|
18565
18777
|
seq: number().int().positive(),
|
|
18566
18778
|
kind: literal("data"),
|
|
@@ -18580,10 +18792,9 @@ var TerminalOutputBatchSchema = object({
|
|
|
18580
18792
|
/**
|
|
18581
18793
|
* terminal-session — singleton system capability for interactive TTY sessions.
|
|
18582
18794
|
*
|
|
18583
|
-
*
|
|
18584
|
-
*
|
|
18585
|
-
*
|
|
18586
|
-
* change this contract.
|
|
18795
|
+
* Owns both live PTY lifecycle and durable Terminal instance management.
|
|
18796
|
+
* Profiles are allowlisted templates; an explicit instance is the only path
|
|
18797
|
+
* that declares a camera.
|
|
18587
18798
|
*/
|
|
18588
18799
|
var terminalSessionCapability = {
|
|
18589
18800
|
name: "terminal-session",
|
|
@@ -18592,6 +18803,37 @@ var terminalSessionCapability = {
|
|
|
18592
18803
|
methods: {
|
|
18593
18804
|
/** Pre-declared profiles the operator may open. */
|
|
18594
18805
|
listProfiles: method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }),
|
|
18806
|
+
/** Explicit durable Terminal instances, managed centrally on the hub. */
|
|
18807
|
+
listInstances: method(_void(), array(TerminalInstanceInfoSchema).readonly(), { auth: "admin" }),
|
|
18808
|
+
createInstance: method(object({
|
|
18809
|
+
targetNodeId: string().min(1),
|
|
18810
|
+
profileId: string().min(1),
|
|
18811
|
+
name: string().trim().min(1).max(160).optional()
|
|
18812
|
+
}), TerminalInstanceInfoSchema, {
|
|
18813
|
+
kind: "mutation",
|
|
18814
|
+
auth: "admin"
|
|
18815
|
+
}),
|
|
18816
|
+
deleteInstance: method(object({ instanceId: string().min(1) }), _void(), {
|
|
18817
|
+
kind: "mutation",
|
|
18818
|
+
auth: "admin"
|
|
18819
|
+
}),
|
|
18820
|
+
setInstanceEnabled: method(object({
|
|
18821
|
+
instanceId: string().min(1),
|
|
18822
|
+
enabled: boolean()
|
|
18823
|
+
}), TerminalInstanceInfoSchema, {
|
|
18824
|
+
kind: "mutation",
|
|
18825
|
+
auth: "admin"
|
|
18826
|
+
}),
|
|
18827
|
+
/** Existing automatic cameras are shown for explicit migration only. */
|
|
18828
|
+
listLegacyCameras: method(_void(), array(TerminalLegacyCameraSchema).readonly(), { auth: "admin" }),
|
|
18829
|
+
/** Explicitly adopt one legacy monitor camera, retaining its stable id. */
|
|
18830
|
+
adoptLegacyMonitor: method(object({
|
|
18831
|
+
stableId: string().min(1),
|
|
18832
|
+
name: string().trim().min(1).max(160).optional()
|
|
18833
|
+
}), TerminalInstanceInfoSchema, {
|
|
18834
|
+
kind: "mutation",
|
|
18835
|
+
auth: "admin"
|
|
18836
|
+
}),
|
|
18595
18837
|
/** Live sessions currently hosted by the provider. */
|
|
18596
18838
|
listSessions: method(_void(), array(TerminalSessionInfoSchema).readonly(), { auth: "admin" }),
|
|
18597
18839
|
/**
|
|
@@ -18623,7 +18865,13 @@ var terminalSessionCapability = {
|
|
|
18623
18865
|
pullOutput: method(object({
|
|
18624
18866
|
sessionId: string(),
|
|
18625
18867
|
afterSeq: number().int().nonnegative(),
|
|
18626
|
-
waitMs: number().int().min(0).max(2e3).default(0)
|
|
18868
|
+
waitMs: number().int().min(0).max(2e3).default(0),
|
|
18869
|
+
/**
|
|
18870
|
+
* Wait when a just-opened session has no output yet. Kept opt-in so a
|
|
18871
|
+
* browser's initial repaint remains immediate; the camera snapshot
|
|
18872
|
+
* relay uses it to avoid encoding a blank startup frame.
|
|
18873
|
+
*/
|
|
18874
|
+
waitForOutput: boolean().optional()
|
|
18627
18875
|
}), TerminalOutputBatchSchema, {
|
|
18628
18876
|
kind: "mutation",
|
|
18629
18877
|
auth: "admin",
|
|
@@ -21127,6 +21375,7 @@ var FaceInfoSchema = object({
|
|
|
21127
21375
|
var FaceFilterEnum = _enum([
|
|
21128
21376
|
"unassigned",
|
|
21129
21377
|
"recognized",
|
|
21378
|
+
"identified",
|
|
21130
21379
|
"all"
|
|
21131
21380
|
]);
|
|
21132
21381
|
var MediaFileLiteSchema$1 = object({
|
|
@@ -21155,6 +21404,8 @@ method(_void(), array(IdentitySchema).readonly()), method(object({ name: string(
|
|
|
21155
21404
|
kind: "mutation",
|
|
21156
21405
|
auth: "admin"
|
|
21157
21406
|
}), method(object({
|
|
21407
|
+
/** Restrict to one camera. Absent keeps the cluster-wide gallery view. */
|
|
21408
|
+
deviceId: number().int().optional(),
|
|
21158
21409
|
limit: number().int().positive().optional(),
|
|
21159
21410
|
filter: FaceFilterEnum.optional(),
|
|
21160
21411
|
/**
|
|
@@ -23384,6 +23635,10 @@ var OsdSourceSchema = discriminatedUnion("kind", [
|
|
|
23384
23635
|
capName: string().min(1).max(64),
|
|
23385
23636
|
/** Dot path inside the slice, e.g. `detected`, `value`, `mode`. */
|
|
23386
23637
|
valuePath: string().min(1).max(64)
|
|
23638
|
+
}),
|
|
23639
|
+
object({
|
|
23640
|
+
kind: literal("latest-recognition"),
|
|
23641
|
+
recognition: _enum(["person", "plate"])
|
|
23387
23642
|
})
|
|
23388
23643
|
]);
|
|
23389
23644
|
var OsdSlotBindingSchema = object({
|
|
@@ -23489,6 +23744,15 @@ method(object({ deviceId: number().int() }), object({
|
|
|
23489
23744
|
}), object({ success: literal(true) }), {
|
|
23490
23745
|
kind: "mutation",
|
|
23491
23746
|
auth: "admin"
|
|
23747
|
+
}), method(object({
|
|
23748
|
+
sourceDeviceId: number().int(),
|
|
23749
|
+
targetDeviceId: number().int()
|
|
23750
|
+
}), object({
|
|
23751
|
+
copied: number().int().nonnegative(),
|
|
23752
|
+
skipped: number().int().nonnegative()
|
|
23753
|
+
}), {
|
|
23754
|
+
kind: "mutation",
|
|
23755
|
+
auth: "admin"
|
|
23492
23756
|
}), method(object({
|
|
23493
23757
|
deviceId: number().int(),
|
|
23494
23758
|
slotId: string().min(1),
|
|
@@ -24586,13 +24850,19 @@ method(object({
|
|
|
24586
24850
|
}), {
|
|
24587
24851
|
kind: "mutation",
|
|
24588
24852
|
auth: "admin"
|
|
24589
|
-
}), method(
|
|
24853
|
+
}), method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
|
|
24590
24854
|
kind: "mutation",
|
|
24591
24855
|
auth: "admin"
|
|
24592
|
-
}), method(object({
|
|
24593
|
-
kind: "
|
|
24856
|
+
}), method(StorageMigrationLeaseInputSchema, object({ resumed: literal(true) }), {
|
|
24857
|
+
kind: "mutation",
|
|
24858
|
+
auth: "admin"
|
|
24859
|
+
}), method(StorageMigrationLeaseInputSchema, object({ refreshed: literal(true) }), {
|
|
24860
|
+
kind: "mutation",
|
|
24594
24861
|
auth: "admin"
|
|
24595
|
-
}), method(object({ jobId: string() }),
|
|
24862
|
+
}), method(StorageMigrationFootageMoveInputSchema, object({ jobId: string() }), {
|
|
24863
|
+
kind: "mutation",
|
|
24864
|
+
auth: "admin"
|
|
24865
|
+
}), method(object({ jobId: string() }), RelocateJobSchema.nullable(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
|
|
24596
24866
|
kind: "mutation",
|
|
24597
24867
|
auth: "admin"
|
|
24598
24868
|
});
|
|
@@ -30182,6 +30452,12 @@ Object.freeze({
|
|
|
30182
30452
|
addonId: null,
|
|
30183
30453
|
access: "delete"
|
|
30184
30454
|
},
|
|
30455
|
+
"osdManager.copyDeviceConfiguration": {
|
|
30456
|
+
capName: "osd-manager",
|
|
30457
|
+
capScope: "system",
|
|
30458
|
+
addonId: null,
|
|
30459
|
+
access: "create"
|
|
30460
|
+
},
|
|
30185
30461
|
"osdManager.getConditionSupport": {
|
|
30186
30462
|
capName: "osd-manager",
|
|
30187
30463
|
capScope: "system",
|
|
@@ -30278,7 +30554,7 @@ Object.freeze({
|
|
|
30278
30554
|
addonId: null,
|
|
30279
30555
|
access: "create"
|
|
30280
30556
|
},
|
|
30281
|
-
"pipelineAnalytics.
|
|
30557
|
+
"pipelineAnalytics.cancelStorageMigrationMove": {
|
|
30282
30558
|
capName: "pipeline-analytics",
|
|
30283
30559
|
capScope: "device",
|
|
30284
30560
|
addonId: null,
|
|
@@ -30350,12 +30626,6 @@ Object.freeze({
|
|
|
30350
30626
|
addonId: null,
|
|
30351
30627
|
access: "view"
|
|
30352
30628
|
},
|
|
30353
|
-
"pipelineAnalytics.getMediaRelocateStatus": {
|
|
30354
|
-
capName: "pipeline-analytics",
|
|
30355
|
-
capScope: "device",
|
|
30356
|
-
addonId: null,
|
|
30357
|
-
access: "view"
|
|
30358
|
-
},
|
|
30359
30629
|
"pipelineAnalytics.getMotionEvents": {
|
|
30360
30630
|
capName: "pipeline-analytics",
|
|
30361
30631
|
capScope: "device",
|
|
@@ -30392,6 +30662,12 @@ Object.freeze({
|
|
|
30392
30662
|
addonId: null,
|
|
30393
30663
|
access: "view"
|
|
30394
30664
|
},
|
|
30665
|
+
"pipelineAnalytics.getStorageMigrationMoveStatus": {
|
|
30666
|
+
capName: "pipeline-analytics",
|
|
30667
|
+
capScope: "device",
|
|
30668
|
+
addonId: null,
|
|
30669
|
+
access: "view"
|
|
30670
|
+
},
|
|
30395
30671
|
"pipelineAnalytics.getTrack": {
|
|
30396
30672
|
capName: "pipeline-analytics",
|
|
30397
30673
|
capScope: "device",
|
|
@@ -30470,6 +30746,12 @@ Object.freeze({
|
|
|
30470
30746
|
addonId: null,
|
|
30471
30747
|
access: "view"
|
|
30472
30748
|
},
|
|
30749
|
+
"pipelineAnalytics.pauseForStorageMigration": {
|
|
30750
|
+
capName: "pipeline-analytics",
|
|
30751
|
+
capScope: "device",
|
|
30752
|
+
addonId: null,
|
|
30753
|
+
access: "create"
|
|
30754
|
+
},
|
|
30473
30755
|
"pipelineAnalytics.proposeRetrainAnnotations": {
|
|
30474
30756
|
capName: "pipeline-analytics",
|
|
30475
30757
|
capScope: "device",
|
|
@@ -30500,7 +30782,7 @@ Object.freeze({
|
|
|
30500
30782
|
addonId: null,
|
|
30501
30783
|
access: "create"
|
|
30502
30784
|
},
|
|
30503
|
-
"pipelineAnalytics.
|
|
30785
|
+
"pipelineAnalytics.refreshStorageLocationsForMigration": {
|
|
30504
30786
|
capName: "pipeline-analytics",
|
|
30505
30787
|
capScope: "device",
|
|
30506
30788
|
addonId: null,
|
|
@@ -30512,6 +30794,12 @@ Object.freeze({
|
|
|
30512
30794
|
addonId: null,
|
|
30513
30795
|
access: "create"
|
|
30514
30796
|
},
|
|
30797
|
+
"pipelineAnalytics.resumeForStorageMigration": {
|
|
30798
|
+
capName: "pipeline-analytics",
|
|
30799
|
+
capScope: "device",
|
|
30800
|
+
addonId: null,
|
|
30801
|
+
access: "create"
|
|
30802
|
+
},
|
|
30515
30803
|
"pipelineAnalytics.saveRetrainAnnotations": {
|
|
30516
30804
|
capName: "pipeline-analytics",
|
|
30517
30805
|
capScope: "device",
|
|
@@ -30536,6 +30824,12 @@ Object.freeze({
|
|
|
30536
30824
|
addonId: null,
|
|
30537
30825
|
access: "create"
|
|
30538
30826
|
},
|
|
30827
|
+
"pipelineAnalytics.startStorageMigrationMove": {
|
|
30828
|
+
capName: "pipeline-analytics",
|
|
30829
|
+
capScope: "device",
|
|
30830
|
+
addonId: null,
|
|
30831
|
+
access: "create"
|
|
30832
|
+
},
|
|
30539
30833
|
"pipelineAnalytics.wipeAllAnalytics": {
|
|
30540
30834
|
capName: "pipeline-analytics",
|
|
30541
30835
|
capScope: "device",
|
|
@@ -30902,6 +31196,12 @@ Object.freeze({
|
|
|
30902
31196
|
addonId: null,
|
|
30903
31197
|
access: "view"
|
|
30904
31198
|
},
|
|
31199
|
+
"pipelineOrchestrator.pauseForStorageMigration": {
|
|
31200
|
+
capName: "pipeline-orchestrator",
|
|
31201
|
+
capScope: "system",
|
|
31202
|
+
addonId: null,
|
|
31203
|
+
access: "create"
|
|
31204
|
+
},
|
|
30905
31205
|
"pipelineOrchestrator.rebalance": {
|
|
30906
31206
|
capName: "pipeline-orchestrator",
|
|
30907
31207
|
capScope: "system",
|
|
@@ -30926,6 +31226,12 @@ Object.freeze({
|
|
|
30926
31226
|
addonId: null,
|
|
30927
31227
|
access: "view"
|
|
30928
31228
|
},
|
|
31229
|
+
"pipelineOrchestrator.resumeForStorageMigration": {
|
|
31230
|
+
capName: "pipeline-orchestrator",
|
|
31231
|
+
capScope: "system",
|
|
31232
|
+
addonId: null,
|
|
31233
|
+
access: "create"
|
|
31234
|
+
},
|
|
30929
31235
|
"pipelineOrchestrator.saveTemplate": {
|
|
30930
31236
|
capName: "pipeline-orchestrator",
|
|
30931
31237
|
capScope: "system",
|
|
@@ -31322,7 +31628,7 @@ Object.freeze({
|
|
|
31322
31628
|
addonId: null,
|
|
31323
31629
|
access: "create"
|
|
31324
31630
|
},
|
|
31325
|
-
"recording.
|
|
31631
|
+
"recording.cancelStorageMigrationMove": {
|
|
31326
31632
|
capName: "recording",
|
|
31327
31633
|
capScope: "system",
|
|
31328
31634
|
addonId: null,
|
|
@@ -31358,7 +31664,7 @@ Object.freeze({
|
|
|
31358
31664
|
addonId: null,
|
|
31359
31665
|
access: "view"
|
|
31360
31666
|
},
|
|
31361
|
-
"recording.
|
|
31667
|
+
"recording.getStorageMigrationMoveStatus": {
|
|
31362
31668
|
capName: "recording",
|
|
31363
31669
|
capScope: "system",
|
|
31364
31670
|
addonId: null,
|
|
@@ -31382,6 +31688,12 @@ Object.freeze({
|
|
|
31382
31688
|
addonId: null,
|
|
31383
31689
|
access: "view"
|
|
31384
31690
|
},
|
|
31691
|
+
"recording.pauseForStorageMigration": {
|
|
31692
|
+
capName: "recording",
|
|
31693
|
+
capScope: "system",
|
|
31694
|
+
addonId: null,
|
|
31695
|
+
access: "create"
|
|
31696
|
+
},
|
|
31385
31697
|
"recording.pruneFootage": {
|
|
31386
31698
|
capName: "recording",
|
|
31387
31699
|
capScope: "system",
|
|
@@ -31400,7 +31712,7 @@ Object.freeze({
|
|
|
31400
31712
|
addonId: null,
|
|
31401
31713
|
access: "view"
|
|
31402
31714
|
},
|
|
31403
|
-
"recording.
|
|
31715
|
+
"recording.refreshStorageLocationsForMigration": {
|
|
31404
31716
|
capName: "recording",
|
|
31405
31717
|
capScope: "system",
|
|
31406
31718
|
addonId: null,
|
|
@@ -31424,12 +31736,24 @@ Object.freeze({
|
|
|
31424
31736
|
addonId: null,
|
|
31425
31737
|
access: "create"
|
|
31426
31738
|
},
|
|
31739
|
+
"recording.resumeForStorageMigration": {
|
|
31740
|
+
capName: "recording",
|
|
31741
|
+
capScope: "system",
|
|
31742
|
+
addonId: null,
|
|
31743
|
+
access: "create"
|
|
31744
|
+
},
|
|
31427
31745
|
"recording.setDeviceConfig": {
|
|
31428
31746
|
capName: "recording",
|
|
31429
31747
|
capScope: "system",
|
|
31430
31748
|
addonId: null,
|
|
31431
31749
|
access: "create"
|
|
31432
31750
|
},
|
|
31751
|
+
"recording.startStorageMigrationMove": {
|
|
31752
|
+
capName: "recording",
|
|
31753
|
+
capScope: "system",
|
|
31754
|
+
addonId: null,
|
|
31755
|
+
access: "create"
|
|
31756
|
+
},
|
|
31433
31757
|
"recordingExport.cancelExport": {
|
|
31434
31758
|
capName: "recordingExport",
|
|
31435
31759
|
capScope: "system",
|
|
@@ -31820,6 +32144,30 @@ Object.freeze({
|
|
|
31820
32144
|
addonId: null,
|
|
31821
32145
|
access: "view"
|
|
31822
32146
|
},
|
|
32147
|
+
"storageMigration.cancel": {
|
|
32148
|
+
capName: "storage-migration",
|
|
32149
|
+
capScope: "system",
|
|
32150
|
+
addonId: null,
|
|
32151
|
+
access: "create"
|
|
32152
|
+
},
|
|
32153
|
+
"storageMigration.plan": {
|
|
32154
|
+
capName: "storage-migration",
|
|
32155
|
+
capScope: "system",
|
|
32156
|
+
addonId: null,
|
|
32157
|
+
access: "view"
|
|
32158
|
+
},
|
|
32159
|
+
"storageMigration.start": {
|
|
32160
|
+
capName: "storage-migration",
|
|
32161
|
+
capScope: "system",
|
|
32162
|
+
addonId: null,
|
|
32163
|
+
access: "create"
|
|
32164
|
+
},
|
|
32165
|
+
"storageMigration.status": {
|
|
32166
|
+
capName: "storage-migration",
|
|
32167
|
+
capScope: "system",
|
|
32168
|
+
addonId: null,
|
|
32169
|
+
access: "view"
|
|
32170
|
+
},
|
|
31823
32171
|
"storageProvider.abortUpload": {
|
|
31824
32172
|
capName: "storage-provider",
|
|
31825
32173
|
capScope: "system",
|
|
@@ -32198,12 +32546,42 @@ Object.freeze({
|
|
|
32198
32546
|
addonId: null,
|
|
32199
32547
|
access: "create"
|
|
32200
32548
|
},
|
|
32549
|
+
"terminalSession.adoptLegacyMonitor": {
|
|
32550
|
+
capName: "terminal-session",
|
|
32551
|
+
capScope: "system",
|
|
32552
|
+
addonId: null,
|
|
32553
|
+
access: "create"
|
|
32554
|
+
},
|
|
32201
32555
|
"terminalSession.close": {
|
|
32202
32556
|
capName: "terminal-session",
|
|
32203
32557
|
capScope: "system",
|
|
32204
32558
|
addonId: null,
|
|
32205
32559
|
access: "create"
|
|
32206
32560
|
},
|
|
32561
|
+
"terminalSession.createInstance": {
|
|
32562
|
+
capName: "terminal-session",
|
|
32563
|
+
capScope: "system",
|
|
32564
|
+
addonId: null,
|
|
32565
|
+
access: "create"
|
|
32566
|
+
},
|
|
32567
|
+
"terminalSession.deleteInstance": {
|
|
32568
|
+
capName: "terminal-session",
|
|
32569
|
+
capScope: "system",
|
|
32570
|
+
addonId: null,
|
|
32571
|
+
access: "delete"
|
|
32572
|
+
},
|
|
32573
|
+
"terminalSession.listInstances": {
|
|
32574
|
+
capName: "terminal-session",
|
|
32575
|
+
capScope: "system",
|
|
32576
|
+
addonId: null,
|
|
32577
|
+
access: "view"
|
|
32578
|
+
},
|
|
32579
|
+
"terminalSession.listLegacyCameras": {
|
|
32580
|
+
capName: "terminal-session",
|
|
32581
|
+
capScope: "system",
|
|
32582
|
+
addonId: null,
|
|
32583
|
+
access: "view"
|
|
32584
|
+
},
|
|
32207
32585
|
"terminalSession.listProfiles": {
|
|
32208
32586
|
capName: "terminal-session",
|
|
32209
32587
|
capScope: "system",
|
|
@@ -32234,6 +32612,12 @@ Object.freeze({
|
|
|
32234
32612
|
addonId: null,
|
|
32235
32613
|
access: "create"
|
|
32236
32614
|
},
|
|
32615
|
+
"terminalSession.setInstanceEnabled": {
|
|
32616
|
+
capName: "terminal-session",
|
|
32617
|
+
capScope: "system",
|
|
32618
|
+
addonId: null,
|
|
32619
|
+
access: "create"
|
|
32620
|
+
},
|
|
32237
32621
|
"terminalSession.writeInput": {
|
|
32238
32622
|
capName: "terminal-session",
|
|
32239
32623
|
capScope: "system",
|
|
@@ -33034,6 +33418,41 @@ function createNodePtySpawner() {
|
|
|
33034
33418
|
async function warmNodePty() {
|
|
33035
33419
|
await loadNodePty();
|
|
33036
33420
|
}
|
|
33421
|
+
//#endregion
|
|
33422
|
+
//#region src/terminal-camera-declarations.ts
|
|
33423
|
+
/**
|
|
33424
|
+
* Feed DeclaredDevices every live declaration plus one deterministic orphan
|
|
33425
|
+
* batch. The generic sweep intentionally refuses an over-limit set; selecting
|
|
33426
|
+
* a batch here drains large historical Terminal orphan sets across convergence
|
|
33427
|
+
* passes without weakening that global safety guard.
|
|
33428
|
+
*/
|
|
33429
|
+
function terminalCameraReconciliationIndex(rows, integrationId, declarations, managedStableIds = /* @__PURE__ */ new Set()) {
|
|
33430
|
+
if (!integrationId) return [];
|
|
33431
|
+
const declared = new Set(declarations.map((camera) => camera.stableId));
|
|
33432
|
+
const owned = rows.filter((row) => row.integrationId === integrationId && (declared.has(row.stableId) || managedStableIds.has(row.stableId) || row.stableId.startsWith("terminal-camera-instance-")));
|
|
33433
|
+
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)];
|
|
33434
|
+
}
|
|
33435
|
+
/** Explicit persisted instances, never the node × profile template matrix. */
|
|
33436
|
+
function buildTerminalInstanceCameraDeclarations(instances) {
|
|
33437
|
+
return instances.filter((instance) => instance.enabled).map((instance) => ({
|
|
33438
|
+
stableId: instance.cameraStableId,
|
|
33439
|
+
name: instance.name,
|
|
33440
|
+
config: {
|
|
33441
|
+
instanceId: instance.id,
|
|
33442
|
+
nodeId: instance.nodeId,
|
|
33443
|
+
profileId: instance.profileId,
|
|
33444
|
+
profileLabel: instance.profileLabel
|
|
33445
|
+
}
|
|
33446
|
+
}));
|
|
33447
|
+
}
|
|
33448
|
+
/**
|
|
33449
|
+
* `DeviceConfig` materializes schema defaults in memory, so comparing
|
|
33450
|
+
* `config.get()` cannot detect a legacy `{ nodeId }` row. Reconciliation must
|
|
33451
|
+
* inspect the raw persisted blob to make the profile migration durable.
|
|
33452
|
+
*/
|
|
33453
|
+
function needsTerminalCameraConfigMigration(persistedConfig, declaration) {
|
|
33454
|
+
return persistedConfig.instanceId !== declaration.config.instanceId || persistedConfig.nodeId !== declaration.config.nodeId || persistedConfig.profileId !== declaration.config.profileId || persistedConfig.profileLabel !== declaration.config.profileLabel;
|
|
33455
|
+
}
|
|
33037
33456
|
function escapeXml(value) {
|
|
33038
33457
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
33039
33458
|
}
|
|
@@ -33048,40 +33467,66 @@ async function renderTerminalJpeg(lines) {
|
|
|
33048
33467
|
}
|
|
33049
33468
|
//#endregion
|
|
33050
33469
|
//#region src/terminal-camera-device.ts
|
|
33051
|
-
var terminalCameraSchema = object({
|
|
33470
|
+
var terminalCameraSchema = object({
|
|
33471
|
+
instanceId: string().min(1).optional(),
|
|
33472
|
+
nodeId: string().min(1),
|
|
33473
|
+
profileId: string().min(1).default("monitor"),
|
|
33474
|
+
profileLabel: string().min(1).default("BTM")
|
|
33475
|
+
});
|
|
33052
33476
|
var relay = null;
|
|
33053
33477
|
function installTerminalCameraRelay(next) {
|
|
33054
33478
|
relay = next;
|
|
33055
33479
|
}
|
|
33056
33480
|
var TerminalCameraDevice = class extends BaseDevice {
|
|
33057
|
-
features = [];
|
|
33481
|
+
features = [DeviceFeature.NativeSnapshot];
|
|
33058
33482
|
constructor(ctx) {
|
|
33059
33483
|
super(ctx, terminalCameraSchema, { type: ctx.deviceMeta.type });
|
|
33060
33484
|
this.ctx.registerNativeCap(streamCatalogCapability, { getCatalog: async ({ deviceId }) => {
|
|
33061
33485
|
if (deviceId !== this.id) return [];
|
|
33062
33486
|
return this.catalog();
|
|
33063
33487
|
} });
|
|
33488
|
+
this.ctx.registerNativeCap(snapshotCapability, {
|
|
33489
|
+
getSnapshot: async ({ deviceId }) => {
|
|
33490
|
+
if (deviceId !== this.id) throw new Error(`TerminalCameraDevice: deviceId mismatch, expected ${String(this.id)}, got ${String(deviceId)}`);
|
|
33491
|
+
const activeRelay = relay;
|
|
33492
|
+
if (!activeRelay) throw new Error("terminal camera relay is unavailable");
|
|
33493
|
+
return {
|
|
33494
|
+
base64: (await activeRelay.snapshot(this.relayInstanceId(), this.config.get("nodeId"), this.config.get("profileId"))).toString("base64"),
|
|
33495
|
+
contentType: "image/jpeg"
|
|
33496
|
+
};
|
|
33497
|
+
},
|
|
33498
|
+
invalidateCache: async () => {}
|
|
33499
|
+
});
|
|
33064
33500
|
this.markOnline(true);
|
|
33065
33501
|
}
|
|
33066
33502
|
async catalog() {
|
|
33067
33503
|
const activeRelay = relay;
|
|
33068
33504
|
if (!activeRelay) throw new Error("terminal camera relay is unavailable");
|
|
33069
33505
|
const nodeId = this.config.get("nodeId");
|
|
33070
|
-
|
|
33071
|
-
|
|
33506
|
+
const profileId = this.config.get("profileId");
|
|
33507
|
+
const instanceId = this.relayInstanceId();
|
|
33508
|
+
return [{
|
|
33509
|
+
camStreamId: profileId,
|
|
33072
33510
|
kind: "pull-http",
|
|
33073
|
-
url: activeRelay.streamUrl(nodeId,
|
|
33511
|
+
url: activeRelay.streamUrl(instanceId, nodeId, profileId),
|
|
33074
33512
|
codec: "h264",
|
|
33075
33513
|
resolution: {
|
|
33076
33514
|
width: 960,
|
|
33077
33515
|
height: 640
|
|
33078
33516
|
},
|
|
33079
33517
|
fps: 2,
|
|
33080
|
-
label:
|
|
33081
|
-
}
|
|
33518
|
+
label: this.config.get("profileLabel")
|
|
33519
|
+
}];
|
|
33082
33520
|
}
|
|
33083
33521
|
setNodeOnline(online) {
|
|
33084
33522
|
this.markOnline(online);
|
|
33523
|
+
if (!online) relay?.closeInstance(this.relayInstanceId());
|
|
33524
|
+
}
|
|
33525
|
+
async removeDevice() {
|
|
33526
|
+
await relay?.closeInstance(this.relayInstanceId());
|
|
33527
|
+
}
|
|
33528
|
+
relayInstanceId() {
|
|
33529
|
+
return this.config.get("instanceId") ?? `legacy:${this.stableId}`;
|
|
33085
33530
|
}
|
|
33086
33531
|
};
|
|
33087
33532
|
//#endregion
|
|
@@ -37930,18 +38375,24 @@ function createXtermScreen(cols, rows) {
|
|
|
37930
38375
|
//#region src/terminal-camera-relay.ts
|
|
37931
38376
|
var MJPEG_BOUNDARY = "camstack-terminal-frame";
|
|
37932
38377
|
var SESSION_IDLE_MS = 3e4;
|
|
37933
|
-
|
|
37934
|
-
|
|
38378
|
+
var SNAPSHOT_STARTUP_WAIT_MS = 1500;
|
|
38379
|
+
var CLOSE_RETRY_BASE_MS = 50;
|
|
38380
|
+
var CLOSE_RETRY_MAX_MS = 1e3;
|
|
38381
|
+
var CLOSE_ATTEMPTS_PER_PASS = 3;
|
|
38382
|
+
function relayKey(instanceId) {
|
|
38383
|
+
return instanceId;
|
|
37935
38384
|
}
|
|
37936
38385
|
function parseStreamPath(url) {
|
|
37937
38386
|
const parts = ((url ?? "").split("?")[0] ?? "").split("/").filter(Boolean);
|
|
37938
|
-
if (parts.length !==
|
|
38387
|
+
if (parts.length !== 4 || parts[0] !== "stream") return null;
|
|
37939
38388
|
try {
|
|
37940
|
-
const
|
|
37941
|
-
const
|
|
38389
|
+
const instanceId = decodeURIComponent(parts[1] ?? "");
|
|
38390
|
+
const nodeId = decodeURIComponent(parts[2] ?? "");
|
|
38391
|
+
const profilePart = parts[3] ?? "";
|
|
37942
38392
|
if (!profilePart.endsWith(".mjpeg")) return null;
|
|
37943
38393
|
const profileId = decodeURIComponent(profilePart.slice(0, -6));
|
|
37944
|
-
return nodeId && profileId ? {
|
|
38394
|
+
return instanceId && nodeId && profileId ? {
|
|
38395
|
+
instanceId,
|
|
37945
38396
|
nodeId,
|
|
37946
38397
|
profileId
|
|
37947
38398
|
} : null;
|
|
@@ -37968,7 +38419,7 @@ var TerminalCameraRelay = class {
|
|
|
37968
38419
|
res.writeHead(404).end();
|
|
37969
38420
|
return;
|
|
37970
38421
|
}
|
|
37971
|
-
this.serve(target.nodeId, target.profileId, res);
|
|
38422
|
+
this.serve(target.instanceId, target.nodeId, target.profileId, res);
|
|
37972
38423
|
});
|
|
37973
38424
|
await new Promise((resolve, reject) => {
|
|
37974
38425
|
server.once("error", reject);
|
|
@@ -37982,55 +38433,105 @@ var TerminalCameraRelay = class {
|
|
|
37982
38433
|
this.server = server;
|
|
37983
38434
|
this.baseUrl = `http://127.0.0.1:${String(address.port)}`;
|
|
37984
38435
|
}
|
|
37985
|
-
streamUrl(nodeId, profileId) {
|
|
38436
|
+
streamUrl(instanceId, nodeId, profileId) {
|
|
37986
38437
|
if (!this.baseUrl) throw new Error("terminal camera relay is not started");
|
|
37987
|
-
return `${this.baseUrl}/stream/${encodeURIComponent(nodeId)}/${encodeURIComponent(profileId)}.mjpeg`;
|
|
38438
|
+
return `${this.baseUrl}/stream/${encodeURIComponent(instanceId)}/${encodeURIComponent(nodeId)}/${encodeURIComponent(profileId)}.mjpeg`;
|
|
37988
38439
|
}
|
|
37989
38440
|
async listProfiles(nodeId) {
|
|
37990
38441
|
return this.api.listProfiles(nodeId);
|
|
37991
38442
|
}
|
|
37992
|
-
state(nodeId, profileId) {
|
|
37993
|
-
const key = relayKey(
|
|
38443
|
+
state(instanceId, nodeId, profileId) {
|
|
38444
|
+
const key = relayKey(instanceId);
|
|
37994
38445
|
const existing = this.states.get(key);
|
|
37995
38446
|
if (existing) return existing;
|
|
37996
38447
|
const created = {
|
|
38448
|
+
instanceId,
|
|
37997
38449
|
nodeId,
|
|
37998
38450
|
profileId,
|
|
37999
38451
|
screen: createXtermScreen(120, 40),
|
|
38000
38452
|
sessionId: null,
|
|
38001
38453
|
cursor: 0,
|
|
38002
38454
|
clients: 0,
|
|
38455
|
+
leases: 0,
|
|
38003
38456
|
jpeg: null,
|
|
38004
38457
|
renderedCursor: -1,
|
|
38005
38458
|
framePromise: null,
|
|
38006
|
-
|
|
38459
|
+
openPromise: null,
|
|
38460
|
+
idleTimer: null,
|
|
38461
|
+
closing: false,
|
|
38462
|
+
closePromise: null,
|
|
38463
|
+
closeRetryTimer: null,
|
|
38464
|
+
closeAttempts: 0,
|
|
38465
|
+
closed: false,
|
|
38466
|
+
responses: /* @__PURE__ */ new Set()
|
|
38007
38467
|
};
|
|
38008
38468
|
this.states.set(key, created);
|
|
38009
38469
|
return created;
|
|
38010
38470
|
}
|
|
38011
38471
|
async ensureSession(state) {
|
|
38472
|
+
if (state.closed || state.closing) throw new Error("terminal camera relay is waiting for prior session cleanup");
|
|
38012
38473
|
if (state.sessionId) return state.sessionId;
|
|
38013
|
-
|
|
38474
|
+
if (state.openPromise) {
|
|
38475
|
+
const opened = await state.openPromise;
|
|
38476
|
+
if (state.closed || state.closing) throw new Error("terminal camera relay state closed while opening its session");
|
|
38477
|
+
return opened.sessionId;
|
|
38478
|
+
}
|
|
38479
|
+
const opening = this.api.openSession(state.nodeId, {
|
|
38014
38480
|
profileId: state.profileId,
|
|
38015
38481
|
cols: 120,
|
|
38016
38482
|
rows: 40
|
|
38017
38483
|
});
|
|
38018
|
-
state.
|
|
38019
|
-
|
|
38020
|
-
|
|
38484
|
+
state.openPromise = opening;
|
|
38485
|
+
try {
|
|
38486
|
+
const opened = await opening;
|
|
38487
|
+
state.openPromise = null;
|
|
38488
|
+
if (state.closed || state.closing) {
|
|
38489
|
+
state.sessionId = opened.sessionId;
|
|
38490
|
+
await this.closeState(state);
|
|
38491
|
+
throw new Error("terminal camera relay state closed while opening its session");
|
|
38492
|
+
}
|
|
38493
|
+
state.sessionId = opened.sessionId;
|
|
38494
|
+
state.cursor = 0;
|
|
38495
|
+
return opened.sessionId;
|
|
38496
|
+
} catch (error) {
|
|
38497
|
+
if (state.closing && !state.sessionId) this.finishClose(state);
|
|
38498
|
+
throw error;
|
|
38499
|
+
} finally {
|
|
38500
|
+
if (state.openPromise === opening) state.openPromise = null;
|
|
38501
|
+
}
|
|
38021
38502
|
}
|
|
38022
|
-
async nextFrame(state) {
|
|
38023
|
-
if (state.framePromise)
|
|
38503
|
+
async nextFrame(state, forceRender = false, waitForInitialOutput = false) {
|
|
38504
|
+
if (state.framePromise) {
|
|
38505
|
+
await state.framePromise;
|
|
38506
|
+
return forceRender ? this.nextFrame(state, true, waitForInitialOutput) : state.jpeg ?? this.nextFrame(state, false, waitForInitialOutput);
|
|
38507
|
+
}
|
|
38024
38508
|
const render = async () => {
|
|
38509
|
+
const openingSession = state.sessionId === null;
|
|
38025
38510
|
const sessionId = await this.ensureSession(state);
|
|
38026
38511
|
let batch;
|
|
38027
38512
|
try {
|
|
38028
38513
|
batch = await this.api.pullOutput(state.nodeId, {
|
|
38029
38514
|
sessionId,
|
|
38030
|
-
afterSeq: state.cursor
|
|
38515
|
+
afterSeq: state.cursor,
|
|
38516
|
+
...openingSession && waitForInitialOutput ? {
|
|
38517
|
+
waitMs: SNAPSHOT_STARTUP_WAIT_MS,
|
|
38518
|
+
waitForOutput: true
|
|
38519
|
+
} : {}
|
|
38031
38520
|
});
|
|
38032
38521
|
} catch (error) {
|
|
38033
|
-
state.sessionId
|
|
38522
|
+
if (state.sessionId === sessionId) {
|
|
38523
|
+
let closed = false;
|
|
38524
|
+
await this.api.closeSession(state.nodeId, sessionId).then(() => {
|
|
38525
|
+
closed = true;
|
|
38526
|
+
}).catch((closeError) => {
|
|
38527
|
+
this.logger.warn("terminal camera session cleanup after output failure failed", { meta: {
|
|
38528
|
+
nodeId: state.nodeId,
|
|
38529
|
+
sessionId,
|
|
38530
|
+
error: closeError instanceof Error ? closeError.message : String(closeError)
|
|
38531
|
+
} });
|
|
38532
|
+
});
|
|
38533
|
+
if (closed) state.sessionId = null;
|
|
38534
|
+
}
|
|
38034
38535
|
state.cursor = 0;
|
|
38035
38536
|
throw error;
|
|
38036
38537
|
}
|
|
@@ -38047,7 +38548,7 @@ var TerminalCameraRelay = class {
|
|
|
38047
38548
|
}
|
|
38048
38549
|
state.cursor = exited ? 0 : batch.cursor;
|
|
38049
38550
|
await state.screen.flush();
|
|
38050
|
-
if (state.jpeg === null || state.renderedCursor !== state.cursor) {
|
|
38551
|
+
if (forceRender || state.jpeg === null || state.renderedCursor !== state.cursor) {
|
|
38051
38552
|
state.jpeg = await renderTerminalJpeg(state.screen.lines());
|
|
38052
38553
|
state.renderedCursor = state.cursor;
|
|
38053
38554
|
}
|
|
@@ -38058,14 +38559,15 @@ var TerminalCameraRelay = class {
|
|
|
38058
38559
|
});
|
|
38059
38560
|
return state.framePromise;
|
|
38060
38561
|
}
|
|
38061
|
-
async serve(nodeId, profileId, res) {
|
|
38562
|
+
async serve(instanceId, nodeId, profileId, res) {
|
|
38062
38563
|
this.responses.add(res);
|
|
38063
|
-
const state = this.state(nodeId, profileId);
|
|
38564
|
+
const state = this.state(instanceId, nodeId, profileId);
|
|
38064
38565
|
if (state.idleTimer) clearTimeout(state.idleTimer);
|
|
38065
38566
|
state.idleTimer = null;
|
|
38066
38567
|
state.clients += 1;
|
|
38568
|
+
state.responses.add(res);
|
|
38067
38569
|
try {
|
|
38068
|
-
const first = await this.nextFrame(state);
|
|
38570
|
+
const first = await this.nextFrame(state, false, true);
|
|
38069
38571
|
res.writeHead(200, {
|
|
38070
38572
|
"content-type": `multipart/x-mixed-replace; boundary=${MJPEG_BOUNDARY}`,
|
|
38071
38573
|
"cache-control": "no-store",
|
|
@@ -38093,11 +38595,13 @@ var TerminalCameraRelay = class {
|
|
|
38093
38595
|
res.end("terminal camera unavailable");
|
|
38094
38596
|
} finally {
|
|
38095
38597
|
this.responses.delete(res);
|
|
38598
|
+
state.responses.delete(res);
|
|
38096
38599
|
state.clients = Math.max(0, state.clients - 1);
|
|
38097
|
-
if (state.clients === 0) this.scheduleIdleClose(state);
|
|
38600
|
+
if (state.clients === 0 && state.leases === 0) this.scheduleIdleClose(state);
|
|
38098
38601
|
}
|
|
38099
38602
|
}
|
|
38100
38603
|
scheduleIdleClose(state) {
|
|
38604
|
+
if (state.closed || state.closing || state.clients > 0 || state.leases > 0) return;
|
|
38101
38605
|
if (state.idleTimer) clearTimeout(state.idleTimer);
|
|
38102
38606
|
state.idleTimer = setTimeout(() => {
|
|
38103
38607
|
this.closeState(state);
|
|
@@ -38105,26 +38609,116 @@ var TerminalCameraRelay = class {
|
|
|
38105
38609
|
state.idleTimer.unref?.();
|
|
38106
38610
|
}
|
|
38107
38611
|
async closeState(state) {
|
|
38108
|
-
if (state.
|
|
38109
|
-
if (state.
|
|
38612
|
+
if (state.closed) return;
|
|
38613
|
+
if (state.clients > 0 || state.leases > 0) return;
|
|
38614
|
+
if (state.closePromise) {
|
|
38615
|
+
await state.closePromise;
|
|
38616
|
+
return;
|
|
38617
|
+
}
|
|
38618
|
+
if (state.openPromise && !state.sessionId) {
|
|
38619
|
+
state.closing = true;
|
|
38620
|
+
if (state.idleTimer) clearTimeout(state.idleTimer);
|
|
38621
|
+
state.idleTimer = null;
|
|
38622
|
+
return;
|
|
38623
|
+
}
|
|
38624
|
+
if (state.closing && !state.sessionId) return;
|
|
38625
|
+
state.closing = true;
|
|
38626
|
+
if (state.idleTimer) clearTimeout(state.idleTimer);
|
|
38627
|
+
state.idleTimer = null;
|
|
38628
|
+
state.closePromise = this.closeWithRetries(state).finally(() => {
|
|
38629
|
+
state.closePromise = null;
|
|
38630
|
+
});
|
|
38631
|
+
await state.closePromise;
|
|
38632
|
+
}
|
|
38633
|
+
async closeWithRetries(state) {
|
|
38634
|
+
const sessionId = state.sessionId;
|
|
38635
|
+
if (!sessionId) {
|
|
38636
|
+
this.finishClose(state);
|
|
38637
|
+
return;
|
|
38638
|
+
}
|
|
38639
|
+
for (let attempt = 0; attempt < CLOSE_ATTEMPTS_PER_PASS; attempt += 1) try {
|
|
38640
|
+
await this.api.closeSession(state.nodeId, sessionId);
|
|
38641
|
+
if (state.sessionId === sessionId) this.finishClose(state);
|
|
38642
|
+
return;
|
|
38643
|
+
} catch (error) {
|
|
38644
|
+
state.closeAttempts += 1;
|
|
38110
38645
|
this.logger.warn("terminal camera session close failed", { meta: {
|
|
38111
38646
|
nodeId: state.nodeId,
|
|
38112
|
-
sessionId
|
|
38647
|
+
sessionId,
|
|
38648
|
+
attempt: state.closeAttempts,
|
|
38113
38649
|
error: error instanceof Error ? error.message : String(error)
|
|
38114
38650
|
} });
|
|
38115
|
-
|
|
38651
|
+
if (attempt + 1 < CLOSE_ATTEMPTS_PER_PASS) await new Promise((resolve) => setTimeout(resolve, this.closeRetryDelay(state.closeAttempts)));
|
|
38652
|
+
}
|
|
38653
|
+
this.scheduleCloseRetry(state);
|
|
38654
|
+
}
|
|
38655
|
+
scheduleCloseRetry(state) {
|
|
38656
|
+
if (state.closeRetryTimer || !state.sessionId) return;
|
|
38657
|
+
state.closeRetryTimer = setTimeout(() => {
|
|
38658
|
+
state.closeRetryTimer = null;
|
|
38659
|
+
state.closing = false;
|
|
38660
|
+
this.closeState(state);
|
|
38661
|
+
}, this.closeRetryDelay(state.closeAttempts));
|
|
38662
|
+
state.closeRetryTimer.unref?.();
|
|
38663
|
+
}
|
|
38664
|
+
closeRetryDelay(attempt) {
|
|
38665
|
+
return Math.min(CLOSE_RETRY_BASE_MS * 2 ** Math.min(attempt - 1, 5), CLOSE_RETRY_MAX_MS);
|
|
38666
|
+
}
|
|
38667
|
+
finishClose(state) {
|
|
38668
|
+
if (state.closed) return;
|
|
38669
|
+
state.closed = true;
|
|
38670
|
+
if (state.closeRetryTimer) clearTimeout(state.closeRetryTimer);
|
|
38671
|
+
state.closeRetryTimer = null;
|
|
38672
|
+
state.sessionId = null;
|
|
38673
|
+
state.closeAttempts = 0;
|
|
38674
|
+
state.closing = false;
|
|
38675
|
+
if (this.states.get(relayKey(state.instanceId)) === state) this.states.delete(relayKey(state.instanceId));
|
|
38116
38676
|
state.screen.dispose();
|
|
38117
|
-
|
|
38677
|
+
}
|
|
38678
|
+
/**
|
|
38679
|
+
* Capture one fresh JPEG using the same xterm renderer as the MJPEG relay.
|
|
38680
|
+
* A snapshot-only caller owns a short lease and tears the state down as soon
|
|
38681
|
+
* as the image is rendered, so snapshots never leave a monitor PTY running.
|
|
38682
|
+
*/
|
|
38683
|
+
async snapshot(instanceId, nodeId, profileId) {
|
|
38684
|
+
const state = this.state(instanceId, nodeId, profileId);
|
|
38685
|
+
if (state.idleTimer) clearTimeout(state.idleTimer);
|
|
38686
|
+
state.idleTimer = null;
|
|
38687
|
+
state.leases += 1;
|
|
38688
|
+
try {
|
|
38689
|
+
return await this.nextFrame(state, true, true);
|
|
38690
|
+
} finally {
|
|
38691
|
+
state.leases = Math.max(0, state.leases - 1);
|
|
38692
|
+
if (state.clients === 0 && state.leases === 0) await this.closeState(state);
|
|
38693
|
+
}
|
|
38694
|
+
}
|
|
38695
|
+
/** Stop a withdrawn/offline camera's relay, including active HTTP readers. */
|
|
38696
|
+
async closeInstance(instanceId) {
|
|
38697
|
+
const state = this.states.get(relayKey(instanceId));
|
|
38698
|
+
if (!state) return;
|
|
38699
|
+
for (const response of state.responses) response.destroy();
|
|
38700
|
+
state.responses.clear();
|
|
38701
|
+
state.clients = 0;
|
|
38702
|
+
state.leases = 0;
|
|
38703
|
+
await this.closeState(state);
|
|
38704
|
+
if (state.openPromise) {
|
|
38705
|
+
await state.openPromise.catch(() => {});
|
|
38706
|
+
await this.closeState(state);
|
|
38707
|
+
}
|
|
38118
38708
|
}
|
|
38119
38709
|
async dispose() {
|
|
38120
38710
|
for (const response of this.responses) response.destroy();
|
|
38121
38711
|
this.responses.clear();
|
|
38122
|
-
for (const state of this.states.values()) {
|
|
38712
|
+
for (const state of [...this.states.values()]) {
|
|
38123
38713
|
if (state.idleTimer) clearTimeout(state.idleTimer);
|
|
38124
38714
|
state.clients = 0;
|
|
38715
|
+
state.leases = 0;
|
|
38125
38716
|
await this.closeState(state);
|
|
38717
|
+
if (state.openPromise) {
|
|
38718
|
+
await state.openPromise.catch(() => {});
|
|
38719
|
+
await this.closeState(state);
|
|
38720
|
+
}
|
|
38126
38721
|
}
|
|
38127
|
-
this.states.clear();
|
|
38128
38722
|
if (this.server) {
|
|
38129
38723
|
const server = this.server;
|
|
38130
38724
|
this.server = null;
|
|
@@ -38240,6 +38834,113 @@ function createTerminalDataPlaneHandler(manager) {
|
|
|
38240
38834
|
};
|
|
38241
38835
|
}
|
|
38242
38836
|
//#endregion
|
|
38837
|
+
//#region src/terminal-instances.ts
|
|
38838
|
+
var TerminalInstanceSchema = object({
|
|
38839
|
+
id: string().uuid(),
|
|
38840
|
+
cameraStableId: string().min(1).max(256),
|
|
38841
|
+
nodeId: string().min(1).max(256),
|
|
38842
|
+
profileId: string().min(1).max(64),
|
|
38843
|
+
profileLabel: string().min(1).max(120),
|
|
38844
|
+
name: string().min(1).max(160),
|
|
38845
|
+
enabled: boolean()
|
|
38846
|
+
});
|
|
38847
|
+
/**
|
|
38848
|
+
* Config is operator-writable, so malformed or duplicate rows are ignored
|
|
38849
|
+
* rather than allowed to make declaration reconciliation destructive.
|
|
38850
|
+
*/
|
|
38851
|
+
function readTerminalInstances(raw, onInvalid) {
|
|
38852
|
+
const ids = /* @__PURE__ */ new Set();
|
|
38853
|
+
const stableIds = /* @__PURE__ */ new Set();
|
|
38854
|
+
const instances = [];
|
|
38855
|
+
for (const value of raw) {
|
|
38856
|
+
const parsed = TerminalInstanceSchema.safeParse(value);
|
|
38857
|
+
if (!parsed.success) {
|
|
38858
|
+
onInvalid?.("Ignoring malformed Terminal instance configuration");
|
|
38859
|
+
continue;
|
|
38860
|
+
}
|
|
38861
|
+
const instance = parsed.data;
|
|
38862
|
+
if (ids.has(instance.id) || stableIds.has(instance.cameraStableId)) {
|
|
38863
|
+
onInvalid?.(`Ignoring duplicate Terminal instance ${instance.id}`);
|
|
38864
|
+
continue;
|
|
38865
|
+
}
|
|
38866
|
+
ids.add(instance.id);
|
|
38867
|
+
stableIds.add(instance.cameraStableId);
|
|
38868
|
+
instances.push(instance);
|
|
38869
|
+
}
|
|
38870
|
+
return instances;
|
|
38871
|
+
}
|
|
38872
|
+
function newTerminalCameraStableId(instanceId) {
|
|
38873
|
+
return `terminal-camera-instance-${instanceId}`;
|
|
38874
|
+
}
|
|
38875
|
+
/**
|
|
38876
|
+
* Legacy automatic cameras are migration candidates only. A tombstone is
|
|
38877
|
+
* durable deletion intent, so a lingering failed device removal must never
|
|
38878
|
+
* make that camera adoptable again.
|
|
38879
|
+
*/
|
|
38880
|
+
function listLegacyTerminalCameraCandidates(rows, instanceStableIds, tombstones) {
|
|
38881
|
+
const legacy = [];
|
|
38882
|
+
for (const row of rows) {
|
|
38883
|
+
if (tombstones.has(row.stableId) || instanceStableIds.has(row.stableId) || row.stableId.startsWith("terminal-camera-instance-") || !row.stableId.startsWith("terminal-camera-")) continue;
|
|
38884
|
+
const nodeId = typeof row.config.nodeId === "string" ? row.config.nodeId : null;
|
|
38885
|
+
if (!nodeId) continue;
|
|
38886
|
+
const profileId = typeof row.config.profileId === "string" ? row.config.profileId : "monitor";
|
|
38887
|
+
const profileLabel = typeof row.config.profileLabel === "string" ? row.config.profileLabel : profileId === "monitor" ? "BTM" : profileId;
|
|
38888
|
+
legacy.push({
|
|
38889
|
+
stableId: row.stableId,
|
|
38890
|
+
nodeId,
|
|
38891
|
+
profileId,
|
|
38892
|
+
profileLabel,
|
|
38893
|
+
name: row.name,
|
|
38894
|
+
adoptable: profileId === "monitor" && row.stableId === `terminal-camera-${nodeId}`
|
|
38895
|
+
});
|
|
38896
|
+
}
|
|
38897
|
+
return legacy;
|
|
38898
|
+
}
|
|
38899
|
+
/** Serializes config read-modify-write operations and their reconciliation. */
|
|
38900
|
+
var TerminalInstanceMutationQueue = class {
|
|
38901
|
+
tail = Promise.resolve();
|
|
38902
|
+
async run(mutation) {
|
|
38903
|
+
const previous = this.tail;
|
|
38904
|
+
let release;
|
|
38905
|
+
this.tail = new Promise((resolve) => {
|
|
38906
|
+
release = resolve;
|
|
38907
|
+
});
|
|
38908
|
+
await previous;
|
|
38909
|
+
try {
|
|
38910
|
+
return await mutation();
|
|
38911
|
+
} finally {
|
|
38912
|
+
release?.();
|
|
38913
|
+
}
|
|
38914
|
+
}
|
|
38915
|
+
};
|
|
38916
|
+
/**
|
|
38917
|
+
* Coalesces periodic/config reconciliation requests onto the same serialized
|
|
38918
|
+
* lane as instance mutations. A pass never applies a declaration snapshot
|
|
38919
|
+
* concurrently with a create/delete/enable write.
|
|
38920
|
+
*/
|
|
38921
|
+
var TerminalInstanceReconcileCoordinator = class {
|
|
38922
|
+
queue;
|
|
38923
|
+
dirty = false;
|
|
38924
|
+
running = null;
|
|
38925
|
+
constructor(queue) {
|
|
38926
|
+
this.queue = queue;
|
|
38927
|
+
}
|
|
38928
|
+
request(apply) {
|
|
38929
|
+
this.dirty = true;
|
|
38930
|
+
if (this.running) return this.running;
|
|
38931
|
+
const running = this.queue.run(async () => {
|
|
38932
|
+
while (this.dirty) {
|
|
38933
|
+
this.dirty = false;
|
|
38934
|
+
await apply();
|
|
38935
|
+
}
|
|
38936
|
+
});
|
|
38937
|
+
this.running = running.finally(() => {
|
|
38938
|
+
this.running = null;
|
|
38939
|
+
});
|
|
38940
|
+
return this.running;
|
|
38941
|
+
}
|
|
38942
|
+
};
|
|
38943
|
+
//#endregion
|
|
38243
38944
|
//#region src/profiles.ts
|
|
38244
38945
|
/**
|
|
38245
38946
|
* The allowlist of programs an operator may open. The capability accepts a
|
|
@@ -38250,7 +38951,12 @@ function createTerminalDataPlaneHandler(manager) {
|
|
|
38250
38951
|
var ProfileIdSchema = string().trim().min(1).max(64).regex(/^[a-z][a-z0-9-]*$/, "must be a lowercase slug");
|
|
38251
38952
|
var ConfiguredTerminalProfileSchema = object({
|
|
38252
38953
|
enabled: boolean().default(true),
|
|
38253
|
-
profileId: ProfileIdSchema.refine((id) =>
|
|
38954
|
+
profileId: ProfileIdSchema.refine((id) => ![
|
|
38955
|
+
"monitor",
|
|
38956
|
+
"top",
|
|
38957
|
+
"glances",
|
|
38958
|
+
"shell"
|
|
38959
|
+
].includes(id), { message: "monitor, top, glances and shell are reserved profile IDs" }),
|
|
38254
38960
|
label: string().trim().min(1).max(100),
|
|
38255
38961
|
description: string().trim().max(500).optional().default(""),
|
|
38256
38962
|
executable: string().trim().min(1).max(1024).refine((value) => !value.includes("\0"), { message: "must not contain NUL bytes" }),
|
|
@@ -38265,17 +38971,36 @@ var ConfiguredTerminalProfileSchema = object({
|
|
|
38265
38971
|
* surface.
|
|
38266
38972
|
*/
|
|
38267
38973
|
function buildProfiles(options) {
|
|
38268
|
-
const profiles = [
|
|
38974
|
+
const profiles = [];
|
|
38975
|
+
if (options.btmEnabled !== false) profiles.push({
|
|
38269
38976
|
profileId: "monitor",
|
|
38270
|
-
label: "
|
|
38977
|
+
label: "BTM",
|
|
38271
38978
|
description: "bottom (btm) — CPU, memory, network and process monitor",
|
|
38272
38979
|
file: options.btmPath.trim().length > 0 ? options.btmPath.trim() : "btm",
|
|
38273
|
-
args: []
|
|
38274
|
-
|
|
38275
|
-
|
|
38276
|
-
|
|
38277
|
-
|
|
38278
|
-
|
|
38980
|
+
args: options.btmArgs ?? []
|
|
38981
|
+
});
|
|
38982
|
+
if (options.topEnabled !== false) profiles.push({
|
|
38983
|
+
profileId: "top",
|
|
38984
|
+
label: "Top",
|
|
38985
|
+
description: "The operating system process and resource monitor",
|
|
38986
|
+
file: options.topPath?.trim() || "top",
|
|
38987
|
+
args: options.topArgs ?? []
|
|
38988
|
+
});
|
|
38989
|
+
if (options.glancesEnabled !== false) {
|
|
38990
|
+
const configuredPath = options.glancesPath?.trim();
|
|
38991
|
+
const pythonPath = options.glancesPythonPath?.trim();
|
|
38992
|
+
profiles.push({
|
|
38993
|
+
profileId: "glances",
|
|
38994
|
+
label: "Glances",
|
|
38995
|
+
description: "Cross-platform curses monitor installed in CamStack embedded Python",
|
|
38996
|
+
file: configuredPath || pythonPath || "glances",
|
|
38997
|
+
args: configuredPath || !pythonPath ? options.glancesArgs ?? [] : [
|
|
38998
|
+
"-m",
|
|
38999
|
+
"glances",
|
|
39000
|
+
...options.glancesArgs ?? []
|
|
39001
|
+
]
|
|
39002
|
+
});
|
|
39003
|
+
}
|
|
38279
39004
|
if (options.allowShell) profiles.push({
|
|
38280
39005
|
profileId: "shell",
|
|
38281
39006
|
label: "Shell (interactive)",
|
|
@@ -38344,10 +39069,20 @@ var TerminalSessionManager = class {
|
|
|
38344
39069
|
now;
|
|
38345
39070
|
resolveBinary;
|
|
38346
39071
|
maxSessions;
|
|
39072
|
+
instanceControl = null;
|
|
38347
39073
|
constructor(opts) {
|
|
38348
39074
|
this.opts = opts;
|
|
38349
39075
|
this.profiles = buildProfiles({
|
|
38350
39076
|
btmPath: opts.btmPath,
|
|
39077
|
+
btmEnabled: opts.btmEnabled,
|
|
39078
|
+
btmArgs: opts.btmArgs,
|
|
39079
|
+
topEnabled: opts.topEnabled,
|
|
39080
|
+
topPath: opts.topPath,
|
|
39081
|
+
topArgs: opts.topArgs,
|
|
39082
|
+
glancesEnabled: opts.glancesEnabled,
|
|
39083
|
+
glancesPath: opts.glancesPath,
|
|
39084
|
+
glancesArgs: opts.glancesArgs,
|
|
39085
|
+
glancesPythonPath: opts.glancesPythonPath,
|
|
38351
39086
|
allowShell: opts.allowShell,
|
|
38352
39087
|
shellPath: opts.shellPath,
|
|
38353
39088
|
customProfiles: opts.customProfiles,
|
|
@@ -38373,6 +39108,27 @@ var TerminalSessionManager = class {
|
|
|
38373
39108
|
...p.description !== void 0 ? { description: p.description } : {}
|
|
38374
39109
|
}));
|
|
38375
39110
|
}
|
|
39111
|
+
setInstanceControl(control) {
|
|
39112
|
+
this.instanceControl = control;
|
|
39113
|
+
}
|
|
39114
|
+
async listInstances() {
|
|
39115
|
+
return this.instanceControl?.listInstances() ?? [];
|
|
39116
|
+
}
|
|
39117
|
+
async createInstance(input) {
|
|
39118
|
+
return this.requireInstanceControl().createInstance(input);
|
|
39119
|
+
}
|
|
39120
|
+
async deleteInstance(input) {
|
|
39121
|
+
await this.requireInstanceControl().deleteInstance(input);
|
|
39122
|
+
}
|
|
39123
|
+
async setInstanceEnabled(input) {
|
|
39124
|
+
return this.requireInstanceControl().setInstanceEnabled(input);
|
|
39125
|
+
}
|
|
39126
|
+
async listLegacyCameras() {
|
|
39127
|
+
return this.instanceControl?.listLegacyCameras() ?? [];
|
|
39128
|
+
}
|
|
39129
|
+
async adoptLegacyMonitor(input) {
|
|
39130
|
+
return this.requireInstanceControl().adoptLegacyMonitor(input);
|
|
39131
|
+
}
|
|
38376
39132
|
async listSessions() {
|
|
38377
39133
|
return [...this.sessions.values()].filter((s) => !s.exited).map((s) => s.info);
|
|
38378
39134
|
}
|
|
@@ -38420,10 +39176,12 @@ var TerminalSessionManager = class {
|
|
|
38420
39176
|
outputWaiters: /* @__PURE__ */ new Set(),
|
|
38421
39177
|
outputChars: 0,
|
|
38422
39178
|
nextSeq: 1,
|
|
38423
|
-
exited: false
|
|
39179
|
+
exited: false,
|
|
39180
|
+
disposed: false
|
|
38424
39181
|
};
|
|
38425
39182
|
this.sessions.set(sessionId, session);
|
|
38426
39183
|
pty.onData((data) => {
|
|
39184
|
+
if (session.exited) return;
|
|
38427
39185
|
session.screen.write(data);
|
|
38428
39186
|
this.appendOutput(session, {
|
|
38429
39187
|
kind: "data",
|
|
@@ -38437,27 +39195,7 @@ var TerminalSessionManager = class {
|
|
|
38437
39195
|
} catch {}
|
|
38438
39196
|
});
|
|
38439
39197
|
pty.onExit((event) => {
|
|
38440
|
-
|
|
38441
|
-
session.lastExit = event;
|
|
38442
|
-
this.appendOutput(session, {
|
|
38443
|
-
kind: "exit",
|
|
38444
|
-
exitCode: event.exitCode,
|
|
38445
|
-
...event.signal !== void 0 ? { signal: event.signal } : {}
|
|
38446
|
-
});
|
|
38447
|
-
for (const sink of session.sinks) try {
|
|
38448
|
-
sink({
|
|
38449
|
-
kind: "exit",
|
|
38450
|
-
exitCode: event.exitCode,
|
|
38451
|
-
signal: event.signal
|
|
38452
|
-
});
|
|
38453
|
-
} catch {}
|
|
38454
|
-
session.sinks.clear();
|
|
38455
|
-
const retire = setTimeout(() => {
|
|
38456
|
-
session.screen.dispose();
|
|
38457
|
-
this.sessions.delete(sessionId);
|
|
38458
|
-
}, EXITED_RETENTION_MS);
|
|
38459
|
-
retire.unref?.();
|
|
38460
|
-
session.retireTimer = retire;
|
|
39198
|
+
this.finishSession(sessionId, session, event, true);
|
|
38461
39199
|
});
|
|
38462
39200
|
this.opts.logger.info("terminal: session opened", { meta: {
|
|
38463
39201
|
sessionId,
|
|
@@ -38488,6 +39226,7 @@ var TerminalSessionManager = class {
|
|
|
38488
39226
|
async close(input) {
|
|
38489
39227
|
const session = this.sessions.get(input.sessionId);
|
|
38490
39228
|
if (!session) return;
|
|
39229
|
+
this.finishSession(input.sessionId, session, { exitCode: 0 }, false);
|
|
38491
39230
|
try {
|
|
38492
39231
|
session.pty.kill();
|
|
38493
39232
|
} catch {}
|
|
@@ -38496,7 +39235,7 @@ var TerminalSessionManager = class {
|
|
|
38496
39235
|
async pullOutput(input) {
|
|
38497
39236
|
const session = this.sessions.get(input.sessionId);
|
|
38498
39237
|
if (!session) throw new Error(`No such terminal session: ${input.sessionId}`);
|
|
38499
|
-
if (input.afterSeq > 0 && input.afterSeq === session.nextSeq - 1 && !session.exited && (input.waitMs ?? 0) > 0) await new Promise((resolve) => {
|
|
39238
|
+
if ((input.afterSeq > 0 || input.waitForOutput === true) && input.afterSeq === session.nextSeq - 1 && !session.exited && (input.waitMs ?? 0) > 0) await new Promise((resolve) => {
|
|
38500
39239
|
const wake = () => {
|
|
38501
39240
|
clearTimeout(timer);
|
|
38502
39241
|
session.outputWaiters.delete(wake);
|
|
@@ -38507,6 +39246,11 @@ var TerminalSessionManager = class {
|
|
|
38507
39246
|
session.outputWaiters.add(wake);
|
|
38508
39247
|
});
|
|
38509
39248
|
const cursor = session.nextSeq - 1;
|
|
39249
|
+
if (session.disposed) return {
|
|
39250
|
+
cursor,
|
|
39251
|
+
reset: false,
|
|
39252
|
+
events: session.output.filter((event) => event.seq > input.afterSeq)
|
|
39253
|
+
};
|
|
38510
39254
|
const oldestSeq = session.output[0]?.seq ?? session.nextSeq;
|
|
38511
39255
|
if (input.afterSeq === 0 || input.afterSeq < oldestSeq - 1 || input.afterSeq > cursor) {
|
|
38512
39256
|
await session.screen.flush();
|
|
@@ -38587,24 +39331,71 @@ var TerminalSessionManager = class {
|
|
|
38587
39331
|
}
|
|
38588
39332
|
/** Kill every live session — called on addon shutdown. */
|
|
38589
39333
|
disposeAll() {
|
|
38590
|
-
for (const session of this.sessions
|
|
38591
|
-
|
|
39334
|
+
for (const [sessionId, session] of this.sessions) {
|
|
39335
|
+
this.finishSession(sessionId, session, { exitCode: 0 }, false);
|
|
38592
39336
|
try {
|
|
38593
39337
|
session.pty.kill();
|
|
38594
39338
|
} catch {}
|
|
38595
|
-
session.screen.dispose();
|
|
38596
39339
|
}
|
|
38597
|
-
|
|
39340
|
+
}
|
|
39341
|
+
finishSession(sessionId, session, exit, retainForLateExit) {
|
|
39342
|
+
if (session.exited) return;
|
|
39343
|
+
session.exited = true;
|
|
39344
|
+
session.lastExit = exit;
|
|
39345
|
+
this.appendOutput(session, {
|
|
39346
|
+
kind: "exit",
|
|
39347
|
+
exitCode: exit.exitCode,
|
|
39348
|
+
...exit.signal !== void 0 ? { signal: exit.signal } : {}
|
|
39349
|
+
});
|
|
39350
|
+
for (const sink of session.sinks) try {
|
|
39351
|
+
sink({
|
|
39352
|
+
kind: "exit",
|
|
39353
|
+
exitCode: exit.exitCode,
|
|
39354
|
+
...exit.signal !== void 0 ? { signal: exit.signal } : {}
|
|
39355
|
+
});
|
|
39356
|
+
} catch {}
|
|
39357
|
+
session.sinks.clear();
|
|
39358
|
+
if (!retainForLateExit) {
|
|
39359
|
+
this.disposeSession(session);
|
|
39360
|
+
this.sessions.delete(sessionId);
|
|
39361
|
+
return;
|
|
39362
|
+
}
|
|
39363
|
+
const retire = setTimeout(() => {
|
|
39364
|
+
this.disposeSession(session);
|
|
39365
|
+
this.sessions.delete(sessionId);
|
|
39366
|
+
}, EXITED_RETENTION_MS);
|
|
39367
|
+
retire.unref?.();
|
|
39368
|
+
session.retireTimer = retire;
|
|
39369
|
+
}
|
|
39370
|
+
disposeSession(session) {
|
|
39371
|
+
if (session.retireTimer !== void 0) clearTimeout(session.retireTimer);
|
|
39372
|
+
if (session.disposed) return;
|
|
39373
|
+
session.disposed = true;
|
|
39374
|
+
session.screen.dispose();
|
|
39375
|
+
}
|
|
39376
|
+
requireInstanceControl() {
|
|
39377
|
+
if (!this.instanceControl) throw new Error("Terminal instances are managed on the hub");
|
|
39378
|
+
return this.instanceControl;
|
|
38598
39379
|
}
|
|
38599
39380
|
};
|
|
38600
39381
|
//#endregion
|
|
38601
39382
|
//#region src/addon.ts
|
|
38602
39383
|
var DEFAULTS = {
|
|
38603
39384
|
btmPath: "",
|
|
39385
|
+
btmEnabled: true,
|
|
39386
|
+
btmArgs: [],
|
|
39387
|
+
topEnabled: true,
|
|
39388
|
+
topPath: "",
|
|
39389
|
+
topArgs: [],
|
|
39390
|
+
glancesEnabled: true,
|
|
39391
|
+
glancesPath: "",
|
|
39392
|
+
glancesArgs: [],
|
|
38604
39393
|
allowShell: false,
|
|
38605
39394
|
shellPath: "",
|
|
38606
39395
|
maxSessions: 4,
|
|
38607
|
-
customProfiles: []
|
|
39396
|
+
customProfiles: [],
|
|
39397
|
+
terminalInstances: [],
|
|
39398
|
+
terminalCameraTombstones: []
|
|
38608
39399
|
};
|
|
38609
39400
|
var DATA_PLANE_PREFIX = "io";
|
|
38610
39401
|
var CAMERA_RECONCILE_MS = 6e4;
|
|
@@ -38614,35 +39405,56 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
38614
39405
|
dataPlane = null;
|
|
38615
39406
|
cameraRelay = null;
|
|
38616
39407
|
cameraReconcileTimer = null;
|
|
39408
|
+
cameraProfilesByNode = /* @__PURE__ */ new Map();
|
|
39409
|
+
/** Prevent repeat writes when a legacy device stays live after migration. */
|
|
39410
|
+
migratedTerminalCameraConfigIds = /* @__PURE__ */ new Set();
|
|
39411
|
+
terminalCameraTombstones = /* @__PURE__ */ new Set();
|
|
39412
|
+
instanceMutationQueue = new TerminalInstanceMutationQueue();
|
|
39413
|
+
terminalReconcileCoordinator = new TerminalInstanceReconcileCoordinator(this.instanceMutationQueue);
|
|
39414
|
+
glancesPythonPath = "";
|
|
38617
39415
|
constructor() {
|
|
38618
39416
|
super({ ...DEFAULTS });
|
|
38619
39417
|
}
|
|
38620
39418
|
async onInitialize() {
|
|
38621
39419
|
await warmNodePty();
|
|
39420
|
+
this.glancesPythonPath = await this.ctx.deps.ensurePython() ?? "";
|
|
38622
39421
|
const manager = new TerminalSessionManager({
|
|
38623
39422
|
spawn: createNodePtySpawner(),
|
|
38624
39423
|
screenFactory: createXtermScreen,
|
|
38625
39424
|
resolveBinary: resolveExecutable,
|
|
38626
39425
|
logger: this.ctx.logger,
|
|
38627
39426
|
btmPath: this.config.btmPath,
|
|
39427
|
+
btmEnabled: this.config.btmEnabled,
|
|
39428
|
+
btmArgs: this.config.btmArgs,
|
|
39429
|
+
topEnabled: this.config.topEnabled,
|
|
39430
|
+
topPath: this.config.topPath,
|
|
39431
|
+
topArgs: this.config.topArgs,
|
|
39432
|
+
glancesEnabled: this.config.glancesEnabled,
|
|
39433
|
+
glancesPath: this.config.glancesPath,
|
|
39434
|
+
glancesArgs: this.config.glancesArgs,
|
|
39435
|
+
glancesPythonPath: this.glancesPythonPath,
|
|
38628
39436
|
allowShell: this.config.allowShell,
|
|
38629
39437
|
shellPath: this.config.shellPath,
|
|
38630
39438
|
maxSessions: this.config.maxSessions,
|
|
38631
39439
|
customProfiles: this.config.customProfiles
|
|
38632
39440
|
});
|
|
38633
39441
|
this.manager = manager;
|
|
39442
|
+
this.replaceTerminalCameraTombstones(this.config.terminalCameraTombstones);
|
|
38634
39443
|
if (declarationOwnerNodeId(this.ctx.kernel.localNodeId) === "hub") {
|
|
39444
|
+
const localNodeId = declarationOwnerNodeId(this.ctx.kernel.localNodeId);
|
|
38635
39445
|
const cameraRelay = new TerminalCameraRelay({
|
|
38636
|
-
listProfiles: (nodeId) => this.ctx.api.terminalSession.listProfiles.query(void 0, nodePin(nodeId)),
|
|
38637
|
-
openSession: (nodeId, input) => this.ctx.api.terminalSession.openSession.mutate(input, nodePin(nodeId)),
|
|
38638
|
-
pullOutput: (nodeId, input) => this.ctx.api.terminalSession.pullOutput.mutate(input, nodePin(nodeId)),
|
|
39446
|
+
listProfiles: (nodeId) => nodeId === localNodeId ? manager.listProfiles() : this.ctx.api.terminalSession.listProfiles.query(void 0, nodePin(nodeId)),
|
|
39447
|
+
openSession: (nodeId, input) => nodeId === localNodeId ? manager.openSession(input) : this.ctx.api.terminalSession.openSession.mutate(input, nodePin(nodeId)),
|
|
39448
|
+
pullOutput: (nodeId, input) => nodeId === localNodeId ? manager.pullOutput(input) : this.ctx.api.terminalSession.pullOutput.mutate(input, nodePin(nodeId)),
|
|
38639
39449
|
closeSession: async (nodeId, sessionId) => {
|
|
38640
|
-
await
|
|
39450
|
+
if (nodeId === localNodeId) await manager.close({ sessionId });
|
|
39451
|
+
else await this.ctx.api.terminalSession.close.mutate({ sessionId }, nodePin(nodeId));
|
|
38641
39452
|
}
|
|
38642
39453
|
}, this.ctx.logger.child("camera"));
|
|
38643
39454
|
await cameraRelay.start();
|
|
38644
39455
|
this.cameraRelay = cameraRelay;
|
|
38645
39456
|
installTerminalCameraRelay(cameraRelay);
|
|
39457
|
+
manager.setInstanceControl(this.terminalInstanceControl());
|
|
38646
39458
|
await this.reconcileTerminalCameras().catch((error) => {
|
|
38647
39459
|
this.ctx.logger.warn("initial terminal camera reconciliation failed — will retry", { meta: { error: error instanceof Error ? error.message : String(error) } });
|
|
38648
39460
|
});
|
|
@@ -38667,13 +39479,26 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
38667
39479
|
}];
|
|
38668
39480
|
}
|
|
38669
39481
|
async onConfigChanged() {
|
|
39482
|
+
this.replaceTerminalCameraTombstones(this.config.terminalCameraTombstones);
|
|
38670
39483
|
this.manager?.reconfigureProfiles({
|
|
38671
39484
|
btmPath: this.config.btmPath,
|
|
39485
|
+
btmEnabled: this.config.btmEnabled,
|
|
39486
|
+
btmArgs: this.config.btmArgs,
|
|
39487
|
+
topEnabled: this.config.topEnabled,
|
|
39488
|
+
topPath: this.config.topPath,
|
|
39489
|
+
topArgs: this.config.topArgs,
|
|
39490
|
+
glancesEnabled: this.config.glancesEnabled,
|
|
39491
|
+
glancesPath: this.config.glancesPath,
|
|
39492
|
+
glancesArgs: this.config.glancesArgs,
|
|
39493
|
+
glancesPythonPath: this.glancesPythonPath,
|
|
38672
39494
|
allowShell: this.config.allowShell,
|
|
38673
39495
|
shellPath: this.config.shellPath,
|
|
38674
39496
|
maxSessions: this.config.maxSessions,
|
|
38675
39497
|
customProfiles: this.config.customProfiles
|
|
38676
39498
|
});
|
|
39499
|
+
if (this.cameraRelay) this.reconcileTerminalCameras().catch((error) => {
|
|
39500
|
+
this.ctx.logger.warn("terminal camera reconciliation after config change failed", { meta: { error: error instanceof Error ? error.message : String(error) } });
|
|
39501
|
+
});
|
|
38677
39502
|
}
|
|
38678
39503
|
async onShutdown() {
|
|
38679
39504
|
if (this.cameraReconcileTimer) clearInterval(this.cameraReconcileTimer);
|
|
@@ -38689,17 +39514,46 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
38689
39514
|
this.manager = null;
|
|
38690
39515
|
}
|
|
38691
39516
|
async reconcileTerminalCameras() {
|
|
39517
|
+
return this.terminalReconcileCoordinator.request(() => this.applyTerminalCameraReconciliation());
|
|
39518
|
+
}
|
|
39519
|
+
async applyTerminalCameraReconciliation() {
|
|
38692
39520
|
if (!this.cameraRelay) return;
|
|
39521
|
+
let terminalIntegrationId;
|
|
38693
39522
|
const topology = await this.ctx.api.nodes.topology.query();
|
|
38694
39523
|
if (topology.length === 0) throw new Error("cluster topology returned no nodes; refusing to withdraw declared cameras");
|
|
38695
39524
|
const nodes = topology.filter((node) => typeof node.id === "string" && node.id.length > 0);
|
|
39525
|
+
const nodeIds = new Set(nodes.map((node) => node.id));
|
|
39526
|
+
for (const cachedNodeId of this.cameraProfilesByNode.keys()) if (!nodeIds.has(cachedNodeId)) this.cameraProfilesByNode.delete(cachedNodeId);
|
|
39527
|
+
const unavailableNodeIds = /* @__PURE__ */ new Set();
|
|
39528
|
+
await Promise.all(nodes.map(async (node) => {
|
|
39529
|
+
try {
|
|
39530
|
+
this.cameraProfilesByNode.set(node.id, await this.cameraRelay.listProfiles(node.id));
|
|
39531
|
+
} catch (error) {
|
|
39532
|
+
unavailableNodeIds.add(node.id);
|
|
39533
|
+
this.ctx.logger.warn("terminal profiles unavailable — keeping Terminal instance cameras offline", { meta: {
|
|
39534
|
+
nodeId: node.id,
|
|
39535
|
+
cachedProfiles: this.cameraProfilesByNode.has(node.id),
|
|
39536
|
+
error: error instanceof Error ? error.message : String(error)
|
|
39537
|
+
} });
|
|
39538
|
+
}
|
|
39539
|
+
}));
|
|
39540
|
+
const cameraDeclarations = buildTerminalInstanceCameraDeclarations(this.terminalInstances());
|
|
39541
|
+
const declarationsByStableId = new Map(cameraDeclarations.map((camera) => [camera.stableId, camera]));
|
|
38696
39542
|
const result = await new DeclaredDevices({
|
|
38697
39543
|
logger: this.ctx.logger.child("camera-declaration"),
|
|
38698
39544
|
addonId: this.ctx.id,
|
|
38699
39545
|
devices: this.ctx.kernel.devices,
|
|
38700
39546
|
localNodeId: this.ctx.kernel.localNodeId,
|
|
38701
|
-
getIntegration: async (addonId) =>
|
|
38702
|
-
|
|
39547
|
+
getIntegration: async (addonId) => {
|
|
39548
|
+
const integration = await this.ctx.api.integrations.getByAddonId.query({ addonId });
|
|
39549
|
+
terminalIntegrationId = integration?.id ?? null;
|
|
39550
|
+
return integration;
|
|
39551
|
+
},
|
|
39552
|
+
createIntegration: async (input) => {
|
|
39553
|
+
const integration = await this.ctx.api.integrations.create.mutate(input);
|
|
39554
|
+
terminalIntegrationId = integration.id;
|
|
39555
|
+
return integration;
|
|
39556
|
+
},
|
|
38703
39557
|
updateIntegration: async ({ id, info }) => {
|
|
38704
39558
|
await this.ctx.api.integrations.update.mutate({
|
|
38705
39559
|
id,
|
|
@@ -38707,25 +39561,157 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
38707
39561
|
skipRestart: true
|
|
38708
39562
|
});
|
|
38709
39563
|
},
|
|
38710
|
-
listOwnDevices: async () =>
|
|
39564
|
+
listOwnDevices: async () => {
|
|
39565
|
+
return terminalCameraReconciliationIndex(await this.ctx.api.deviceManager.listAll.query({ addonId: this.ctx.id }), terminalIntegrationId, cameraDeclarations, this.terminalCameraTombstones);
|
|
39566
|
+
}
|
|
38711
39567
|
}).reconcile({
|
|
38712
39568
|
integrationName: TERMINAL_CAMERA_INTEGRATION,
|
|
38713
39569
|
placement: "hub",
|
|
38714
|
-
devices:
|
|
38715
|
-
stableId:
|
|
38716
|
-
name:
|
|
39570
|
+
devices: cameraDeclarations.map((camera) => ({
|
|
39571
|
+
stableId: camera.stableId,
|
|
39572
|
+
name: camera.name,
|
|
38717
39573
|
type: DeviceType.Camera,
|
|
38718
39574
|
DeviceClass: TerminalCameraDevice,
|
|
38719
|
-
config:
|
|
39575
|
+
config: camera.config,
|
|
38720
39576
|
role: "terminal-camera"
|
|
38721
39577
|
}))
|
|
38722
39578
|
});
|
|
38723
|
-
const onlineByNode = new Map(nodes.map((node) => [node.id, node.isOnline]));
|
|
39579
|
+
const onlineByNode = new Map(nodes.map((node) => [node.id, node.isOnline && !unavailableNodeIds.has(node.id)]));
|
|
38724
39580
|
for (const outcome of result.devices) if (outcome.device instanceof TerminalCameraDevice) {
|
|
39581
|
+
const declaration = declarationsByStableId.get(outcome.stableId);
|
|
39582
|
+
if (declaration) {
|
|
39583
|
+
const config = outcome.device.config;
|
|
39584
|
+
if (!(config.get("instanceId") === declaration.config.instanceId && config.get("nodeId") === declaration.config.nodeId && config.get("profileId") === declaration.config.profileId && config.get("profileLabel") === declaration.config.profileLabel) || !this.migratedTerminalCameraConfigIds.has(outcome.device.id) && needsTerminalCameraConfigMigration(outcome.device.ctx.persistedConfig ?? {}, declaration)) {
|
|
39585
|
+
await config.setAll(declaration.config);
|
|
39586
|
+
this.migratedTerminalCameraConfigIds.add(outcome.device.id);
|
|
39587
|
+
}
|
|
39588
|
+
}
|
|
38725
39589
|
const nodeId = outcome.device.config.get("nodeId");
|
|
38726
|
-
outcome.device.
|
|
39590
|
+
const profileId = outcome.device.config.get("profileId");
|
|
39591
|
+
const profileAvailable = this.cameraProfilesByNode.get(nodeId)?.some((profile) => profile.profileId === profileId) ?? false;
|
|
39592
|
+
outcome.device.setNodeOnline((onlineByNode.get(nodeId) ?? false) && profileAvailable);
|
|
38727
39593
|
}
|
|
38728
39594
|
}
|
|
39595
|
+
terminalInstanceControl() {
|
|
39596
|
+
return {
|
|
39597
|
+
listInstances: async () => this.terminalInstances().map((instance) => this.instanceInfo(instance)),
|
|
39598
|
+
createInstance: async (input) => this.createTerminalInstance(input),
|
|
39599
|
+
deleteInstance: async ({ instanceId }) => this.deleteTerminalInstance(instanceId),
|
|
39600
|
+
setInstanceEnabled: async ({ instanceId, enabled }) => this.setTerminalInstanceEnabled(instanceId, enabled),
|
|
39601
|
+
listLegacyCameras: async () => this.listLegacyTerminalCameras(),
|
|
39602
|
+
adoptLegacyMonitor: async ({ stableId, name }) => this.adoptLegacyMonitor(stableId, name)
|
|
39603
|
+
};
|
|
39604
|
+
}
|
|
39605
|
+
terminalInstances() {
|
|
39606
|
+
return readTerminalInstances(this.config.terminalInstances, (message) => {
|
|
39607
|
+
this.ctx.logger.warn(message);
|
|
39608
|
+
});
|
|
39609
|
+
}
|
|
39610
|
+
instanceInfo(instance) {
|
|
39611
|
+
return {
|
|
39612
|
+
instanceId: instance.id,
|
|
39613
|
+
cameraStableId: instance.cameraStableId,
|
|
39614
|
+
nodeId: instance.nodeId,
|
|
39615
|
+
profileId: instance.profileId,
|
|
39616
|
+
profileLabel: instance.profileLabel,
|
|
39617
|
+
name: instance.name,
|
|
39618
|
+
enabled: instance.enabled
|
|
39619
|
+
};
|
|
39620
|
+
}
|
|
39621
|
+
replaceTerminalCameraTombstones(stableIds) {
|
|
39622
|
+
this.terminalCameraTombstones.clear();
|
|
39623
|
+
for (const stableId of stableIds) if (typeof stableId === "string" && stableId.length > 0) this.terminalCameraTombstones.add(stableId);
|
|
39624
|
+
}
|
|
39625
|
+
async createTerminalInstance(input) {
|
|
39626
|
+
const instance = await this.instanceMutationQueue.run(() => this.createTerminalInstanceUnlocked(input));
|
|
39627
|
+
await this.reconcileTerminalCameras();
|
|
39628
|
+
return instance;
|
|
39629
|
+
}
|
|
39630
|
+
async createTerminalInstanceUnlocked(input) {
|
|
39631
|
+
const relay = this.cameraRelay;
|
|
39632
|
+
if (!relay) throw new Error("Terminal instances are managed on the hub");
|
|
39633
|
+
const profile = (await relay.listProfiles(input.targetNodeId)).find((candidate) => candidate.profileId === input.profileId);
|
|
39634
|
+
if (!profile) throw new Error(`Terminal profile '${input.profileId}' is not available on ${input.targetNodeId}`);
|
|
39635
|
+
const node = (await this.ctx.api.nodes.topology.query()).find((candidate) => candidate.id === input.targetNodeId);
|
|
39636
|
+
if (!node) throw new Error(`Terminal node '${input.targetNodeId}' no longer exists`);
|
|
39637
|
+
const id = crypto.randomUUID();
|
|
39638
|
+
const name = input.name?.trim() || `Terminal ${profile.label} - ${node.isHub ? "Hub" : node.name}`;
|
|
39639
|
+
const instance = {
|
|
39640
|
+
id,
|
|
39641
|
+
cameraStableId: newTerminalCameraStableId(id),
|
|
39642
|
+
nodeId: input.targetNodeId,
|
|
39643
|
+
profileId: profile.profileId,
|
|
39644
|
+
profileLabel: profile.label,
|
|
39645
|
+
name,
|
|
39646
|
+
enabled: true
|
|
39647
|
+
};
|
|
39648
|
+
await this.updateGlobalSettings({ terminalInstances: [...this.config.terminalInstances, instance] });
|
|
39649
|
+
return this.instanceInfo(instance);
|
|
39650
|
+
}
|
|
39651
|
+
async deleteTerminalInstance(instanceId) {
|
|
39652
|
+
await this.instanceMutationQueue.run(() => this.deleteTerminalInstanceUnlocked(instanceId));
|
|
39653
|
+
await this.reconcileTerminalCameras();
|
|
39654
|
+
}
|
|
39655
|
+
async deleteTerminalInstanceUnlocked(instanceId) {
|
|
39656
|
+
const instance = this.terminalInstances().find((candidate) => candidate.id === instanceId);
|
|
39657
|
+
if (!instance) return;
|
|
39658
|
+
this.terminalCameraTombstones.add(instance.cameraStableId);
|
|
39659
|
+
await this.cameraRelay?.closeInstance(instance.id);
|
|
39660
|
+
await this.updateGlobalSettings({
|
|
39661
|
+
terminalInstances: this.config.terminalInstances.filter((candidate) => candidate.id !== instanceId),
|
|
39662
|
+
terminalCameraTombstones: [...new Set([...this.config.terminalCameraTombstones, instance.cameraStableId])]
|
|
39663
|
+
});
|
|
39664
|
+
}
|
|
39665
|
+
async setTerminalInstanceEnabled(instanceId, enabled) {
|
|
39666
|
+
const instance = await this.instanceMutationQueue.run(() => this.setTerminalInstanceEnabledUnlocked(instanceId, enabled));
|
|
39667
|
+
await this.reconcileTerminalCameras();
|
|
39668
|
+
return instance;
|
|
39669
|
+
}
|
|
39670
|
+
async setTerminalInstanceEnabledUnlocked(instanceId, enabled) {
|
|
39671
|
+
const instance = this.terminalInstances().find((candidate) => candidate.id === instanceId);
|
|
39672
|
+
if (!instance) throw new Error(`No such Terminal instance: ${instanceId}`);
|
|
39673
|
+
if (!enabled) {
|
|
39674
|
+
this.terminalCameraTombstones.add(instance.cameraStableId);
|
|
39675
|
+
await this.cameraRelay?.closeInstance(instance.id);
|
|
39676
|
+
}
|
|
39677
|
+
const updated = {
|
|
39678
|
+
...instance,
|
|
39679
|
+
enabled
|
|
39680
|
+
};
|
|
39681
|
+
const terminalInstances = this.config.terminalInstances.map((candidate) => candidate.id === instanceId ? updated : candidate);
|
|
39682
|
+
const terminalCameraTombstones = enabled ? this.config.terminalCameraTombstones.filter((stableId) => stableId !== instance.cameraStableId) : [...new Set([...this.config.terminalCameraTombstones, instance.cameraStableId])];
|
|
39683
|
+
await this.updateGlobalSettings({
|
|
39684
|
+
terminalInstances,
|
|
39685
|
+
terminalCameraTombstones
|
|
39686
|
+
});
|
|
39687
|
+
return this.instanceInfo(updated);
|
|
39688
|
+
}
|
|
39689
|
+
async listLegacyTerminalCameras() {
|
|
39690
|
+
const instances = this.terminalInstances();
|
|
39691
|
+
const instanceStableIds = new Set(instances.map((instance) => instance.cameraStableId));
|
|
39692
|
+
return listLegacyTerminalCameraCandidates(await this.ctx.api.deviceManager.listAll.query({ addonId: this.ctx.id }), instanceStableIds, this.terminalCameraTombstones);
|
|
39693
|
+
}
|
|
39694
|
+
async adoptLegacyMonitor(stableId, requestedName) {
|
|
39695
|
+
const instance = await this.instanceMutationQueue.run(() => this.adoptLegacyMonitorUnlocked(stableId, requestedName));
|
|
39696
|
+
await this.reconcileTerminalCameras();
|
|
39697
|
+
return instance;
|
|
39698
|
+
}
|
|
39699
|
+
async adoptLegacyMonitorUnlocked(stableId, requestedName) {
|
|
39700
|
+
if (this.terminalCameraTombstones.has(stableId)) throw new Error("This legacy Terminal camera was deleted and cannot be adopted");
|
|
39701
|
+
const legacy = (await this.listLegacyTerminalCameras()).find((camera) => camera.stableId === stableId);
|
|
39702
|
+
if (!legacy?.adoptable) throw new Error("Only a legacy monitor camera with its original stable id can be adopted");
|
|
39703
|
+
const instance = {
|
|
39704
|
+
id: crypto.randomUUID(),
|
|
39705
|
+
cameraStableId: legacy.stableId,
|
|
39706
|
+
nodeId: legacy.nodeId,
|
|
39707
|
+
profileId: "monitor",
|
|
39708
|
+
profileLabel: legacy.profileLabel,
|
|
39709
|
+
name: requestedName?.trim() || legacy.name,
|
|
39710
|
+
enabled: true
|
|
39711
|
+
};
|
|
39712
|
+
await this.updateGlobalSettings({ terminalInstances: [...this.config.terminalInstances, instance] });
|
|
39713
|
+
return this.instanceInfo(instance);
|
|
39714
|
+
}
|
|
38729
39715
|
globalSettingsSchema() {
|
|
38730
39716
|
return this.schema({ sections: [{
|
|
38731
39717
|
id: "terminal",
|
|
@@ -38733,15 +39719,78 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
38733
39719
|
description: "Interactive terminal sessions in the Admin UI. Only pre-declared profiles can be opened; a free-form command is never accepted.",
|
|
38734
39720
|
columns: 2,
|
|
38735
39721
|
fields: [
|
|
39722
|
+
this.field({
|
|
39723
|
+
type: "boolean",
|
|
39724
|
+
key: "btmEnabled",
|
|
39725
|
+
label: "Enable BTM camera",
|
|
39726
|
+
default: true,
|
|
39727
|
+
perNode: true
|
|
39728
|
+
}),
|
|
38736
39729
|
this.field({
|
|
38737
39730
|
type: "text",
|
|
38738
39731
|
key: "btmPath",
|
|
38739
|
-
label: "
|
|
39732
|
+
label: "BTM binary",
|
|
38740
39733
|
description: "Path to the `btm` (bottom) executable. Leave empty to resolve from PATH.",
|
|
38741
39734
|
placeholder: "btm",
|
|
38742
39735
|
default: "",
|
|
38743
39736
|
perNode: true
|
|
38744
39737
|
}),
|
|
39738
|
+
this.field({
|
|
39739
|
+
type: "tags",
|
|
39740
|
+
key: "btmArgs",
|
|
39741
|
+
label: "BTM arguments",
|
|
39742
|
+
description: "Exact arguments passed to btm.",
|
|
39743
|
+
default: [],
|
|
39744
|
+
perNode: true
|
|
39745
|
+
}),
|
|
39746
|
+
this.field({
|
|
39747
|
+
type: "boolean",
|
|
39748
|
+
key: "topEnabled",
|
|
39749
|
+
label: "Enable Top camera",
|
|
39750
|
+
default: true,
|
|
39751
|
+
perNode: true
|
|
39752
|
+
}),
|
|
39753
|
+
this.field({
|
|
39754
|
+
type: "text",
|
|
39755
|
+
key: "topPath",
|
|
39756
|
+
label: "Top binary",
|
|
39757
|
+
placeholder: "top",
|
|
39758
|
+
default: "",
|
|
39759
|
+
perNode: true
|
|
39760
|
+
}),
|
|
39761
|
+
this.field({
|
|
39762
|
+
type: "tags",
|
|
39763
|
+
key: "topArgs",
|
|
39764
|
+
label: "Top arguments",
|
|
39765
|
+
description: "Exact arguments passed to top.",
|
|
39766
|
+
default: [],
|
|
39767
|
+
perNode: true
|
|
39768
|
+
}),
|
|
39769
|
+
this.field({
|
|
39770
|
+
type: "boolean",
|
|
39771
|
+
key: "glancesEnabled",
|
|
39772
|
+
label: "Enable Glances camera",
|
|
39773
|
+
description: "Glances is installed automatically into CamStack embedded Python.",
|
|
39774
|
+
default: true,
|
|
39775
|
+
perNode: true
|
|
39776
|
+
}),
|
|
39777
|
+
this.field({
|
|
39778
|
+
type: "text",
|
|
39779
|
+
key: "glancesPath",
|
|
39780
|
+
label: "Glances binary override",
|
|
39781
|
+
description: "Optional executable override. Empty uses the automatically managed Python package.",
|
|
39782
|
+
placeholder: "glances",
|
|
39783
|
+
default: "",
|
|
39784
|
+
perNode: true
|
|
39785
|
+
}),
|
|
39786
|
+
this.field({
|
|
39787
|
+
type: "tags",
|
|
39788
|
+
key: "glancesArgs",
|
|
39789
|
+
label: "Glances arguments",
|
|
39790
|
+
description: "Exact arguments passed to Glances.",
|
|
39791
|
+
default: [],
|
|
39792
|
+
perNode: true
|
|
39793
|
+
}),
|
|
38745
39794
|
this.field({
|
|
38746
39795
|
type: "number",
|
|
38747
39796
|
key: "maxSessions",
|
|
@@ -38773,7 +39822,7 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
38773
39822
|
}, {
|
|
38774
39823
|
id: "terminal-profiles",
|
|
38775
39824
|
title: "Custom profiles",
|
|
38776
|
-
description: "
|
|
39825
|
+
description: "Enabled profiles are available templates on this node. Create a Terminal instance on the Terminal page to declare a camera; session requests only carry the profile ID and commands cannot be overridden by clients.",
|
|
38777
39826
|
columns: 1,
|
|
38778
39827
|
fields: [this.field({
|
|
38779
39828
|
type: "editable-array",
|