@camstack/addon-terminal 0.1.9 → 0.1.11
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 +240 -21
- package/dist/addon.mjs +242 -23
- package/package.json +7 -3
- package/python/requirements.txt +1 -0
package/dist/addon.js
CHANGED
|
@@ -23,6 +23,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
23
|
}) : target, mod));
|
|
24
24
|
//#endregion
|
|
25
25
|
let node_fs = require("node:fs");
|
|
26
|
+
let node_module = require("node:module");
|
|
26
27
|
let node_path = require("node:path");
|
|
27
28
|
let sharp = require("sharp");
|
|
28
29
|
sharp = __toESM(sharp);
|
|
@@ -21148,6 +21149,7 @@ var FaceInfoSchema = object({
|
|
|
21148
21149
|
var FaceFilterEnum = _enum([
|
|
21149
21150
|
"unassigned",
|
|
21150
21151
|
"recognized",
|
|
21152
|
+
"identified",
|
|
21151
21153
|
"all"
|
|
21152
21154
|
]);
|
|
21153
21155
|
var MediaFileLiteSchema$1 = object({
|
|
@@ -21176,6 +21178,8 @@ method(_void(), array(IdentitySchema).readonly()), method(object({ name: string(
|
|
|
21176
21178
|
kind: "mutation",
|
|
21177
21179
|
auth: "admin"
|
|
21178
21180
|
}), method(object({
|
|
21181
|
+
/** Restrict to one camera. Absent keeps the cluster-wide gallery view. */
|
|
21182
|
+
deviceId: number().int().optional(),
|
|
21179
21183
|
limit: number().int().positive().optional(),
|
|
21180
21184
|
filter: FaceFilterEnum.optional(),
|
|
21181
21185
|
/**
|
|
@@ -23405,6 +23409,10 @@ var OsdSourceSchema = discriminatedUnion("kind", [
|
|
|
23405
23409
|
capName: string().min(1).max(64),
|
|
23406
23410
|
/** Dot path inside the slice, e.g. `detected`, `value`, `mode`. */
|
|
23407
23411
|
valuePath: string().min(1).max(64)
|
|
23412
|
+
}),
|
|
23413
|
+
object({
|
|
23414
|
+
kind: literal("latest-recognition"),
|
|
23415
|
+
recognition: _enum(["person", "plate"])
|
|
23408
23416
|
})
|
|
23409
23417
|
]);
|
|
23410
23418
|
var OsdSlotBindingSchema = object({
|
|
@@ -23510,6 +23518,15 @@ method(object({ deviceId: number().int() }), object({
|
|
|
23510
23518
|
}), object({ success: literal(true) }), {
|
|
23511
23519
|
kind: "mutation",
|
|
23512
23520
|
auth: "admin"
|
|
23521
|
+
}), method(object({
|
|
23522
|
+
sourceDeviceId: number().int(),
|
|
23523
|
+
targetDeviceId: number().int()
|
|
23524
|
+
}), object({
|
|
23525
|
+
copied: number().int().nonnegative(),
|
|
23526
|
+
skipped: number().int().nonnegative()
|
|
23527
|
+
}), {
|
|
23528
|
+
kind: "mutation",
|
|
23529
|
+
auth: "admin"
|
|
23513
23530
|
}), method(object({
|
|
23514
23531
|
deviceId: number().int(),
|
|
23515
23532
|
slotId: string().min(1),
|
|
@@ -30203,6 +30220,12 @@ Object.freeze({
|
|
|
30203
30220
|
addonId: null,
|
|
30204
30221
|
access: "delete"
|
|
30205
30222
|
},
|
|
30223
|
+
"osdManager.copyDeviceConfiguration": {
|
|
30224
|
+
capName: "osd-manager",
|
|
30225
|
+
capScope: "system",
|
|
30226
|
+
addonId: null,
|
|
30227
|
+
access: "create"
|
|
30228
|
+
},
|
|
30206
30229
|
"osdManager.getConditionSupport": {
|
|
30207
30230
|
capName: "osd-manager",
|
|
30208
30231
|
capScope: "system",
|
|
@@ -32970,6 +32993,7 @@ DEFAULT_NATIVE_LEASE_SETTINGS.admission;
|
|
|
32970
32993
|
* a full-screen program (`btm`) gets no terminal and no runtime resize channel.
|
|
32971
32994
|
* See docs/design/2026-07-26-terminal-session-design.md ("The pty").
|
|
32972
32995
|
*/
|
|
32996
|
+
var require$1 = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
|
|
32973
32997
|
/**
|
|
32974
32998
|
* Resolve `file` to an executable path the way a shell would: a path-qualified
|
|
32975
32999
|
* name is checked directly, a bare name is searched on `PATH`. Returns the
|
|
@@ -32996,8 +33020,19 @@ function isExecutableFile(p) {
|
|
|
32996
33020
|
}
|
|
32997
33021
|
}
|
|
32998
33022
|
var cachedModule = null;
|
|
33023
|
+
function ensureSpawnHelperExecutable() {
|
|
33024
|
+
if (process.platform === "win32") return;
|
|
33025
|
+
const packageRoot = (0, node_path.dirname)(require$1.resolve("node-pty/package.json"));
|
|
33026
|
+
const candidates = [(0, node_path.join)(packageRoot, "prebuilds", `${process.platform}-${process.arch}`, "spawn-helper"), (0, node_path.join)(packageRoot, "build", "Release", "spawn-helper")];
|
|
33027
|
+
for (const candidate of candidates) {
|
|
33028
|
+
if (!(0, node_fs.existsSync)(candidate)) continue;
|
|
33029
|
+
const mode = (0, node_fs.statSync)(candidate).mode & 511;
|
|
33030
|
+
if ((mode & 73) === 0) (0, node_fs.chmodSync)(candidate, mode | (mode & 292) >> 2);
|
|
33031
|
+
}
|
|
33032
|
+
}
|
|
32999
33033
|
async function loadNodePty() {
|
|
33000
33034
|
if (cachedModule) return cachedModule;
|
|
33035
|
+
ensureSpawnHelperExecutable();
|
|
33001
33036
|
const mod = await import("node-pty");
|
|
33002
33037
|
cachedModule = mod;
|
|
33003
33038
|
return mod;
|
|
@@ -33043,6 +33078,22 @@ function createNodePtySpawner() {
|
|
|
33043
33078
|
async function warmNodePty() {
|
|
33044
33079
|
await loadNodePty();
|
|
33045
33080
|
}
|
|
33081
|
+
//#endregion
|
|
33082
|
+
//#region src/terminal-camera-declarations.ts
|
|
33083
|
+
function buildTerminalCameraDeclarations(nodes, profilesByNode) {
|
|
33084
|
+
return nodes.flatMap((node) => {
|
|
33085
|
+
const nodeLabel = node.isHub ? "Hub" : node.name;
|
|
33086
|
+
return (profilesByNode.get(node.id) ?? []).map((profile) => ({
|
|
33087
|
+
stableId: profile.profileId === "monitor" ? `terminal-camera-${node.id}` : `terminal-camera-${node.id}-${profile.profileId}`,
|
|
33088
|
+
name: `Terminal ${profile.label} - ${nodeLabel}`,
|
|
33089
|
+
config: {
|
|
33090
|
+
nodeId: node.id,
|
|
33091
|
+
profileId: profile.profileId,
|
|
33092
|
+
profileLabel: profile.label
|
|
33093
|
+
}
|
|
33094
|
+
}));
|
|
33095
|
+
});
|
|
33096
|
+
}
|
|
33046
33097
|
function escapeXml(value) {
|
|
33047
33098
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
33048
33099
|
}
|
|
@@ -33057,7 +33108,11 @@ async function renderTerminalJpeg(lines) {
|
|
|
33057
33108
|
}
|
|
33058
33109
|
//#endregion
|
|
33059
33110
|
//#region src/terminal-camera-device.ts
|
|
33060
|
-
var terminalCameraSchema = object({
|
|
33111
|
+
var terminalCameraSchema = object({
|
|
33112
|
+
nodeId: string().min(1),
|
|
33113
|
+
profileId: string().min(1),
|
|
33114
|
+
profileLabel: string().min(1)
|
|
33115
|
+
});
|
|
33061
33116
|
var relay = null;
|
|
33062
33117
|
function installTerminalCameraRelay(next) {
|
|
33063
33118
|
relay = next;
|
|
@@ -33076,18 +33131,19 @@ var TerminalCameraDevice = class extends BaseDevice {
|
|
|
33076
33131
|
const activeRelay = relay;
|
|
33077
33132
|
if (!activeRelay) throw new Error("terminal camera relay is unavailable");
|
|
33078
33133
|
const nodeId = this.config.get("nodeId");
|
|
33079
|
-
|
|
33080
|
-
|
|
33134
|
+
const profileId = this.config.get("profileId");
|
|
33135
|
+
return [{
|
|
33136
|
+
camStreamId: profileId,
|
|
33081
33137
|
kind: "pull-http",
|
|
33082
|
-
url: activeRelay.streamUrl(nodeId,
|
|
33138
|
+
url: activeRelay.streamUrl(nodeId, profileId),
|
|
33083
33139
|
codec: "h264",
|
|
33084
33140
|
resolution: {
|
|
33085
33141
|
width: 960,
|
|
33086
33142
|
height: 640
|
|
33087
33143
|
},
|
|
33088
33144
|
fps: 2,
|
|
33089
|
-
label:
|
|
33090
|
-
}
|
|
33145
|
+
label: this.config.get("profileLabel")
|
|
33146
|
+
}];
|
|
33091
33147
|
}
|
|
33092
33148
|
setNodeOnline(online) {
|
|
33093
33149
|
this.markOnline(online);
|
|
@@ -38259,7 +38315,12 @@ function createTerminalDataPlaneHandler(manager) {
|
|
|
38259
38315
|
var ProfileIdSchema = string().trim().min(1).max(64).regex(/^[a-z][a-z0-9-]*$/, "must be a lowercase slug");
|
|
38260
38316
|
var ConfiguredTerminalProfileSchema = object({
|
|
38261
38317
|
enabled: boolean().default(true),
|
|
38262
|
-
profileId: ProfileIdSchema.refine((id) =>
|
|
38318
|
+
profileId: ProfileIdSchema.refine((id) => ![
|
|
38319
|
+
"monitor",
|
|
38320
|
+
"top",
|
|
38321
|
+
"glances",
|
|
38322
|
+
"shell"
|
|
38323
|
+
].includes(id), { message: "monitor, top, glances and shell are reserved profile IDs" }),
|
|
38263
38324
|
label: string().trim().min(1).max(100),
|
|
38264
38325
|
description: string().trim().max(500).optional().default(""),
|
|
38265
38326
|
executable: string().trim().min(1).max(1024).refine((value) => !value.includes("\0"), { message: "must not contain NUL bytes" }),
|
|
@@ -38274,17 +38335,36 @@ var ConfiguredTerminalProfileSchema = object({
|
|
|
38274
38335
|
* surface.
|
|
38275
38336
|
*/
|
|
38276
38337
|
function buildProfiles(options) {
|
|
38277
|
-
const profiles = [
|
|
38338
|
+
const profiles = [];
|
|
38339
|
+
if (options.btmEnabled !== false) profiles.push({
|
|
38278
38340
|
profileId: "monitor",
|
|
38279
|
-
label: "
|
|
38341
|
+
label: "BTM",
|
|
38280
38342
|
description: "bottom (btm) — CPU, memory, network and process monitor",
|
|
38281
38343
|
file: options.btmPath.trim().length > 0 ? options.btmPath.trim() : "btm",
|
|
38282
|
-
args: []
|
|
38283
|
-
|
|
38284
|
-
|
|
38285
|
-
|
|
38286
|
-
|
|
38287
|
-
|
|
38344
|
+
args: options.btmArgs ?? []
|
|
38345
|
+
});
|
|
38346
|
+
if (options.topEnabled !== false) profiles.push({
|
|
38347
|
+
profileId: "top",
|
|
38348
|
+
label: "Top",
|
|
38349
|
+
description: "The operating system process and resource monitor",
|
|
38350
|
+
file: options.topPath?.trim() || "top",
|
|
38351
|
+
args: options.topArgs ?? []
|
|
38352
|
+
});
|
|
38353
|
+
if (options.glancesEnabled !== false) {
|
|
38354
|
+
const configuredPath = options.glancesPath?.trim();
|
|
38355
|
+
const pythonPath = options.glancesPythonPath?.trim();
|
|
38356
|
+
profiles.push({
|
|
38357
|
+
profileId: "glances",
|
|
38358
|
+
label: "Glances",
|
|
38359
|
+
description: "Cross-platform curses monitor installed in CamStack embedded Python",
|
|
38360
|
+
file: configuredPath || pythonPath || "glances",
|
|
38361
|
+
args: configuredPath || !pythonPath ? options.glancesArgs ?? [] : [
|
|
38362
|
+
"-m",
|
|
38363
|
+
"glances",
|
|
38364
|
+
...options.glancesArgs ?? []
|
|
38365
|
+
]
|
|
38366
|
+
});
|
|
38367
|
+
}
|
|
38288
38368
|
if (options.allowShell) profiles.push({
|
|
38289
38369
|
profileId: "shell",
|
|
38290
38370
|
label: "Shell (interactive)",
|
|
@@ -38357,6 +38437,15 @@ var TerminalSessionManager = class {
|
|
|
38357
38437
|
this.opts = opts;
|
|
38358
38438
|
this.profiles = buildProfiles({
|
|
38359
38439
|
btmPath: opts.btmPath,
|
|
38440
|
+
btmEnabled: opts.btmEnabled,
|
|
38441
|
+
btmArgs: opts.btmArgs,
|
|
38442
|
+
topEnabled: opts.topEnabled,
|
|
38443
|
+
topPath: opts.topPath,
|
|
38444
|
+
topArgs: opts.topArgs,
|
|
38445
|
+
glancesEnabled: opts.glancesEnabled,
|
|
38446
|
+
glancesPath: opts.glancesPath,
|
|
38447
|
+
glancesArgs: opts.glancesArgs,
|
|
38448
|
+
glancesPythonPath: opts.glancesPythonPath,
|
|
38360
38449
|
allowShell: opts.allowShell,
|
|
38361
38450
|
shellPath: opts.shellPath,
|
|
38362
38451
|
customProfiles: opts.customProfiles,
|
|
@@ -38610,6 +38699,14 @@ var TerminalSessionManager = class {
|
|
|
38610
38699
|
//#region src/addon.ts
|
|
38611
38700
|
var DEFAULTS = {
|
|
38612
38701
|
btmPath: "",
|
|
38702
|
+
btmEnabled: true,
|
|
38703
|
+
btmArgs: [],
|
|
38704
|
+
topEnabled: true,
|
|
38705
|
+
topPath: "",
|
|
38706
|
+
topArgs: [],
|
|
38707
|
+
glancesEnabled: true,
|
|
38708
|
+
glancesPath: "",
|
|
38709
|
+
glancesArgs: [],
|
|
38613
38710
|
allowShell: false,
|
|
38614
38711
|
shellPath: "",
|
|
38615
38712
|
maxSessions: 4,
|
|
@@ -38623,17 +38720,29 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
38623
38720
|
dataPlane = null;
|
|
38624
38721
|
cameraRelay = null;
|
|
38625
38722
|
cameraReconcileTimer = null;
|
|
38723
|
+
cameraProfilesByNode = /* @__PURE__ */ new Map();
|
|
38724
|
+
glancesPythonPath = "";
|
|
38626
38725
|
constructor() {
|
|
38627
38726
|
super({ ...DEFAULTS });
|
|
38628
38727
|
}
|
|
38629
38728
|
async onInitialize() {
|
|
38630
38729
|
await warmNodePty();
|
|
38730
|
+
this.glancesPythonPath = await this.ctx.deps.ensurePython() ?? "";
|
|
38631
38731
|
const manager = new TerminalSessionManager({
|
|
38632
38732
|
spawn: createNodePtySpawner(),
|
|
38633
38733
|
screenFactory: createXtermScreen,
|
|
38634
38734
|
resolveBinary: resolveExecutable,
|
|
38635
38735
|
logger: this.ctx.logger,
|
|
38636
38736
|
btmPath: this.config.btmPath,
|
|
38737
|
+
btmEnabled: this.config.btmEnabled,
|
|
38738
|
+
btmArgs: this.config.btmArgs,
|
|
38739
|
+
topEnabled: this.config.topEnabled,
|
|
38740
|
+
topPath: this.config.topPath,
|
|
38741
|
+
topArgs: this.config.topArgs,
|
|
38742
|
+
glancesEnabled: this.config.glancesEnabled,
|
|
38743
|
+
glancesPath: this.config.glancesPath,
|
|
38744
|
+
glancesArgs: this.config.glancesArgs,
|
|
38745
|
+
glancesPythonPath: this.glancesPythonPath,
|
|
38637
38746
|
allowShell: this.config.allowShell,
|
|
38638
38747
|
shellPath: this.config.shellPath,
|
|
38639
38748
|
maxSessions: this.config.maxSessions,
|
|
@@ -38678,6 +38787,15 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
38678
38787
|
async onConfigChanged() {
|
|
38679
38788
|
this.manager?.reconfigureProfiles({
|
|
38680
38789
|
btmPath: this.config.btmPath,
|
|
38790
|
+
btmEnabled: this.config.btmEnabled,
|
|
38791
|
+
btmArgs: this.config.btmArgs,
|
|
38792
|
+
topEnabled: this.config.topEnabled,
|
|
38793
|
+
topPath: this.config.topPath,
|
|
38794
|
+
topArgs: this.config.topArgs,
|
|
38795
|
+
glancesEnabled: this.config.glancesEnabled,
|
|
38796
|
+
glancesPath: this.config.glancesPath,
|
|
38797
|
+
glancesArgs: this.config.glancesArgs,
|
|
38798
|
+
glancesPythonPath: this.glancesPythonPath,
|
|
38681
38799
|
allowShell: this.config.allowShell,
|
|
38682
38800
|
shellPath: this.config.shellPath,
|
|
38683
38801
|
maxSessions: this.config.maxSessions,
|
|
@@ -38702,6 +38820,44 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
38702
38820
|
const topology = await this.ctx.api.nodes.topology.query();
|
|
38703
38821
|
if (topology.length === 0) throw new Error("cluster topology returned no nodes; refusing to withdraw declared cameras");
|
|
38704
38822
|
const nodes = topology.filter((node) => typeof node.id === "string" && node.id.length > 0);
|
|
38823
|
+
const unavailableNodeIds = /* @__PURE__ */ new Set();
|
|
38824
|
+
await Promise.all(nodes.map(async (node) => {
|
|
38825
|
+
try {
|
|
38826
|
+
this.cameraProfilesByNode.set(node.id, await this.cameraRelay.listProfiles(node.id));
|
|
38827
|
+
} catch (error) {
|
|
38828
|
+
if (!this.cameraProfilesByNode.has(node.id)) {
|
|
38829
|
+
unavailableNodeIds.add(node.id);
|
|
38830
|
+
this.ctx.logger.warn("terminal profiles unavailable — preserving existing cameras for this node", { meta: {
|
|
38831
|
+
nodeId: node.id,
|
|
38832
|
+
error: error instanceof Error ? error.message : String(error)
|
|
38833
|
+
} });
|
|
38834
|
+
}
|
|
38835
|
+
}
|
|
38836
|
+
}));
|
|
38837
|
+
const cameraDeclarations = [...buildTerminalCameraDeclarations(nodes, this.cameraProfilesByNode)];
|
|
38838
|
+
if (unavailableNodeIds.size > 0) {
|
|
38839
|
+
const existing = await this.ctx.api.deviceManager.listAll.query({ addonId: this.ctx.id });
|
|
38840
|
+
const unavailableNodes = nodes.filter((node) => unavailableNodeIds.has(node.id)).sort((left, right) => right.id.length - left.id.length);
|
|
38841
|
+
for (const row of existing) {
|
|
38842
|
+
const node = unavailableNodes.find((candidate) => {
|
|
38843
|
+
const base = `terminal-camera-${candidate.id}`;
|
|
38844
|
+
return row.stableId === base || row.stableId.startsWith(`${base}-`);
|
|
38845
|
+
});
|
|
38846
|
+
if (!node || cameraDeclarations.some((camera) => camera.stableId === row.stableId)) continue;
|
|
38847
|
+
const base = `terminal-camera-${node.id}`;
|
|
38848
|
+
const profileId = row.stableId === base ? "monitor" : row.stableId.slice(base.length + 1);
|
|
38849
|
+
const profileLabel = profileId === "monitor" ? "BTM" : profileId;
|
|
38850
|
+
cameraDeclarations.push({
|
|
38851
|
+
stableId: row.stableId,
|
|
38852
|
+
name: `Terminal ${profileLabel} - ${node.isHub ? "Hub" : node.name}`,
|
|
38853
|
+
config: {
|
|
38854
|
+
nodeId: node.id,
|
|
38855
|
+
profileId,
|
|
38856
|
+
profileLabel
|
|
38857
|
+
}
|
|
38858
|
+
});
|
|
38859
|
+
}
|
|
38860
|
+
}
|
|
38705
38861
|
const result = await new DeclaredDevices({
|
|
38706
38862
|
logger: this.ctx.logger.child("camera-declaration"),
|
|
38707
38863
|
addonId: this.ctx.id,
|
|
@@ -38720,12 +38876,12 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
38720
38876
|
}).reconcile({
|
|
38721
38877
|
integrationName: TERMINAL_CAMERA_INTEGRATION,
|
|
38722
38878
|
placement: "hub",
|
|
38723
|
-
devices:
|
|
38724
|
-
stableId:
|
|
38725
|
-
name:
|
|
38879
|
+
devices: cameraDeclarations.map((camera) => ({
|
|
38880
|
+
stableId: camera.stableId,
|
|
38881
|
+
name: camera.name,
|
|
38726
38882
|
type: DeviceType.Camera,
|
|
38727
38883
|
DeviceClass: TerminalCameraDevice,
|
|
38728
|
-
config:
|
|
38884
|
+
config: camera.config,
|
|
38729
38885
|
role: "terminal-camera"
|
|
38730
38886
|
}))
|
|
38731
38887
|
});
|
|
@@ -38742,15 +38898,78 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
38742
38898
|
description: "Interactive terminal sessions in the Admin UI. Only pre-declared profiles can be opened; a free-form command is never accepted.",
|
|
38743
38899
|
columns: 2,
|
|
38744
38900
|
fields: [
|
|
38901
|
+
this.field({
|
|
38902
|
+
type: "boolean",
|
|
38903
|
+
key: "btmEnabled",
|
|
38904
|
+
label: "Enable BTM camera",
|
|
38905
|
+
default: true,
|
|
38906
|
+
perNode: true
|
|
38907
|
+
}),
|
|
38745
38908
|
this.field({
|
|
38746
38909
|
type: "text",
|
|
38747
38910
|
key: "btmPath",
|
|
38748
|
-
label: "
|
|
38911
|
+
label: "BTM binary",
|
|
38749
38912
|
description: "Path to the `btm` (bottom) executable. Leave empty to resolve from PATH.",
|
|
38750
38913
|
placeholder: "btm",
|
|
38751
38914
|
default: "",
|
|
38752
38915
|
perNode: true
|
|
38753
38916
|
}),
|
|
38917
|
+
this.field({
|
|
38918
|
+
type: "tags",
|
|
38919
|
+
key: "btmArgs",
|
|
38920
|
+
label: "BTM arguments",
|
|
38921
|
+
description: "Exact arguments passed to btm.",
|
|
38922
|
+
default: [],
|
|
38923
|
+
perNode: true
|
|
38924
|
+
}),
|
|
38925
|
+
this.field({
|
|
38926
|
+
type: "boolean",
|
|
38927
|
+
key: "topEnabled",
|
|
38928
|
+
label: "Enable Top camera",
|
|
38929
|
+
default: true,
|
|
38930
|
+
perNode: true
|
|
38931
|
+
}),
|
|
38932
|
+
this.field({
|
|
38933
|
+
type: "text",
|
|
38934
|
+
key: "topPath",
|
|
38935
|
+
label: "Top binary",
|
|
38936
|
+
placeholder: "top",
|
|
38937
|
+
default: "",
|
|
38938
|
+
perNode: true
|
|
38939
|
+
}),
|
|
38940
|
+
this.field({
|
|
38941
|
+
type: "tags",
|
|
38942
|
+
key: "topArgs",
|
|
38943
|
+
label: "Top arguments",
|
|
38944
|
+
description: "Exact arguments passed to top.",
|
|
38945
|
+
default: [],
|
|
38946
|
+
perNode: true
|
|
38947
|
+
}),
|
|
38948
|
+
this.field({
|
|
38949
|
+
type: "boolean",
|
|
38950
|
+
key: "glancesEnabled",
|
|
38951
|
+
label: "Enable Glances camera",
|
|
38952
|
+
description: "Glances is installed automatically into CamStack embedded Python.",
|
|
38953
|
+
default: true,
|
|
38954
|
+
perNode: true
|
|
38955
|
+
}),
|
|
38956
|
+
this.field({
|
|
38957
|
+
type: "text",
|
|
38958
|
+
key: "glancesPath",
|
|
38959
|
+
label: "Glances binary override",
|
|
38960
|
+
description: "Optional executable override. Empty uses the automatically managed Python package.",
|
|
38961
|
+
placeholder: "glances",
|
|
38962
|
+
default: "",
|
|
38963
|
+
perNode: true
|
|
38964
|
+
}),
|
|
38965
|
+
this.field({
|
|
38966
|
+
type: "tags",
|
|
38967
|
+
key: "glancesArgs",
|
|
38968
|
+
label: "Glances arguments",
|
|
38969
|
+
description: "Exact arguments passed to Glances.",
|
|
38970
|
+
default: [],
|
|
38971
|
+
perNode: true
|
|
38972
|
+
}),
|
|
38754
38973
|
this.field({
|
|
38755
38974
|
type: "number",
|
|
38756
38975
|
key: "maxSessions",
|
|
@@ -38782,7 +39001,7 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
38782
39001
|
}, {
|
|
38783
39002
|
id: "terminal-profiles",
|
|
38784
39003
|
title: "Custom profiles",
|
|
38785
|
-
description: "
|
|
39004
|
+
description: "Each enabled profile creates its own Terminal camera on this node and is available from the Terminal page. Session requests only carry the profile ID; commands cannot be overridden by clients.",
|
|
38786
39005
|
columns: 1,
|
|
38787
39006
|
fields: [this.field({
|
|
38788
39007
|
type: "editable-array",
|
package/dist/addon.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { accessSync, chmodSync, constants, existsSync, statSync } from "node:fs";
|
|
3
|
+
import { delimiter, dirname, join } from "node:path";
|
|
3
4
|
import sharp from "sharp";
|
|
4
5
|
import { createServer } from "node:http";
|
|
5
6
|
//#region \0rolldown/runtime.js
|
|
@@ -21126,6 +21127,7 @@ var FaceInfoSchema = object({
|
|
|
21126
21127
|
var FaceFilterEnum = _enum([
|
|
21127
21128
|
"unassigned",
|
|
21128
21129
|
"recognized",
|
|
21130
|
+
"identified",
|
|
21129
21131
|
"all"
|
|
21130
21132
|
]);
|
|
21131
21133
|
var MediaFileLiteSchema$1 = object({
|
|
@@ -21154,6 +21156,8 @@ method(_void(), array(IdentitySchema).readonly()), method(object({ name: string(
|
|
|
21154
21156
|
kind: "mutation",
|
|
21155
21157
|
auth: "admin"
|
|
21156
21158
|
}), method(object({
|
|
21159
|
+
/** Restrict to one camera. Absent keeps the cluster-wide gallery view. */
|
|
21160
|
+
deviceId: number().int().optional(),
|
|
21157
21161
|
limit: number().int().positive().optional(),
|
|
21158
21162
|
filter: FaceFilterEnum.optional(),
|
|
21159
21163
|
/**
|
|
@@ -23383,6 +23387,10 @@ var OsdSourceSchema = discriminatedUnion("kind", [
|
|
|
23383
23387
|
capName: string().min(1).max(64),
|
|
23384
23388
|
/** Dot path inside the slice, e.g. `detected`, `value`, `mode`. */
|
|
23385
23389
|
valuePath: string().min(1).max(64)
|
|
23390
|
+
}),
|
|
23391
|
+
object({
|
|
23392
|
+
kind: literal("latest-recognition"),
|
|
23393
|
+
recognition: _enum(["person", "plate"])
|
|
23386
23394
|
})
|
|
23387
23395
|
]);
|
|
23388
23396
|
var OsdSlotBindingSchema = object({
|
|
@@ -23488,6 +23496,15 @@ method(object({ deviceId: number().int() }), object({
|
|
|
23488
23496
|
}), object({ success: literal(true) }), {
|
|
23489
23497
|
kind: "mutation",
|
|
23490
23498
|
auth: "admin"
|
|
23499
|
+
}), method(object({
|
|
23500
|
+
sourceDeviceId: number().int(),
|
|
23501
|
+
targetDeviceId: number().int()
|
|
23502
|
+
}), object({
|
|
23503
|
+
copied: number().int().nonnegative(),
|
|
23504
|
+
skipped: number().int().nonnegative()
|
|
23505
|
+
}), {
|
|
23506
|
+
kind: "mutation",
|
|
23507
|
+
auth: "admin"
|
|
23491
23508
|
}), method(object({
|
|
23492
23509
|
deviceId: number().int(),
|
|
23493
23510
|
slotId: string().min(1),
|
|
@@ -30181,6 +30198,12 @@ Object.freeze({
|
|
|
30181
30198
|
addonId: null,
|
|
30182
30199
|
access: "delete"
|
|
30183
30200
|
},
|
|
30201
|
+
"osdManager.copyDeviceConfiguration": {
|
|
30202
|
+
capName: "osd-manager",
|
|
30203
|
+
capScope: "system",
|
|
30204
|
+
addonId: null,
|
|
30205
|
+
access: "create"
|
|
30206
|
+
},
|
|
30184
30207
|
"osdManager.getConditionSupport": {
|
|
30185
30208
|
capName: "osd-manager",
|
|
30186
30209
|
capScope: "system",
|
|
@@ -32948,6 +32971,7 @@ DEFAULT_NATIVE_LEASE_SETTINGS.admission;
|
|
|
32948
32971
|
* a full-screen program (`btm`) gets no terminal and no runtime resize channel.
|
|
32949
32972
|
* See docs/design/2026-07-26-terminal-session-design.md ("The pty").
|
|
32950
32973
|
*/
|
|
32974
|
+
var require = createRequire(import.meta.url);
|
|
32951
32975
|
/**
|
|
32952
32976
|
* Resolve `file` to an executable path the way a shell would: a path-qualified
|
|
32953
32977
|
* name is checked directly, a bare name is searched on `PATH`. Returns the
|
|
@@ -32974,8 +32998,19 @@ function isExecutableFile(p) {
|
|
|
32974
32998
|
}
|
|
32975
32999
|
}
|
|
32976
33000
|
var cachedModule = null;
|
|
33001
|
+
function ensureSpawnHelperExecutable() {
|
|
33002
|
+
if (process.platform === "win32") return;
|
|
33003
|
+
const packageRoot = dirname(require.resolve("node-pty/package.json"));
|
|
33004
|
+
const candidates = [join(packageRoot, "prebuilds", `${process.platform}-${process.arch}`, "spawn-helper"), join(packageRoot, "build", "Release", "spawn-helper")];
|
|
33005
|
+
for (const candidate of candidates) {
|
|
33006
|
+
if (!existsSync(candidate)) continue;
|
|
33007
|
+
const mode = statSync(candidate).mode & 511;
|
|
33008
|
+
if ((mode & 73) === 0) chmodSync(candidate, mode | (mode & 292) >> 2);
|
|
33009
|
+
}
|
|
33010
|
+
}
|
|
32977
33011
|
async function loadNodePty() {
|
|
32978
33012
|
if (cachedModule) return cachedModule;
|
|
33013
|
+
ensureSpawnHelperExecutable();
|
|
32979
33014
|
const mod = await import("node-pty");
|
|
32980
33015
|
cachedModule = mod;
|
|
32981
33016
|
return mod;
|
|
@@ -33021,6 +33056,22 @@ function createNodePtySpawner() {
|
|
|
33021
33056
|
async function warmNodePty() {
|
|
33022
33057
|
await loadNodePty();
|
|
33023
33058
|
}
|
|
33059
|
+
//#endregion
|
|
33060
|
+
//#region src/terminal-camera-declarations.ts
|
|
33061
|
+
function buildTerminalCameraDeclarations(nodes, profilesByNode) {
|
|
33062
|
+
return nodes.flatMap((node) => {
|
|
33063
|
+
const nodeLabel = node.isHub ? "Hub" : node.name;
|
|
33064
|
+
return (profilesByNode.get(node.id) ?? []).map((profile) => ({
|
|
33065
|
+
stableId: profile.profileId === "monitor" ? `terminal-camera-${node.id}` : `terminal-camera-${node.id}-${profile.profileId}`,
|
|
33066
|
+
name: `Terminal ${profile.label} - ${nodeLabel}`,
|
|
33067
|
+
config: {
|
|
33068
|
+
nodeId: node.id,
|
|
33069
|
+
profileId: profile.profileId,
|
|
33070
|
+
profileLabel: profile.label
|
|
33071
|
+
}
|
|
33072
|
+
}));
|
|
33073
|
+
});
|
|
33074
|
+
}
|
|
33024
33075
|
function escapeXml(value) {
|
|
33025
33076
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
33026
33077
|
}
|
|
@@ -33035,7 +33086,11 @@ async function renderTerminalJpeg(lines) {
|
|
|
33035
33086
|
}
|
|
33036
33087
|
//#endregion
|
|
33037
33088
|
//#region src/terminal-camera-device.ts
|
|
33038
|
-
var terminalCameraSchema = object({
|
|
33089
|
+
var terminalCameraSchema = object({
|
|
33090
|
+
nodeId: string().min(1),
|
|
33091
|
+
profileId: string().min(1),
|
|
33092
|
+
profileLabel: string().min(1)
|
|
33093
|
+
});
|
|
33039
33094
|
var relay = null;
|
|
33040
33095
|
function installTerminalCameraRelay(next) {
|
|
33041
33096
|
relay = next;
|
|
@@ -33054,18 +33109,19 @@ var TerminalCameraDevice = class extends BaseDevice {
|
|
|
33054
33109
|
const activeRelay = relay;
|
|
33055
33110
|
if (!activeRelay) throw new Error("terminal camera relay is unavailable");
|
|
33056
33111
|
const nodeId = this.config.get("nodeId");
|
|
33057
|
-
|
|
33058
|
-
|
|
33112
|
+
const profileId = this.config.get("profileId");
|
|
33113
|
+
return [{
|
|
33114
|
+
camStreamId: profileId,
|
|
33059
33115
|
kind: "pull-http",
|
|
33060
|
-
url: activeRelay.streamUrl(nodeId,
|
|
33116
|
+
url: activeRelay.streamUrl(nodeId, profileId),
|
|
33061
33117
|
codec: "h264",
|
|
33062
33118
|
resolution: {
|
|
33063
33119
|
width: 960,
|
|
33064
33120
|
height: 640
|
|
33065
33121
|
},
|
|
33066
33122
|
fps: 2,
|
|
33067
|
-
label:
|
|
33068
|
-
}
|
|
33123
|
+
label: this.config.get("profileLabel")
|
|
33124
|
+
}];
|
|
33069
33125
|
}
|
|
33070
33126
|
setNodeOnline(online) {
|
|
33071
33127
|
this.markOnline(online);
|
|
@@ -38237,7 +38293,12 @@ function createTerminalDataPlaneHandler(manager) {
|
|
|
38237
38293
|
var ProfileIdSchema = string().trim().min(1).max(64).regex(/^[a-z][a-z0-9-]*$/, "must be a lowercase slug");
|
|
38238
38294
|
var ConfiguredTerminalProfileSchema = object({
|
|
38239
38295
|
enabled: boolean().default(true),
|
|
38240
|
-
profileId: ProfileIdSchema.refine((id) =>
|
|
38296
|
+
profileId: ProfileIdSchema.refine((id) => ![
|
|
38297
|
+
"monitor",
|
|
38298
|
+
"top",
|
|
38299
|
+
"glances",
|
|
38300
|
+
"shell"
|
|
38301
|
+
].includes(id), { message: "monitor, top, glances and shell are reserved profile IDs" }),
|
|
38241
38302
|
label: string().trim().min(1).max(100),
|
|
38242
38303
|
description: string().trim().max(500).optional().default(""),
|
|
38243
38304
|
executable: string().trim().min(1).max(1024).refine((value) => !value.includes("\0"), { message: "must not contain NUL bytes" }),
|
|
@@ -38252,17 +38313,36 @@ var ConfiguredTerminalProfileSchema = object({
|
|
|
38252
38313
|
* surface.
|
|
38253
38314
|
*/
|
|
38254
38315
|
function buildProfiles(options) {
|
|
38255
|
-
const profiles = [
|
|
38316
|
+
const profiles = [];
|
|
38317
|
+
if (options.btmEnabled !== false) profiles.push({
|
|
38256
38318
|
profileId: "monitor",
|
|
38257
|
-
label: "
|
|
38319
|
+
label: "BTM",
|
|
38258
38320
|
description: "bottom (btm) — CPU, memory, network and process monitor",
|
|
38259
38321
|
file: options.btmPath.trim().length > 0 ? options.btmPath.trim() : "btm",
|
|
38260
|
-
args: []
|
|
38261
|
-
|
|
38262
|
-
|
|
38263
|
-
|
|
38264
|
-
|
|
38265
|
-
|
|
38322
|
+
args: options.btmArgs ?? []
|
|
38323
|
+
});
|
|
38324
|
+
if (options.topEnabled !== false) profiles.push({
|
|
38325
|
+
profileId: "top",
|
|
38326
|
+
label: "Top",
|
|
38327
|
+
description: "The operating system process and resource monitor",
|
|
38328
|
+
file: options.topPath?.trim() || "top",
|
|
38329
|
+
args: options.topArgs ?? []
|
|
38330
|
+
});
|
|
38331
|
+
if (options.glancesEnabled !== false) {
|
|
38332
|
+
const configuredPath = options.glancesPath?.trim();
|
|
38333
|
+
const pythonPath = options.glancesPythonPath?.trim();
|
|
38334
|
+
profiles.push({
|
|
38335
|
+
profileId: "glances",
|
|
38336
|
+
label: "Glances",
|
|
38337
|
+
description: "Cross-platform curses monitor installed in CamStack embedded Python",
|
|
38338
|
+
file: configuredPath || pythonPath || "glances",
|
|
38339
|
+
args: configuredPath || !pythonPath ? options.glancesArgs ?? [] : [
|
|
38340
|
+
"-m",
|
|
38341
|
+
"glances",
|
|
38342
|
+
...options.glancesArgs ?? []
|
|
38343
|
+
]
|
|
38344
|
+
});
|
|
38345
|
+
}
|
|
38266
38346
|
if (options.allowShell) profiles.push({
|
|
38267
38347
|
profileId: "shell",
|
|
38268
38348
|
label: "Shell (interactive)",
|
|
@@ -38335,6 +38415,15 @@ var TerminalSessionManager = class {
|
|
|
38335
38415
|
this.opts = opts;
|
|
38336
38416
|
this.profiles = buildProfiles({
|
|
38337
38417
|
btmPath: opts.btmPath,
|
|
38418
|
+
btmEnabled: opts.btmEnabled,
|
|
38419
|
+
btmArgs: opts.btmArgs,
|
|
38420
|
+
topEnabled: opts.topEnabled,
|
|
38421
|
+
topPath: opts.topPath,
|
|
38422
|
+
topArgs: opts.topArgs,
|
|
38423
|
+
glancesEnabled: opts.glancesEnabled,
|
|
38424
|
+
glancesPath: opts.glancesPath,
|
|
38425
|
+
glancesArgs: opts.glancesArgs,
|
|
38426
|
+
glancesPythonPath: opts.glancesPythonPath,
|
|
38338
38427
|
allowShell: opts.allowShell,
|
|
38339
38428
|
shellPath: opts.shellPath,
|
|
38340
38429
|
customProfiles: opts.customProfiles,
|
|
@@ -38588,6 +38677,14 @@ var TerminalSessionManager = class {
|
|
|
38588
38677
|
//#region src/addon.ts
|
|
38589
38678
|
var DEFAULTS = {
|
|
38590
38679
|
btmPath: "",
|
|
38680
|
+
btmEnabled: true,
|
|
38681
|
+
btmArgs: [],
|
|
38682
|
+
topEnabled: true,
|
|
38683
|
+
topPath: "",
|
|
38684
|
+
topArgs: [],
|
|
38685
|
+
glancesEnabled: true,
|
|
38686
|
+
glancesPath: "",
|
|
38687
|
+
glancesArgs: [],
|
|
38591
38688
|
allowShell: false,
|
|
38592
38689
|
shellPath: "",
|
|
38593
38690
|
maxSessions: 4,
|
|
@@ -38601,17 +38698,29 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
38601
38698
|
dataPlane = null;
|
|
38602
38699
|
cameraRelay = null;
|
|
38603
38700
|
cameraReconcileTimer = null;
|
|
38701
|
+
cameraProfilesByNode = /* @__PURE__ */ new Map();
|
|
38702
|
+
glancesPythonPath = "";
|
|
38604
38703
|
constructor() {
|
|
38605
38704
|
super({ ...DEFAULTS });
|
|
38606
38705
|
}
|
|
38607
38706
|
async onInitialize() {
|
|
38608
38707
|
await warmNodePty();
|
|
38708
|
+
this.glancesPythonPath = await this.ctx.deps.ensurePython() ?? "";
|
|
38609
38709
|
const manager = new TerminalSessionManager({
|
|
38610
38710
|
spawn: createNodePtySpawner(),
|
|
38611
38711
|
screenFactory: createXtermScreen,
|
|
38612
38712
|
resolveBinary: resolveExecutable,
|
|
38613
38713
|
logger: this.ctx.logger,
|
|
38614
38714
|
btmPath: this.config.btmPath,
|
|
38715
|
+
btmEnabled: this.config.btmEnabled,
|
|
38716
|
+
btmArgs: this.config.btmArgs,
|
|
38717
|
+
topEnabled: this.config.topEnabled,
|
|
38718
|
+
topPath: this.config.topPath,
|
|
38719
|
+
topArgs: this.config.topArgs,
|
|
38720
|
+
glancesEnabled: this.config.glancesEnabled,
|
|
38721
|
+
glancesPath: this.config.glancesPath,
|
|
38722
|
+
glancesArgs: this.config.glancesArgs,
|
|
38723
|
+
glancesPythonPath: this.glancesPythonPath,
|
|
38615
38724
|
allowShell: this.config.allowShell,
|
|
38616
38725
|
shellPath: this.config.shellPath,
|
|
38617
38726
|
maxSessions: this.config.maxSessions,
|
|
@@ -38656,6 +38765,15 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
38656
38765
|
async onConfigChanged() {
|
|
38657
38766
|
this.manager?.reconfigureProfiles({
|
|
38658
38767
|
btmPath: this.config.btmPath,
|
|
38768
|
+
btmEnabled: this.config.btmEnabled,
|
|
38769
|
+
btmArgs: this.config.btmArgs,
|
|
38770
|
+
topEnabled: this.config.topEnabled,
|
|
38771
|
+
topPath: this.config.topPath,
|
|
38772
|
+
topArgs: this.config.topArgs,
|
|
38773
|
+
glancesEnabled: this.config.glancesEnabled,
|
|
38774
|
+
glancesPath: this.config.glancesPath,
|
|
38775
|
+
glancesArgs: this.config.glancesArgs,
|
|
38776
|
+
glancesPythonPath: this.glancesPythonPath,
|
|
38659
38777
|
allowShell: this.config.allowShell,
|
|
38660
38778
|
shellPath: this.config.shellPath,
|
|
38661
38779
|
maxSessions: this.config.maxSessions,
|
|
@@ -38680,6 +38798,44 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
38680
38798
|
const topology = await this.ctx.api.nodes.topology.query();
|
|
38681
38799
|
if (topology.length === 0) throw new Error("cluster topology returned no nodes; refusing to withdraw declared cameras");
|
|
38682
38800
|
const nodes = topology.filter((node) => typeof node.id === "string" && node.id.length > 0);
|
|
38801
|
+
const unavailableNodeIds = /* @__PURE__ */ new Set();
|
|
38802
|
+
await Promise.all(nodes.map(async (node) => {
|
|
38803
|
+
try {
|
|
38804
|
+
this.cameraProfilesByNode.set(node.id, await this.cameraRelay.listProfiles(node.id));
|
|
38805
|
+
} catch (error) {
|
|
38806
|
+
if (!this.cameraProfilesByNode.has(node.id)) {
|
|
38807
|
+
unavailableNodeIds.add(node.id);
|
|
38808
|
+
this.ctx.logger.warn("terminal profiles unavailable — preserving existing cameras for this node", { meta: {
|
|
38809
|
+
nodeId: node.id,
|
|
38810
|
+
error: error instanceof Error ? error.message : String(error)
|
|
38811
|
+
} });
|
|
38812
|
+
}
|
|
38813
|
+
}
|
|
38814
|
+
}));
|
|
38815
|
+
const cameraDeclarations = [...buildTerminalCameraDeclarations(nodes, this.cameraProfilesByNode)];
|
|
38816
|
+
if (unavailableNodeIds.size > 0) {
|
|
38817
|
+
const existing = await this.ctx.api.deviceManager.listAll.query({ addonId: this.ctx.id });
|
|
38818
|
+
const unavailableNodes = nodes.filter((node) => unavailableNodeIds.has(node.id)).sort((left, right) => right.id.length - left.id.length);
|
|
38819
|
+
for (const row of existing) {
|
|
38820
|
+
const node = unavailableNodes.find((candidate) => {
|
|
38821
|
+
const base = `terminal-camera-${candidate.id}`;
|
|
38822
|
+
return row.stableId === base || row.stableId.startsWith(`${base}-`);
|
|
38823
|
+
});
|
|
38824
|
+
if (!node || cameraDeclarations.some((camera) => camera.stableId === row.stableId)) continue;
|
|
38825
|
+
const base = `terminal-camera-${node.id}`;
|
|
38826
|
+
const profileId = row.stableId === base ? "monitor" : row.stableId.slice(base.length + 1);
|
|
38827
|
+
const profileLabel = profileId === "monitor" ? "BTM" : profileId;
|
|
38828
|
+
cameraDeclarations.push({
|
|
38829
|
+
stableId: row.stableId,
|
|
38830
|
+
name: `Terminal ${profileLabel} - ${node.isHub ? "Hub" : node.name}`,
|
|
38831
|
+
config: {
|
|
38832
|
+
nodeId: node.id,
|
|
38833
|
+
profileId,
|
|
38834
|
+
profileLabel
|
|
38835
|
+
}
|
|
38836
|
+
});
|
|
38837
|
+
}
|
|
38838
|
+
}
|
|
38683
38839
|
const result = await new DeclaredDevices({
|
|
38684
38840
|
logger: this.ctx.logger.child("camera-declaration"),
|
|
38685
38841
|
addonId: this.ctx.id,
|
|
@@ -38698,12 +38854,12 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
38698
38854
|
}).reconcile({
|
|
38699
38855
|
integrationName: TERMINAL_CAMERA_INTEGRATION,
|
|
38700
38856
|
placement: "hub",
|
|
38701
|
-
devices:
|
|
38702
|
-
stableId:
|
|
38703
|
-
name:
|
|
38857
|
+
devices: cameraDeclarations.map((camera) => ({
|
|
38858
|
+
stableId: camera.stableId,
|
|
38859
|
+
name: camera.name,
|
|
38704
38860
|
type: DeviceType.Camera,
|
|
38705
38861
|
DeviceClass: TerminalCameraDevice,
|
|
38706
|
-
config:
|
|
38862
|
+
config: camera.config,
|
|
38707
38863
|
role: "terminal-camera"
|
|
38708
38864
|
}))
|
|
38709
38865
|
});
|
|
@@ -38720,15 +38876,78 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
38720
38876
|
description: "Interactive terminal sessions in the Admin UI. Only pre-declared profiles can be opened; a free-form command is never accepted.",
|
|
38721
38877
|
columns: 2,
|
|
38722
38878
|
fields: [
|
|
38879
|
+
this.field({
|
|
38880
|
+
type: "boolean",
|
|
38881
|
+
key: "btmEnabled",
|
|
38882
|
+
label: "Enable BTM camera",
|
|
38883
|
+
default: true,
|
|
38884
|
+
perNode: true
|
|
38885
|
+
}),
|
|
38723
38886
|
this.field({
|
|
38724
38887
|
type: "text",
|
|
38725
38888
|
key: "btmPath",
|
|
38726
|
-
label: "
|
|
38889
|
+
label: "BTM binary",
|
|
38727
38890
|
description: "Path to the `btm` (bottom) executable. Leave empty to resolve from PATH.",
|
|
38728
38891
|
placeholder: "btm",
|
|
38729
38892
|
default: "",
|
|
38730
38893
|
perNode: true
|
|
38731
38894
|
}),
|
|
38895
|
+
this.field({
|
|
38896
|
+
type: "tags",
|
|
38897
|
+
key: "btmArgs",
|
|
38898
|
+
label: "BTM arguments",
|
|
38899
|
+
description: "Exact arguments passed to btm.",
|
|
38900
|
+
default: [],
|
|
38901
|
+
perNode: true
|
|
38902
|
+
}),
|
|
38903
|
+
this.field({
|
|
38904
|
+
type: "boolean",
|
|
38905
|
+
key: "topEnabled",
|
|
38906
|
+
label: "Enable Top camera",
|
|
38907
|
+
default: true,
|
|
38908
|
+
perNode: true
|
|
38909
|
+
}),
|
|
38910
|
+
this.field({
|
|
38911
|
+
type: "text",
|
|
38912
|
+
key: "topPath",
|
|
38913
|
+
label: "Top binary",
|
|
38914
|
+
placeholder: "top",
|
|
38915
|
+
default: "",
|
|
38916
|
+
perNode: true
|
|
38917
|
+
}),
|
|
38918
|
+
this.field({
|
|
38919
|
+
type: "tags",
|
|
38920
|
+
key: "topArgs",
|
|
38921
|
+
label: "Top arguments",
|
|
38922
|
+
description: "Exact arguments passed to top.",
|
|
38923
|
+
default: [],
|
|
38924
|
+
perNode: true
|
|
38925
|
+
}),
|
|
38926
|
+
this.field({
|
|
38927
|
+
type: "boolean",
|
|
38928
|
+
key: "glancesEnabled",
|
|
38929
|
+
label: "Enable Glances camera",
|
|
38930
|
+
description: "Glances is installed automatically into CamStack embedded Python.",
|
|
38931
|
+
default: true,
|
|
38932
|
+
perNode: true
|
|
38933
|
+
}),
|
|
38934
|
+
this.field({
|
|
38935
|
+
type: "text",
|
|
38936
|
+
key: "glancesPath",
|
|
38937
|
+
label: "Glances binary override",
|
|
38938
|
+
description: "Optional executable override. Empty uses the automatically managed Python package.",
|
|
38939
|
+
placeholder: "glances",
|
|
38940
|
+
default: "",
|
|
38941
|
+
perNode: true
|
|
38942
|
+
}),
|
|
38943
|
+
this.field({
|
|
38944
|
+
type: "tags",
|
|
38945
|
+
key: "glancesArgs",
|
|
38946
|
+
label: "Glances arguments",
|
|
38947
|
+
description: "Exact arguments passed to Glances.",
|
|
38948
|
+
default: [],
|
|
38949
|
+
perNode: true
|
|
38950
|
+
}),
|
|
38732
38951
|
this.field({
|
|
38733
38952
|
type: "number",
|
|
38734
38953
|
key: "maxSessions",
|
|
@@ -38760,7 +38979,7 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
38760
38979
|
}, {
|
|
38761
38980
|
id: "terminal-profiles",
|
|
38762
38981
|
title: "Custom profiles",
|
|
38763
|
-
description: "
|
|
38982
|
+
description: "Each enabled profile creates its own Terminal camera on this node and is available from the Terminal page. Session requests only carry the profile ID; commands cannot be overridden by clients.",
|
|
38764
38983
|
columns: 1,
|
|
38765
38984
|
fields: [this.field({
|
|
38766
38985
|
type: "editable-array",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-terminal",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "Interactive terminal sessions (pty + xterm) as a CamStack addon",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|
|
@@ -34,8 +34,11 @@
|
|
|
34
34
|
"category": "system",
|
|
35
35
|
"name": "Terminal",
|
|
36
36
|
"version": "0.1.0",
|
|
37
|
-
"description": "Interactive terminal sessions on any cluster node,
|
|
37
|
+
"description": "Interactive terminal sessions on any cluster node, with one virtual camera for each configured profile. Commands remain bound to administrator-configured profiles.",
|
|
38
38
|
"entry": "./dist/index.js",
|
|
39
|
+
"python": {
|
|
40
|
+
"requirements": "./python/requirements.txt"
|
|
41
|
+
},
|
|
39
42
|
"icon": "assets/icon.svg",
|
|
40
43
|
"color": "#0ea5e9",
|
|
41
44
|
"instanceMode": "unique",
|
|
@@ -53,7 +56,8 @@
|
|
|
53
56
|
},
|
|
54
57
|
"files": [
|
|
55
58
|
"dist",
|
|
56
|
-
"assets"
|
|
59
|
+
"assets",
|
|
60
|
+
"python"
|
|
57
61
|
],
|
|
58
62
|
"scripts": {
|
|
59
63
|
"build": "vite build",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
glances>=4.3,<5
|