@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.js
CHANGED
|
@@ -7627,12 +7627,11 @@ var RecordingConfigSchema = object({
|
|
|
7627
7627
|
/**
|
|
7628
7628
|
* Entity-relocation job state (storage entity-routing spec, Phase 4).
|
|
7629
7629
|
*
|
|
7630
|
-
* One shape shared by the recorder
|
|
7631
|
-
*
|
|
7632
|
-
*
|
|
7633
|
-
*
|
|
7634
|
-
*
|
|
7635
|
-
* row on the owning addon's surface.
|
|
7630
|
+
* One shape shared by the recorder and pipeline-analytics internal movers.
|
|
7631
|
+
* The public admin surface is `storage-migration`; child jobs remain in RAM
|
|
7632
|
+
* because copy-if-absent, verify, delete and index/row repoint are resumable.
|
|
7633
|
+
* Each completed/failed run also lands one durable ops-log row on its owning
|
|
7634
|
+
* addon surface.
|
|
7636
7635
|
*/
|
|
7637
7636
|
var RelocateJobStateSchema = _enum([
|
|
7638
7637
|
"running",
|
|
@@ -7659,19 +7658,100 @@ var RelocateJobSchema = object({
|
|
|
7659
7658
|
finishedAt: number().nullable(),
|
|
7660
7659
|
error: string().nullable()
|
|
7661
7660
|
});
|
|
7661
|
+
/** Profile-derived footage selection used only by the migration coordinator:
|
|
7662
|
+
* `recordings` owns high+mid; `recordingsLow` owns low. */
|
|
7663
|
+
var RelocateFootageClassSchema = _enum(["recordings", "recordingsLow"]);
|
|
7662
7664
|
var RelocateFootageInputSchema = object({
|
|
7663
|
-
deviceId: number().optional(),
|
|
7664
7665
|
fromLocationId: string(),
|
|
7665
7666
|
toLocationId: string(),
|
|
7666
7667
|
entities: array(_enum(["segments"])).optional(),
|
|
7668
|
+
/** Limits relocation to the logical profile class. Omit only for the
|
|
7669
|
+
* pre-orchestration compatibility path. */
|
|
7670
|
+
footageClass: RelocateFootageClassSchema.optional(),
|
|
7667
7671
|
/** Copy throttle in MB/s (default 40) — the drain is a background chore,
|
|
7668
7672
|
* never allowed to starve live writers. */
|
|
7669
7673
|
throttleMbps: number().min(1).max(1e3).optional()
|
|
7670
7674
|
});
|
|
7671
|
-
|
|
7672
|
-
|
|
7675
|
+
/** Internal, lease-scoped participant operation. It is intentionally separate
|
|
7676
|
+
* from persistent recording settings: a migration never changes
|
|
7677
|
+
* `RecordingConfig.enabled` or camera wrapper bindings. */
|
|
7678
|
+
var StorageMigrationLeaseInputSchema = object({ leaseId: string().min(1) });
|
|
7679
|
+
var StorageMigrationFootageMoveInputSchema = RelocateFootageInputSchema.extend({ leaseId: string().min(1) });
|
|
7680
|
+
var StorageMigrationMediaMoveInputSchema = object({
|
|
7673
7681
|
toLocationId: string(),
|
|
7674
7682
|
throttleMbps: number().min(1).max(1e3).optional()
|
|
7683
|
+
}).extend({ leaseId: string().min(1) });
|
|
7684
|
+
/** The independently selectable logical storage classes. `recordings`
|
|
7685
|
+
* encompasses the high and mid segment profiles; `recordingsLow` is low
|
|
7686
|
+
* segments; `eventMedia` is post-analysis blobs. */
|
|
7687
|
+
var StorageMigrationClassSchema = _enum([
|
|
7688
|
+
"recordings",
|
|
7689
|
+
"recordingsLow",
|
|
7690
|
+
"eventMedia"
|
|
7691
|
+
]);
|
|
7692
|
+
/** A destination is always an existing, fully-qualified location id. The
|
|
7693
|
+
* migration API intentionally never changes a source location's `basePath`:
|
|
7694
|
+
* callers create a new `<type>:<slug>` location, then select it here. */
|
|
7695
|
+
var StorageMigrationDestinationsSchema = object({
|
|
7696
|
+
recordings: string().min(1).optional(),
|
|
7697
|
+
recordingsLow: string().min(1).optional(),
|
|
7698
|
+
eventMedia: string().min(1).optional()
|
|
7699
|
+
}).refine((value) => Object.keys(value).length > 0, { message: "select at least one storage class" });
|
|
7700
|
+
/** Shared input for planning and starting an orchestrated storage migration. */
|
|
7701
|
+
var StorageMigrationInputSchema = object({
|
|
7702
|
+
destinations: StorageMigrationDestinationsSchema,
|
|
7703
|
+
throttleMbps: number().min(1).max(1e3).optional()
|
|
7704
|
+
});
|
|
7705
|
+
/** The durable coordinator state machine. The only phase that changes default
|
|
7706
|
+
* locations is `repointing`, after every selected mover has completed and been
|
|
7707
|
+
* verified. */
|
|
7708
|
+
var StorageMigrationPhaseSchema = _enum([
|
|
7709
|
+
"planning",
|
|
7710
|
+
"pausing",
|
|
7711
|
+
"moving",
|
|
7712
|
+
"verifying",
|
|
7713
|
+
"repointing",
|
|
7714
|
+
"refreshing",
|
|
7715
|
+
"resuming",
|
|
7716
|
+
"done",
|
|
7717
|
+
"failed",
|
|
7718
|
+
"cancelled"
|
|
7719
|
+
]);
|
|
7720
|
+
var StorageMigrationParticipantSchema = _enum([
|
|
7721
|
+
"pipeline",
|
|
7722
|
+
"recorder",
|
|
7723
|
+
"analytics"
|
|
7724
|
+
]);
|
|
7725
|
+
var StorageMigrationMoveSchema = object({
|
|
7726
|
+
storageClass: StorageMigrationClassSchema,
|
|
7727
|
+
fromLocationId: string(),
|
|
7728
|
+
toLocationId: string(),
|
|
7729
|
+
moverJobId: string().nullable(),
|
|
7730
|
+
state: RelocateJobStateSchema.nullable(),
|
|
7731
|
+
error: string().nullable()
|
|
7732
|
+
});
|
|
7733
|
+
var StorageMigrationJobSchema = object({
|
|
7734
|
+
jobId: string(),
|
|
7735
|
+
phase: StorageMigrationPhaseSchema,
|
|
7736
|
+
destinations: StorageMigrationDestinationsSchema,
|
|
7737
|
+
throttleMbps: number(),
|
|
7738
|
+
moves: array(StorageMigrationMoveSchema),
|
|
7739
|
+
pauseLeaseId: string().nullable(),
|
|
7740
|
+
pausedParticipants: array(StorageMigrationParticipantSchema),
|
|
7741
|
+
repointed: boolean(),
|
|
7742
|
+
cancelRequested: boolean(),
|
|
7743
|
+
startedAt: number(),
|
|
7744
|
+
updatedAt: number(),
|
|
7745
|
+
finishedAt: number().nullable(),
|
|
7746
|
+
error: string().nullable()
|
|
7747
|
+
});
|
|
7748
|
+
var StorageMigrationPlanSchema = object({
|
|
7749
|
+
destinations: StorageMigrationDestinationsSchema,
|
|
7750
|
+
moves: array(object({
|
|
7751
|
+
storageClass: StorageMigrationClassSchema,
|
|
7752
|
+
fromLocationId: string(),
|
|
7753
|
+
toLocationId: string()
|
|
7754
|
+
}))
|
|
7675
7755
|
});
|
|
7676
7756
|
/**
|
|
7677
7757
|
* `StorageLocationType` — an addon-declared id that identifies the *kind* of
|
|
@@ -16270,13 +16350,19 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
16270
16350
|
}), method(object({ deviceId: number() }), EventPruneCountsSchema, {
|
|
16271
16351
|
kind: "mutation",
|
|
16272
16352
|
auth: "admin"
|
|
16273
|
-
}), method(
|
|
16353
|
+
}), method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
|
|
16274
16354
|
kind: "mutation",
|
|
16275
16355
|
auth: "admin"
|
|
16276
|
-
}), method(object({
|
|
16277
|
-
kind: "
|
|
16356
|
+
}), method(StorageMigrationLeaseInputSchema, object({ resumed: literal(true) }), {
|
|
16357
|
+
kind: "mutation",
|
|
16358
|
+
auth: "admin"
|
|
16359
|
+
}), method(StorageMigrationLeaseInputSchema, object({ refreshed: literal(true) }), {
|
|
16360
|
+
kind: "mutation",
|
|
16278
16361
|
auth: "admin"
|
|
16279
|
-
}), method(object({ jobId: string() }),
|
|
16362
|
+
}), method(StorageMigrationMediaMoveInputSchema, object({ jobId: string() }), {
|
|
16363
|
+
kind: "mutation",
|
|
16364
|
+
auth: "admin"
|
|
16365
|
+
}), method(object({ jobId: string() }), RelocateJobSchema.nullable(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
|
|
16280
16366
|
kind: "mutation",
|
|
16281
16367
|
auth: "admin"
|
|
16282
16368
|
}), method(OpsLogQueryInputSchema, array(OpsLogEntrySchema).readonly(), {
|
|
@@ -17806,7 +17892,13 @@ var NodeInferenceDevicesSchema = object({
|
|
|
17806
17892
|
reachable: boolean(),
|
|
17807
17893
|
devices: array(NodeInferenceDeviceSchema).readonly()
|
|
17808
17894
|
});
|
|
17809
|
-
method(object({
|
|
17895
|
+
method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
|
|
17896
|
+
kind: "mutation",
|
|
17897
|
+
auth: "admin"
|
|
17898
|
+
}), method(StorageMigrationLeaseInputSchema, object({ resumed: literal(true) }), {
|
|
17899
|
+
kind: "mutation",
|
|
17900
|
+
auth: "admin"
|
|
17901
|
+
}), method(object({
|
|
17810
17902
|
deviceId: number(),
|
|
17811
17903
|
agentNodeId: string()
|
|
17812
17904
|
}), object({ success: literal(true) }), {
|
|
@@ -18246,7 +18338,33 @@ var SnapshotImageSchema = object({
|
|
|
18246
18338
|
base64: string(),
|
|
18247
18339
|
contentType: string()
|
|
18248
18340
|
});
|
|
18249
|
-
|
|
18341
|
+
/**
|
|
18342
|
+
* snapshot — device-scoped capability for camera image capture.
|
|
18343
|
+
*
|
|
18344
|
+
* Two kinds of providers coexist behind this cap name:
|
|
18345
|
+
*
|
|
18346
|
+
* - **Native** providers (kind:'native'): registered per-device by
|
|
18347
|
+
* device-driver addons (RtspCamera, OnvifCamera, …) via
|
|
18348
|
+
* `DeviceContext.registerNativeCap`. Each knows how to fetch a frame
|
|
18349
|
+
* straight from the camera (HTTP snapshot URL, ONVIF action, etc.).
|
|
18350
|
+
*
|
|
18351
|
+
* - **Wrapper** providers (kind:'wrapper'): register as a system
|
|
18352
|
+
* provider (SnapshotAddon in `@camstack/system/builtins/snapshot`). The
|
|
18353
|
+
* wrapper owns the cache and invokes the native via
|
|
18354
|
+
* `ctx.getNativeProvider(snapshotCapability, deviceId)` on miss.
|
|
18355
|
+
*
|
|
18356
|
+
* Device-scoped routing: callers use `ctx.fetchDevice(id).snapshot.*`;
|
|
18357
|
+
* the DeviceProxy auto-injects `deviceId` + `nodeId` and dispatches to
|
|
18358
|
+
* the provider currently active for that device (wrapper wins when
|
|
18359
|
+
* activated via `setWrapperActive`, otherwise the native).
|
|
18360
|
+
*/
|
|
18361
|
+
/**
|
|
18362
|
+
* Live readable snapshot state — diagnostic info that a consumer can
|
|
18363
|
+
* pull to know when the last image was captured, how stale the cache
|
|
18364
|
+
* is, and which stream was used. Distinct from `getSnapshot` which
|
|
18365
|
+
* returns the JPEG itself.
|
|
18366
|
+
*/
|
|
18367
|
+
var SnapshotStatusSchema = object({
|
|
18250
18368
|
/** Ms epoch of the last successful capture. Null if none yet. */
|
|
18251
18369
|
lastCapturedAt: number().nullable(),
|
|
18252
18370
|
/** Age of the cached image in ms. Null if no cache. */
|
|
@@ -18256,64 +18374,129 @@ object({
|
|
|
18256
18374
|
/** Stream id used for the last capture ('high'|'mid'|'low' or custom). Null if via HTTP endpoint. */
|
|
18257
18375
|
lastStreamId: string().nullable()
|
|
18258
18376
|
});
|
|
18259
|
-
|
|
18260
|
-
|
|
18261
|
-
|
|
18262
|
-
|
|
18263
|
-
|
|
18264
|
-
|
|
18265
|
-
|
|
18266
|
-
|
|
18267
|
-
|
|
18268
|
-
|
|
18269
|
-
|
|
18270
|
-
|
|
18271
|
-
|
|
18272
|
-
|
|
18273
|
-
|
|
18274
|
-
|
|
18275
|
-
|
|
18276
|
-
|
|
18277
|
-
|
|
18278
|
-
|
|
18279
|
-
|
|
18280
|
-
|
|
18281
|
-
|
|
18282
|
-
|
|
18283
|
-
|
|
18284
|
-
|
|
18285
|
-
|
|
18286
|
-
|
|
18287
|
-
|
|
18288
|
-
|
|
18377
|
+
var snapshotCapability = {
|
|
18378
|
+
name: "snapshot",
|
|
18379
|
+
scope: "device",
|
|
18380
|
+
deviceNative: true,
|
|
18381
|
+
mode: "singleton",
|
|
18382
|
+
kind: "wrapper",
|
|
18383
|
+
defaultActive: true,
|
|
18384
|
+
deviceTypes: [DeviceType.Camera],
|
|
18385
|
+
exposesDeviceSettings: true,
|
|
18386
|
+
methods: {
|
|
18387
|
+
getSnapshot: method(object({
|
|
18388
|
+
deviceId: number(),
|
|
18389
|
+
streamId: string().optional(),
|
|
18390
|
+
/**
|
|
18391
|
+
* Bypass the cache freshness check and fetch directly from the
|
|
18392
|
+
* native (or stream-broker fallback). Triggered by the UI's
|
|
18393
|
+
* "refresh" button so an operator can force a fresh frame
|
|
18394
|
+
* even when the cache is well within the device's
|
|
18395
|
+
* `snapshotMaxAgeS` window.
|
|
18396
|
+
*
|
|
18397
|
+
* **`force` is an OPERATOR signal, not a freshness preference.** On a
|
|
18398
|
+
* battery camera it is the one thing that walks past the wrapper's
|
|
18399
|
+
* sleep gate and wakes the camera, so a background caller — a poller,
|
|
18400
|
+
* an event handler, a thumbnail — must NEVER set it. Every such caller
|
|
18401
|
+
* gets the cached frame, which on a sleeping battery camera is the
|
|
18402
|
+
* correct answer: stale but honest beats woken.
|
|
18403
|
+
*/
|
|
18404
|
+
force: boolean().optional()
|
|
18405
|
+
}), SnapshotImageSchema.nullable()),
|
|
18406
|
+
invalidateCache: method(object({ deviceId: number() }), _void(), {
|
|
18407
|
+
kind: "mutation",
|
|
18408
|
+
auth: "admin"
|
|
18409
|
+
}),
|
|
18410
|
+
/**
|
|
18411
|
+
* Cache-only batch overview — answers from the wrapper's in-memory cache in
|
|
18412
|
+
* O(n) and NEVER triggers a capture. Lets a grid skip requesting images for
|
|
18413
|
+
* devices that never produced a frame, and gives it an ETag per device for
|
|
18414
|
+
* conditional (304-able) image fetches. `lastCapturedAt`/`cacheAgeMs`/`etag`
|
|
18415
|
+
* are null for a device with no cached frame.
|
|
18416
|
+
*/
|
|
18417
|
+
getSnapshotOverview: systemMethod(object({ deviceIds: array(number()).min(1).max(200) }), array(object({
|
|
18418
|
+
deviceId: number(),
|
|
18419
|
+
lastCapturedAt: number().nullable(),
|
|
18420
|
+
cacheAgeMs: number().nullable(),
|
|
18421
|
+
etag: string().nullable()
|
|
18422
|
+
}))),
|
|
18423
|
+
/**
|
|
18424
|
+
* Signed, expiring links to a CLIENT-SIZED frame — and the demand signal
|
|
18425
|
+
* that makes those frames current.
|
|
18426
|
+
*
|
|
18427
|
+
* ## The problem it replaces
|
|
18428
|
+
*
|
|
18429
|
+
* `getSnapshotOverview` is cache-only by contract: it answers from whatever
|
|
18430
|
+
* the wrapper happens to hold and never captures. Under D93 the client
|
|
18431
|
+
* versions its image URL on that answer, and an image REQUEST is what enrols
|
|
18432
|
+
* a camera in the keep-warm loop. Both of those are satisfiable by the
|
|
18433
|
+
* client's own image cache — `expo-image` is URL-keyed and never revalidates
|
|
18434
|
+
* — so a URL painted in a previous session comes off disk with no network,
|
|
18435
|
+
* no enrolment, and nothing warming. Measured on the live hub: reopening
|
|
18436
|
+
* after two minutes idle painted 15 of 16 tiles at **168 s old** with zero
|
|
18437
|
+
* HTTP requests, and the fleet only recovered because a later poll happened
|
|
18438
|
+
* to observe a different identity.
|
|
18439
|
+
*
|
|
18440
|
+
* ## The two properties that fix it
|
|
18441
|
+
*
|
|
18442
|
+
* **It is an RPC, so no client cache can answer it.** The demand signal
|
|
18443
|
+
* always reaches the wrapper. This method therefore MAY create keep-warm
|
|
18444
|
+
* subscriptions, where `getSnapshotOverview` must never (D93) — the
|
|
18445
|
+
* distinction is not "one is newer" but that the overview poll is app-wide
|
|
18446
|
+
* (a creating overview would warm every camera on the install) while this is
|
|
18447
|
+
* called by a rendered surface naming the tiles it is actually painting, at
|
|
18448
|
+
* the width it is painting them.
|
|
18449
|
+
*
|
|
18450
|
+
* **It waits, briefly and boundedly, for the capture it triggered.** The
|
|
18451
|
+
* returned `capturedAt` is the frame the link will serve, not the frame the
|
|
18452
|
+
* cache held when the client asked, so a first paint is honest and current
|
|
18453
|
+
* instead of a generation behind. A device that does not settle inside the
|
|
18454
|
+
* bound still gets a link and its real (older) `capturedAt` — the next poll
|
|
18455
|
+
* carries it forward.
|
|
18456
|
+
*
|
|
18457
|
+
* `force` is never set on behalf of a client here. A sleeping battery camera
|
|
18458
|
+
* is reported with `sleeping: true` and the last frame it produced, however
|
|
18459
|
+
* old; the wrapper's existing sleep gate owns that decision and this method
|
|
18460
|
+
* adds no second one.
|
|
18461
|
+
*/
|
|
18462
|
+
getSnapshotLinks: systemMethod(object({
|
|
18463
|
+
/** The tiles a surface is actually rendering. One entry per (device,
|
|
18464
|
+
* width) the caller will paint — the width is snapped to the server's
|
|
18465
|
+
* ladder and becomes part of the link's SIGNED identity. */
|
|
18289
18466
|
targets: array(object({
|
|
18290
|
-
|
|
18291
|
-
|
|
18292
|
-
|
|
18293
|
-
|
|
18294
|
-
})).min(1).max(200) }), array(object({
|
|
18295
|
-
|
|
18296
|
-
|
|
18297
|
-
|
|
18298
|
-
|
|
18299
|
-
|
|
18300
|
-
|
|
18301
|
-
|
|
18302
|
-
|
|
18303
|
-
|
|
18304
|
-
|
|
18305
|
-
|
|
18306
|
-
|
|
18307
|
-
|
|
18308
|
-
|
|
18309
|
-
|
|
18310
|
-
|
|
18311
|
-
|
|
18312
|
-
|
|
18313
|
-
|
|
18314
|
-
|
|
18315
|
-
|
|
18316
|
-
})))
|
|
18467
|
+
deviceId: number(),
|
|
18468
|
+
/** Target width in px. Omit for the frame as captured — correct
|
|
18469
|
+
* for a full-bleed surface, wrong (and expensive) for a grid. */
|
|
18470
|
+
width: number().int().positive().optional()
|
|
18471
|
+
})).min(1).max(200) }), array(object({
|
|
18472
|
+
deviceId: number(),
|
|
18473
|
+
/** Root-relative signed path, or null when the link plane is not
|
|
18474
|
+
* served (no data-plane facility). Present even for a device that has
|
|
18475
|
+
* never captured — the request is what triggers the first one (D94). */
|
|
18476
|
+
url: string().nullable(),
|
|
18477
|
+
/** Epoch ms of the frame this link serves. Null = never captured.
|
|
18478
|
+
* THE honest age: the tRPC path carried none before this. */
|
|
18479
|
+
capturedAt: number().nullable(),
|
|
18480
|
+
/** Age of that frame at the moment the answer was built. */
|
|
18481
|
+
ageMs: number().nullable(),
|
|
18482
|
+
/** Epoch ms after which `url` stops verifying. */
|
|
18483
|
+
expiresAt: number().nullable(),
|
|
18484
|
+
/** Ladder rung the bytes are at; null = the frame as captured. */
|
|
18485
|
+
width: number().nullable(),
|
|
18486
|
+
/** The device has never produced a frame. An empty state, not a
|
|
18487
|
+
* failure — and never a reason to withhold the link (D94). */
|
|
18488
|
+
neverCaptured: boolean(),
|
|
18489
|
+
/** A sleeping battery camera: the frame is deliberately stale and will
|
|
18490
|
+
* NOT refresh in the background. A surface should say so rather than
|
|
18491
|
+
* present it as current. */
|
|
18492
|
+
sleeping: boolean()
|
|
18493
|
+
})))
|
|
18494
|
+
},
|
|
18495
|
+
status: {
|
|
18496
|
+
schema: SnapshotStatusSchema,
|
|
18497
|
+
kind: "poll"
|
|
18498
|
+
}
|
|
18499
|
+
};
|
|
18317
18500
|
/**
|
|
18318
18501
|
* `sso-bridge` — internal hub-only cap that lets SSO-style auth
|
|
18319
18502
|
* providers (OIDC, SAML, magic-link, …) mint an HMAC-signed token
|
|
@@ -18480,6 +18663,13 @@ method(object({ locationId: string() }), EvictableUsageSchema), method(object({
|
|
|
18480
18663
|
locationId: string(),
|
|
18481
18664
|
targetBytes: number().int().positive()
|
|
18482
18665
|
}), EvictResultSchema, { kind: "mutation" });
|
|
18666
|
+
method(StorageMigrationInputSchema, StorageMigrationPlanSchema, { auth: "admin" }), method(StorageMigrationInputSchema, object({ jobId: string() }), {
|
|
18667
|
+
kind: "mutation",
|
|
18668
|
+
auth: "admin"
|
|
18669
|
+
}), method(object({ jobId: string().optional() }), StorageMigrationJobSchema.nullable(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
|
|
18670
|
+
kind: "mutation",
|
|
18671
|
+
auth: "admin"
|
|
18672
|
+
});
|
|
18483
18673
|
var ProviderInfoSchema = discriminatedUnion("shouldSaveDiskSpace", [object({
|
|
18484
18674
|
providerId: string().min(1),
|
|
18485
18675
|
displayName: string().min(1),
|
|
@@ -18583,6 +18773,28 @@ var TerminalProfileInfoSchema = object({
|
|
|
18583
18773
|
label: string(),
|
|
18584
18774
|
description: string().optional()
|
|
18585
18775
|
});
|
|
18776
|
+
/**
|
|
18777
|
+
* A durable operator-created Terminal instance. Profiles are templates; only
|
|
18778
|
+
* an instance declares a camera.
|
|
18779
|
+
*/
|
|
18780
|
+
var TerminalInstanceInfoSchema = object({
|
|
18781
|
+
instanceId: string(),
|
|
18782
|
+
cameraStableId: string(),
|
|
18783
|
+
nodeId: string(),
|
|
18784
|
+
profileId: string(),
|
|
18785
|
+
profileLabel: string(),
|
|
18786
|
+
name: string(),
|
|
18787
|
+
enabled: boolean()
|
|
18788
|
+
});
|
|
18789
|
+
var TerminalLegacyCameraSchema = object({
|
|
18790
|
+
stableId: string(),
|
|
18791
|
+
nodeId: string(),
|
|
18792
|
+
profileId: string(),
|
|
18793
|
+
profileLabel: string(),
|
|
18794
|
+
name: string(),
|
|
18795
|
+
/** Only legacy monitor cameras can retain their historic stable identity. */
|
|
18796
|
+
adoptable: boolean()
|
|
18797
|
+
});
|
|
18586
18798
|
var TerminalOutputEventSchema = discriminatedUnion("kind", [object({
|
|
18587
18799
|
seq: number().int().positive(),
|
|
18588
18800
|
kind: literal("data"),
|
|
@@ -18602,10 +18814,9 @@ var TerminalOutputBatchSchema = object({
|
|
|
18602
18814
|
/**
|
|
18603
18815
|
* terminal-session — singleton system capability for interactive TTY sessions.
|
|
18604
18816
|
*
|
|
18605
|
-
*
|
|
18606
|
-
*
|
|
18607
|
-
*
|
|
18608
|
-
* change this contract.
|
|
18817
|
+
* Owns both live PTY lifecycle and durable Terminal instance management.
|
|
18818
|
+
* Profiles are allowlisted templates; an explicit instance is the only path
|
|
18819
|
+
* that declares a camera.
|
|
18609
18820
|
*/
|
|
18610
18821
|
var terminalSessionCapability = {
|
|
18611
18822
|
name: "terminal-session",
|
|
@@ -18614,6 +18825,37 @@ var terminalSessionCapability = {
|
|
|
18614
18825
|
methods: {
|
|
18615
18826
|
/** Pre-declared profiles the operator may open. */
|
|
18616
18827
|
listProfiles: method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }),
|
|
18828
|
+
/** Explicit durable Terminal instances, managed centrally on the hub. */
|
|
18829
|
+
listInstances: method(_void(), array(TerminalInstanceInfoSchema).readonly(), { auth: "admin" }),
|
|
18830
|
+
createInstance: method(object({
|
|
18831
|
+
targetNodeId: string().min(1),
|
|
18832
|
+
profileId: string().min(1),
|
|
18833
|
+
name: string().trim().min(1).max(160).optional()
|
|
18834
|
+
}), TerminalInstanceInfoSchema, {
|
|
18835
|
+
kind: "mutation",
|
|
18836
|
+
auth: "admin"
|
|
18837
|
+
}),
|
|
18838
|
+
deleteInstance: method(object({ instanceId: string().min(1) }), _void(), {
|
|
18839
|
+
kind: "mutation",
|
|
18840
|
+
auth: "admin"
|
|
18841
|
+
}),
|
|
18842
|
+
setInstanceEnabled: method(object({
|
|
18843
|
+
instanceId: string().min(1),
|
|
18844
|
+
enabled: boolean()
|
|
18845
|
+
}), TerminalInstanceInfoSchema, {
|
|
18846
|
+
kind: "mutation",
|
|
18847
|
+
auth: "admin"
|
|
18848
|
+
}),
|
|
18849
|
+
/** Existing automatic cameras are shown for explicit migration only. */
|
|
18850
|
+
listLegacyCameras: method(_void(), array(TerminalLegacyCameraSchema).readonly(), { auth: "admin" }),
|
|
18851
|
+
/** Explicitly adopt one legacy monitor camera, retaining its stable id. */
|
|
18852
|
+
adoptLegacyMonitor: method(object({
|
|
18853
|
+
stableId: string().min(1),
|
|
18854
|
+
name: string().trim().min(1).max(160).optional()
|
|
18855
|
+
}), TerminalInstanceInfoSchema, {
|
|
18856
|
+
kind: "mutation",
|
|
18857
|
+
auth: "admin"
|
|
18858
|
+
}),
|
|
18617
18859
|
/** Live sessions currently hosted by the provider. */
|
|
18618
18860
|
listSessions: method(_void(), array(TerminalSessionInfoSchema).readonly(), { auth: "admin" }),
|
|
18619
18861
|
/**
|
|
@@ -18645,7 +18887,13 @@ var terminalSessionCapability = {
|
|
|
18645
18887
|
pullOutput: method(object({
|
|
18646
18888
|
sessionId: string(),
|
|
18647
18889
|
afterSeq: number().int().nonnegative(),
|
|
18648
|
-
waitMs: number().int().min(0).max(2e3).default(0)
|
|
18890
|
+
waitMs: number().int().min(0).max(2e3).default(0),
|
|
18891
|
+
/**
|
|
18892
|
+
* Wait when a just-opened session has no output yet. Kept opt-in so a
|
|
18893
|
+
* browser's initial repaint remains immediate; the camera snapshot
|
|
18894
|
+
* relay uses it to avoid encoding a blank startup frame.
|
|
18895
|
+
*/
|
|
18896
|
+
waitForOutput: boolean().optional()
|
|
18649
18897
|
}), TerminalOutputBatchSchema, {
|
|
18650
18898
|
kind: "mutation",
|
|
18651
18899
|
auth: "admin",
|
|
@@ -21149,6 +21397,7 @@ var FaceInfoSchema = object({
|
|
|
21149
21397
|
var FaceFilterEnum = _enum([
|
|
21150
21398
|
"unassigned",
|
|
21151
21399
|
"recognized",
|
|
21400
|
+
"identified",
|
|
21152
21401
|
"all"
|
|
21153
21402
|
]);
|
|
21154
21403
|
var MediaFileLiteSchema$1 = object({
|
|
@@ -21177,6 +21426,8 @@ method(_void(), array(IdentitySchema).readonly()), method(object({ name: string(
|
|
|
21177
21426
|
kind: "mutation",
|
|
21178
21427
|
auth: "admin"
|
|
21179
21428
|
}), method(object({
|
|
21429
|
+
/** Restrict to one camera. Absent keeps the cluster-wide gallery view. */
|
|
21430
|
+
deviceId: number().int().optional(),
|
|
21180
21431
|
limit: number().int().positive().optional(),
|
|
21181
21432
|
filter: FaceFilterEnum.optional(),
|
|
21182
21433
|
/**
|
|
@@ -23406,6 +23657,10 @@ var OsdSourceSchema = discriminatedUnion("kind", [
|
|
|
23406
23657
|
capName: string().min(1).max(64),
|
|
23407
23658
|
/** Dot path inside the slice, e.g. `detected`, `value`, `mode`. */
|
|
23408
23659
|
valuePath: string().min(1).max(64)
|
|
23660
|
+
}),
|
|
23661
|
+
object({
|
|
23662
|
+
kind: literal("latest-recognition"),
|
|
23663
|
+
recognition: _enum(["person", "plate"])
|
|
23409
23664
|
})
|
|
23410
23665
|
]);
|
|
23411
23666
|
var OsdSlotBindingSchema = object({
|
|
@@ -23511,6 +23766,15 @@ method(object({ deviceId: number().int() }), object({
|
|
|
23511
23766
|
}), object({ success: literal(true) }), {
|
|
23512
23767
|
kind: "mutation",
|
|
23513
23768
|
auth: "admin"
|
|
23769
|
+
}), method(object({
|
|
23770
|
+
sourceDeviceId: number().int(),
|
|
23771
|
+
targetDeviceId: number().int()
|
|
23772
|
+
}), object({
|
|
23773
|
+
copied: number().int().nonnegative(),
|
|
23774
|
+
skipped: number().int().nonnegative()
|
|
23775
|
+
}), {
|
|
23776
|
+
kind: "mutation",
|
|
23777
|
+
auth: "admin"
|
|
23514
23778
|
}), method(object({
|
|
23515
23779
|
deviceId: number().int(),
|
|
23516
23780
|
slotId: string().min(1),
|
|
@@ -24608,13 +24872,19 @@ method(object({
|
|
|
24608
24872
|
}), {
|
|
24609
24873
|
kind: "mutation",
|
|
24610
24874
|
auth: "admin"
|
|
24611
|
-
}), method(
|
|
24875
|
+
}), method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
|
|
24612
24876
|
kind: "mutation",
|
|
24613
24877
|
auth: "admin"
|
|
24614
|
-
}), method(object({
|
|
24615
|
-
kind: "
|
|
24878
|
+
}), method(StorageMigrationLeaseInputSchema, object({ resumed: literal(true) }), {
|
|
24879
|
+
kind: "mutation",
|
|
24880
|
+
auth: "admin"
|
|
24881
|
+
}), method(StorageMigrationLeaseInputSchema, object({ refreshed: literal(true) }), {
|
|
24882
|
+
kind: "mutation",
|
|
24616
24883
|
auth: "admin"
|
|
24617
|
-
}), method(object({ jobId: string() }),
|
|
24884
|
+
}), method(StorageMigrationFootageMoveInputSchema, object({ jobId: string() }), {
|
|
24885
|
+
kind: "mutation",
|
|
24886
|
+
auth: "admin"
|
|
24887
|
+
}), method(object({ jobId: string() }), RelocateJobSchema.nullable(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
|
|
24618
24888
|
kind: "mutation",
|
|
24619
24889
|
auth: "admin"
|
|
24620
24890
|
});
|
|
@@ -30204,6 +30474,12 @@ Object.freeze({
|
|
|
30204
30474
|
addonId: null,
|
|
30205
30475
|
access: "delete"
|
|
30206
30476
|
},
|
|
30477
|
+
"osdManager.copyDeviceConfiguration": {
|
|
30478
|
+
capName: "osd-manager",
|
|
30479
|
+
capScope: "system",
|
|
30480
|
+
addonId: null,
|
|
30481
|
+
access: "create"
|
|
30482
|
+
},
|
|
30207
30483
|
"osdManager.getConditionSupport": {
|
|
30208
30484
|
capName: "osd-manager",
|
|
30209
30485
|
capScope: "system",
|
|
@@ -30300,7 +30576,7 @@ Object.freeze({
|
|
|
30300
30576
|
addonId: null,
|
|
30301
30577
|
access: "create"
|
|
30302
30578
|
},
|
|
30303
|
-
"pipelineAnalytics.
|
|
30579
|
+
"pipelineAnalytics.cancelStorageMigrationMove": {
|
|
30304
30580
|
capName: "pipeline-analytics",
|
|
30305
30581
|
capScope: "device",
|
|
30306
30582
|
addonId: null,
|
|
@@ -30372,12 +30648,6 @@ Object.freeze({
|
|
|
30372
30648
|
addonId: null,
|
|
30373
30649
|
access: "view"
|
|
30374
30650
|
},
|
|
30375
|
-
"pipelineAnalytics.getMediaRelocateStatus": {
|
|
30376
|
-
capName: "pipeline-analytics",
|
|
30377
|
-
capScope: "device",
|
|
30378
|
-
addonId: null,
|
|
30379
|
-
access: "view"
|
|
30380
|
-
},
|
|
30381
30651
|
"pipelineAnalytics.getMotionEvents": {
|
|
30382
30652
|
capName: "pipeline-analytics",
|
|
30383
30653
|
capScope: "device",
|
|
@@ -30414,6 +30684,12 @@ Object.freeze({
|
|
|
30414
30684
|
addonId: null,
|
|
30415
30685
|
access: "view"
|
|
30416
30686
|
},
|
|
30687
|
+
"pipelineAnalytics.getStorageMigrationMoveStatus": {
|
|
30688
|
+
capName: "pipeline-analytics",
|
|
30689
|
+
capScope: "device",
|
|
30690
|
+
addonId: null,
|
|
30691
|
+
access: "view"
|
|
30692
|
+
},
|
|
30417
30693
|
"pipelineAnalytics.getTrack": {
|
|
30418
30694
|
capName: "pipeline-analytics",
|
|
30419
30695
|
capScope: "device",
|
|
@@ -30492,6 +30768,12 @@ Object.freeze({
|
|
|
30492
30768
|
addonId: null,
|
|
30493
30769
|
access: "view"
|
|
30494
30770
|
},
|
|
30771
|
+
"pipelineAnalytics.pauseForStorageMigration": {
|
|
30772
|
+
capName: "pipeline-analytics",
|
|
30773
|
+
capScope: "device",
|
|
30774
|
+
addonId: null,
|
|
30775
|
+
access: "create"
|
|
30776
|
+
},
|
|
30495
30777
|
"pipelineAnalytics.proposeRetrainAnnotations": {
|
|
30496
30778
|
capName: "pipeline-analytics",
|
|
30497
30779
|
capScope: "device",
|
|
@@ -30522,7 +30804,7 @@ Object.freeze({
|
|
|
30522
30804
|
addonId: null,
|
|
30523
30805
|
access: "create"
|
|
30524
30806
|
},
|
|
30525
|
-
"pipelineAnalytics.
|
|
30807
|
+
"pipelineAnalytics.refreshStorageLocationsForMigration": {
|
|
30526
30808
|
capName: "pipeline-analytics",
|
|
30527
30809
|
capScope: "device",
|
|
30528
30810
|
addonId: null,
|
|
@@ -30534,6 +30816,12 @@ Object.freeze({
|
|
|
30534
30816
|
addonId: null,
|
|
30535
30817
|
access: "create"
|
|
30536
30818
|
},
|
|
30819
|
+
"pipelineAnalytics.resumeForStorageMigration": {
|
|
30820
|
+
capName: "pipeline-analytics",
|
|
30821
|
+
capScope: "device",
|
|
30822
|
+
addonId: null,
|
|
30823
|
+
access: "create"
|
|
30824
|
+
},
|
|
30537
30825
|
"pipelineAnalytics.saveRetrainAnnotations": {
|
|
30538
30826
|
capName: "pipeline-analytics",
|
|
30539
30827
|
capScope: "device",
|
|
@@ -30558,6 +30846,12 @@ Object.freeze({
|
|
|
30558
30846
|
addonId: null,
|
|
30559
30847
|
access: "create"
|
|
30560
30848
|
},
|
|
30849
|
+
"pipelineAnalytics.startStorageMigrationMove": {
|
|
30850
|
+
capName: "pipeline-analytics",
|
|
30851
|
+
capScope: "device",
|
|
30852
|
+
addonId: null,
|
|
30853
|
+
access: "create"
|
|
30854
|
+
},
|
|
30561
30855
|
"pipelineAnalytics.wipeAllAnalytics": {
|
|
30562
30856
|
capName: "pipeline-analytics",
|
|
30563
30857
|
capScope: "device",
|
|
@@ -30924,6 +31218,12 @@ Object.freeze({
|
|
|
30924
31218
|
addonId: null,
|
|
30925
31219
|
access: "view"
|
|
30926
31220
|
},
|
|
31221
|
+
"pipelineOrchestrator.pauseForStorageMigration": {
|
|
31222
|
+
capName: "pipeline-orchestrator",
|
|
31223
|
+
capScope: "system",
|
|
31224
|
+
addonId: null,
|
|
31225
|
+
access: "create"
|
|
31226
|
+
},
|
|
30927
31227
|
"pipelineOrchestrator.rebalance": {
|
|
30928
31228
|
capName: "pipeline-orchestrator",
|
|
30929
31229
|
capScope: "system",
|
|
@@ -30948,6 +31248,12 @@ Object.freeze({
|
|
|
30948
31248
|
addonId: null,
|
|
30949
31249
|
access: "view"
|
|
30950
31250
|
},
|
|
31251
|
+
"pipelineOrchestrator.resumeForStorageMigration": {
|
|
31252
|
+
capName: "pipeline-orchestrator",
|
|
31253
|
+
capScope: "system",
|
|
31254
|
+
addonId: null,
|
|
31255
|
+
access: "create"
|
|
31256
|
+
},
|
|
30951
31257
|
"pipelineOrchestrator.saveTemplate": {
|
|
30952
31258
|
capName: "pipeline-orchestrator",
|
|
30953
31259
|
capScope: "system",
|
|
@@ -31344,7 +31650,7 @@ Object.freeze({
|
|
|
31344
31650
|
addonId: null,
|
|
31345
31651
|
access: "create"
|
|
31346
31652
|
},
|
|
31347
|
-
"recording.
|
|
31653
|
+
"recording.cancelStorageMigrationMove": {
|
|
31348
31654
|
capName: "recording",
|
|
31349
31655
|
capScope: "system",
|
|
31350
31656
|
addonId: null,
|
|
@@ -31380,7 +31686,7 @@ Object.freeze({
|
|
|
31380
31686
|
addonId: null,
|
|
31381
31687
|
access: "view"
|
|
31382
31688
|
},
|
|
31383
|
-
"recording.
|
|
31689
|
+
"recording.getStorageMigrationMoveStatus": {
|
|
31384
31690
|
capName: "recording",
|
|
31385
31691
|
capScope: "system",
|
|
31386
31692
|
addonId: null,
|
|
@@ -31404,6 +31710,12 @@ Object.freeze({
|
|
|
31404
31710
|
addonId: null,
|
|
31405
31711
|
access: "view"
|
|
31406
31712
|
},
|
|
31713
|
+
"recording.pauseForStorageMigration": {
|
|
31714
|
+
capName: "recording",
|
|
31715
|
+
capScope: "system",
|
|
31716
|
+
addonId: null,
|
|
31717
|
+
access: "create"
|
|
31718
|
+
},
|
|
31407
31719
|
"recording.pruneFootage": {
|
|
31408
31720
|
capName: "recording",
|
|
31409
31721
|
capScope: "system",
|
|
@@ -31422,7 +31734,7 @@ Object.freeze({
|
|
|
31422
31734
|
addonId: null,
|
|
31423
31735
|
access: "view"
|
|
31424
31736
|
},
|
|
31425
|
-
"recording.
|
|
31737
|
+
"recording.refreshStorageLocationsForMigration": {
|
|
31426
31738
|
capName: "recording",
|
|
31427
31739
|
capScope: "system",
|
|
31428
31740
|
addonId: null,
|
|
@@ -31446,12 +31758,24 @@ Object.freeze({
|
|
|
31446
31758
|
addonId: null,
|
|
31447
31759
|
access: "create"
|
|
31448
31760
|
},
|
|
31761
|
+
"recording.resumeForStorageMigration": {
|
|
31762
|
+
capName: "recording",
|
|
31763
|
+
capScope: "system",
|
|
31764
|
+
addonId: null,
|
|
31765
|
+
access: "create"
|
|
31766
|
+
},
|
|
31449
31767
|
"recording.setDeviceConfig": {
|
|
31450
31768
|
capName: "recording",
|
|
31451
31769
|
capScope: "system",
|
|
31452
31770
|
addonId: null,
|
|
31453
31771
|
access: "create"
|
|
31454
31772
|
},
|
|
31773
|
+
"recording.startStorageMigrationMove": {
|
|
31774
|
+
capName: "recording",
|
|
31775
|
+
capScope: "system",
|
|
31776
|
+
addonId: null,
|
|
31777
|
+
access: "create"
|
|
31778
|
+
},
|
|
31455
31779
|
"recordingExport.cancelExport": {
|
|
31456
31780
|
capName: "recordingExport",
|
|
31457
31781
|
capScope: "system",
|
|
@@ -31842,6 +32166,30 @@ Object.freeze({
|
|
|
31842
32166
|
addonId: null,
|
|
31843
32167
|
access: "view"
|
|
31844
32168
|
},
|
|
32169
|
+
"storageMigration.cancel": {
|
|
32170
|
+
capName: "storage-migration",
|
|
32171
|
+
capScope: "system",
|
|
32172
|
+
addonId: null,
|
|
32173
|
+
access: "create"
|
|
32174
|
+
},
|
|
32175
|
+
"storageMigration.plan": {
|
|
32176
|
+
capName: "storage-migration",
|
|
32177
|
+
capScope: "system",
|
|
32178
|
+
addonId: null,
|
|
32179
|
+
access: "view"
|
|
32180
|
+
},
|
|
32181
|
+
"storageMigration.start": {
|
|
32182
|
+
capName: "storage-migration",
|
|
32183
|
+
capScope: "system",
|
|
32184
|
+
addonId: null,
|
|
32185
|
+
access: "create"
|
|
32186
|
+
},
|
|
32187
|
+
"storageMigration.status": {
|
|
32188
|
+
capName: "storage-migration",
|
|
32189
|
+
capScope: "system",
|
|
32190
|
+
addonId: null,
|
|
32191
|
+
access: "view"
|
|
32192
|
+
},
|
|
31845
32193
|
"storageProvider.abortUpload": {
|
|
31846
32194
|
capName: "storage-provider",
|
|
31847
32195
|
capScope: "system",
|
|
@@ -32220,12 +32568,42 @@ Object.freeze({
|
|
|
32220
32568
|
addonId: null,
|
|
32221
32569
|
access: "create"
|
|
32222
32570
|
},
|
|
32571
|
+
"terminalSession.adoptLegacyMonitor": {
|
|
32572
|
+
capName: "terminal-session",
|
|
32573
|
+
capScope: "system",
|
|
32574
|
+
addonId: null,
|
|
32575
|
+
access: "create"
|
|
32576
|
+
},
|
|
32223
32577
|
"terminalSession.close": {
|
|
32224
32578
|
capName: "terminal-session",
|
|
32225
32579
|
capScope: "system",
|
|
32226
32580
|
addonId: null,
|
|
32227
32581
|
access: "create"
|
|
32228
32582
|
},
|
|
32583
|
+
"terminalSession.createInstance": {
|
|
32584
|
+
capName: "terminal-session",
|
|
32585
|
+
capScope: "system",
|
|
32586
|
+
addonId: null,
|
|
32587
|
+
access: "create"
|
|
32588
|
+
},
|
|
32589
|
+
"terminalSession.deleteInstance": {
|
|
32590
|
+
capName: "terminal-session",
|
|
32591
|
+
capScope: "system",
|
|
32592
|
+
addonId: null,
|
|
32593
|
+
access: "delete"
|
|
32594
|
+
},
|
|
32595
|
+
"terminalSession.listInstances": {
|
|
32596
|
+
capName: "terminal-session",
|
|
32597
|
+
capScope: "system",
|
|
32598
|
+
addonId: null,
|
|
32599
|
+
access: "view"
|
|
32600
|
+
},
|
|
32601
|
+
"terminalSession.listLegacyCameras": {
|
|
32602
|
+
capName: "terminal-session",
|
|
32603
|
+
capScope: "system",
|
|
32604
|
+
addonId: null,
|
|
32605
|
+
access: "view"
|
|
32606
|
+
},
|
|
32229
32607
|
"terminalSession.listProfiles": {
|
|
32230
32608
|
capName: "terminal-session",
|
|
32231
32609
|
capScope: "system",
|
|
@@ -32256,6 +32634,12 @@ Object.freeze({
|
|
|
32256
32634
|
addonId: null,
|
|
32257
32635
|
access: "create"
|
|
32258
32636
|
},
|
|
32637
|
+
"terminalSession.setInstanceEnabled": {
|
|
32638
|
+
capName: "terminal-session",
|
|
32639
|
+
capScope: "system",
|
|
32640
|
+
addonId: null,
|
|
32641
|
+
access: "create"
|
|
32642
|
+
},
|
|
32259
32643
|
"terminalSession.writeInput": {
|
|
32260
32644
|
capName: "terminal-session",
|
|
32261
32645
|
capScope: "system",
|
|
@@ -33056,6 +33440,41 @@ function createNodePtySpawner() {
|
|
|
33056
33440
|
async function warmNodePty() {
|
|
33057
33441
|
await loadNodePty();
|
|
33058
33442
|
}
|
|
33443
|
+
//#endregion
|
|
33444
|
+
//#region src/terminal-camera-declarations.ts
|
|
33445
|
+
/**
|
|
33446
|
+
* Feed DeclaredDevices every live declaration plus one deterministic orphan
|
|
33447
|
+
* batch. The generic sweep intentionally refuses an over-limit set; selecting
|
|
33448
|
+
* a batch here drains large historical Terminal orphan sets across convergence
|
|
33449
|
+
* passes without weakening that global safety guard.
|
|
33450
|
+
*/
|
|
33451
|
+
function terminalCameraReconciliationIndex(rows, integrationId, declarations, managedStableIds = /* @__PURE__ */ new Set()) {
|
|
33452
|
+
if (!integrationId) return [];
|
|
33453
|
+
const declared = new Set(declarations.map((camera) => camera.stableId));
|
|
33454
|
+
const owned = rows.filter((row) => row.integrationId === integrationId && (declared.has(row.stableId) || managedStableIds.has(row.stableId) || row.stableId.startsWith("terminal-camera-instance-")));
|
|
33455
|
+
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)];
|
|
33456
|
+
}
|
|
33457
|
+
/** Explicit persisted instances, never the node × profile template matrix. */
|
|
33458
|
+
function buildTerminalInstanceCameraDeclarations(instances) {
|
|
33459
|
+
return instances.filter((instance) => instance.enabled).map((instance) => ({
|
|
33460
|
+
stableId: instance.cameraStableId,
|
|
33461
|
+
name: instance.name,
|
|
33462
|
+
config: {
|
|
33463
|
+
instanceId: instance.id,
|
|
33464
|
+
nodeId: instance.nodeId,
|
|
33465
|
+
profileId: instance.profileId,
|
|
33466
|
+
profileLabel: instance.profileLabel
|
|
33467
|
+
}
|
|
33468
|
+
}));
|
|
33469
|
+
}
|
|
33470
|
+
/**
|
|
33471
|
+
* `DeviceConfig` materializes schema defaults in memory, so comparing
|
|
33472
|
+
* `config.get()` cannot detect a legacy `{ nodeId }` row. Reconciliation must
|
|
33473
|
+
* inspect the raw persisted blob to make the profile migration durable.
|
|
33474
|
+
*/
|
|
33475
|
+
function needsTerminalCameraConfigMigration(persistedConfig, declaration) {
|
|
33476
|
+
return persistedConfig.instanceId !== declaration.config.instanceId || persistedConfig.nodeId !== declaration.config.nodeId || persistedConfig.profileId !== declaration.config.profileId || persistedConfig.profileLabel !== declaration.config.profileLabel;
|
|
33477
|
+
}
|
|
33059
33478
|
function escapeXml(value) {
|
|
33060
33479
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
33061
33480
|
}
|
|
@@ -33070,40 +33489,66 @@ async function renderTerminalJpeg(lines) {
|
|
|
33070
33489
|
}
|
|
33071
33490
|
//#endregion
|
|
33072
33491
|
//#region src/terminal-camera-device.ts
|
|
33073
|
-
var terminalCameraSchema = object({
|
|
33492
|
+
var terminalCameraSchema = object({
|
|
33493
|
+
instanceId: string().min(1).optional(),
|
|
33494
|
+
nodeId: string().min(1),
|
|
33495
|
+
profileId: string().min(1).default("monitor"),
|
|
33496
|
+
profileLabel: string().min(1).default("BTM")
|
|
33497
|
+
});
|
|
33074
33498
|
var relay = null;
|
|
33075
33499
|
function installTerminalCameraRelay(next) {
|
|
33076
33500
|
relay = next;
|
|
33077
33501
|
}
|
|
33078
33502
|
var TerminalCameraDevice = class extends BaseDevice {
|
|
33079
|
-
features = [];
|
|
33503
|
+
features = [DeviceFeature.NativeSnapshot];
|
|
33080
33504
|
constructor(ctx) {
|
|
33081
33505
|
super(ctx, terminalCameraSchema, { type: ctx.deviceMeta.type });
|
|
33082
33506
|
this.ctx.registerNativeCap(streamCatalogCapability, { getCatalog: async ({ deviceId }) => {
|
|
33083
33507
|
if (deviceId !== this.id) return [];
|
|
33084
33508
|
return this.catalog();
|
|
33085
33509
|
} });
|
|
33510
|
+
this.ctx.registerNativeCap(snapshotCapability, {
|
|
33511
|
+
getSnapshot: async ({ deviceId }) => {
|
|
33512
|
+
if (deviceId !== this.id) throw new Error(`TerminalCameraDevice: deviceId mismatch, expected ${String(this.id)}, got ${String(deviceId)}`);
|
|
33513
|
+
const activeRelay = relay;
|
|
33514
|
+
if (!activeRelay) throw new Error("terminal camera relay is unavailable");
|
|
33515
|
+
return {
|
|
33516
|
+
base64: (await activeRelay.snapshot(this.relayInstanceId(), this.config.get("nodeId"), this.config.get("profileId"))).toString("base64"),
|
|
33517
|
+
contentType: "image/jpeg"
|
|
33518
|
+
};
|
|
33519
|
+
},
|
|
33520
|
+
invalidateCache: async () => {}
|
|
33521
|
+
});
|
|
33086
33522
|
this.markOnline(true);
|
|
33087
33523
|
}
|
|
33088
33524
|
async catalog() {
|
|
33089
33525
|
const activeRelay = relay;
|
|
33090
33526
|
if (!activeRelay) throw new Error("terminal camera relay is unavailable");
|
|
33091
33527
|
const nodeId = this.config.get("nodeId");
|
|
33092
|
-
|
|
33093
|
-
|
|
33528
|
+
const profileId = this.config.get("profileId");
|
|
33529
|
+
const instanceId = this.relayInstanceId();
|
|
33530
|
+
return [{
|
|
33531
|
+
camStreamId: profileId,
|
|
33094
33532
|
kind: "pull-http",
|
|
33095
|
-
url: activeRelay.streamUrl(nodeId,
|
|
33533
|
+
url: activeRelay.streamUrl(instanceId, nodeId, profileId),
|
|
33096
33534
|
codec: "h264",
|
|
33097
33535
|
resolution: {
|
|
33098
33536
|
width: 960,
|
|
33099
33537
|
height: 640
|
|
33100
33538
|
},
|
|
33101
33539
|
fps: 2,
|
|
33102
|
-
label:
|
|
33103
|
-
}
|
|
33540
|
+
label: this.config.get("profileLabel")
|
|
33541
|
+
}];
|
|
33104
33542
|
}
|
|
33105
33543
|
setNodeOnline(online) {
|
|
33106
33544
|
this.markOnline(online);
|
|
33545
|
+
if (!online) relay?.closeInstance(this.relayInstanceId());
|
|
33546
|
+
}
|
|
33547
|
+
async removeDevice() {
|
|
33548
|
+
await relay?.closeInstance(this.relayInstanceId());
|
|
33549
|
+
}
|
|
33550
|
+
relayInstanceId() {
|
|
33551
|
+
return this.config.get("instanceId") ?? `legacy:${this.stableId}`;
|
|
33107
33552
|
}
|
|
33108
33553
|
};
|
|
33109
33554
|
//#endregion
|
|
@@ -37952,18 +38397,24 @@ function createXtermScreen(cols, rows) {
|
|
|
37952
38397
|
//#region src/terminal-camera-relay.ts
|
|
37953
38398
|
var MJPEG_BOUNDARY = "camstack-terminal-frame";
|
|
37954
38399
|
var SESSION_IDLE_MS = 3e4;
|
|
37955
|
-
|
|
37956
|
-
|
|
38400
|
+
var SNAPSHOT_STARTUP_WAIT_MS = 1500;
|
|
38401
|
+
var CLOSE_RETRY_BASE_MS = 50;
|
|
38402
|
+
var CLOSE_RETRY_MAX_MS = 1e3;
|
|
38403
|
+
var CLOSE_ATTEMPTS_PER_PASS = 3;
|
|
38404
|
+
function relayKey(instanceId) {
|
|
38405
|
+
return instanceId;
|
|
37957
38406
|
}
|
|
37958
38407
|
function parseStreamPath(url) {
|
|
37959
38408
|
const parts = ((url ?? "").split("?")[0] ?? "").split("/").filter(Boolean);
|
|
37960
|
-
if (parts.length !==
|
|
38409
|
+
if (parts.length !== 4 || parts[0] !== "stream") return null;
|
|
37961
38410
|
try {
|
|
37962
|
-
const
|
|
37963
|
-
const
|
|
38411
|
+
const instanceId = decodeURIComponent(parts[1] ?? "");
|
|
38412
|
+
const nodeId = decodeURIComponent(parts[2] ?? "");
|
|
38413
|
+
const profilePart = parts[3] ?? "";
|
|
37964
38414
|
if (!profilePart.endsWith(".mjpeg")) return null;
|
|
37965
38415
|
const profileId = decodeURIComponent(profilePart.slice(0, -6));
|
|
37966
|
-
return nodeId && profileId ? {
|
|
38416
|
+
return instanceId && nodeId && profileId ? {
|
|
38417
|
+
instanceId,
|
|
37967
38418
|
nodeId,
|
|
37968
38419
|
profileId
|
|
37969
38420
|
} : null;
|
|
@@ -37990,7 +38441,7 @@ var TerminalCameraRelay = class {
|
|
|
37990
38441
|
res.writeHead(404).end();
|
|
37991
38442
|
return;
|
|
37992
38443
|
}
|
|
37993
|
-
this.serve(target.nodeId, target.profileId, res);
|
|
38444
|
+
this.serve(target.instanceId, target.nodeId, target.profileId, res);
|
|
37994
38445
|
});
|
|
37995
38446
|
await new Promise((resolve, reject) => {
|
|
37996
38447
|
server.once("error", reject);
|
|
@@ -38004,55 +38455,105 @@ var TerminalCameraRelay = class {
|
|
|
38004
38455
|
this.server = server;
|
|
38005
38456
|
this.baseUrl = `http://127.0.0.1:${String(address.port)}`;
|
|
38006
38457
|
}
|
|
38007
|
-
streamUrl(nodeId, profileId) {
|
|
38458
|
+
streamUrl(instanceId, nodeId, profileId) {
|
|
38008
38459
|
if (!this.baseUrl) throw new Error("terminal camera relay is not started");
|
|
38009
|
-
return `${this.baseUrl}/stream/${encodeURIComponent(nodeId)}/${encodeURIComponent(profileId)}.mjpeg`;
|
|
38460
|
+
return `${this.baseUrl}/stream/${encodeURIComponent(instanceId)}/${encodeURIComponent(nodeId)}/${encodeURIComponent(profileId)}.mjpeg`;
|
|
38010
38461
|
}
|
|
38011
38462
|
async listProfiles(nodeId) {
|
|
38012
38463
|
return this.api.listProfiles(nodeId);
|
|
38013
38464
|
}
|
|
38014
|
-
state(nodeId, profileId) {
|
|
38015
|
-
const key = relayKey(
|
|
38465
|
+
state(instanceId, nodeId, profileId) {
|
|
38466
|
+
const key = relayKey(instanceId);
|
|
38016
38467
|
const existing = this.states.get(key);
|
|
38017
38468
|
if (existing) return existing;
|
|
38018
38469
|
const created = {
|
|
38470
|
+
instanceId,
|
|
38019
38471
|
nodeId,
|
|
38020
38472
|
profileId,
|
|
38021
38473
|
screen: createXtermScreen(120, 40),
|
|
38022
38474
|
sessionId: null,
|
|
38023
38475
|
cursor: 0,
|
|
38024
38476
|
clients: 0,
|
|
38477
|
+
leases: 0,
|
|
38025
38478
|
jpeg: null,
|
|
38026
38479
|
renderedCursor: -1,
|
|
38027
38480
|
framePromise: null,
|
|
38028
|
-
|
|
38481
|
+
openPromise: null,
|
|
38482
|
+
idleTimer: null,
|
|
38483
|
+
closing: false,
|
|
38484
|
+
closePromise: null,
|
|
38485
|
+
closeRetryTimer: null,
|
|
38486
|
+
closeAttempts: 0,
|
|
38487
|
+
closed: false,
|
|
38488
|
+
responses: /* @__PURE__ */ new Set()
|
|
38029
38489
|
};
|
|
38030
38490
|
this.states.set(key, created);
|
|
38031
38491
|
return created;
|
|
38032
38492
|
}
|
|
38033
38493
|
async ensureSession(state) {
|
|
38494
|
+
if (state.closed || state.closing) throw new Error("terminal camera relay is waiting for prior session cleanup");
|
|
38034
38495
|
if (state.sessionId) return state.sessionId;
|
|
38035
|
-
|
|
38496
|
+
if (state.openPromise) {
|
|
38497
|
+
const opened = await state.openPromise;
|
|
38498
|
+
if (state.closed || state.closing) throw new Error("terminal camera relay state closed while opening its session");
|
|
38499
|
+
return opened.sessionId;
|
|
38500
|
+
}
|
|
38501
|
+
const opening = this.api.openSession(state.nodeId, {
|
|
38036
38502
|
profileId: state.profileId,
|
|
38037
38503
|
cols: 120,
|
|
38038
38504
|
rows: 40
|
|
38039
38505
|
});
|
|
38040
|
-
state.
|
|
38041
|
-
|
|
38042
|
-
|
|
38506
|
+
state.openPromise = opening;
|
|
38507
|
+
try {
|
|
38508
|
+
const opened = await opening;
|
|
38509
|
+
state.openPromise = null;
|
|
38510
|
+
if (state.closed || state.closing) {
|
|
38511
|
+
state.sessionId = opened.sessionId;
|
|
38512
|
+
await this.closeState(state);
|
|
38513
|
+
throw new Error("terminal camera relay state closed while opening its session");
|
|
38514
|
+
}
|
|
38515
|
+
state.sessionId = opened.sessionId;
|
|
38516
|
+
state.cursor = 0;
|
|
38517
|
+
return opened.sessionId;
|
|
38518
|
+
} catch (error) {
|
|
38519
|
+
if (state.closing && !state.sessionId) this.finishClose(state);
|
|
38520
|
+
throw error;
|
|
38521
|
+
} finally {
|
|
38522
|
+
if (state.openPromise === opening) state.openPromise = null;
|
|
38523
|
+
}
|
|
38043
38524
|
}
|
|
38044
|
-
async nextFrame(state) {
|
|
38045
|
-
if (state.framePromise)
|
|
38525
|
+
async nextFrame(state, forceRender = false, waitForInitialOutput = false) {
|
|
38526
|
+
if (state.framePromise) {
|
|
38527
|
+
await state.framePromise;
|
|
38528
|
+
return forceRender ? this.nextFrame(state, true, waitForInitialOutput) : state.jpeg ?? this.nextFrame(state, false, waitForInitialOutput);
|
|
38529
|
+
}
|
|
38046
38530
|
const render = async () => {
|
|
38531
|
+
const openingSession = state.sessionId === null;
|
|
38047
38532
|
const sessionId = await this.ensureSession(state);
|
|
38048
38533
|
let batch;
|
|
38049
38534
|
try {
|
|
38050
38535
|
batch = await this.api.pullOutput(state.nodeId, {
|
|
38051
38536
|
sessionId,
|
|
38052
|
-
afterSeq: state.cursor
|
|
38537
|
+
afterSeq: state.cursor,
|
|
38538
|
+
...openingSession && waitForInitialOutput ? {
|
|
38539
|
+
waitMs: SNAPSHOT_STARTUP_WAIT_MS,
|
|
38540
|
+
waitForOutput: true
|
|
38541
|
+
} : {}
|
|
38053
38542
|
});
|
|
38054
38543
|
} catch (error) {
|
|
38055
|
-
state.sessionId
|
|
38544
|
+
if (state.sessionId === sessionId) {
|
|
38545
|
+
let closed = false;
|
|
38546
|
+
await this.api.closeSession(state.nodeId, sessionId).then(() => {
|
|
38547
|
+
closed = true;
|
|
38548
|
+
}).catch((closeError) => {
|
|
38549
|
+
this.logger.warn("terminal camera session cleanup after output failure failed", { meta: {
|
|
38550
|
+
nodeId: state.nodeId,
|
|
38551
|
+
sessionId,
|
|
38552
|
+
error: closeError instanceof Error ? closeError.message : String(closeError)
|
|
38553
|
+
} });
|
|
38554
|
+
});
|
|
38555
|
+
if (closed) state.sessionId = null;
|
|
38556
|
+
}
|
|
38056
38557
|
state.cursor = 0;
|
|
38057
38558
|
throw error;
|
|
38058
38559
|
}
|
|
@@ -38069,7 +38570,7 @@ var TerminalCameraRelay = class {
|
|
|
38069
38570
|
}
|
|
38070
38571
|
state.cursor = exited ? 0 : batch.cursor;
|
|
38071
38572
|
await state.screen.flush();
|
|
38072
|
-
if (state.jpeg === null || state.renderedCursor !== state.cursor) {
|
|
38573
|
+
if (forceRender || state.jpeg === null || state.renderedCursor !== state.cursor) {
|
|
38073
38574
|
state.jpeg = await renderTerminalJpeg(state.screen.lines());
|
|
38074
38575
|
state.renderedCursor = state.cursor;
|
|
38075
38576
|
}
|
|
@@ -38080,14 +38581,15 @@ var TerminalCameraRelay = class {
|
|
|
38080
38581
|
});
|
|
38081
38582
|
return state.framePromise;
|
|
38082
38583
|
}
|
|
38083
|
-
async serve(nodeId, profileId, res) {
|
|
38584
|
+
async serve(instanceId, nodeId, profileId, res) {
|
|
38084
38585
|
this.responses.add(res);
|
|
38085
|
-
const state = this.state(nodeId, profileId);
|
|
38586
|
+
const state = this.state(instanceId, nodeId, profileId);
|
|
38086
38587
|
if (state.idleTimer) clearTimeout(state.idleTimer);
|
|
38087
38588
|
state.idleTimer = null;
|
|
38088
38589
|
state.clients += 1;
|
|
38590
|
+
state.responses.add(res);
|
|
38089
38591
|
try {
|
|
38090
|
-
const first = await this.nextFrame(state);
|
|
38592
|
+
const first = await this.nextFrame(state, false, true);
|
|
38091
38593
|
res.writeHead(200, {
|
|
38092
38594
|
"content-type": `multipart/x-mixed-replace; boundary=${MJPEG_BOUNDARY}`,
|
|
38093
38595
|
"cache-control": "no-store",
|
|
@@ -38115,11 +38617,13 @@ var TerminalCameraRelay = class {
|
|
|
38115
38617
|
res.end("terminal camera unavailable");
|
|
38116
38618
|
} finally {
|
|
38117
38619
|
this.responses.delete(res);
|
|
38620
|
+
state.responses.delete(res);
|
|
38118
38621
|
state.clients = Math.max(0, state.clients - 1);
|
|
38119
|
-
if (state.clients === 0) this.scheduleIdleClose(state);
|
|
38622
|
+
if (state.clients === 0 && state.leases === 0) this.scheduleIdleClose(state);
|
|
38120
38623
|
}
|
|
38121
38624
|
}
|
|
38122
38625
|
scheduleIdleClose(state) {
|
|
38626
|
+
if (state.closed || state.closing || state.clients > 0 || state.leases > 0) return;
|
|
38123
38627
|
if (state.idleTimer) clearTimeout(state.idleTimer);
|
|
38124
38628
|
state.idleTimer = setTimeout(() => {
|
|
38125
38629
|
this.closeState(state);
|
|
@@ -38127,26 +38631,116 @@ var TerminalCameraRelay = class {
|
|
|
38127
38631
|
state.idleTimer.unref?.();
|
|
38128
38632
|
}
|
|
38129
38633
|
async closeState(state) {
|
|
38130
|
-
if (state.
|
|
38131
|
-
if (state.
|
|
38634
|
+
if (state.closed) return;
|
|
38635
|
+
if (state.clients > 0 || state.leases > 0) return;
|
|
38636
|
+
if (state.closePromise) {
|
|
38637
|
+
await state.closePromise;
|
|
38638
|
+
return;
|
|
38639
|
+
}
|
|
38640
|
+
if (state.openPromise && !state.sessionId) {
|
|
38641
|
+
state.closing = true;
|
|
38642
|
+
if (state.idleTimer) clearTimeout(state.idleTimer);
|
|
38643
|
+
state.idleTimer = null;
|
|
38644
|
+
return;
|
|
38645
|
+
}
|
|
38646
|
+
if (state.closing && !state.sessionId) return;
|
|
38647
|
+
state.closing = true;
|
|
38648
|
+
if (state.idleTimer) clearTimeout(state.idleTimer);
|
|
38649
|
+
state.idleTimer = null;
|
|
38650
|
+
state.closePromise = this.closeWithRetries(state).finally(() => {
|
|
38651
|
+
state.closePromise = null;
|
|
38652
|
+
});
|
|
38653
|
+
await state.closePromise;
|
|
38654
|
+
}
|
|
38655
|
+
async closeWithRetries(state) {
|
|
38656
|
+
const sessionId = state.sessionId;
|
|
38657
|
+
if (!sessionId) {
|
|
38658
|
+
this.finishClose(state);
|
|
38659
|
+
return;
|
|
38660
|
+
}
|
|
38661
|
+
for (let attempt = 0; attempt < CLOSE_ATTEMPTS_PER_PASS; attempt += 1) try {
|
|
38662
|
+
await this.api.closeSession(state.nodeId, sessionId);
|
|
38663
|
+
if (state.sessionId === sessionId) this.finishClose(state);
|
|
38664
|
+
return;
|
|
38665
|
+
} catch (error) {
|
|
38666
|
+
state.closeAttempts += 1;
|
|
38132
38667
|
this.logger.warn("terminal camera session close failed", { meta: {
|
|
38133
38668
|
nodeId: state.nodeId,
|
|
38134
|
-
sessionId
|
|
38669
|
+
sessionId,
|
|
38670
|
+
attempt: state.closeAttempts,
|
|
38135
38671
|
error: error instanceof Error ? error.message : String(error)
|
|
38136
38672
|
} });
|
|
38137
|
-
|
|
38673
|
+
if (attempt + 1 < CLOSE_ATTEMPTS_PER_PASS) await new Promise((resolve) => setTimeout(resolve, this.closeRetryDelay(state.closeAttempts)));
|
|
38674
|
+
}
|
|
38675
|
+
this.scheduleCloseRetry(state);
|
|
38676
|
+
}
|
|
38677
|
+
scheduleCloseRetry(state) {
|
|
38678
|
+
if (state.closeRetryTimer || !state.sessionId) return;
|
|
38679
|
+
state.closeRetryTimer = setTimeout(() => {
|
|
38680
|
+
state.closeRetryTimer = null;
|
|
38681
|
+
state.closing = false;
|
|
38682
|
+
this.closeState(state);
|
|
38683
|
+
}, this.closeRetryDelay(state.closeAttempts));
|
|
38684
|
+
state.closeRetryTimer.unref?.();
|
|
38685
|
+
}
|
|
38686
|
+
closeRetryDelay(attempt) {
|
|
38687
|
+
return Math.min(CLOSE_RETRY_BASE_MS * 2 ** Math.min(attempt - 1, 5), CLOSE_RETRY_MAX_MS);
|
|
38688
|
+
}
|
|
38689
|
+
finishClose(state) {
|
|
38690
|
+
if (state.closed) return;
|
|
38691
|
+
state.closed = true;
|
|
38692
|
+
if (state.closeRetryTimer) clearTimeout(state.closeRetryTimer);
|
|
38693
|
+
state.closeRetryTimer = null;
|
|
38694
|
+
state.sessionId = null;
|
|
38695
|
+
state.closeAttempts = 0;
|
|
38696
|
+
state.closing = false;
|
|
38697
|
+
if (this.states.get(relayKey(state.instanceId)) === state) this.states.delete(relayKey(state.instanceId));
|
|
38138
38698
|
state.screen.dispose();
|
|
38139
|
-
|
|
38699
|
+
}
|
|
38700
|
+
/**
|
|
38701
|
+
* Capture one fresh JPEG using the same xterm renderer as the MJPEG relay.
|
|
38702
|
+
* A snapshot-only caller owns a short lease and tears the state down as soon
|
|
38703
|
+
* as the image is rendered, so snapshots never leave a monitor PTY running.
|
|
38704
|
+
*/
|
|
38705
|
+
async snapshot(instanceId, nodeId, profileId) {
|
|
38706
|
+
const state = this.state(instanceId, nodeId, profileId);
|
|
38707
|
+
if (state.idleTimer) clearTimeout(state.idleTimer);
|
|
38708
|
+
state.idleTimer = null;
|
|
38709
|
+
state.leases += 1;
|
|
38710
|
+
try {
|
|
38711
|
+
return await this.nextFrame(state, true, true);
|
|
38712
|
+
} finally {
|
|
38713
|
+
state.leases = Math.max(0, state.leases - 1);
|
|
38714
|
+
if (state.clients === 0 && state.leases === 0) await this.closeState(state);
|
|
38715
|
+
}
|
|
38716
|
+
}
|
|
38717
|
+
/** Stop a withdrawn/offline camera's relay, including active HTTP readers. */
|
|
38718
|
+
async closeInstance(instanceId) {
|
|
38719
|
+
const state = this.states.get(relayKey(instanceId));
|
|
38720
|
+
if (!state) return;
|
|
38721
|
+
for (const response of state.responses) response.destroy();
|
|
38722
|
+
state.responses.clear();
|
|
38723
|
+
state.clients = 0;
|
|
38724
|
+
state.leases = 0;
|
|
38725
|
+
await this.closeState(state);
|
|
38726
|
+
if (state.openPromise) {
|
|
38727
|
+
await state.openPromise.catch(() => {});
|
|
38728
|
+
await this.closeState(state);
|
|
38729
|
+
}
|
|
38140
38730
|
}
|
|
38141
38731
|
async dispose() {
|
|
38142
38732
|
for (const response of this.responses) response.destroy();
|
|
38143
38733
|
this.responses.clear();
|
|
38144
|
-
for (const state of this.states.values()) {
|
|
38734
|
+
for (const state of [...this.states.values()]) {
|
|
38145
38735
|
if (state.idleTimer) clearTimeout(state.idleTimer);
|
|
38146
38736
|
state.clients = 0;
|
|
38737
|
+
state.leases = 0;
|
|
38147
38738
|
await this.closeState(state);
|
|
38739
|
+
if (state.openPromise) {
|
|
38740
|
+
await state.openPromise.catch(() => {});
|
|
38741
|
+
await this.closeState(state);
|
|
38742
|
+
}
|
|
38148
38743
|
}
|
|
38149
|
-
this.states.clear();
|
|
38150
38744
|
if (this.server) {
|
|
38151
38745
|
const server = this.server;
|
|
38152
38746
|
this.server = null;
|
|
@@ -38262,6 +38856,113 @@ function createTerminalDataPlaneHandler(manager) {
|
|
|
38262
38856
|
};
|
|
38263
38857
|
}
|
|
38264
38858
|
//#endregion
|
|
38859
|
+
//#region src/terminal-instances.ts
|
|
38860
|
+
var TerminalInstanceSchema = object({
|
|
38861
|
+
id: string().uuid(),
|
|
38862
|
+
cameraStableId: string().min(1).max(256),
|
|
38863
|
+
nodeId: string().min(1).max(256),
|
|
38864
|
+
profileId: string().min(1).max(64),
|
|
38865
|
+
profileLabel: string().min(1).max(120),
|
|
38866
|
+
name: string().min(1).max(160),
|
|
38867
|
+
enabled: boolean()
|
|
38868
|
+
});
|
|
38869
|
+
/**
|
|
38870
|
+
* Config is operator-writable, so malformed or duplicate rows are ignored
|
|
38871
|
+
* rather than allowed to make declaration reconciliation destructive.
|
|
38872
|
+
*/
|
|
38873
|
+
function readTerminalInstances(raw, onInvalid) {
|
|
38874
|
+
const ids = /* @__PURE__ */ new Set();
|
|
38875
|
+
const stableIds = /* @__PURE__ */ new Set();
|
|
38876
|
+
const instances = [];
|
|
38877
|
+
for (const value of raw) {
|
|
38878
|
+
const parsed = TerminalInstanceSchema.safeParse(value);
|
|
38879
|
+
if (!parsed.success) {
|
|
38880
|
+
onInvalid?.("Ignoring malformed Terminal instance configuration");
|
|
38881
|
+
continue;
|
|
38882
|
+
}
|
|
38883
|
+
const instance = parsed.data;
|
|
38884
|
+
if (ids.has(instance.id) || stableIds.has(instance.cameraStableId)) {
|
|
38885
|
+
onInvalid?.(`Ignoring duplicate Terminal instance ${instance.id}`);
|
|
38886
|
+
continue;
|
|
38887
|
+
}
|
|
38888
|
+
ids.add(instance.id);
|
|
38889
|
+
stableIds.add(instance.cameraStableId);
|
|
38890
|
+
instances.push(instance);
|
|
38891
|
+
}
|
|
38892
|
+
return instances;
|
|
38893
|
+
}
|
|
38894
|
+
function newTerminalCameraStableId(instanceId) {
|
|
38895
|
+
return `terminal-camera-instance-${instanceId}`;
|
|
38896
|
+
}
|
|
38897
|
+
/**
|
|
38898
|
+
* Legacy automatic cameras are migration candidates only. A tombstone is
|
|
38899
|
+
* durable deletion intent, so a lingering failed device removal must never
|
|
38900
|
+
* make that camera adoptable again.
|
|
38901
|
+
*/
|
|
38902
|
+
function listLegacyTerminalCameraCandidates(rows, instanceStableIds, tombstones) {
|
|
38903
|
+
const legacy = [];
|
|
38904
|
+
for (const row of rows) {
|
|
38905
|
+
if (tombstones.has(row.stableId) || instanceStableIds.has(row.stableId) || row.stableId.startsWith("terminal-camera-instance-") || !row.stableId.startsWith("terminal-camera-")) continue;
|
|
38906
|
+
const nodeId = typeof row.config.nodeId === "string" ? row.config.nodeId : null;
|
|
38907
|
+
if (!nodeId) continue;
|
|
38908
|
+
const profileId = typeof row.config.profileId === "string" ? row.config.profileId : "monitor";
|
|
38909
|
+
const profileLabel = typeof row.config.profileLabel === "string" ? row.config.profileLabel : profileId === "monitor" ? "BTM" : profileId;
|
|
38910
|
+
legacy.push({
|
|
38911
|
+
stableId: row.stableId,
|
|
38912
|
+
nodeId,
|
|
38913
|
+
profileId,
|
|
38914
|
+
profileLabel,
|
|
38915
|
+
name: row.name,
|
|
38916
|
+
adoptable: profileId === "monitor" && row.stableId === `terminal-camera-${nodeId}`
|
|
38917
|
+
});
|
|
38918
|
+
}
|
|
38919
|
+
return legacy;
|
|
38920
|
+
}
|
|
38921
|
+
/** Serializes config read-modify-write operations and their reconciliation. */
|
|
38922
|
+
var TerminalInstanceMutationQueue = class {
|
|
38923
|
+
tail = Promise.resolve();
|
|
38924
|
+
async run(mutation) {
|
|
38925
|
+
const previous = this.tail;
|
|
38926
|
+
let release;
|
|
38927
|
+
this.tail = new Promise((resolve) => {
|
|
38928
|
+
release = resolve;
|
|
38929
|
+
});
|
|
38930
|
+
await previous;
|
|
38931
|
+
try {
|
|
38932
|
+
return await mutation();
|
|
38933
|
+
} finally {
|
|
38934
|
+
release?.();
|
|
38935
|
+
}
|
|
38936
|
+
}
|
|
38937
|
+
};
|
|
38938
|
+
/**
|
|
38939
|
+
* Coalesces periodic/config reconciliation requests onto the same serialized
|
|
38940
|
+
* lane as instance mutations. A pass never applies a declaration snapshot
|
|
38941
|
+
* concurrently with a create/delete/enable write.
|
|
38942
|
+
*/
|
|
38943
|
+
var TerminalInstanceReconcileCoordinator = class {
|
|
38944
|
+
queue;
|
|
38945
|
+
dirty = false;
|
|
38946
|
+
running = null;
|
|
38947
|
+
constructor(queue) {
|
|
38948
|
+
this.queue = queue;
|
|
38949
|
+
}
|
|
38950
|
+
request(apply) {
|
|
38951
|
+
this.dirty = true;
|
|
38952
|
+
if (this.running) return this.running;
|
|
38953
|
+
const running = this.queue.run(async () => {
|
|
38954
|
+
while (this.dirty) {
|
|
38955
|
+
this.dirty = false;
|
|
38956
|
+
await apply();
|
|
38957
|
+
}
|
|
38958
|
+
});
|
|
38959
|
+
this.running = running.finally(() => {
|
|
38960
|
+
this.running = null;
|
|
38961
|
+
});
|
|
38962
|
+
return this.running;
|
|
38963
|
+
}
|
|
38964
|
+
};
|
|
38965
|
+
//#endregion
|
|
38265
38966
|
//#region src/profiles.ts
|
|
38266
38967
|
/**
|
|
38267
38968
|
* The allowlist of programs an operator may open. The capability accepts a
|
|
@@ -38272,7 +38973,12 @@ function createTerminalDataPlaneHandler(manager) {
|
|
|
38272
38973
|
var ProfileIdSchema = string().trim().min(1).max(64).regex(/^[a-z][a-z0-9-]*$/, "must be a lowercase slug");
|
|
38273
38974
|
var ConfiguredTerminalProfileSchema = object({
|
|
38274
38975
|
enabled: boolean().default(true),
|
|
38275
|
-
profileId: ProfileIdSchema.refine((id) =>
|
|
38976
|
+
profileId: ProfileIdSchema.refine((id) => ![
|
|
38977
|
+
"monitor",
|
|
38978
|
+
"top",
|
|
38979
|
+
"glances",
|
|
38980
|
+
"shell"
|
|
38981
|
+
].includes(id), { message: "monitor, top, glances and shell are reserved profile IDs" }),
|
|
38276
38982
|
label: string().trim().min(1).max(100),
|
|
38277
38983
|
description: string().trim().max(500).optional().default(""),
|
|
38278
38984
|
executable: string().trim().min(1).max(1024).refine((value) => !value.includes("\0"), { message: "must not contain NUL bytes" }),
|
|
@@ -38287,17 +38993,36 @@ var ConfiguredTerminalProfileSchema = object({
|
|
|
38287
38993
|
* surface.
|
|
38288
38994
|
*/
|
|
38289
38995
|
function buildProfiles(options) {
|
|
38290
|
-
const profiles = [
|
|
38996
|
+
const profiles = [];
|
|
38997
|
+
if (options.btmEnabled !== false) profiles.push({
|
|
38291
38998
|
profileId: "monitor",
|
|
38292
|
-
label: "
|
|
38999
|
+
label: "BTM",
|
|
38293
39000
|
description: "bottom (btm) — CPU, memory, network and process monitor",
|
|
38294
39001
|
file: options.btmPath.trim().length > 0 ? options.btmPath.trim() : "btm",
|
|
38295
|
-
args: []
|
|
38296
|
-
|
|
38297
|
-
|
|
38298
|
-
|
|
38299
|
-
|
|
38300
|
-
|
|
39002
|
+
args: options.btmArgs ?? []
|
|
39003
|
+
});
|
|
39004
|
+
if (options.topEnabled !== false) profiles.push({
|
|
39005
|
+
profileId: "top",
|
|
39006
|
+
label: "Top",
|
|
39007
|
+
description: "The operating system process and resource monitor",
|
|
39008
|
+
file: options.topPath?.trim() || "top",
|
|
39009
|
+
args: options.topArgs ?? []
|
|
39010
|
+
});
|
|
39011
|
+
if (options.glancesEnabled !== false) {
|
|
39012
|
+
const configuredPath = options.glancesPath?.trim();
|
|
39013
|
+
const pythonPath = options.glancesPythonPath?.trim();
|
|
39014
|
+
profiles.push({
|
|
39015
|
+
profileId: "glances",
|
|
39016
|
+
label: "Glances",
|
|
39017
|
+
description: "Cross-platform curses monitor installed in CamStack embedded Python",
|
|
39018
|
+
file: configuredPath || pythonPath || "glances",
|
|
39019
|
+
args: configuredPath || !pythonPath ? options.glancesArgs ?? [] : [
|
|
39020
|
+
"-m",
|
|
39021
|
+
"glances",
|
|
39022
|
+
...options.glancesArgs ?? []
|
|
39023
|
+
]
|
|
39024
|
+
});
|
|
39025
|
+
}
|
|
38301
39026
|
if (options.allowShell) profiles.push({
|
|
38302
39027
|
profileId: "shell",
|
|
38303
39028
|
label: "Shell (interactive)",
|
|
@@ -38366,10 +39091,20 @@ var TerminalSessionManager = class {
|
|
|
38366
39091
|
now;
|
|
38367
39092
|
resolveBinary;
|
|
38368
39093
|
maxSessions;
|
|
39094
|
+
instanceControl = null;
|
|
38369
39095
|
constructor(opts) {
|
|
38370
39096
|
this.opts = opts;
|
|
38371
39097
|
this.profiles = buildProfiles({
|
|
38372
39098
|
btmPath: opts.btmPath,
|
|
39099
|
+
btmEnabled: opts.btmEnabled,
|
|
39100
|
+
btmArgs: opts.btmArgs,
|
|
39101
|
+
topEnabled: opts.topEnabled,
|
|
39102
|
+
topPath: opts.topPath,
|
|
39103
|
+
topArgs: opts.topArgs,
|
|
39104
|
+
glancesEnabled: opts.glancesEnabled,
|
|
39105
|
+
glancesPath: opts.glancesPath,
|
|
39106
|
+
glancesArgs: opts.glancesArgs,
|
|
39107
|
+
glancesPythonPath: opts.glancesPythonPath,
|
|
38373
39108
|
allowShell: opts.allowShell,
|
|
38374
39109
|
shellPath: opts.shellPath,
|
|
38375
39110
|
customProfiles: opts.customProfiles,
|
|
@@ -38395,6 +39130,27 @@ var TerminalSessionManager = class {
|
|
|
38395
39130
|
...p.description !== void 0 ? { description: p.description } : {}
|
|
38396
39131
|
}));
|
|
38397
39132
|
}
|
|
39133
|
+
setInstanceControl(control) {
|
|
39134
|
+
this.instanceControl = control;
|
|
39135
|
+
}
|
|
39136
|
+
async listInstances() {
|
|
39137
|
+
return this.instanceControl?.listInstances() ?? [];
|
|
39138
|
+
}
|
|
39139
|
+
async createInstance(input) {
|
|
39140
|
+
return this.requireInstanceControl().createInstance(input);
|
|
39141
|
+
}
|
|
39142
|
+
async deleteInstance(input) {
|
|
39143
|
+
await this.requireInstanceControl().deleteInstance(input);
|
|
39144
|
+
}
|
|
39145
|
+
async setInstanceEnabled(input) {
|
|
39146
|
+
return this.requireInstanceControl().setInstanceEnabled(input);
|
|
39147
|
+
}
|
|
39148
|
+
async listLegacyCameras() {
|
|
39149
|
+
return this.instanceControl?.listLegacyCameras() ?? [];
|
|
39150
|
+
}
|
|
39151
|
+
async adoptLegacyMonitor(input) {
|
|
39152
|
+
return this.requireInstanceControl().adoptLegacyMonitor(input);
|
|
39153
|
+
}
|
|
38398
39154
|
async listSessions() {
|
|
38399
39155
|
return [...this.sessions.values()].filter((s) => !s.exited).map((s) => s.info);
|
|
38400
39156
|
}
|
|
@@ -38442,10 +39198,12 @@ var TerminalSessionManager = class {
|
|
|
38442
39198
|
outputWaiters: /* @__PURE__ */ new Set(),
|
|
38443
39199
|
outputChars: 0,
|
|
38444
39200
|
nextSeq: 1,
|
|
38445
|
-
exited: false
|
|
39201
|
+
exited: false,
|
|
39202
|
+
disposed: false
|
|
38446
39203
|
};
|
|
38447
39204
|
this.sessions.set(sessionId, session);
|
|
38448
39205
|
pty.onData((data) => {
|
|
39206
|
+
if (session.exited) return;
|
|
38449
39207
|
session.screen.write(data);
|
|
38450
39208
|
this.appendOutput(session, {
|
|
38451
39209
|
kind: "data",
|
|
@@ -38459,27 +39217,7 @@ var TerminalSessionManager = class {
|
|
|
38459
39217
|
} catch {}
|
|
38460
39218
|
});
|
|
38461
39219
|
pty.onExit((event) => {
|
|
38462
|
-
|
|
38463
|
-
session.lastExit = event;
|
|
38464
|
-
this.appendOutput(session, {
|
|
38465
|
-
kind: "exit",
|
|
38466
|
-
exitCode: event.exitCode,
|
|
38467
|
-
...event.signal !== void 0 ? { signal: event.signal } : {}
|
|
38468
|
-
});
|
|
38469
|
-
for (const sink of session.sinks) try {
|
|
38470
|
-
sink({
|
|
38471
|
-
kind: "exit",
|
|
38472
|
-
exitCode: event.exitCode,
|
|
38473
|
-
signal: event.signal
|
|
38474
|
-
});
|
|
38475
|
-
} catch {}
|
|
38476
|
-
session.sinks.clear();
|
|
38477
|
-
const retire = setTimeout(() => {
|
|
38478
|
-
session.screen.dispose();
|
|
38479
|
-
this.sessions.delete(sessionId);
|
|
38480
|
-
}, EXITED_RETENTION_MS);
|
|
38481
|
-
retire.unref?.();
|
|
38482
|
-
session.retireTimer = retire;
|
|
39220
|
+
this.finishSession(sessionId, session, event, true);
|
|
38483
39221
|
});
|
|
38484
39222
|
this.opts.logger.info("terminal: session opened", { meta: {
|
|
38485
39223
|
sessionId,
|
|
@@ -38510,6 +39248,7 @@ var TerminalSessionManager = class {
|
|
|
38510
39248
|
async close(input) {
|
|
38511
39249
|
const session = this.sessions.get(input.sessionId);
|
|
38512
39250
|
if (!session) return;
|
|
39251
|
+
this.finishSession(input.sessionId, session, { exitCode: 0 }, false);
|
|
38513
39252
|
try {
|
|
38514
39253
|
session.pty.kill();
|
|
38515
39254
|
} catch {}
|
|
@@ -38518,7 +39257,7 @@ var TerminalSessionManager = class {
|
|
|
38518
39257
|
async pullOutput(input) {
|
|
38519
39258
|
const session = this.sessions.get(input.sessionId);
|
|
38520
39259
|
if (!session) throw new Error(`No such terminal session: ${input.sessionId}`);
|
|
38521
|
-
if (input.afterSeq > 0 && input.afterSeq === session.nextSeq - 1 && !session.exited && (input.waitMs ?? 0) > 0) await new Promise((resolve) => {
|
|
39260
|
+
if ((input.afterSeq > 0 || input.waitForOutput === true) && input.afterSeq === session.nextSeq - 1 && !session.exited && (input.waitMs ?? 0) > 0) await new Promise((resolve) => {
|
|
38522
39261
|
const wake = () => {
|
|
38523
39262
|
clearTimeout(timer);
|
|
38524
39263
|
session.outputWaiters.delete(wake);
|
|
@@ -38529,6 +39268,11 @@ var TerminalSessionManager = class {
|
|
|
38529
39268
|
session.outputWaiters.add(wake);
|
|
38530
39269
|
});
|
|
38531
39270
|
const cursor = session.nextSeq - 1;
|
|
39271
|
+
if (session.disposed) return {
|
|
39272
|
+
cursor,
|
|
39273
|
+
reset: false,
|
|
39274
|
+
events: session.output.filter((event) => event.seq > input.afterSeq)
|
|
39275
|
+
};
|
|
38532
39276
|
const oldestSeq = session.output[0]?.seq ?? session.nextSeq;
|
|
38533
39277
|
if (input.afterSeq === 0 || input.afterSeq < oldestSeq - 1 || input.afterSeq > cursor) {
|
|
38534
39278
|
await session.screen.flush();
|
|
@@ -38609,24 +39353,71 @@ var TerminalSessionManager = class {
|
|
|
38609
39353
|
}
|
|
38610
39354
|
/** Kill every live session — called on addon shutdown. */
|
|
38611
39355
|
disposeAll() {
|
|
38612
|
-
for (const session of this.sessions
|
|
38613
|
-
|
|
39356
|
+
for (const [sessionId, session] of this.sessions) {
|
|
39357
|
+
this.finishSession(sessionId, session, { exitCode: 0 }, false);
|
|
38614
39358
|
try {
|
|
38615
39359
|
session.pty.kill();
|
|
38616
39360
|
} catch {}
|
|
38617
|
-
session.screen.dispose();
|
|
38618
39361
|
}
|
|
38619
|
-
|
|
39362
|
+
}
|
|
39363
|
+
finishSession(sessionId, session, exit, retainForLateExit) {
|
|
39364
|
+
if (session.exited) return;
|
|
39365
|
+
session.exited = true;
|
|
39366
|
+
session.lastExit = exit;
|
|
39367
|
+
this.appendOutput(session, {
|
|
39368
|
+
kind: "exit",
|
|
39369
|
+
exitCode: exit.exitCode,
|
|
39370
|
+
...exit.signal !== void 0 ? { signal: exit.signal } : {}
|
|
39371
|
+
});
|
|
39372
|
+
for (const sink of session.sinks) try {
|
|
39373
|
+
sink({
|
|
39374
|
+
kind: "exit",
|
|
39375
|
+
exitCode: exit.exitCode,
|
|
39376
|
+
...exit.signal !== void 0 ? { signal: exit.signal } : {}
|
|
39377
|
+
});
|
|
39378
|
+
} catch {}
|
|
39379
|
+
session.sinks.clear();
|
|
39380
|
+
if (!retainForLateExit) {
|
|
39381
|
+
this.disposeSession(session);
|
|
39382
|
+
this.sessions.delete(sessionId);
|
|
39383
|
+
return;
|
|
39384
|
+
}
|
|
39385
|
+
const retire = setTimeout(() => {
|
|
39386
|
+
this.disposeSession(session);
|
|
39387
|
+
this.sessions.delete(sessionId);
|
|
39388
|
+
}, EXITED_RETENTION_MS);
|
|
39389
|
+
retire.unref?.();
|
|
39390
|
+
session.retireTimer = retire;
|
|
39391
|
+
}
|
|
39392
|
+
disposeSession(session) {
|
|
39393
|
+
if (session.retireTimer !== void 0) clearTimeout(session.retireTimer);
|
|
39394
|
+
if (session.disposed) return;
|
|
39395
|
+
session.disposed = true;
|
|
39396
|
+
session.screen.dispose();
|
|
39397
|
+
}
|
|
39398
|
+
requireInstanceControl() {
|
|
39399
|
+
if (!this.instanceControl) throw new Error("Terminal instances are managed on the hub");
|
|
39400
|
+
return this.instanceControl;
|
|
38620
39401
|
}
|
|
38621
39402
|
};
|
|
38622
39403
|
//#endregion
|
|
38623
39404
|
//#region src/addon.ts
|
|
38624
39405
|
var DEFAULTS = {
|
|
38625
39406
|
btmPath: "",
|
|
39407
|
+
btmEnabled: true,
|
|
39408
|
+
btmArgs: [],
|
|
39409
|
+
topEnabled: true,
|
|
39410
|
+
topPath: "",
|
|
39411
|
+
topArgs: [],
|
|
39412
|
+
glancesEnabled: true,
|
|
39413
|
+
glancesPath: "",
|
|
39414
|
+
glancesArgs: [],
|
|
38626
39415
|
allowShell: false,
|
|
38627
39416
|
shellPath: "",
|
|
38628
39417
|
maxSessions: 4,
|
|
38629
|
-
customProfiles: []
|
|
39418
|
+
customProfiles: [],
|
|
39419
|
+
terminalInstances: [],
|
|
39420
|
+
terminalCameraTombstones: []
|
|
38630
39421
|
};
|
|
38631
39422
|
var DATA_PLANE_PREFIX = "io";
|
|
38632
39423
|
var CAMERA_RECONCILE_MS = 6e4;
|
|
@@ -38636,35 +39427,56 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
38636
39427
|
dataPlane = null;
|
|
38637
39428
|
cameraRelay = null;
|
|
38638
39429
|
cameraReconcileTimer = null;
|
|
39430
|
+
cameraProfilesByNode = /* @__PURE__ */ new Map();
|
|
39431
|
+
/** Prevent repeat writes when a legacy device stays live after migration. */
|
|
39432
|
+
migratedTerminalCameraConfigIds = /* @__PURE__ */ new Set();
|
|
39433
|
+
terminalCameraTombstones = /* @__PURE__ */ new Set();
|
|
39434
|
+
instanceMutationQueue = new TerminalInstanceMutationQueue();
|
|
39435
|
+
terminalReconcileCoordinator = new TerminalInstanceReconcileCoordinator(this.instanceMutationQueue);
|
|
39436
|
+
glancesPythonPath = "";
|
|
38639
39437
|
constructor() {
|
|
38640
39438
|
super({ ...DEFAULTS });
|
|
38641
39439
|
}
|
|
38642
39440
|
async onInitialize() {
|
|
38643
39441
|
await warmNodePty();
|
|
39442
|
+
this.glancesPythonPath = await this.ctx.deps.ensurePython() ?? "";
|
|
38644
39443
|
const manager = new TerminalSessionManager({
|
|
38645
39444
|
spawn: createNodePtySpawner(),
|
|
38646
39445
|
screenFactory: createXtermScreen,
|
|
38647
39446
|
resolveBinary: resolveExecutable,
|
|
38648
39447
|
logger: this.ctx.logger,
|
|
38649
39448
|
btmPath: this.config.btmPath,
|
|
39449
|
+
btmEnabled: this.config.btmEnabled,
|
|
39450
|
+
btmArgs: this.config.btmArgs,
|
|
39451
|
+
topEnabled: this.config.topEnabled,
|
|
39452
|
+
topPath: this.config.topPath,
|
|
39453
|
+
topArgs: this.config.topArgs,
|
|
39454
|
+
glancesEnabled: this.config.glancesEnabled,
|
|
39455
|
+
glancesPath: this.config.glancesPath,
|
|
39456
|
+
glancesArgs: this.config.glancesArgs,
|
|
39457
|
+
glancesPythonPath: this.glancesPythonPath,
|
|
38650
39458
|
allowShell: this.config.allowShell,
|
|
38651
39459
|
shellPath: this.config.shellPath,
|
|
38652
39460
|
maxSessions: this.config.maxSessions,
|
|
38653
39461
|
customProfiles: this.config.customProfiles
|
|
38654
39462
|
});
|
|
38655
39463
|
this.manager = manager;
|
|
39464
|
+
this.replaceTerminalCameraTombstones(this.config.terminalCameraTombstones);
|
|
38656
39465
|
if (declarationOwnerNodeId(this.ctx.kernel.localNodeId) === "hub") {
|
|
39466
|
+
const localNodeId = declarationOwnerNodeId(this.ctx.kernel.localNodeId);
|
|
38657
39467
|
const cameraRelay = new TerminalCameraRelay({
|
|
38658
|
-
listProfiles: (nodeId) => this.ctx.api.terminalSession.listProfiles.query(void 0, nodePin(nodeId)),
|
|
38659
|
-
openSession: (nodeId, input) => this.ctx.api.terminalSession.openSession.mutate(input, nodePin(nodeId)),
|
|
38660
|
-
pullOutput: (nodeId, input) => this.ctx.api.terminalSession.pullOutput.mutate(input, nodePin(nodeId)),
|
|
39468
|
+
listProfiles: (nodeId) => nodeId === localNodeId ? manager.listProfiles() : this.ctx.api.terminalSession.listProfiles.query(void 0, nodePin(nodeId)),
|
|
39469
|
+
openSession: (nodeId, input) => nodeId === localNodeId ? manager.openSession(input) : this.ctx.api.terminalSession.openSession.mutate(input, nodePin(nodeId)),
|
|
39470
|
+
pullOutput: (nodeId, input) => nodeId === localNodeId ? manager.pullOutput(input) : this.ctx.api.terminalSession.pullOutput.mutate(input, nodePin(nodeId)),
|
|
38661
39471
|
closeSession: async (nodeId, sessionId) => {
|
|
38662
|
-
await
|
|
39472
|
+
if (nodeId === localNodeId) await manager.close({ sessionId });
|
|
39473
|
+
else await this.ctx.api.terminalSession.close.mutate({ sessionId }, nodePin(nodeId));
|
|
38663
39474
|
}
|
|
38664
39475
|
}, this.ctx.logger.child("camera"));
|
|
38665
39476
|
await cameraRelay.start();
|
|
38666
39477
|
this.cameraRelay = cameraRelay;
|
|
38667
39478
|
installTerminalCameraRelay(cameraRelay);
|
|
39479
|
+
manager.setInstanceControl(this.terminalInstanceControl());
|
|
38668
39480
|
await this.reconcileTerminalCameras().catch((error) => {
|
|
38669
39481
|
this.ctx.logger.warn("initial terminal camera reconciliation failed — will retry", { meta: { error: error instanceof Error ? error.message : String(error) } });
|
|
38670
39482
|
});
|
|
@@ -38689,13 +39501,26 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
38689
39501
|
}];
|
|
38690
39502
|
}
|
|
38691
39503
|
async onConfigChanged() {
|
|
39504
|
+
this.replaceTerminalCameraTombstones(this.config.terminalCameraTombstones);
|
|
38692
39505
|
this.manager?.reconfigureProfiles({
|
|
38693
39506
|
btmPath: this.config.btmPath,
|
|
39507
|
+
btmEnabled: this.config.btmEnabled,
|
|
39508
|
+
btmArgs: this.config.btmArgs,
|
|
39509
|
+
topEnabled: this.config.topEnabled,
|
|
39510
|
+
topPath: this.config.topPath,
|
|
39511
|
+
topArgs: this.config.topArgs,
|
|
39512
|
+
glancesEnabled: this.config.glancesEnabled,
|
|
39513
|
+
glancesPath: this.config.glancesPath,
|
|
39514
|
+
glancesArgs: this.config.glancesArgs,
|
|
39515
|
+
glancesPythonPath: this.glancesPythonPath,
|
|
38694
39516
|
allowShell: this.config.allowShell,
|
|
38695
39517
|
shellPath: this.config.shellPath,
|
|
38696
39518
|
maxSessions: this.config.maxSessions,
|
|
38697
39519
|
customProfiles: this.config.customProfiles
|
|
38698
39520
|
});
|
|
39521
|
+
if (this.cameraRelay) this.reconcileTerminalCameras().catch((error) => {
|
|
39522
|
+
this.ctx.logger.warn("terminal camera reconciliation after config change failed", { meta: { error: error instanceof Error ? error.message : String(error) } });
|
|
39523
|
+
});
|
|
38699
39524
|
}
|
|
38700
39525
|
async onShutdown() {
|
|
38701
39526
|
if (this.cameraReconcileTimer) clearInterval(this.cameraReconcileTimer);
|
|
@@ -38711,17 +39536,46 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
38711
39536
|
this.manager = null;
|
|
38712
39537
|
}
|
|
38713
39538
|
async reconcileTerminalCameras() {
|
|
39539
|
+
return this.terminalReconcileCoordinator.request(() => this.applyTerminalCameraReconciliation());
|
|
39540
|
+
}
|
|
39541
|
+
async applyTerminalCameraReconciliation() {
|
|
38714
39542
|
if (!this.cameraRelay) return;
|
|
39543
|
+
let terminalIntegrationId;
|
|
38715
39544
|
const topology = await this.ctx.api.nodes.topology.query();
|
|
38716
39545
|
if (topology.length === 0) throw new Error("cluster topology returned no nodes; refusing to withdraw declared cameras");
|
|
38717
39546
|
const nodes = topology.filter((node) => typeof node.id === "string" && node.id.length > 0);
|
|
39547
|
+
const nodeIds = new Set(nodes.map((node) => node.id));
|
|
39548
|
+
for (const cachedNodeId of this.cameraProfilesByNode.keys()) if (!nodeIds.has(cachedNodeId)) this.cameraProfilesByNode.delete(cachedNodeId);
|
|
39549
|
+
const unavailableNodeIds = /* @__PURE__ */ new Set();
|
|
39550
|
+
await Promise.all(nodes.map(async (node) => {
|
|
39551
|
+
try {
|
|
39552
|
+
this.cameraProfilesByNode.set(node.id, await this.cameraRelay.listProfiles(node.id));
|
|
39553
|
+
} catch (error) {
|
|
39554
|
+
unavailableNodeIds.add(node.id);
|
|
39555
|
+
this.ctx.logger.warn("terminal profiles unavailable — keeping Terminal instance cameras offline", { meta: {
|
|
39556
|
+
nodeId: node.id,
|
|
39557
|
+
cachedProfiles: this.cameraProfilesByNode.has(node.id),
|
|
39558
|
+
error: error instanceof Error ? error.message : String(error)
|
|
39559
|
+
} });
|
|
39560
|
+
}
|
|
39561
|
+
}));
|
|
39562
|
+
const cameraDeclarations = buildTerminalInstanceCameraDeclarations(this.terminalInstances());
|
|
39563
|
+
const declarationsByStableId = new Map(cameraDeclarations.map((camera) => [camera.stableId, camera]));
|
|
38718
39564
|
const result = await new DeclaredDevices({
|
|
38719
39565
|
logger: this.ctx.logger.child("camera-declaration"),
|
|
38720
39566
|
addonId: this.ctx.id,
|
|
38721
39567
|
devices: this.ctx.kernel.devices,
|
|
38722
39568
|
localNodeId: this.ctx.kernel.localNodeId,
|
|
38723
|
-
getIntegration: async (addonId) =>
|
|
38724
|
-
|
|
39569
|
+
getIntegration: async (addonId) => {
|
|
39570
|
+
const integration = await this.ctx.api.integrations.getByAddonId.query({ addonId });
|
|
39571
|
+
terminalIntegrationId = integration?.id ?? null;
|
|
39572
|
+
return integration;
|
|
39573
|
+
},
|
|
39574
|
+
createIntegration: async (input) => {
|
|
39575
|
+
const integration = await this.ctx.api.integrations.create.mutate(input);
|
|
39576
|
+
terminalIntegrationId = integration.id;
|
|
39577
|
+
return integration;
|
|
39578
|
+
},
|
|
38725
39579
|
updateIntegration: async ({ id, info }) => {
|
|
38726
39580
|
await this.ctx.api.integrations.update.mutate({
|
|
38727
39581
|
id,
|
|
@@ -38729,25 +39583,157 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
38729
39583
|
skipRestart: true
|
|
38730
39584
|
});
|
|
38731
39585
|
},
|
|
38732
|
-
listOwnDevices: async () =>
|
|
39586
|
+
listOwnDevices: async () => {
|
|
39587
|
+
return terminalCameraReconciliationIndex(await this.ctx.api.deviceManager.listAll.query({ addonId: this.ctx.id }), terminalIntegrationId, cameraDeclarations, this.terminalCameraTombstones);
|
|
39588
|
+
}
|
|
38733
39589
|
}).reconcile({
|
|
38734
39590
|
integrationName: TERMINAL_CAMERA_INTEGRATION,
|
|
38735
39591
|
placement: "hub",
|
|
38736
|
-
devices:
|
|
38737
|
-
stableId:
|
|
38738
|
-
name:
|
|
39592
|
+
devices: cameraDeclarations.map((camera) => ({
|
|
39593
|
+
stableId: camera.stableId,
|
|
39594
|
+
name: camera.name,
|
|
38739
39595
|
type: DeviceType.Camera,
|
|
38740
39596
|
DeviceClass: TerminalCameraDevice,
|
|
38741
|
-
config:
|
|
39597
|
+
config: camera.config,
|
|
38742
39598
|
role: "terminal-camera"
|
|
38743
39599
|
}))
|
|
38744
39600
|
});
|
|
38745
|
-
const onlineByNode = new Map(nodes.map((node) => [node.id, node.isOnline]));
|
|
39601
|
+
const onlineByNode = new Map(nodes.map((node) => [node.id, node.isOnline && !unavailableNodeIds.has(node.id)]));
|
|
38746
39602
|
for (const outcome of result.devices) if (outcome.device instanceof TerminalCameraDevice) {
|
|
39603
|
+
const declaration = declarationsByStableId.get(outcome.stableId);
|
|
39604
|
+
if (declaration) {
|
|
39605
|
+
const config = outcome.device.config;
|
|
39606
|
+
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)) {
|
|
39607
|
+
await config.setAll(declaration.config);
|
|
39608
|
+
this.migratedTerminalCameraConfigIds.add(outcome.device.id);
|
|
39609
|
+
}
|
|
39610
|
+
}
|
|
38747
39611
|
const nodeId = outcome.device.config.get("nodeId");
|
|
38748
|
-
outcome.device.
|
|
39612
|
+
const profileId = outcome.device.config.get("profileId");
|
|
39613
|
+
const profileAvailable = this.cameraProfilesByNode.get(nodeId)?.some((profile) => profile.profileId === profileId) ?? false;
|
|
39614
|
+
outcome.device.setNodeOnline((onlineByNode.get(nodeId) ?? false) && profileAvailable);
|
|
38749
39615
|
}
|
|
38750
39616
|
}
|
|
39617
|
+
terminalInstanceControl() {
|
|
39618
|
+
return {
|
|
39619
|
+
listInstances: async () => this.terminalInstances().map((instance) => this.instanceInfo(instance)),
|
|
39620
|
+
createInstance: async (input) => this.createTerminalInstance(input),
|
|
39621
|
+
deleteInstance: async ({ instanceId }) => this.deleteTerminalInstance(instanceId),
|
|
39622
|
+
setInstanceEnabled: async ({ instanceId, enabled }) => this.setTerminalInstanceEnabled(instanceId, enabled),
|
|
39623
|
+
listLegacyCameras: async () => this.listLegacyTerminalCameras(),
|
|
39624
|
+
adoptLegacyMonitor: async ({ stableId, name }) => this.adoptLegacyMonitor(stableId, name)
|
|
39625
|
+
};
|
|
39626
|
+
}
|
|
39627
|
+
terminalInstances() {
|
|
39628
|
+
return readTerminalInstances(this.config.terminalInstances, (message) => {
|
|
39629
|
+
this.ctx.logger.warn(message);
|
|
39630
|
+
});
|
|
39631
|
+
}
|
|
39632
|
+
instanceInfo(instance) {
|
|
39633
|
+
return {
|
|
39634
|
+
instanceId: instance.id,
|
|
39635
|
+
cameraStableId: instance.cameraStableId,
|
|
39636
|
+
nodeId: instance.nodeId,
|
|
39637
|
+
profileId: instance.profileId,
|
|
39638
|
+
profileLabel: instance.profileLabel,
|
|
39639
|
+
name: instance.name,
|
|
39640
|
+
enabled: instance.enabled
|
|
39641
|
+
};
|
|
39642
|
+
}
|
|
39643
|
+
replaceTerminalCameraTombstones(stableIds) {
|
|
39644
|
+
this.terminalCameraTombstones.clear();
|
|
39645
|
+
for (const stableId of stableIds) if (typeof stableId === "string" && stableId.length > 0) this.terminalCameraTombstones.add(stableId);
|
|
39646
|
+
}
|
|
39647
|
+
async createTerminalInstance(input) {
|
|
39648
|
+
const instance = await this.instanceMutationQueue.run(() => this.createTerminalInstanceUnlocked(input));
|
|
39649
|
+
await this.reconcileTerminalCameras();
|
|
39650
|
+
return instance;
|
|
39651
|
+
}
|
|
39652
|
+
async createTerminalInstanceUnlocked(input) {
|
|
39653
|
+
const relay = this.cameraRelay;
|
|
39654
|
+
if (!relay) throw new Error("Terminal instances are managed on the hub");
|
|
39655
|
+
const profile = (await relay.listProfiles(input.targetNodeId)).find((candidate) => candidate.profileId === input.profileId);
|
|
39656
|
+
if (!profile) throw new Error(`Terminal profile '${input.profileId}' is not available on ${input.targetNodeId}`);
|
|
39657
|
+
const node = (await this.ctx.api.nodes.topology.query()).find((candidate) => candidate.id === input.targetNodeId);
|
|
39658
|
+
if (!node) throw new Error(`Terminal node '${input.targetNodeId}' no longer exists`);
|
|
39659
|
+
const id = crypto.randomUUID();
|
|
39660
|
+
const name = input.name?.trim() || `Terminal ${profile.label} - ${node.isHub ? "Hub" : node.name}`;
|
|
39661
|
+
const instance = {
|
|
39662
|
+
id,
|
|
39663
|
+
cameraStableId: newTerminalCameraStableId(id),
|
|
39664
|
+
nodeId: input.targetNodeId,
|
|
39665
|
+
profileId: profile.profileId,
|
|
39666
|
+
profileLabel: profile.label,
|
|
39667
|
+
name,
|
|
39668
|
+
enabled: true
|
|
39669
|
+
};
|
|
39670
|
+
await this.updateGlobalSettings({ terminalInstances: [...this.config.terminalInstances, instance] });
|
|
39671
|
+
return this.instanceInfo(instance);
|
|
39672
|
+
}
|
|
39673
|
+
async deleteTerminalInstance(instanceId) {
|
|
39674
|
+
await this.instanceMutationQueue.run(() => this.deleteTerminalInstanceUnlocked(instanceId));
|
|
39675
|
+
await this.reconcileTerminalCameras();
|
|
39676
|
+
}
|
|
39677
|
+
async deleteTerminalInstanceUnlocked(instanceId) {
|
|
39678
|
+
const instance = this.terminalInstances().find((candidate) => candidate.id === instanceId);
|
|
39679
|
+
if (!instance) return;
|
|
39680
|
+
this.terminalCameraTombstones.add(instance.cameraStableId);
|
|
39681
|
+
await this.cameraRelay?.closeInstance(instance.id);
|
|
39682
|
+
await this.updateGlobalSettings({
|
|
39683
|
+
terminalInstances: this.config.terminalInstances.filter((candidate) => candidate.id !== instanceId),
|
|
39684
|
+
terminalCameraTombstones: [...new Set([...this.config.terminalCameraTombstones, instance.cameraStableId])]
|
|
39685
|
+
});
|
|
39686
|
+
}
|
|
39687
|
+
async setTerminalInstanceEnabled(instanceId, enabled) {
|
|
39688
|
+
const instance = await this.instanceMutationQueue.run(() => this.setTerminalInstanceEnabledUnlocked(instanceId, enabled));
|
|
39689
|
+
await this.reconcileTerminalCameras();
|
|
39690
|
+
return instance;
|
|
39691
|
+
}
|
|
39692
|
+
async setTerminalInstanceEnabledUnlocked(instanceId, enabled) {
|
|
39693
|
+
const instance = this.terminalInstances().find((candidate) => candidate.id === instanceId);
|
|
39694
|
+
if (!instance) throw new Error(`No such Terminal instance: ${instanceId}`);
|
|
39695
|
+
if (!enabled) {
|
|
39696
|
+
this.terminalCameraTombstones.add(instance.cameraStableId);
|
|
39697
|
+
await this.cameraRelay?.closeInstance(instance.id);
|
|
39698
|
+
}
|
|
39699
|
+
const updated = {
|
|
39700
|
+
...instance,
|
|
39701
|
+
enabled
|
|
39702
|
+
};
|
|
39703
|
+
const terminalInstances = this.config.terminalInstances.map((candidate) => candidate.id === instanceId ? updated : candidate);
|
|
39704
|
+
const terminalCameraTombstones = enabled ? this.config.terminalCameraTombstones.filter((stableId) => stableId !== instance.cameraStableId) : [...new Set([...this.config.terminalCameraTombstones, instance.cameraStableId])];
|
|
39705
|
+
await this.updateGlobalSettings({
|
|
39706
|
+
terminalInstances,
|
|
39707
|
+
terminalCameraTombstones
|
|
39708
|
+
});
|
|
39709
|
+
return this.instanceInfo(updated);
|
|
39710
|
+
}
|
|
39711
|
+
async listLegacyTerminalCameras() {
|
|
39712
|
+
const instances = this.terminalInstances();
|
|
39713
|
+
const instanceStableIds = new Set(instances.map((instance) => instance.cameraStableId));
|
|
39714
|
+
return listLegacyTerminalCameraCandidates(await this.ctx.api.deviceManager.listAll.query({ addonId: this.ctx.id }), instanceStableIds, this.terminalCameraTombstones);
|
|
39715
|
+
}
|
|
39716
|
+
async adoptLegacyMonitor(stableId, requestedName) {
|
|
39717
|
+
const instance = await this.instanceMutationQueue.run(() => this.adoptLegacyMonitorUnlocked(stableId, requestedName));
|
|
39718
|
+
await this.reconcileTerminalCameras();
|
|
39719
|
+
return instance;
|
|
39720
|
+
}
|
|
39721
|
+
async adoptLegacyMonitorUnlocked(stableId, requestedName) {
|
|
39722
|
+
if (this.terminalCameraTombstones.has(stableId)) throw new Error("This legacy Terminal camera was deleted and cannot be adopted");
|
|
39723
|
+
const legacy = (await this.listLegacyTerminalCameras()).find((camera) => camera.stableId === stableId);
|
|
39724
|
+
if (!legacy?.adoptable) throw new Error("Only a legacy monitor camera with its original stable id can be adopted");
|
|
39725
|
+
const instance = {
|
|
39726
|
+
id: crypto.randomUUID(),
|
|
39727
|
+
cameraStableId: legacy.stableId,
|
|
39728
|
+
nodeId: legacy.nodeId,
|
|
39729
|
+
profileId: "monitor",
|
|
39730
|
+
profileLabel: legacy.profileLabel,
|
|
39731
|
+
name: requestedName?.trim() || legacy.name,
|
|
39732
|
+
enabled: true
|
|
39733
|
+
};
|
|
39734
|
+
await this.updateGlobalSettings({ terminalInstances: [...this.config.terminalInstances, instance] });
|
|
39735
|
+
return this.instanceInfo(instance);
|
|
39736
|
+
}
|
|
38751
39737
|
globalSettingsSchema() {
|
|
38752
39738
|
return this.schema({ sections: [{
|
|
38753
39739
|
id: "terminal",
|
|
@@ -38755,15 +39741,78 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
38755
39741
|
description: "Interactive terminal sessions in the Admin UI. Only pre-declared profiles can be opened; a free-form command is never accepted.",
|
|
38756
39742
|
columns: 2,
|
|
38757
39743
|
fields: [
|
|
39744
|
+
this.field({
|
|
39745
|
+
type: "boolean",
|
|
39746
|
+
key: "btmEnabled",
|
|
39747
|
+
label: "Enable BTM camera",
|
|
39748
|
+
default: true,
|
|
39749
|
+
perNode: true
|
|
39750
|
+
}),
|
|
38758
39751
|
this.field({
|
|
38759
39752
|
type: "text",
|
|
38760
39753
|
key: "btmPath",
|
|
38761
|
-
label: "
|
|
39754
|
+
label: "BTM binary",
|
|
38762
39755
|
description: "Path to the `btm` (bottom) executable. Leave empty to resolve from PATH.",
|
|
38763
39756
|
placeholder: "btm",
|
|
38764
39757
|
default: "",
|
|
38765
39758
|
perNode: true
|
|
38766
39759
|
}),
|
|
39760
|
+
this.field({
|
|
39761
|
+
type: "tags",
|
|
39762
|
+
key: "btmArgs",
|
|
39763
|
+
label: "BTM arguments",
|
|
39764
|
+
description: "Exact arguments passed to btm.",
|
|
39765
|
+
default: [],
|
|
39766
|
+
perNode: true
|
|
39767
|
+
}),
|
|
39768
|
+
this.field({
|
|
39769
|
+
type: "boolean",
|
|
39770
|
+
key: "topEnabled",
|
|
39771
|
+
label: "Enable Top camera",
|
|
39772
|
+
default: true,
|
|
39773
|
+
perNode: true
|
|
39774
|
+
}),
|
|
39775
|
+
this.field({
|
|
39776
|
+
type: "text",
|
|
39777
|
+
key: "topPath",
|
|
39778
|
+
label: "Top binary",
|
|
39779
|
+
placeholder: "top",
|
|
39780
|
+
default: "",
|
|
39781
|
+
perNode: true
|
|
39782
|
+
}),
|
|
39783
|
+
this.field({
|
|
39784
|
+
type: "tags",
|
|
39785
|
+
key: "topArgs",
|
|
39786
|
+
label: "Top arguments",
|
|
39787
|
+
description: "Exact arguments passed to top.",
|
|
39788
|
+
default: [],
|
|
39789
|
+
perNode: true
|
|
39790
|
+
}),
|
|
39791
|
+
this.field({
|
|
39792
|
+
type: "boolean",
|
|
39793
|
+
key: "glancesEnabled",
|
|
39794
|
+
label: "Enable Glances camera",
|
|
39795
|
+
description: "Glances is installed automatically into CamStack embedded Python.",
|
|
39796
|
+
default: true,
|
|
39797
|
+
perNode: true
|
|
39798
|
+
}),
|
|
39799
|
+
this.field({
|
|
39800
|
+
type: "text",
|
|
39801
|
+
key: "glancesPath",
|
|
39802
|
+
label: "Glances binary override",
|
|
39803
|
+
description: "Optional executable override. Empty uses the automatically managed Python package.",
|
|
39804
|
+
placeholder: "glances",
|
|
39805
|
+
default: "",
|
|
39806
|
+
perNode: true
|
|
39807
|
+
}),
|
|
39808
|
+
this.field({
|
|
39809
|
+
type: "tags",
|
|
39810
|
+
key: "glancesArgs",
|
|
39811
|
+
label: "Glances arguments",
|
|
39812
|
+
description: "Exact arguments passed to Glances.",
|
|
39813
|
+
default: [],
|
|
39814
|
+
perNode: true
|
|
39815
|
+
}),
|
|
38767
39816
|
this.field({
|
|
38768
39817
|
type: "number",
|
|
38769
39818
|
key: "maxSessions",
|
|
@@ -38795,7 +39844,7 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
38795
39844
|
}, {
|
|
38796
39845
|
id: "terminal-profiles",
|
|
38797
39846
|
title: "Custom profiles",
|
|
38798
|
-
description: "
|
|
39847
|
+
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.",
|
|
38799
39848
|
columns: 1,
|
|
38800
39849
|
fields: [this.field({
|
|
38801
39850
|
type: "editable-array",
|