@camstack/addon-terminal 0.1.27 → 0.1.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/addon.js +310 -173
  2. package/dist/addon.mjs +310 -173
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -31,7 +31,7 @@ let node_module = require("node:module");
31
31
  let sharp = require("sharp");
32
32
  sharp = __toESM(sharp);
33
33
  let node_http = require("node:http");
34
- //#region ../types/dist/event-category-XfKNtfCc.mjs
34
+ //#region ../types/dist/event-category-CIa_iT6b.mjs
35
35
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
36
36
  EventCategory["SystemBoot"] = "system.boot";
37
37
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -47,6 +47,15 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
47
47
  */
48
48
  EventCategory["SystemRestartCompleted"] = "system.restart-completed";
49
49
  /**
50
+ * The hub reissued its own TLS certificate at boot (`ensureTlsCert`).
51
+ * Emitted only when the material on disk actually changed, so an
52
+ * operator who trusted the old certificate by hand is told rather than
53
+ * discovering it as a browser error. Payload `TlsCertChangedPayload`.
54
+ *
55
+ * Rule: docs/decisions/adr-0227-*.md
56
+ */
57
+ EventCategory["SystemTlsCertChanged"] = "system.tls-cert-changed";
58
+ /**
50
59
  * A newer addon or server-root package version was found by the
51
60
  * authoritative registry check. Emitted once when any observed
52
61
  * `latestVersion` changes (or a package/node first appears behind);
@@ -19025,6 +19034,12 @@ var CameraStatusSchema = object({
19025
19034
  /** Unix timestamp (ms) when this snapshot was composed server-side. */
19026
19035
  fetchedAt: number()
19027
19036
  });
19037
+ var InferenceDeviceExclusionReasonSchema = _enum([
19038
+ "disabled",
19039
+ "unavailable",
19040
+ "cannot-host-camera-root",
19041
+ "accelerator-preferred"
19042
+ ]);
19028
19043
  var NodeInferenceDeviceSchema = object({
19029
19044
  /** Stable per-node device key, e.g. `openvino:npu`, `edgetpu:usb`, `cpu`. */
19030
19045
  key: string(),
@@ -19055,7 +19070,17 @@ var NodeInferenceDeviceSchema = object({
19055
19070
  * available per format; this is the stored selection that becomes the
19056
19071
  * default for EVERY camera landing on this accelerator.
19057
19072
  */
19058
- steps: record(string(), DeviceStepConfigSchema).optional()
19073
+ steps: record(string(), DeviceStepConfigSchema).optional(),
19074
+ /**
19075
+ * `null` when the device IS a camera-root candidate on this node; otherwise
19076
+ * the reason the dispatcher drops it. Computed by the SAME
19077
+ * `resolveInferenceDeviceEligibility` the dispatcher runs, so this view can
19078
+ * never disagree with the election — deriving it in the UI from
19079
+ * `enabled`/`available` would silently miss `cannot-host-camera-root` (needs
19080
+ * the node's model catalog) and `accelerator-preferred` (needs the node-wide
19081
+ * "an accelerator is serving" predicate).
19082
+ */
19083
+ exclusion: InferenceDeviceExclusionReasonSchema.nullable()
19059
19084
  });
19060
19085
  var NodeInferenceDevicesSchema = object({
19061
19086
  nodeId: string(),
@@ -24053,7 +24078,12 @@ var ListResultSchema = object({
24053
24078
  probedAt: number()
24054
24079
  });
24055
24080
  var PreferredSchema = LocalInterfaceSchema.nullable();
24056
- var GetConnectionEndpointsResultSchema = object({ endpoints: array(object({
24081
+ /**
24082
+ * Candidate base URL for the SDK to race on connect. Order matters —
24083
+ * the SDK should attempt these top-to-bottom with a short per-candidate
24084
+ * timeout (e.g. 1500ms) and cache the winner for the session.
24085
+ */
24086
+ var ConnectionEndpointSchema = object({
24057
24087
  /** Operator-facing label (e.g. "LAN — en0", "Public tunnel"). */
24058
24088
  label: string(),
24059
24089
  /** Fully-formed base URL with scheme + host + port. */
@@ -24096,7 +24126,42 @@ var GetConnectionEndpointsResultSchema = object({ endpoints: array(object({
24096
24126
  * ordering between polls.
24097
24127
  */
24098
24128
  priority: number()
24099
- })).readonly() });
24129
+ });
24130
+ /**
24131
+ * Where the advertised local port came from. Ordered most → least
24132
+ * authoritative, and the whole point of returning it: a client must be able to
24133
+ * tell a FACT about the hub's socket from an echo of its own guess.
24134
+ */
24135
+ var LocalPortSourceEnum = _enum([
24136
+ "server-config",
24137
+ "server-env",
24138
+ "caller-hint",
24139
+ "default"
24140
+ ]);
24141
+ /** The port every LAN/loopback `baseUrl` in the same result was built with. */
24142
+ var AdvertisedLocalPortSchema = object({
24143
+ port: number().int().min(1).max(65535),
24144
+ source: LocalPortSourceEnum
24145
+ });
24146
+ var GetConnectionEndpointsResultSchema = object({
24147
+ endpoints: array(ConnectionEndpointSchema).readonly(),
24148
+ /**
24149
+ * The port the hub built the LAN/loopback URLs with, and where that number
24150
+ * came from.
24151
+ *
24152
+ * Returned rather than merely applied, because "the URL is right" and "the
24153
+ * client can KNOW the URL is right" are different properties. A client that
24154
+ * only sees a corrected URL cannot distinguish a hub that fixed the port from
24155
+ * a hub that echoed the port the client sent, so it cannot decide whether to
24156
+ * race the candidate or discard it. With `source` it can: anything but
24157
+ * `caller-hint` is the hub's own socket.
24158
+ *
24159
+ * Absent on hubs predating this field — a client that finds it missing is
24160
+ * talking to an echoing hub and must degrade exactly as it does for
24161
+ * `caller-hint`.
24162
+ */
24163
+ localPort: AdvertisedLocalPortSchema
24164
+ });
24100
24165
  /**
24101
24166
  * The chosen outbound endpoint for notification artifacts. `baseUrl: null` =
24102
24167
  * AUTO (resolved from the candidate ranking at send time); `resolved` reports
@@ -24117,8 +24182,13 @@ var AllowedAddressesSchema = object({
24117
24182
  */
24118
24183
  addresses: array(string()).readonly() });
24119
24184
  method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
24120
- /** Local hub HTTP port to use in base URLs. */
24121
- port: number().int().min(1).max(65535),
24185
+ /**
24186
+ * LEGACY HINT — do not send from new code. Kept optional so clients
24187
+ * written against the echoing contract keep working; the hub uses it
24188
+ * only when it cannot read its own port, and says so via
24189
+ * `localPort.source === 'caller-hint'`.
24190
+ */
24191
+ port: number().int().min(1).max(65535).optional(),
24122
24192
  /** Include `http(s)://127.0.0.1:<port>` as the lowest-priority
24123
24193
  * candidate. Default `true`. */
24124
24194
  includeLoopback: boolean().optional(),
@@ -44216,6 +44286,38 @@ var TerminalSessionManager = class {
44216
44286
  * (`node-pty`) and should update via `camstack deploy` independently of the
44217
44287
  * framework, exactly like the node-av decoder addon.
44218
44288
  */
44289
+ /**
44290
+ * Default argv for the Glances profile — deliberately EMPTY.
44291
+ *
44292
+ * Measured on the hub (2026-08-21, glances 4.5.6, embedded Python, a real pty
44293
+ * at the terminal camera's own 120x40 grid, CPU sampled from
44294
+ * /proc/<pid>/{utime,stime} so the number is the process's own and not the
44295
+ * box's contention):
44296
+ *
44297
+ * argv CPU (of one core)
44298
+ * (none — this default) 0.8 %
44299
+ * -t 3 0.7 %
44300
+ * -t 5 0.6 %
44301
+ * --disable-plugin processlist 0.7 %
44302
+ * --disable-plugin containers 0.9 %
44303
+ * --disable-plugin sensors 0.8 %
44304
+ * --disable-process 0.8 %
44305
+ * -t 3 --disable-plugin processlist,containers,sensors 0.5 %
44306
+ * -t 5 --disable-plugin processlist,containers,sensors 0.4 %
44307
+ *
44308
+ * Startup costs 0.19 core-seconds; steady state is 1.0 % of one core at 36 MB
44309
+ * RSS. The whole tuning range is worth ~0.4 % of ONE core out of 20 — below the
44310
+ * noise floor of a box that idles at 2.6 cores — while every entry that buys
44311
+ * something removes a panel the operator opened the camera to read. `-t` is
44312
+ * additionally counter-productive here: the terminal camera renders at 2 FPS
44313
+ * and glances already repaints every 2 s, so slowing glances only makes the
44314
+ * camera show stale numbers.
44315
+ *
44316
+ * So the default ships empty and the knob stays in front of the operator.
44317
+ * Anyone who does want to trade panels for CPU has the numbers above.
44318
+ */
44319
+ var DEFAULT_GLANCES_ARGS = [];
44320
+ var GLANCES_ARGS_DESCRIPTION = "Exact arguments passed to Glances. One tag per argv entry. Empty is the measured-best default: at the camera grid Glances costs ~1% of one core, and the cheapest tuning found (`-t 5 --disable-plugin processlist,containers,sensors`) saves only ~0.4% of one core while removing the panels most people open it for. `-t` also makes the camera show stale numbers — it renders at 2 FPS and Glances already repaints every 2s.";
44219
44321
  var DEFAULTS = {
44220
44322
  btmPath: "",
44221
44323
  btmEnabled: true,
@@ -44225,7 +44327,7 @@ var DEFAULTS = {
44225
44327
  topArgs: [],
44226
44328
  glancesEnabled: true,
44227
44329
  glancesPath: "",
44228
- glancesArgs: [],
44330
+ glancesArgs: DEFAULT_GLANCES_ARGS,
44229
44331
  allowShell: false,
44230
44332
  shellPath: "",
44231
44333
  maxSessions: 4,
@@ -44612,85 +44714,13 @@ var TerminalAddon = class extends BaseAddon {
44612
44714
  return this.instanceInfo(instance);
44613
44715
  }
44614
44716
  globalSettingsSchema() {
44615
- return this.schema({ sections: [{
44616
- id: "terminal",
44617
- title: "Terminal",
44618
- description: "Interactive terminal sessions in the Admin UI. Only pre-declared profiles can be opened; a free-form command is never accepted.",
44619
- columns: 2,
44620
- fields: [
44621
- this.field({
44622
- type: "boolean",
44623
- key: "btmEnabled",
44624
- label: "Enable BTM camera",
44625
- default: true,
44626
- perNode: true
44627
- }),
44628
- this.field({
44629
- type: "text",
44630
- key: "btmPath",
44631
- label: "BTM binary",
44632
- description: "Path to the `btm` (bottom) executable. Leave empty to resolve from PATH.",
44633
- placeholder: "btm",
44634
- default: "",
44635
- perNode: true
44636
- }),
44637
- this.field({
44638
- type: "tags",
44639
- key: "btmArgs",
44640
- label: "BTM arguments",
44641
- description: "Exact arguments passed to btm.",
44642
- default: [],
44643
- perNode: true
44644
- }),
44645
- this.field({
44646
- type: "boolean",
44647
- key: "topEnabled",
44648
- label: "Enable Top camera",
44649
- default: true,
44650
- perNode: true
44651
- }),
44652
- this.field({
44653
- type: "text",
44654
- key: "topPath",
44655
- label: "Top binary",
44656
- placeholder: "top",
44657
- default: "",
44658
- perNode: true
44659
- }),
44660
- this.field({
44661
- type: "tags",
44662
- key: "topArgs",
44663
- label: "Top arguments",
44664
- description: "Exact arguments passed to top.",
44665
- default: [],
44666
- perNode: true
44667
- }),
44668
- this.field({
44669
- type: "boolean",
44670
- key: "glancesEnabled",
44671
- label: "Enable Glances camera",
44672
- description: "Glances is installed automatically into CamStack embedded Python.",
44673
- default: true,
44674
- perNode: true
44675
- }),
44676
- this.field({
44677
- type: "text",
44678
- key: "glancesPath",
44679
- label: "Glances binary override",
44680
- description: "Optional executable override. Empty uses the automatically managed Python package.",
44681
- placeholder: "glances",
44682
- default: "",
44683
- perNode: true
44684
- }),
44685
- this.field({
44686
- type: "tags",
44687
- key: "glancesArgs",
44688
- label: "Glances arguments",
44689
- description: "Exact arguments passed to Glances.",
44690
- default: [],
44691
- perNode: true
44692
- }),
44693
- this.field({
44717
+ return this.schema({ sections: [
44718
+ {
44719
+ id: "terminal",
44720
+ title: "Terminal",
44721
+ description: "Interactive terminal sessions in the Admin UI. Only pre-declared profiles can be opened; a free-form command is never accepted. Every setting below is per node — the values you see belong to the node selected above.",
44722
+ columns: 2,
44723
+ fields: [this.field({
44694
44724
  type: "number",
44695
44725
  key: "maxSessions",
44696
44726
  label: "Max concurrent sessions",
@@ -44699,16 +44729,122 @@ var TerminalAddon = class extends BaseAddon {
44699
44729
  step: 1,
44700
44730
  default: 4,
44701
44731
  perNode: true
44702
- }),
44703
- this.field({
44732
+ })]
44733
+ },
44734
+ {
44735
+ id: "terminal-btm",
44736
+ title: "BTM camera",
44737
+ description: "bottom (btm) — CPU, memory, network and process monitor.",
44738
+ columns: 2,
44739
+ fields: [
44740
+ this.field({
44741
+ type: "boolean",
44742
+ key: "btmEnabled",
44743
+ label: "Enabled",
44744
+ description: "Offer the BTM profile on this node.",
44745
+ default: true,
44746
+ perNode: true
44747
+ }),
44748
+ this.field({
44749
+ type: "text",
44750
+ key: "btmPath",
44751
+ label: "Binary",
44752
+ description: "Path to the `btm` (bottom) executable. Leave empty to resolve from PATH.",
44753
+ placeholder: "btm",
44754
+ default: "",
44755
+ perNode: true
44756
+ }),
44757
+ this.field({
44758
+ type: "tags",
44759
+ key: "btmArgs",
44760
+ label: "Arguments",
44761
+ description: "Exact arguments passed to btm. One tag per argv entry.",
44762
+ default: [],
44763
+ perNode: true,
44764
+ span: 2
44765
+ })
44766
+ ]
44767
+ },
44768
+ {
44769
+ id: "terminal-top",
44770
+ title: "Top camera",
44771
+ description: "The operating system process and resource monitor.",
44772
+ columns: 2,
44773
+ fields: [
44774
+ this.field({
44775
+ type: "boolean",
44776
+ key: "topEnabled",
44777
+ label: "Enabled",
44778
+ description: "Offer the Top profile on this node.",
44779
+ default: true,
44780
+ perNode: true
44781
+ }),
44782
+ this.field({
44783
+ type: "text",
44784
+ key: "topPath",
44785
+ label: "Binary",
44786
+ placeholder: "top",
44787
+ default: "",
44788
+ perNode: true
44789
+ }),
44790
+ this.field({
44791
+ type: "tags",
44792
+ key: "topArgs",
44793
+ label: "Arguments",
44794
+ description: "Exact arguments passed to top. One tag per argv entry.",
44795
+ default: [],
44796
+ perNode: true,
44797
+ span: 2
44798
+ })
44799
+ ]
44800
+ },
44801
+ {
44802
+ id: "terminal-glances",
44803
+ title: "Glances camera",
44804
+ description: "Cross-platform curses monitor, installed automatically into CamStack embedded Python. Glances is the most expensive built-in profile — it only runs while somebody is watching, but while it runs it costs real CPU. See the Arguments help below before widening it.",
44805
+ columns: 2,
44806
+ fields: [
44807
+ this.field({
44808
+ type: "boolean",
44809
+ key: "glancesEnabled",
44810
+ label: "Enabled",
44811
+ description: "Offer the Glances profile on this node.",
44812
+ default: true,
44813
+ perNode: true
44814
+ }),
44815
+ this.field({
44816
+ type: "text",
44817
+ key: "glancesPath",
44818
+ label: "Binary override",
44819
+ description: "Optional executable override. Empty uses the automatically managed Python package.",
44820
+ placeholder: "glances",
44821
+ default: "",
44822
+ perNode: true
44823
+ }),
44824
+ this.field({
44825
+ type: "tags",
44826
+ key: "glancesArgs",
44827
+ label: "Arguments",
44828
+ description: GLANCES_ARGS_DESCRIPTION,
44829
+ default: [...DEFAULT_GLANCES_ARGS],
44830
+ perNode: true,
44831
+ span: 2
44832
+ })
44833
+ ]
44834
+ },
44835
+ {
44836
+ id: "terminal-shell",
44837
+ title: "Interactive shell",
44838
+ description: "The one profile that is genuinely a command surface. Off by default; turning it on grants command execution as the server user to every admin.",
44839
+ columns: 2,
44840
+ fields: [this.field({
44704
44841
  type: "boolean",
44705
44842
  key: "allowShell",
44706
44843
  label: "Allow interactive shell",
44707
- description: "Adds a \"Shell\" profile that grants command execution as the server user. Off by default.",
44844
+ description: "Adds a \"Shell\" profile on this node.",
44708
44845
  default: false,
44709
44846
  perNode: true
44710
- }),
44711
- this.field({
44847
+ }), this.field({
44712
44848
  type: "text",
44713
44849
  key: "shellPath",
44714
44850
  label: "Shell binary",
@@ -44716,89 +44852,90 @@ var TerminalAddon = class extends BaseAddon {
44716
44852
  placeholder: "/bin/bash",
44717
44853
  default: "",
44718
44854
  perNode: true
44719
- })
44720
- ]
44721
- }, {
44722
- id: "terminal-profiles",
44723
- title: "Custom profiles",
44724
- 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.",
44725
- columns: 1,
44726
- fields: [this.field({
44727
- type: "editable-array",
44728
- key: "customProfiles",
44729
- label: "Profiles",
44730
- perNode: true,
44731
- default: [],
44732
- maxRows: 32,
44733
- addLabel: "Add profile",
44734
- emptyMessage: "No custom profiles configured.",
44735
- rowTitleTemplate: "{label} — {executable}",
44736
- defaultItem: {
44737
- enabled: true,
44738
- profileId: "",
44739
- label: "",
44740
- description: "",
44741
- executable: "",
44742
- args: [],
44743
- cwd: "",
44744
- environment: []
44745
- },
44746
- itemFields: [
44747
- {
44748
- type: "boolean",
44749
- key: "enabled",
44750
- label: "Enabled",
44751
- default: true
44752
- },
44753
- {
44754
- type: "text",
44755
- key: "profileId",
44756
- label: "Profile ID",
44757
- description: "Stable lowercase slug, for example gpu-monitor.",
44758
- required: true
44759
- },
44760
- {
44761
- type: "text",
44762
- key: "label",
44763
- label: "Display name",
44764
- required: true
44765
- },
44766
- {
44767
- type: "textarea",
44768
- key: "description",
44769
- label: "Description",
44770
- rows: 2
44771
- },
44772
- {
44773
- type: "text",
44774
- key: "executable",
44775
- label: "Executable",
44776
- description: "Absolute path or a binary resolved from PATH.",
44777
- required: true
44778
- },
44779
- {
44780
- type: "tags",
44781
- key: "args",
44782
- label: "Arguments",
44783
- description: "One exact process argument per tag.",
44784
- maxTags: 64
44785
- },
44786
- {
44787
- type: "text",
44788
- key: "cwd",
44789
- label: "Working directory",
44790
- description: "Optional working directory for this profile."
44855
+ })]
44856
+ },
44857
+ {
44858
+ id: "terminal-profiles",
44859
+ title: "Custom profiles",
44860
+ 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.",
44861
+ columns: 1,
44862
+ fields: [this.field({
44863
+ type: "editable-array",
44864
+ key: "customProfiles",
44865
+ label: "Profiles",
44866
+ perNode: true,
44867
+ default: [],
44868
+ maxRows: 32,
44869
+ addLabel: "Add profile",
44870
+ emptyMessage: "No custom profiles configured.",
44871
+ rowTitleTemplate: "{label} — {executable}",
44872
+ defaultItem: {
44873
+ enabled: true,
44874
+ profileId: "",
44875
+ label: "",
44876
+ description: "",
44877
+ executable: "",
44878
+ args: [],
44879
+ cwd: "",
44880
+ environment: []
44791
44881
  },
44792
- {
44793
- type: "tags",
44794
- key: "environment",
44795
- label: "Environment",
44796
- description: "Optional NAME=value entries merged into the process environment.",
44797
- maxTags: 64
44798
- }
44799
- ]
44800
- })]
44801
- }] });
44882
+ itemFields: [
44883
+ {
44884
+ type: "boolean",
44885
+ key: "enabled",
44886
+ label: "Enabled",
44887
+ default: true
44888
+ },
44889
+ {
44890
+ type: "text",
44891
+ key: "profileId",
44892
+ label: "Profile ID",
44893
+ description: "Stable lowercase slug, for example gpu-monitor.",
44894
+ required: true
44895
+ },
44896
+ {
44897
+ type: "text",
44898
+ key: "label",
44899
+ label: "Display name",
44900
+ required: true
44901
+ },
44902
+ {
44903
+ type: "textarea",
44904
+ key: "description",
44905
+ label: "Description",
44906
+ rows: 2
44907
+ },
44908
+ {
44909
+ type: "text",
44910
+ key: "executable",
44911
+ label: "Executable",
44912
+ description: "Absolute path or a binary resolved from PATH.",
44913
+ required: true
44914
+ },
44915
+ {
44916
+ type: "tags",
44917
+ key: "args",
44918
+ label: "Arguments",
44919
+ description: "One exact process argument per tag.",
44920
+ maxTags: 64
44921
+ },
44922
+ {
44923
+ type: "text",
44924
+ key: "cwd",
44925
+ label: "Working directory",
44926
+ description: "Optional working directory for this profile."
44927
+ },
44928
+ {
44929
+ type: "tags",
44930
+ key: "environment",
44931
+ label: "Environment",
44932
+ description: "Optional NAME=value entries merged into the process environment.",
44933
+ maxTags: 64
44934
+ }
44935
+ ]
44936
+ })]
44937
+ }
44938
+ ] });
44802
44939
  }
44803
44940
  };
44804
44941
  //#endregion
package/dist/addon.mjs CHANGED
@@ -8,7 +8,7 @@ import { createServer } from "node:http";
8
8
  //#region \0rolldown/runtime.js
9
9
  var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
10
10
  //#endregion
11
- //#region ../types/dist/event-category-XfKNtfCc.mjs
11
+ //#region ../types/dist/event-category-CIa_iT6b.mjs
12
12
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
13
13
  EventCategory["SystemBoot"] = "system.boot";
14
14
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -24,6 +24,15 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
24
24
  */
25
25
  EventCategory["SystemRestartCompleted"] = "system.restart-completed";
26
26
  /**
27
+ * The hub reissued its own TLS certificate at boot (`ensureTlsCert`).
28
+ * Emitted only when the material on disk actually changed, so an
29
+ * operator who trusted the old certificate by hand is told rather than
30
+ * discovering it as a browser error. Payload `TlsCertChangedPayload`.
31
+ *
32
+ * Rule: docs/decisions/adr-0227-*.md
33
+ */
34
+ EventCategory["SystemTlsCertChanged"] = "system.tls-cert-changed";
35
+ /**
27
36
  * A newer addon or server-root package version was found by the
28
37
  * authoritative registry check. Emitted once when any observed
29
38
  * `latestVersion` changes (or a package/node first appears behind);
@@ -19002,6 +19011,12 @@ var CameraStatusSchema = object({
19002
19011
  /** Unix timestamp (ms) when this snapshot was composed server-side. */
19003
19012
  fetchedAt: number()
19004
19013
  });
19014
+ var InferenceDeviceExclusionReasonSchema = _enum([
19015
+ "disabled",
19016
+ "unavailable",
19017
+ "cannot-host-camera-root",
19018
+ "accelerator-preferred"
19019
+ ]);
19005
19020
  var NodeInferenceDeviceSchema = object({
19006
19021
  /** Stable per-node device key, e.g. `openvino:npu`, `edgetpu:usb`, `cpu`. */
19007
19022
  key: string(),
@@ -19032,7 +19047,17 @@ var NodeInferenceDeviceSchema = object({
19032
19047
  * available per format; this is the stored selection that becomes the
19033
19048
  * default for EVERY camera landing on this accelerator.
19034
19049
  */
19035
- steps: record(string(), DeviceStepConfigSchema).optional()
19050
+ steps: record(string(), DeviceStepConfigSchema).optional(),
19051
+ /**
19052
+ * `null` when the device IS a camera-root candidate on this node; otherwise
19053
+ * the reason the dispatcher drops it. Computed by the SAME
19054
+ * `resolveInferenceDeviceEligibility` the dispatcher runs, so this view can
19055
+ * never disagree with the election — deriving it in the UI from
19056
+ * `enabled`/`available` would silently miss `cannot-host-camera-root` (needs
19057
+ * the node's model catalog) and `accelerator-preferred` (needs the node-wide
19058
+ * "an accelerator is serving" predicate).
19059
+ */
19060
+ exclusion: InferenceDeviceExclusionReasonSchema.nullable()
19036
19061
  });
19037
19062
  var NodeInferenceDevicesSchema = object({
19038
19063
  nodeId: string(),
@@ -24030,7 +24055,12 @@ var ListResultSchema = object({
24030
24055
  probedAt: number()
24031
24056
  });
24032
24057
  var PreferredSchema = LocalInterfaceSchema.nullable();
24033
- var GetConnectionEndpointsResultSchema = object({ endpoints: array(object({
24058
+ /**
24059
+ * Candidate base URL for the SDK to race on connect. Order matters —
24060
+ * the SDK should attempt these top-to-bottom with a short per-candidate
24061
+ * timeout (e.g. 1500ms) and cache the winner for the session.
24062
+ */
24063
+ var ConnectionEndpointSchema = object({
24034
24064
  /** Operator-facing label (e.g. "LAN — en0", "Public tunnel"). */
24035
24065
  label: string(),
24036
24066
  /** Fully-formed base URL with scheme + host + port. */
@@ -24073,7 +24103,42 @@ var GetConnectionEndpointsResultSchema = object({ endpoints: array(object({
24073
24103
  * ordering between polls.
24074
24104
  */
24075
24105
  priority: number()
24076
- })).readonly() });
24106
+ });
24107
+ /**
24108
+ * Where the advertised local port came from. Ordered most → least
24109
+ * authoritative, and the whole point of returning it: a client must be able to
24110
+ * tell a FACT about the hub's socket from an echo of its own guess.
24111
+ */
24112
+ var LocalPortSourceEnum = _enum([
24113
+ "server-config",
24114
+ "server-env",
24115
+ "caller-hint",
24116
+ "default"
24117
+ ]);
24118
+ /** The port every LAN/loopback `baseUrl` in the same result was built with. */
24119
+ var AdvertisedLocalPortSchema = object({
24120
+ port: number().int().min(1).max(65535),
24121
+ source: LocalPortSourceEnum
24122
+ });
24123
+ var GetConnectionEndpointsResultSchema = object({
24124
+ endpoints: array(ConnectionEndpointSchema).readonly(),
24125
+ /**
24126
+ * The port the hub built the LAN/loopback URLs with, and where that number
24127
+ * came from.
24128
+ *
24129
+ * Returned rather than merely applied, because "the URL is right" and "the
24130
+ * client can KNOW the URL is right" are different properties. A client that
24131
+ * only sees a corrected URL cannot distinguish a hub that fixed the port from
24132
+ * a hub that echoed the port the client sent, so it cannot decide whether to
24133
+ * race the candidate or discard it. With `source` it can: anything but
24134
+ * `caller-hint` is the hub's own socket.
24135
+ *
24136
+ * Absent on hubs predating this field — a client that finds it missing is
24137
+ * talking to an echoing hub and must degrade exactly as it does for
24138
+ * `caller-hint`.
24139
+ */
24140
+ localPort: AdvertisedLocalPortSchema
24141
+ });
24077
24142
  /**
24078
24143
  * The chosen outbound endpoint for notification artifacts. `baseUrl: null` =
24079
24144
  * AUTO (resolved from the candidate ranking at send time); `resolved` reports
@@ -24094,8 +24159,13 @@ var AllowedAddressesSchema = object({
24094
24159
  */
24095
24160
  addresses: array(string()).readonly() });
24096
24161
  method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
24097
- /** Local hub HTTP port to use in base URLs. */
24098
- port: number().int().min(1).max(65535),
24162
+ /**
24163
+ * LEGACY HINT — do not send from new code. Kept optional so clients
24164
+ * written against the echoing contract keep working; the hub uses it
24165
+ * only when it cannot read its own port, and says so via
24166
+ * `localPort.source === 'caller-hint'`.
24167
+ */
24168
+ port: number().int().min(1).max(65535).optional(),
24099
24169
  /** Include `http(s)://127.0.0.1:<port>` as the lowest-priority
24100
24170
  * candidate. Default `true`. */
24101
24171
  includeLoopback: boolean().optional(),
@@ -44193,6 +44263,38 @@ var TerminalSessionManager = class {
44193
44263
  * (`node-pty`) and should update via `camstack deploy` independently of the
44194
44264
  * framework, exactly like the node-av decoder addon.
44195
44265
  */
44266
+ /**
44267
+ * Default argv for the Glances profile — deliberately EMPTY.
44268
+ *
44269
+ * Measured on the hub (2026-08-21, glances 4.5.6, embedded Python, a real pty
44270
+ * at the terminal camera's own 120x40 grid, CPU sampled from
44271
+ * /proc/<pid>/{utime,stime} so the number is the process's own and not the
44272
+ * box's contention):
44273
+ *
44274
+ * argv CPU (of one core)
44275
+ * (none — this default) 0.8 %
44276
+ * -t 3 0.7 %
44277
+ * -t 5 0.6 %
44278
+ * --disable-plugin processlist 0.7 %
44279
+ * --disable-plugin containers 0.9 %
44280
+ * --disable-plugin sensors 0.8 %
44281
+ * --disable-process 0.8 %
44282
+ * -t 3 --disable-plugin processlist,containers,sensors 0.5 %
44283
+ * -t 5 --disable-plugin processlist,containers,sensors 0.4 %
44284
+ *
44285
+ * Startup costs 0.19 core-seconds; steady state is 1.0 % of one core at 36 MB
44286
+ * RSS. The whole tuning range is worth ~0.4 % of ONE core out of 20 — below the
44287
+ * noise floor of a box that idles at 2.6 cores — while every entry that buys
44288
+ * something removes a panel the operator opened the camera to read. `-t` is
44289
+ * additionally counter-productive here: the terminal camera renders at 2 FPS
44290
+ * and glances already repaints every 2 s, so slowing glances only makes the
44291
+ * camera show stale numbers.
44292
+ *
44293
+ * So the default ships empty and the knob stays in front of the operator.
44294
+ * Anyone who does want to trade panels for CPU has the numbers above.
44295
+ */
44296
+ var DEFAULT_GLANCES_ARGS = [];
44297
+ var GLANCES_ARGS_DESCRIPTION = "Exact arguments passed to Glances. One tag per argv entry. Empty is the measured-best default: at the camera grid Glances costs ~1% of one core, and the cheapest tuning found (`-t 5 --disable-plugin processlist,containers,sensors`) saves only ~0.4% of one core while removing the panels most people open it for. `-t` also makes the camera show stale numbers — it renders at 2 FPS and Glances already repaints every 2s.";
44196
44298
  var DEFAULTS = {
44197
44299
  btmPath: "",
44198
44300
  btmEnabled: true,
@@ -44202,7 +44304,7 @@ var DEFAULTS = {
44202
44304
  topArgs: [],
44203
44305
  glancesEnabled: true,
44204
44306
  glancesPath: "",
44205
- glancesArgs: [],
44307
+ glancesArgs: DEFAULT_GLANCES_ARGS,
44206
44308
  allowShell: false,
44207
44309
  shellPath: "",
44208
44310
  maxSessions: 4,
@@ -44589,85 +44691,13 @@ var TerminalAddon = class extends BaseAddon {
44589
44691
  return this.instanceInfo(instance);
44590
44692
  }
44591
44693
  globalSettingsSchema() {
44592
- return this.schema({ sections: [{
44593
- id: "terminal",
44594
- title: "Terminal",
44595
- description: "Interactive terminal sessions in the Admin UI. Only pre-declared profiles can be opened; a free-form command is never accepted.",
44596
- columns: 2,
44597
- fields: [
44598
- this.field({
44599
- type: "boolean",
44600
- key: "btmEnabled",
44601
- label: "Enable BTM camera",
44602
- default: true,
44603
- perNode: true
44604
- }),
44605
- this.field({
44606
- type: "text",
44607
- key: "btmPath",
44608
- label: "BTM binary",
44609
- description: "Path to the `btm` (bottom) executable. Leave empty to resolve from PATH.",
44610
- placeholder: "btm",
44611
- default: "",
44612
- perNode: true
44613
- }),
44614
- this.field({
44615
- type: "tags",
44616
- key: "btmArgs",
44617
- label: "BTM arguments",
44618
- description: "Exact arguments passed to btm.",
44619
- default: [],
44620
- perNode: true
44621
- }),
44622
- this.field({
44623
- type: "boolean",
44624
- key: "topEnabled",
44625
- label: "Enable Top camera",
44626
- default: true,
44627
- perNode: true
44628
- }),
44629
- this.field({
44630
- type: "text",
44631
- key: "topPath",
44632
- label: "Top binary",
44633
- placeholder: "top",
44634
- default: "",
44635
- perNode: true
44636
- }),
44637
- this.field({
44638
- type: "tags",
44639
- key: "topArgs",
44640
- label: "Top arguments",
44641
- description: "Exact arguments passed to top.",
44642
- default: [],
44643
- perNode: true
44644
- }),
44645
- this.field({
44646
- type: "boolean",
44647
- key: "glancesEnabled",
44648
- label: "Enable Glances camera",
44649
- description: "Glances is installed automatically into CamStack embedded Python.",
44650
- default: true,
44651
- perNode: true
44652
- }),
44653
- this.field({
44654
- type: "text",
44655
- key: "glancesPath",
44656
- label: "Glances binary override",
44657
- description: "Optional executable override. Empty uses the automatically managed Python package.",
44658
- placeholder: "glances",
44659
- default: "",
44660
- perNode: true
44661
- }),
44662
- this.field({
44663
- type: "tags",
44664
- key: "glancesArgs",
44665
- label: "Glances arguments",
44666
- description: "Exact arguments passed to Glances.",
44667
- default: [],
44668
- perNode: true
44669
- }),
44670
- this.field({
44694
+ return this.schema({ sections: [
44695
+ {
44696
+ id: "terminal",
44697
+ title: "Terminal",
44698
+ description: "Interactive terminal sessions in the Admin UI. Only pre-declared profiles can be opened; a free-form command is never accepted. Every setting below is per node — the values you see belong to the node selected above.",
44699
+ columns: 2,
44700
+ fields: [this.field({
44671
44701
  type: "number",
44672
44702
  key: "maxSessions",
44673
44703
  label: "Max concurrent sessions",
@@ -44676,16 +44706,122 @@ var TerminalAddon = class extends BaseAddon {
44676
44706
  step: 1,
44677
44707
  default: 4,
44678
44708
  perNode: true
44679
- }),
44680
- this.field({
44709
+ })]
44710
+ },
44711
+ {
44712
+ id: "terminal-btm",
44713
+ title: "BTM camera",
44714
+ description: "bottom (btm) — CPU, memory, network and process monitor.",
44715
+ columns: 2,
44716
+ fields: [
44717
+ this.field({
44718
+ type: "boolean",
44719
+ key: "btmEnabled",
44720
+ label: "Enabled",
44721
+ description: "Offer the BTM profile on this node.",
44722
+ default: true,
44723
+ perNode: true
44724
+ }),
44725
+ this.field({
44726
+ type: "text",
44727
+ key: "btmPath",
44728
+ label: "Binary",
44729
+ description: "Path to the `btm` (bottom) executable. Leave empty to resolve from PATH.",
44730
+ placeholder: "btm",
44731
+ default: "",
44732
+ perNode: true
44733
+ }),
44734
+ this.field({
44735
+ type: "tags",
44736
+ key: "btmArgs",
44737
+ label: "Arguments",
44738
+ description: "Exact arguments passed to btm. One tag per argv entry.",
44739
+ default: [],
44740
+ perNode: true,
44741
+ span: 2
44742
+ })
44743
+ ]
44744
+ },
44745
+ {
44746
+ id: "terminal-top",
44747
+ title: "Top camera",
44748
+ description: "The operating system process and resource monitor.",
44749
+ columns: 2,
44750
+ fields: [
44751
+ this.field({
44752
+ type: "boolean",
44753
+ key: "topEnabled",
44754
+ label: "Enabled",
44755
+ description: "Offer the Top profile on this node.",
44756
+ default: true,
44757
+ perNode: true
44758
+ }),
44759
+ this.field({
44760
+ type: "text",
44761
+ key: "topPath",
44762
+ label: "Binary",
44763
+ placeholder: "top",
44764
+ default: "",
44765
+ perNode: true
44766
+ }),
44767
+ this.field({
44768
+ type: "tags",
44769
+ key: "topArgs",
44770
+ label: "Arguments",
44771
+ description: "Exact arguments passed to top. One tag per argv entry.",
44772
+ default: [],
44773
+ perNode: true,
44774
+ span: 2
44775
+ })
44776
+ ]
44777
+ },
44778
+ {
44779
+ id: "terminal-glances",
44780
+ title: "Glances camera",
44781
+ description: "Cross-platform curses monitor, installed automatically into CamStack embedded Python. Glances is the most expensive built-in profile — it only runs while somebody is watching, but while it runs it costs real CPU. See the Arguments help below before widening it.",
44782
+ columns: 2,
44783
+ fields: [
44784
+ this.field({
44785
+ type: "boolean",
44786
+ key: "glancesEnabled",
44787
+ label: "Enabled",
44788
+ description: "Offer the Glances profile on this node.",
44789
+ default: true,
44790
+ perNode: true
44791
+ }),
44792
+ this.field({
44793
+ type: "text",
44794
+ key: "glancesPath",
44795
+ label: "Binary override",
44796
+ description: "Optional executable override. Empty uses the automatically managed Python package.",
44797
+ placeholder: "glances",
44798
+ default: "",
44799
+ perNode: true
44800
+ }),
44801
+ this.field({
44802
+ type: "tags",
44803
+ key: "glancesArgs",
44804
+ label: "Arguments",
44805
+ description: GLANCES_ARGS_DESCRIPTION,
44806
+ default: [...DEFAULT_GLANCES_ARGS],
44807
+ perNode: true,
44808
+ span: 2
44809
+ })
44810
+ ]
44811
+ },
44812
+ {
44813
+ id: "terminal-shell",
44814
+ title: "Interactive shell",
44815
+ description: "The one profile that is genuinely a command surface. Off by default; turning it on grants command execution as the server user to every admin.",
44816
+ columns: 2,
44817
+ fields: [this.field({
44681
44818
  type: "boolean",
44682
44819
  key: "allowShell",
44683
44820
  label: "Allow interactive shell",
44684
- description: "Adds a \"Shell\" profile that grants command execution as the server user. Off by default.",
44821
+ description: "Adds a \"Shell\" profile on this node.",
44685
44822
  default: false,
44686
44823
  perNode: true
44687
- }),
44688
- this.field({
44824
+ }), this.field({
44689
44825
  type: "text",
44690
44826
  key: "shellPath",
44691
44827
  label: "Shell binary",
@@ -44693,89 +44829,90 @@ var TerminalAddon = class extends BaseAddon {
44693
44829
  placeholder: "/bin/bash",
44694
44830
  default: "",
44695
44831
  perNode: true
44696
- })
44697
- ]
44698
- }, {
44699
- id: "terminal-profiles",
44700
- title: "Custom profiles",
44701
- 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.",
44702
- columns: 1,
44703
- fields: [this.field({
44704
- type: "editable-array",
44705
- key: "customProfiles",
44706
- label: "Profiles",
44707
- perNode: true,
44708
- default: [],
44709
- maxRows: 32,
44710
- addLabel: "Add profile",
44711
- emptyMessage: "No custom profiles configured.",
44712
- rowTitleTemplate: "{label} — {executable}",
44713
- defaultItem: {
44714
- enabled: true,
44715
- profileId: "",
44716
- label: "",
44717
- description: "",
44718
- executable: "",
44719
- args: [],
44720
- cwd: "",
44721
- environment: []
44722
- },
44723
- itemFields: [
44724
- {
44725
- type: "boolean",
44726
- key: "enabled",
44727
- label: "Enabled",
44728
- default: true
44729
- },
44730
- {
44731
- type: "text",
44732
- key: "profileId",
44733
- label: "Profile ID",
44734
- description: "Stable lowercase slug, for example gpu-monitor.",
44735
- required: true
44736
- },
44737
- {
44738
- type: "text",
44739
- key: "label",
44740
- label: "Display name",
44741
- required: true
44742
- },
44743
- {
44744
- type: "textarea",
44745
- key: "description",
44746
- label: "Description",
44747
- rows: 2
44748
- },
44749
- {
44750
- type: "text",
44751
- key: "executable",
44752
- label: "Executable",
44753
- description: "Absolute path or a binary resolved from PATH.",
44754
- required: true
44755
- },
44756
- {
44757
- type: "tags",
44758
- key: "args",
44759
- label: "Arguments",
44760
- description: "One exact process argument per tag.",
44761
- maxTags: 64
44762
- },
44763
- {
44764
- type: "text",
44765
- key: "cwd",
44766
- label: "Working directory",
44767
- description: "Optional working directory for this profile."
44832
+ })]
44833
+ },
44834
+ {
44835
+ id: "terminal-profiles",
44836
+ title: "Custom profiles",
44837
+ 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.",
44838
+ columns: 1,
44839
+ fields: [this.field({
44840
+ type: "editable-array",
44841
+ key: "customProfiles",
44842
+ label: "Profiles",
44843
+ perNode: true,
44844
+ default: [],
44845
+ maxRows: 32,
44846
+ addLabel: "Add profile",
44847
+ emptyMessage: "No custom profiles configured.",
44848
+ rowTitleTemplate: "{label} — {executable}",
44849
+ defaultItem: {
44850
+ enabled: true,
44851
+ profileId: "",
44852
+ label: "",
44853
+ description: "",
44854
+ executable: "",
44855
+ args: [],
44856
+ cwd: "",
44857
+ environment: []
44768
44858
  },
44769
- {
44770
- type: "tags",
44771
- key: "environment",
44772
- label: "Environment",
44773
- description: "Optional NAME=value entries merged into the process environment.",
44774
- maxTags: 64
44775
- }
44776
- ]
44777
- })]
44778
- }] });
44859
+ itemFields: [
44860
+ {
44861
+ type: "boolean",
44862
+ key: "enabled",
44863
+ label: "Enabled",
44864
+ default: true
44865
+ },
44866
+ {
44867
+ type: "text",
44868
+ key: "profileId",
44869
+ label: "Profile ID",
44870
+ description: "Stable lowercase slug, for example gpu-monitor.",
44871
+ required: true
44872
+ },
44873
+ {
44874
+ type: "text",
44875
+ key: "label",
44876
+ label: "Display name",
44877
+ required: true
44878
+ },
44879
+ {
44880
+ type: "textarea",
44881
+ key: "description",
44882
+ label: "Description",
44883
+ rows: 2
44884
+ },
44885
+ {
44886
+ type: "text",
44887
+ key: "executable",
44888
+ label: "Executable",
44889
+ description: "Absolute path or a binary resolved from PATH.",
44890
+ required: true
44891
+ },
44892
+ {
44893
+ type: "tags",
44894
+ key: "args",
44895
+ label: "Arguments",
44896
+ description: "One exact process argument per tag.",
44897
+ maxTags: 64
44898
+ },
44899
+ {
44900
+ type: "text",
44901
+ key: "cwd",
44902
+ label: "Working directory",
44903
+ description: "Optional working directory for this profile."
44904
+ },
44905
+ {
44906
+ type: "tags",
44907
+ key: "environment",
44908
+ label: "Environment",
44909
+ description: "Optional NAME=value entries merged into the process environment.",
44910
+ maxTags: 64
44911
+ }
44912
+ ]
44913
+ })]
44914
+ }
44915
+ ] });
44779
44916
  }
44780
44917
  };
44781
44918
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-terminal",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
4
4
  "description": "Interactive terminal sessions (pty + xterm) as a CamStack addon",
5
5
  "keywords": [
6
6
  "camstack",