@camstack/addon-terminal 0.1.69 → 0.1.70

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 CHANGED
@@ -13061,7 +13061,12 @@ var ConfigEntrySchema = object({
13061
13061
  value: unknown(),
13062
13062
  description: string().optional()
13063
13063
  });
13064
- var LinkedDevicesModeSchema = _enum(["auto", "manual"]);
13064
+ /**
13065
+ * Only `manual` since D356: the operator picks, nothing links itself. The
13066
+ * field survives on the wire because a viewer already OTA'd parses it —
13067
+ * drop it once no shipped client reads `mode`.
13068
+ */
13069
+ var LinkedDevicesModeSchema = _enum(["manual"]);
13065
13070
  /** One resolved linked device — the compact projection consumers need. */
13066
13071
  var LinkedDeviceSchema = object({
13067
13072
  deviceId: number(),
@@ -47511,6 +47516,29 @@ var TerminalInstanceReconcileCoordinator = class {
47511
47516
  return this.running;
47512
47517
  }
47513
47518
  };
47519
+ /**
47520
+ * Instances bound to a node the cluster no longer lists at all.
47521
+ *
47522
+ * An OFFLINE node is still in the topology; a FORGOTTEN one
47523
+ * (`clusterNodes.forgetNode`) is not. An instance bound to a forgotten node
47524
+ * warned every minute ("terminal profiles unavailable … node hub-test is
47525
+ * offline", 2026-09-05) and could never come back, so it is removed — but
47526
+ * only when two consecutive reads agree (D49): a topology answer that lost a
47527
+ * node once must not destroy an operator's configuration on its own.
47528
+ */
47529
+ function instancesOnForgottenNodes(instances, knownNodeIds, absentBefore) {
47530
+ const absentNow = /* @__PURE__ */ new Set();
47531
+ const remove = [];
47532
+ for (const instance of instances) {
47533
+ if (knownNodeIds.has(instance.nodeId)) continue;
47534
+ absentNow.add(instance.id);
47535
+ if (absentBefore.has(instance.id)) remove.push(instance);
47536
+ }
47537
+ return {
47538
+ remove,
47539
+ absentNow
47540
+ };
47541
+ }
47514
47542
  //#endregion
47515
47543
  //#region src/profiles.ts
47516
47544
  /**
@@ -48039,6 +48067,8 @@ var TerminalAddon = class extends BaseAddon {
48039
48067
  /** Prevent repeat writes when a legacy device stays live after migration. */
48040
48068
  migratedTerminalCameraConfigIds = /* @__PURE__ */ new Set();
48041
48069
  terminalCameraTombstones = /* @__PURE__ */ new Set();
48070
+ /** Instance ids whose node was absent from the last topology read (D49 streak). */
48071
+ instancesOnAbsentNodes = /* @__PURE__ */ new Set();
48042
48072
  instanceMutationQueue = new TerminalInstanceMutationQueue();
48043
48073
  terminalReconcileCoordinator = new TerminalInstanceReconcileCoordinator(this.instanceMutationQueue);
48044
48074
  glancesPythonPath = "";
@@ -48219,6 +48249,16 @@ var TerminalAddon = class extends BaseAddon {
48219
48249
  const nodes = topology.filter((node) => typeof node.id === "string" && node.id.length > 0);
48220
48250
  const nodeIds = new Set(nodes.map((node) => node.id));
48221
48251
  for (const cachedNodeId of this.cameraProfilesByNode.keys()) if (!nodeIds.has(cachedNodeId)) this.cameraProfilesByNode.delete(cachedNodeId);
48252
+ const forgotten = instancesOnForgottenNodes(this.terminalInstances(), nodeIds, this.instancesOnAbsentNodes);
48253
+ this.instancesOnAbsentNodes = forgotten.absentNow;
48254
+ for (const instance of forgotten.remove) {
48255
+ this.ctx.logger.info("terminal instance removed — its node is no longer in the cluster", { meta: {
48256
+ instanceId: instance.id,
48257
+ nodeId: instance.nodeId,
48258
+ name: instance.name
48259
+ } });
48260
+ await this.instanceMutationQueue.run(() => this.deleteTerminalInstanceUnlocked(instance.id));
48261
+ }
48222
48262
  const unavailableNodeIds = /* @__PURE__ */ new Set();
48223
48263
  await Promise.all(nodes.map(async (node) => {
48224
48264
  try {
package/dist/addon.mjs CHANGED
@@ -13038,7 +13038,12 @@ var ConfigEntrySchema = object({
13038
13038
  value: unknown(),
13039
13039
  description: string().optional()
13040
13040
  });
13041
- var LinkedDevicesModeSchema = _enum(["auto", "manual"]);
13041
+ /**
13042
+ * Only `manual` since D356: the operator picks, nothing links itself. The
13043
+ * field survives on the wire because a viewer already OTA'd parses it —
13044
+ * drop it once no shipped client reads `mode`.
13045
+ */
13046
+ var LinkedDevicesModeSchema = _enum(["manual"]);
13042
13047
  /** One resolved linked device — the compact projection consumers need. */
13043
13048
  var LinkedDeviceSchema = object({
13044
13049
  deviceId: number(),
@@ -47488,6 +47493,29 @@ var TerminalInstanceReconcileCoordinator = class {
47488
47493
  return this.running;
47489
47494
  }
47490
47495
  };
47496
+ /**
47497
+ * Instances bound to a node the cluster no longer lists at all.
47498
+ *
47499
+ * An OFFLINE node is still in the topology; a FORGOTTEN one
47500
+ * (`clusterNodes.forgetNode`) is not. An instance bound to a forgotten node
47501
+ * warned every minute ("terminal profiles unavailable … node hub-test is
47502
+ * offline", 2026-09-05) and could never come back, so it is removed — but
47503
+ * only when two consecutive reads agree (D49): a topology answer that lost a
47504
+ * node once must not destroy an operator's configuration on its own.
47505
+ */
47506
+ function instancesOnForgottenNodes(instances, knownNodeIds, absentBefore) {
47507
+ const absentNow = /* @__PURE__ */ new Set();
47508
+ const remove = [];
47509
+ for (const instance of instances) {
47510
+ if (knownNodeIds.has(instance.nodeId)) continue;
47511
+ absentNow.add(instance.id);
47512
+ if (absentBefore.has(instance.id)) remove.push(instance);
47513
+ }
47514
+ return {
47515
+ remove,
47516
+ absentNow
47517
+ };
47518
+ }
47491
47519
  //#endregion
47492
47520
  //#region src/profiles.ts
47493
47521
  /**
@@ -48016,6 +48044,8 @@ var TerminalAddon = class extends BaseAddon {
48016
48044
  /** Prevent repeat writes when a legacy device stays live after migration. */
48017
48045
  migratedTerminalCameraConfigIds = /* @__PURE__ */ new Set();
48018
48046
  terminalCameraTombstones = /* @__PURE__ */ new Set();
48047
+ /** Instance ids whose node was absent from the last topology read (D49 streak). */
48048
+ instancesOnAbsentNodes = /* @__PURE__ */ new Set();
48019
48049
  instanceMutationQueue = new TerminalInstanceMutationQueue();
48020
48050
  terminalReconcileCoordinator = new TerminalInstanceReconcileCoordinator(this.instanceMutationQueue);
48021
48051
  glancesPythonPath = "";
@@ -48196,6 +48226,16 @@ var TerminalAddon = class extends BaseAddon {
48196
48226
  const nodes = topology.filter((node) => typeof node.id === "string" && node.id.length > 0);
48197
48227
  const nodeIds = new Set(nodes.map((node) => node.id));
48198
48228
  for (const cachedNodeId of this.cameraProfilesByNode.keys()) if (!nodeIds.has(cachedNodeId)) this.cameraProfilesByNode.delete(cachedNodeId);
48229
+ const forgotten = instancesOnForgottenNodes(this.terminalInstances(), nodeIds, this.instancesOnAbsentNodes);
48230
+ this.instancesOnAbsentNodes = forgotten.absentNow;
48231
+ for (const instance of forgotten.remove) {
48232
+ this.ctx.logger.info("terminal instance removed — its node is no longer in the cluster", { meta: {
48233
+ instanceId: instance.id,
48234
+ nodeId: instance.nodeId,
48235
+ name: instance.name
48236
+ } });
48237
+ await this.instanceMutationQueue.run(() => this.deleteTerminalInstanceUnlocked(instance.id));
48238
+ }
48199
48239
  const unavailableNodeIds = /* @__PURE__ */ new Set();
48200
48240
  await Promise.all(nodes.map(async (node) => {
48201
48241
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-terminal",
3
- "version": "0.1.69",
3
+ "version": "0.1.70",
4
4
  "description": "Interactive terminal sessions (pty + xterm) as a CamStack addon",
5
5
  "keywords": [
6
6
  "camstack",