@camstack/addon-terminal 0.1.69 → 0.1.71

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
@@ -7644,7 +7644,8 @@ var CameraSwitchAuthoritySchema = discriminatedUnion("kind", [
7644
7644
  var CameraSwitchUnavailableReasonSchema = _enum([
7645
7645
  "no-provider",
7646
7646
  "source-unreachable",
7647
- "not-configured"
7647
+ "not-configured",
7648
+ "device-disabled"
7648
7649
  ]);
7649
7650
  /**
7650
7651
  * One switch, resolved for one camera.
@@ -13061,7 +13062,12 @@ var ConfigEntrySchema = object({
13061
13062
  value: unknown(),
13062
13063
  description: string().optional()
13063
13064
  });
13064
- var LinkedDevicesModeSchema = _enum(["auto", "manual"]);
13065
+ /**
13066
+ * Only `manual` since D356: the operator picks, nothing links itself. The
13067
+ * field survives on the wire because a viewer already OTA'd parses it —
13068
+ * drop it once no shipped client reads `mode`.
13069
+ */
13070
+ var LinkedDevicesModeSchema = _enum(["manual"]);
13065
13071
  /** One resolved linked device — the compact projection consumers need. */
13066
13072
  var LinkedDeviceSchema = object({
13067
13073
  deviceId: number(),
@@ -23737,12 +23743,20 @@ var BatteryStatusSchema = object({
23737
23743
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
23738
23744
  * Reolink-specific for the Solar Panel 2 accessory (will become
23739
23745
  * common on other battery cams). `'none'` means running on battery
23740
- * alone.
23746
+ * alone — a NEGATIVE the firmware actually reported.
23747
+ *
23748
+ * `'unknown'` is the absence of an answer: the provider has not read the
23749
+ * power fields yet, or the firmware reported neither. Before it existed,
23750
+ * two unreadable fields produced `'none'`, and a camera nobody had reached
23751
+ * was drawn "on battery alone" — the same D315 defect `percentage` closed
23752
+ * by going nullable. Consumers SKIP it (no charger state published, no
23753
+ * glyph, no HomeKit ChargingState) rather than coerce it either way.
23741
23754
  */
23742
23755
  charging: _enum([
23743
23756
  "dc",
23744
23757
  "solar",
23745
- "none"
23758
+ "none",
23759
+ "unknown"
23746
23760
  ]),
23747
23761
  /**
23748
23762
  * True when the camera firmware has gone into low-power mode. Battery
@@ -23832,7 +23846,7 @@ onStatusChanged: { data: object({
23832
23846
  kind: "push",
23833
23847
  empty: {
23834
23848
  percentage: 0,
23835
- charging: "none",
23849
+ charging: "unknown",
23836
23850
  sleeping: false,
23837
23851
  lastUpdated: 0
23838
23852
  }
@@ -47511,6 +47525,29 @@ var TerminalInstanceReconcileCoordinator = class {
47511
47525
  return this.running;
47512
47526
  }
47513
47527
  };
47528
+ /**
47529
+ * Instances bound to a node the cluster no longer lists at all.
47530
+ *
47531
+ * An OFFLINE node is still in the topology; a FORGOTTEN one
47532
+ * (`clusterNodes.forgetNode`) is not. An instance bound to a forgotten node
47533
+ * warned every minute ("terminal profiles unavailable … node hub-test is
47534
+ * offline", 2026-09-05) and could never come back, so it is removed — but
47535
+ * only when two consecutive reads agree (D49): a topology answer that lost a
47536
+ * node once must not destroy an operator's configuration on its own.
47537
+ */
47538
+ function instancesOnForgottenNodes(instances, knownNodeIds, absentBefore) {
47539
+ const absentNow = /* @__PURE__ */ new Set();
47540
+ const remove = [];
47541
+ for (const instance of instances) {
47542
+ if (knownNodeIds.has(instance.nodeId)) continue;
47543
+ absentNow.add(instance.id);
47544
+ if (absentBefore.has(instance.id)) remove.push(instance);
47545
+ }
47546
+ return {
47547
+ remove,
47548
+ absentNow
47549
+ };
47550
+ }
47514
47551
  //#endregion
47515
47552
  //#region src/profiles.ts
47516
47553
  /**
@@ -48039,6 +48076,8 @@ var TerminalAddon = class extends BaseAddon {
48039
48076
  /** Prevent repeat writes when a legacy device stays live after migration. */
48040
48077
  migratedTerminalCameraConfigIds = /* @__PURE__ */ new Set();
48041
48078
  terminalCameraTombstones = /* @__PURE__ */ new Set();
48079
+ /** Instance ids whose node was absent from the last topology read (D49 streak). */
48080
+ instancesOnAbsentNodes = /* @__PURE__ */ new Set();
48042
48081
  instanceMutationQueue = new TerminalInstanceMutationQueue();
48043
48082
  terminalReconcileCoordinator = new TerminalInstanceReconcileCoordinator(this.instanceMutationQueue);
48044
48083
  glancesPythonPath = "";
@@ -48219,6 +48258,16 @@ var TerminalAddon = class extends BaseAddon {
48219
48258
  const nodes = topology.filter((node) => typeof node.id === "string" && node.id.length > 0);
48220
48259
  const nodeIds = new Set(nodes.map((node) => node.id));
48221
48260
  for (const cachedNodeId of this.cameraProfilesByNode.keys()) if (!nodeIds.has(cachedNodeId)) this.cameraProfilesByNode.delete(cachedNodeId);
48261
+ const forgotten = instancesOnForgottenNodes(this.terminalInstances(), nodeIds, this.instancesOnAbsentNodes);
48262
+ this.instancesOnAbsentNodes = forgotten.absentNow;
48263
+ for (const instance of forgotten.remove) {
48264
+ this.ctx.logger.info("terminal instance removed — its node is no longer in the cluster", { meta: {
48265
+ instanceId: instance.id,
48266
+ nodeId: instance.nodeId,
48267
+ name: instance.name
48268
+ } });
48269
+ await this.instanceMutationQueue.run(() => this.deleteTerminalInstanceUnlocked(instance.id));
48270
+ }
48222
48271
  const unavailableNodeIds = /* @__PURE__ */ new Set();
48223
48272
  await Promise.all(nodes.map(async (node) => {
48224
48273
  try {
package/dist/addon.mjs CHANGED
@@ -7621,7 +7621,8 @@ var CameraSwitchAuthoritySchema = discriminatedUnion("kind", [
7621
7621
  var CameraSwitchUnavailableReasonSchema = _enum([
7622
7622
  "no-provider",
7623
7623
  "source-unreachable",
7624
- "not-configured"
7624
+ "not-configured",
7625
+ "device-disabled"
7625
7626
  ]);
7626
7627
  /**
7627
7628
  * One switch, resolved for one camera.
@@ -13038,7 +13039,12 @@ var ConfigEntrySchema = object({
13038
13039
  value: unknown(),
13039
13040
  description: string().optional()
13040
13041
  });
13041
- var LinkedDevicesModeSchema = _enum(["auto", "manual"]);
13042
+ /**
13043
+ * Only `manual` since D356: the operator picks, nothing links itself. The
13044
+ * field survives on the wire because a viewer already OTA'd parses it —
13045
+ * drop it once no shipped client reads `mode`.
13046
+ */
13047
+ var LinkedDevicesModeSchema = _enum(["manual"]);
13042
13048
  /** One resolved linked device — the compact projection consumers need. */
13043
13049
  var LinkedDeviceSchema = object({
13044
13050
  deviceId: number(),
@@ -23714,12 +23720,20 @@ var BatteryStatusSchema = object({
23714
23720
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
23715
23721
  * Reolink-specific for the Solar Panel 2 accessory (will become
23716
23722
  * common on other battery cams). `'none'` means running on battery
23717
- * alone.
23723
+ * alone — a NEGATIVE the firmware actually reported.
23724
+ *
23725
+ * `'unknown'` is the absence of an answer: the provider has not read the
23726
+ * power fields yet, or the firmware reported neither. Before it existed,
23727
+ * two unreadable fields produced `'none'`, and a camera nobody had reached
23728
+ * was drawn "on battery alone" — the same D315 defect `percentage` closed
23729
+ * by going nullable. Consumers SKIP it (no charger state published, no
23730
+ * glyph, no HomeKit ChargingState) rather than coerce it either way.
23718
23731
  */
23719
23732
  charging: _enum([
23720
23733
  "dc",
23721
23734
  "solar",
23722
- "none"
23735
+ "none",
23736
+ "unknown"
23723
23737
  ]),
23724
23738
  /**
23725
23739
  * True when the camera firmware has gone into low-power mode. Battery
@@ -23809,7 +23823,7 @@ onStatusChanged: { data: object({
23809
23823
  kind: "push",
23810
23824
  empty: {
23811
23825
  percentage: 0,
23812
- charging: "none",
23826
+ charging: "unknown",
23813
23827
  sleeping: false,
23814
23828
  lastUpdated: 0
23815
23829
  }
@@ -47488,6 +47502,29 @@ var TerminalInstanceReconcileCoordinator = class {
47488
47502
  return this.running;
47489
47503
  }
47490
47504
  };
47505
+ /**
47506
+ * Instances bound to a node the cluster no longer lists at all.
47507
+ *
47508
+ * An OFFLINE node is still in the topology; a FORGOTTEN one
47509
+ * (`clusterNodes.forgetNode`) is not. An instance bound to a forgotten node
47510
+ * warned every minute ("terminal profiles unavailable … node hub-test is
47511
+ * offline", 2026-09-05) and could never come back, so it is removed — but
47512
+ * only when two consecutive reads agree (D49): a topology answer that lost a
47513
+ * node once must not destroy an operator's configuration on its own.
47514
+ */
47515
+ function instancesOnForgottenNodes(instances, knownNodeIds, absentBefore) {
47516
+ const absentNow = /* @__PURE__ */ new Set();
47517
+ const remove = [];
47518
+ for (const instance of instances) {
47519
+ if (knownNodeIds.has(instance.nodeId)) continue;
47520
+ absentNow.add(instance.id);
47521
+ if (absentBefore.has(instance.id)) remove.push(instance);
47522
+ }
47523
+ return {
47524
+ remove,
47525
+ absentNow
47526
+ };
47527
+ }
47491
47528
  //#endregion
47492
47529
  //#region src/profiles.ts
47493
47530
  /**
@@ -48016,6 +48053,8 @@ var TerminalAddon = class extends BaseAddon {
48016
48053
  /** Prevent repeat writes when a legacy device stays live after migration. */
48017
48054
  migratedTerminalCameraConfigIds = /* @__PURE__ */ new Set();
48018
48055
  terminalCameraTombstones = /* @__PURE__ */ new Set();
48056
+ /** Instance ids whose node was absent from the last topology read (D49 streak). */
48057
+ instancesOnAbsentNodes = /* @__PURE__ */ new Set();
48019
48058
  instanceMutationQueue = new TerminalInstanceMutationQueue();
48020
48059
  terminalReconcileCoordinator = new TerminalInstanceReconcileCoordinator(this.instanceMutationQueue);
48021
48060
  glancesPythonPath = "";
@@ -48196,6 +48235,16 @@ var TerminalAddon = class extends BaseAddon {
48196
48235
  const nodes = topology.filter((node) => typeof node.id === "string" && node.id.length > 0);
48197
48236
  const nodeIds = new Set(nodes.map((node) => node.id));
48198
48237
  for (const cachedNodeId of this.cameraProfilesByNode.keys()) if (!nodeIds.has(cachedNodeId)) this.cameraProfilesByNode.delete(cachedNodeId);
48238
+ const forgotten = instancesOnForgottenNodes(this.terminalInstances(), nodeIds, this.instancesOnAbsentNodes);
48239
+ this.instancesOnAbsentNodes = forgotten.absentNow;
48240
+ for (const instance of forgotten.remove) {
48241
+ this.ctx.logger.info("terminal instance removed — its node is no longer in the cluster", { meta: {
48242
+ instanceId: instance.id,
48243
+ nodeId: instance.nodeId,
48244
+ name: instance.name
48245
+ } });
48246
+ await this.instanceMutationQueue.run(() => this.deleteTerminalInstanceUnlocked(instance.id));
48247
+ }
48199
48248
  const unavailableNodeIds = /* @__PURE__ */ new Set();
48200
48249
  await Promise.all(nodes.map(async (node) => {
48201
48250
  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.71",
4
4
  "description": "Interactive terminal sessions (pty + xterm) as a CamStack addon",
5
5
  "keywords": [
6
6
  "camstack",