@camstack/addon-terminal 0.1.27 → 0.1.28

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 +300 -172
  2. package/dist/addon.mjs +300 -172
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -19025,6 +19025,12 @@ var CameraStatusSchema = object({
19025
19025
  /** Unix timestamp (ms) when this snapshot was composed server-side. */
19026
19026
  fetchedAt: number()
19027
19027
  });
19028
+ var InferenceDeviceExclusionReasonSchema = _enum([
19029
+ "disabled",
19030
+ "unavailable",
19031
+ "cannot-host-camera-root",
19032
+ "accelerator-preferred"
19033
+ ]);
19028
19034
  var NodeInferenceDeviceSchema = object({
19029
19035
  /** Stable per-node device key, e.g. `openvino:npu`, `edgetpu:usb`, `cpu`. */
19030
19036
  key: string(),
@@ -19055,7 +19061,17 @@ var NodeInferenceDeviceSchema = object({
19055
19061
  * available per format; this is the stored selection that becomes the
19056
19062
  * default for EVERY camera landing on this accelerator.
19057
19063
  */
19058
- steps: record(string(), DeviceStepConfigSchema).optional()
19064
+ steps: record(string(), DeviceStepConfigSchema).optional(),
19065
+ /**
19066
+ * `null` when the device IS a camera-root candidate on this node; otherwise
19067
+ * the reason the dispatcher drops it. Computed by the SAME
19068
+ * `resolveInferenceDeviceEligibility` the dispatcher runs, so this view can
19069
+ * never disagree with the election — deriving it in the UI from
19070
+ * `enabled`/`available` would silently miss `cannot-host-camera-root` (needs
19071
+ * the node's model catalog) and `accelerator-preferred` (needs the node-wide
19072
+ * "an accelerator is serving" predicate).
19073
+ */
19074
+ exclusion: InferenceDeviceExclusionReasonSchema.nullable()
19059
19075
  });
19060
19076
  var NodeInferenceDevicesSchema = object({
19061
19077
  nodeId: string(),
@@ -24053,7 +24069,12 @@ var ListResultSchema = object({
24053
24069
  probedAt: number()
24054
24070
  });
24055
24071
  var PreferredSchema = LocalInterfaceSchema.nullable();
24056
- var GetConnectionEndpointsResultSchema = object({ endpoints: array(object({
24072
+ /**
24073
+ * Candidate base URL for the SDK to race on connect. Order matters —
24074
+ * the SDK should attempt these top-to-bottom with a short per-candidate
24075
+ * timeout (e.g. 1500ms) and cache the winner for the session.
24076
+ */
24077
+ var ConnectionEndpointSchema = object({
24057
24078
  /** Operator-facing label (e.g. "LAN — en0", "Public tunnel"). */
24058
24079
  label: string(),
24059
24080
  /** Fully-formed base URL with scheme + host + port. */
@@ -24096,7 +24117,42 @@ var GetConnectionEndpointsResultSchema = object({ endpoints: array(object({
24096
24117
  * ordering between polls.
24097
24118
  */
24098
24119
  priority: number()
24099
- })).readonly() });
24120
+ });
24121
+ /**
24122
+ * Where the advertised local port came from. Ordered most → least
24123
+ * authoritative, and the whole point of returning it: a client must be able to
24124
+ * tell a FACT about the hub's socket from an echo of its own guess.
24125
+ */
24126
+ var LocalPortSourceEnum = _enum([
24127
+ "server-config",
24128
+ "server-env",
24129
+ "caller-hint",
24130
+ "default"
24131
+ ]);
24132
+ /** The port every LAN/loopback `baseUrl` in the same result was built with. */
24133
+ var AdvertisedLocalPortSchema = object({
24134
+ port: number().int().min(1).max(65535),
24135
+ source: LocalPortSourceEnum
24136
+ });
24137
+ var GetConnectionEndpointsResultSchema = object({
24138
+ endpoints: array(ConnectionEndpointSchema).readonly(),
24139
+ /**
24140
+ * The port the hub built the LAN/loopback URLs with, and where that number
24141
+ * came from.
24142
+ *
24143
+ * Returned rather than merely applied, because "the URL is right" and "the
24144
+ * client can KNOW the URL is right" are different properties. A client that
24145
+ * only sees a corrected URL cannot distinguish a hub that fixed the port from
24146
+ * a hub that echoed the port the client sent, so it cannot decide whether to
24147
+ * race the candidate or discard it. With `source` it can: anything but
24148
+ * `caller-hint` is the hub's own socket.
24149
+ *
24150
+ * Absent on hubs predating this field — a client that finds it missing is
24151
+ * talking to an echoing hub and must degrade exactly as it does for
24152
+ * `caller-hint`.
24153
+ */
24154
+ localPort: AdvertisedLocalPortSchema
24155
+ });
24100
24156
  /**
24101
24157
  * The chosen outbound endpoint for notification artifacts. `baseUrl: null` =
24102
24158
  * AUTO (resolved from the candidate ranking at send time); `resolved` reports
@@ -24117,8 +24173,13 @@ var AllowedAddressesSchema = object({
24117
24173
  */
24118
24174
  addresses: array(string()).readonly() });
24119
24175
  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),
24176
+ /**
24177
+ * LEGACY HINT — do not send from new code. Kept optional so clients
24178
+ * written against the echoing contract keep working; the hub uses it
24179
+ * only when it cannot read its own port, and says so via
24180
+ * `localPort.source === 'caller-hint'`.
24181
+ */
24182
+ port: number().int().min(1).max(65535).optional(),
24122
24183
  /** Include `http(s)://127.0.0.1:<port>` as the lowest-priority
24123
24184
  * candidate. Default `true`. */
24124
24185
  includeLoopback: boolean().optional(),
@@ -44216,6 +44277,38 @@ var TerminalSessionManager = class {
44216
44277
  * (`node-pty`) and should update via `camstack deploy` independently of the
44217
44278
  * framework, exactly like the node-av decoder addon.
44218
44279
  */
44280
+ /**
44281
+ * Default argv for the Glances profile — deliberately EMPTY.
44282
+ *
44283
+ * Measured on the hub (2026-08-21, glances 4.5.6, embedded Python, a real pty
44284
+ * at the terminal camera's own 120x40 grid, CPU sampled from
44285
+ * /proc/<pid>/{utime,stime} so the number is the process's own and not the
44286
+ * box's contention):
44287
+ *
44288
+ * argv CPU (of one core)
44289
+ * (none — this default) 0.8 %
44290
+ * -t 3 0.7 %
44291
+ * -t 5 0.6 %
44292
+ * --disable-plugin processlist 0.7 %
44293
+ * --disable-plugin containers 0.9 %
44294
+ * --disable-plugin sensors 0.8 %
44295
+ * --disable-process 0.8 %
44296
+ * -t 3 --disable-plugin processlist,containers,sensors 0.5 %
44297
+ * -t 5 --disable-plugin processlist,containers,sensors 0.4 %
44298
+ *
44299
+ * Startup costs 0.19 core-seconds; steady state is 1.0 % of one core at 36 MB
44300
+ * RSS. The whole tuning range is worth ~0.4 % of ONE core out of 20 — below the
44301
+ * noise floor of a box that idles at 2.6 cores — while every entry that buys
44302
+ * something removes a panel the operator opened the camera to read. `-t` is
44303
+ * additionally counter-productive here: the terminal camera renders at 2 FPS
44304
+ * and glances already repaints every 2 s, so slowing glances only makes the
44305
+ * camera show stale numbers.
44306
+ *
44307
+ * So the default ships empty and the knob stays in front of the operator.
44308
+ * Anyone who does want to trade panels for CPU has the numbers above.
44309
+ */
44310
+ var DEFAULT_GLANCES_ARGS = [];
44311
+ 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
44312
  var DEFAULTS = {
44220
44313
  btmPath: "",
44221
44314
  btmEnabled: true,
@@ -44225,7 +44318,7 @@ var DEFAULTS = {
44225
44318
  topArgs: [],
44226
44319
  glancesEnabled: true,
44227
44320
  glancesPath: "",
44228
- glancesArgs: [],
44321
+ glancesArgs: DEFAULT_GLANCES_ARGS,
44229
44322
  allowShell: false,
44230
44323
  shellPath: "",
44231
44324
  maxSessions: 4,
@@ -44612,85 +44705,13 @@ var TerminalAddon = class extends BaseAddon {
44612
44705
  return this.instanceInfo(instance);
44613
44706
  }
44614
44707
  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({
44708
+ return this.schema({ sections: [
44709
+ {
44710
+ id: "terminal",
44711
+ title: "Terminal",
44712
+ 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.",
44713
+ columns: 2,
44714
+ fields: [this.field({
44694
44715
  type: "number",
44695
44716
  key: "maxSessions",
44696
44717
  label: "Max concurrent sessions",
@@ -44699,16 +44720,122 @@ var TerminalAddon = class extends BaseAddon {
44699
44720
  step: 1,
44700
44721
  default: 4,
44701
44722
  perNode: true
44702
- }),
44703
- this.field({
44723
+ })]
44724
+ },
44725
+ {
44726
+ id: "terminal-btm",
44727
+ title: "BTM camera",
44728
+ description: "bottom (btm) — CPU, memory, network and process monitor.",
44729
+ columns: 2,
44730
+ fields: [
44731
+ this.field({
44732
+ type: "boolean",
44733
+ key: "btmEnabled",
44734
+ label: "Enabled",
44735
+ description: "Offer the BTM profile on this node.",
44736
+ default: true,
44737
+ perNode: true
44738
+ }),
44739
+ this.field({
44740
+ type: "text",
44741
+ key: "btmPath",
44742
+ label: "Binary",
44743
+ description: "Path to the `btm` (bottom) executable. Leave empty to resolve from PATH.",
44744
+ placeholder: "btm",
44745
+ default: "",
44746
+ perNode: true
44747
+ }),
44748
+ this.field({
44749
+ type: "tags",
44750
+ key: "btmArgs",
44751
+ label: "Arguments",
44752
+ description: "Exact arguments passed to btm. One tag per argv entry.",
44753
+ default: [],
44754
+ perNode: true,
44755
+ span: 2
44756
+ })
44757
+ ]
44758
+ },
44759
+ {
44760
+ id: "terminal-top",
44761
+ title: "Top camera",
44762
+ description: "The operating system process and resource monitor.",
44763
+ columns: 2,
44764
+ fields: [
44765
+ this.field({
44766
+ type: "boolean",
44767
+ key: "topEnabled",
44768
+ label: "Enabled",
44769
+ description: "Offer the Top profile on this node.",
44770
+ default: true,
44771
+ perNode: true
44772
+ }),
44773
+ this.field({
44774
+ type: "text",
44775
+ key: "topPath",
44776
+ label: "Binary",
44777
+ placeholder: "top",
44778
+ default: "",
44779
+ perNode: true
44780
+ }),
44781
+ this.field({
44782
+ type: "tags",
44783
+ key: "topArgs",
44784
+ label: "Arguments",
44785
+ description: "Exact arguments passed to top. One tag per argv entry.",
44786
+ default: [],
44787
+ perNode: true,
44788
+ span: 2
44789
+ })
44790
+ ]
44791
+ },
44792
+ {
44793
+ id: "terminal-glances",
44794
+ title: "Glances camera",
44795
+ 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.",
44796
+ columns: 2,
44797
+ fields: [
44798
+ this.field({
44799
+ type: "boolean",
44800
+ key: "glancesEnabled",
44801
+ label: "Enabled",
44802
+ description: "Offer the Glances profile on this node.",
44803
+ default: true,
44804
+ perNode: true
44805
+ }),
44806
+ this.field({
44807
+ type: "text",
44808
+ key: "glancesPath",
44809
+ label: "Binary override",
44810
+ description: "Optional executable override. Empty uses the automatically managed Python package.",
44811
+ placeholder: "glances",
44812
+ default: "",
44813
+ perNode: true
44814
+ }),
44815
+ this.field({
44816
+ type: "tags",
44817
+ key: "glancesArgs",
44818
+ label: "Arguments",
44819
+ description: GLANCES_ARGS_DESCRIPTION,
44820
+ default: [...DEFAULT_GLANCES_ARGS],
44821
+ perNode: true,
44822
+ span: 2
44823
+ })
44824
+ ]
44825
+ },
44826
+ {
44827
+ id: "terminal-shell",
44828
+ title: "Interactive shell",
44829
+ 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.",
44830
+ columns: 2,
44831
+ fields: [this.field({
44704
44832
  type: "boolean",
44705
44833
  key: "allowShell",
44706
44834
  label: "Allow interactive shell",
44707
- description: "Adds a \"Shell\" profile that grants command execution as the server user. Off by default.",
44835
+ description: "Adds a \"Shell\" profile on this node.",
44708
44836
  default: false,
44709
44837
  perNode: true
44710
- }),
44711
- this.field({
44838
+ }), this.field({
44712
44839
  type: "text",
44713
44840
  key: "shellPath",
44714
44841
  label: "Shell binary",
@@ -44716,89 +44843,90 @@ var TerminalAddon = class extends BaseAddon {
44716
44843
  placeholder: "/bin/bash",
44717
44844
  default: "",
44718
44845
  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."
44846
+ })]
44847
+ },
44848
+ {
44849
+ id: "terminal-profiles",
44850
+ title: "Custom profiles",
44851
+ 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.",
44852
+ columns: 1,
44853
+ fields: [this.field({
44854
+ type: "editable-array",
44855
+ key: "customProfiles",
44856
+ label: "Profiles",
44857
+ perNode: true,
44858
+ default: [],
44859
+ maxRows: 32,
44860
+ addLabel: "Add profile",
44861
+ emptyMessage: "No custom profiles configured.",
44862
+ rowTitleTemplate: "{label} — {executable}",
44863
+ defaultItem: {
44864
+ enabled: true,
44865
+ profileId: "",
44866
+ label: "",
44867
+ description: "",
44868
+ executable: "",
44869
+ args: [],
44870
+ cwd: "",
44871
+ environment: []
44791
44872
  },
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
- }] });
44873
+ itemFields: [
44874
+ {
44875
+ type: "boolean",
44876
+ key: "enabled",
44877
+ label: "Enabled",
44878
+ default: true
44879
+ },
44880
+ {
44881
+ type: "text",
44882
+ key: "profileId",
44883
+ label: "Profile ID",
44884
+ description: "Stable lowercase slug, for example gpu-monitor.",
44885
+ required: true
44886
+ },
44887
+ {
44888
+ type: "text",
44889
+ key: "label",
44890
+ label: "Display name",
44891
+ required: true
44892
+ },
44893
+ {
44894
+ type: "textarea",
44895
+ key: "description",
44896
+ label: "Description",
44897
+ rows: 2
44898
+ },
44899
+ {
44900
+ type: "text",
44901
+ key: "executable",
44902
+ label: "Executable",
44903
+ description: "Absolute path or a binary resolved from PATH.",
44904
+ required: true
44905
+ },
44906
+ {
44907
+ type: "tags",
44908
+ key: "args",
44909
+ label: "Arguments",
44910
+ description: "One exact process argument per tag.",
44911
+ maxTags: 64
44912
+ },
44913
+ {
44914
+ type: "text",
44915
+ key: "cwd",
44916
+ label: "Working directory",
44917
+ description: "Optional working directory for this profile."
44918
+ },
44919
+ {
44920
+ type: "tags",
44921
+ key: "environment",
44922
+ label: "Environment",
44923
+ description: "Optional NAME=value entries merged into the process environment.",
44924
+ maxTags: 64
44925
+ }
44926
+ ]
44927
+ })]
44928
+ }
44929
+ ] });
44802
44930
  }
44803
44931
  };
44804
44932
  //#endregion
package/dist/addon.mjs CHANGED
@@ -19002,6 +19002,12 @@ var CameraStatusSchema = object({
19002
19002
  /** Unix timestamp (ms) when this snapshot was composed server-side. */
19003
19003
  fetchedAt: number()
19004
19004
  });
19005
+ var InferenceDeviceExclusionReasonSchema = _enum([
19006
+ "disabled",
19007
+ "unavailable",
19008
+ "cannot-host-camera-root",
19009
+ "accelerator-preferred"
19010
+ ]);
19005
19011
  var NodeInferenceDeviceSchema = object({
19006
19012
  /** Stable per-node device key, e.g. `openvino:npu`, `edgetpu:usb`, `cpu`. */
19007
19013
  key: string(),
@@ -19032,7 +19038,17 @@ var NodeInferenceDeviceSchema = object({
19032
19038
  * available per format; this is the stored selection that becomes the
19033
19039
  * default for EVERY camera landing on this accelerator.
19034
19040
  */
19035
- steps: record(string(), DeviceStepConfigSchema).optional()
19041
+ steps: record(string(), DeviceStepConfigSchema).optional(),
19042
+ /**
19043
+ * `null` when the device IS a camera-root candidate on this node; otherwise
19044
+ * the reason the dispatcher drops it. Computed by the SAME
19045
+ * `resolveInferenceDeviceEligibility` the dispatcher runs, so this view can
19046
+ * never disagree with the election — deriving it in the UI from
19047
+ * `enabled`/`available` would silently miss `cannot-host-camera-root` (needs
19048
+ * the node's model catalog) and `accelerator-preferred` (needs the node-wide
19049
+ * "an accelerator is serving" predicate).
19050
+ */
19051
+ exclusion: InferenceDeviceExclusionReasonSchema.nullable()
19036
19052
  });
19037
19053
  var NodeInferenceDevicesSchema = object({
19038
19054
  nodeId: string(),
@@ -24030,7 +24046,12 @@ var ListResultSchema = object({
24030
24046
  probedAt: number()
24031
24047
  });
24032
24048
  var PreferredSchema = LocalInterfaceSchema.nullable();
24033
- var GetConnectionEndpointsResultSchema = object({ endpoints: array(object({
24049
+ /**
24050
+ * Candidate base URL for the SDK to race on connect. Order matters —
24051
+ * the SDK should attempt these top-to-bottom with a short per-candidate
24052
+ * timeout (e.g. 1500ms) and cache the winner for the session.
24053
+ */
24054
+ var ConnectionEndpointSchema = object({
24034
24055
  /** Operator-facing label (e.g. "LAN — en0", "Public tunnel"). */
24035
24056
  label: string(),
24036
24057
  /** Fully-formed base URL with scheme + host + port. */
@@ -24073,7 +24094,42 @@ var GetConnectionEndpointsResultSchema = object({ endpoints: array(object({
24073
24094
  * ordering between polls.
24074
24095
  */
24075
24096
  priority: number()
24076
- })).readonly() });
24097
+ });
24098
+ /**
24099
+ * Where the advertised local port came from. Ordered most → least
24100
+ * authoritative, and the whole point of returning it: a client must be able to
24101
+ * tell a FACT about the hub's socket from an echo of its own guess.
24102
+ */
24103
+ var LocalPortSourceEnum = _enum([
24104
+ "server-config",
24105
+ "server-env",
24106
+ "caller-hint",
24107
+ "default"
24108
+ ]);
24109
+ /** The port every LAN/loopback `baseUrl` in the same result was built with. */
24110
+ var AdvertisedLocalPortSchema = object({
24111
+ port: number().int().min(1).max(65535),
24112
+ source: LocalPortSourceEnum
24113
+ });
24114
+ var GetConnectionEndpointsResultSchema = object({
24115
+ endpoints: array(ConnectionEndpointSchema).readonly(),
24116
+ /**
24117
+ * The port the hub built the LAN/loopback URLs with, and where that number
24118
+ * came from.
24119
+ *
24120
+ * Returned rather than merely applied, because "the URL is right" and "the
24121
+ * client can KNOW the URL is right" are different properties. A client that
24122
+ * only sees a corrected URL cannot distinguish a hub that fixed the port from
24123
+ * a hub that echoed the port the client sent, so it cannot decide whether to
24124
+ * race the candidate or discard it. With `source` it can: anything but
24125
+ * `caller-hint` is the hub's own socket.
24126
+ *
24127
+ * Absent on hubs predating this field — a client that finds it missing is
24128
+ * talking to an echoing hub and must degrade exactly as it does for
24129
+ * `caller-hint`.
24130
+ */
24131
+ localPort: AdvertisedLocalPortSchema
24132
+ });
24077
24133
  /**
24078
24134
  * The chosen outbound endpoint for notification artifacts. `baseUrl: null` =
24079
24135
  * AUTO (resolved from the candidate ranking at send time); `resolved` reports
@@ -24094,8 +24150,13 @@ var AllowedAddressesSchema = object({
24094
24150
  */
24095
24151
  addresses: array(string()).readonly() });
24096
24152
  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),
24153
+ /**
24154
+ * LEGACY HINT — do not send from new code. Kept optional so clients
24155
+ * written against the echoing contract keep working; the hub uses it
24156
+ * only when it cannot read its own port, and says so via
24157
+ * `localPort.source === 'caller-hint'`.
24158
+ */
24159
+ port: number().int().min(1).max(65535).optional(),
24099
24160
  /** Include `http(s)://127.0.0.1:<port>` as the lowest-priority
24100
24161
  * candidate. Default `true`. */
24101
24162
  includeLoopback: boolean().optional(),
@@ -44193,6 +44254,38 @@ var TerminalSessionManager = class {
44193
44254
  * (`node-pty`) and should update via `camstack deploy` independently of the
44194
44255
  * framework, exactly like the node-av decoder addon.
44195
44256
  */
44257
+ /**
44258
+ * Default argv for the Glances profile — deliberately EMPTY.
44259
+ *
44260
+ * Measured on the hub (2026-08-21, glances 4.5.6, embedded Python, a real pty
44261
+ * at the terminal camera's own 120x40 grid, CPU sampled from
44262
+ * /proc/<pid>/{utime,stime} so the number is the process's own and not the
44263
+ * box's contention):
44264
+ *
44265
+ * argv CPU (of one core)
44266
+ * (none — this default) 0.8 %
44267
+ * -t 3 0.7 %
44268
+ * -t 5 0.6 %
44269
+ * --disable-plugin processlist 0.7 %
44270
+ * --disable-plugin containers 0.9 %
44271
+ * --disable-plugin sensors 0.8 %
44272
+ * --disable-process 0.8 %
44273
+ * -t 3 --disable-plugin processlist,containers,sensors 0.5 %
44274
+ * -t 5 --disable-plugin processlist,containers,sensors 0.4 %
44275
+ *
44276
+ * Startup costs 0.19 core-seconds; steady state is 1.0 % of one core at 36 MB
44277
+ * RSS. The whole tuning range is worth ~0.4 % of ONE core out of 20 — below the
44278
+ * noise floor of a box that idles at 2.6 cores — while every entry that buys
44279
+ * something removes a panel the operator opened the camera to read. `-t` is
44280
+ * additionally counter-productive here: the terminal camera renders at 2 FPS
44281
+ * and glances already repaints every 2 s, so slowing glances only makes the
44282
+ * camera show stale numbers.
44283
+ *
44284
+ * So the default ships empty and the knob stays in front of the operator.
44285
+ * Anyone who does want to trade panels for CPU has the numbers above.
44286
+ */
44287
+ var DEFAULT_GLANCES_ARGS = [];
44288
+ 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
44289
  var DEFAULTS = {
44197
44290
  btmPath: "",
44198
44291
  btmEnabled: true,
@@ -44202,7 +44295,7 @@ var DEFAULTS = {
44202
44295
  topArgs: [],
44203
44296
  glancesEnabled: true,
44204
44297
  glancesPath: "",
44205
- glancesArgs: [],
44298
+ glancesArgs: DEFAULT_GLANCES_ARGS,
44206
44299
  allowShell: false,
44207
44300
  shellPath: "",
44208
44301
  maxSessions: 4,
@@ -44589,85 +44682,13 @@ var TerminalAddon = class extends BaseAddon {
44589
44682
  return this.instanceInfo(instance);
44590
44683
  }
44591
44684
  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({
44685
+ return this.schema({ sections: [
44686
+ {
44687
+ id: "terminal",
44688
+ title: "Terminal",
44689
+ 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.",
44690
+ columns: 2,
44691
+ fields: [this.field({
44671
44692
  type: "number",
44672
44693
  key: "maxSessions",
44673
44694
  label: "Max concurrent sessions",
@@ -44676,16 +44697,122 @@ var TerminalAddon = class extends BaseAddon {
44676
44697
  step: 1,
44677
44698
  default: 4,
44678
44699
  perNode: true
44679
- }),
44680
- this.field({
44700
+ })]
44701
+ },
44702
+ {
44703
+ id: "terminal-btm",
44704
+ title: "BTM camera",
44705
+ description: "bottom (btm) — CPU, memory, network and process monitor.",
44706
+ columns: 2,
44707
+ fields: [
44708
+ this.field({
44709
+ type: "boolean",
44710
+ key: "btmEnabled",
44711
+ label: "Enabled",
44712
+ description: "Offer the BTM profile on this node.",
44713
+ default: true,
44714
+ perNode: true
44715
+ }),
44716
+ this.field({
44717
+ type: "text",
44718
+ key: "btmPath",
44719
+ label: "Binary",
44720
+ description: "Path to the `btm` (bottom) executable. Leave empty to resolve from PATH.",
44721
+ placeholder: "btm",
44722
+ default: "",
44723
+ perNode: true
44724
+ }),
44725
+ this.field({
44726
+ type: "tags",
44727
+ key: "btmArgs",
44728
+ label: "Arguments",
44729
+ description: "Exact arguments passed to btm. One tag per argv entry.",
44730
+ default: [],
44731
+ perNode: true,
44732
+ span: 2
44733
+ })
44734
+ ]
44735
+ },
44736
+ {
44737
+ id: "terminal-top",
44738
+ title: "Top camera",
44739
+ description: "The operating system process and resource monitor.",
44740
+ columns: 2,
44741
+ fields: [
44742
+ this.field({
44743
+ type: "boolean",
44744
+ key: "topEnabled",
44745
+ label: "Enabled",
44746
+ description: "Offer the Top profile on this node.",
44747
+ default: true,
44748
+ perNode: true
44749
+ }),
44750
+ this.field({
44751
+ type: "text",
44752
+ key: "topPath",
44753
+ label: "Binary",
44754
+ placeholder: "top",
44755
+ default: "",
44756
+ perNode: true
44757
+ }),
44758
+ this.field({
44759
+ type: "tags",
44760
+ key: "topArgs",
44761
+ label: "Arguments",
44762
+ description: "Exact arguments passed to top. One tag per argv entry.",
44763
+ default: [],
44764
+ perNode: true,
44765
+ span: 2
44766
+ })
44767
+ ]
44768
+ },
44769
+ {
44770
+ id: "terminal-glances",
44771
+ title: "Glances camera",
44772
+ 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.",
44773
+ columns: 2,
44774
+ fields: [
44775
+ this.field({
44776
+ type: "boolean",
44777
+ key: "glancesEnabled",
44778
+ label: "Enabled",
44779
+ description: "Offer the Glances profile on this node.",
44780
+ default: true,
44781
+ perNode: true
44782
+ }),
44783
+ this.field({
44784
+ type: "text",
44785
+ key: "glancesPath",
44786
+ label: "Binary override",
44787
+ description: "Optional executable override. Empty uses the automatically managed Python package.",
44788
+ placeholder: "glances",
44789
+ default: "",
44790
+ perNode: true
44791
+ }),
44792
+ this.field({
44793
+ type: "tags",
44794
+ key: "glancesArgs",
44795
+ label: "Arguments",
44796
+ description: GLANCES_ARGS_DESCRIPTION,
44797
+ default: [...DEFAULT_GLANCES_ARGS],
44798
+ perNode: true,
44799
+ span: 2
44800
+ })
44801
+ ]
44802
+ },
44803
+ {
44804
+ id: "terminal-shell",
44805
+ title: "Interactive shell",
44806
+ 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.",
44807
+ columns: 2,
44808
+ fields: [this.field({
44681
44809
  type: "boolean",
44682
44810
  key: "allowShell",
44683
44811
  label: "Allow interactive shell",
44684
- description: "Adds a \"Shell\" profile that grants command execution as the server user. Off by default.",
44812
+ description: "Adds a \"Shell\" profile on this node.",
44685
44813
  default: false,
44686
44814
  perNode: true
44687
- }),
44688
- this.field({
44815
+ }), this.field({
44689
44816
  type: "text",
44690
44817
  key: "shellPath",
44691
44818
  label: "Shell binary",
@@ -44693,89 +44820,90 @@ var TerminalAddon = class extends BaseAddon {
44693
44820
  placeholder: "/bin/bash",
44694
44821
  default: "",
44695
44822
  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."
44823
+ })]
44824
+ },
44825
+ {
44826
+ id: "terminal-profiles",
44827
+ title: "Custom profiles",
44828
+ 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.",
44829
+ columns: 1,
44830
+ fields: [this.field({
44831
+ type: "editable-array",
44832
+ key: "customProfiles",
44833
+ label: "Profiles",
44834
+ perNode: true,
44835
+ default: [],
44836
+ maxRows: 32,
44837
+ addLabel: "Add profile",
44838
+ emptyMessage: "No custom profiles configured.",
44839
+ rowTitleTemplate: "{label} — {executable}",
44840
+ defaultItem: {
44841
+ enabled: true,
44842
+ profileId: "",
44843
+ label: "",
44844
+ description: "",
44845
+ executable: "",
44846
+ args: [],
44847
+ cwd: "",
44848
+ environment: []
44768
44849
  },
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
- }] });
44850
+ itemFields: [
44851
+ {
44852
+ type: "boolean",
44853
+ key: "enabled",
44854
+ label: "Enabled",
44855
+ default: true
44856
+ },
44857
+ {
44858
+ type: "text",
44859
+ key: "profileId",
44860
+ label: "Profile ID",
44861
+ description: "Stable lowercase slug, for example gpu-monitor.",
44862
+ required: true
44863
+ },
44864
+ {
44865
+ type: "text",
44866
+ key: "label",
44867
+ label: "Display name",
44868
+ required: true
44869
+ },
44870
+ {
44871
+ type: "textarea",
44872
+ key: "description",
44873
+ label: "Description",
44874
+ rows: 2
44875
+ },
44876
+ {
44877
+ type: "text",
44878
+ key: "executable",
44879
+ label: "Executable",
44880
+ description: "Absolute path or a binary resolved from PATH.",
44881
+ required: true
44882
+ },
44883
+ {
44884
+ type: "tags",
44885
+ key: "args",
44886
+ label: "Arguments",
44887
+ description: "One exact process argument per tag.",
44888
+ maxTags: 64
44889
+ },
44890
+ {
44891
+ type: "text",
44892
+ key: "cwd",
44893
+ label: "Working directory",
44894
+ description: "Optional working directory for this profile."
44895
+ },
44896
+ {
44897
+ type: "tags",
44898
+ key: "environment",
44899
+ label: "Environment",
44900
+ description: "Optional NAME=value entries merged into the process environment.",
44901
+ maxTags: 64
44902
+ }
44903
+ ]
44904
+ })]
44905
+ }
44906
+ ] });
44779
44907
  }
44780
44908
  };
44781
44909
  //#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.28",
4
4
  "description": "Interactive terminal sessions (pty + xterm) as a CamStack addon",
5
5
  "keywords": [
6
6
  "camstack",