@camstack/addon-terminal 0.1.68 → 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 +83 -1
- package/dist/addon.mjs +83 -1
- package/package.json +1 -1
package/dist/addon.js
CHANGED
|
@@ -12945,6 +12945,24 @@ method(_void(), _void(), { kind: "mutation" }), method(_void(), _void(), { kind:
|
|
|
12945
12945
|
kind: "mutation",
|
|
12946
12946
|
auth: "admin"
|
|
12947
12947
|
});
|
|
12948
|
+
/**
|
|
12949
|
+
* Device Manager capability — hub-side singleton that unifies device persistence,
|
|
12950
|
+
* live registry access, and all management operations into a single tRPC surface.
|
|
12951
|
+
*
|
|
12952
|
+
* Replaces:
|
|
12953
|
+
* - `device-persistence` capability (persistence methods absorbed here)
|
|
12954
|
+
* - `device-management.router.ts` (deleted in Phase 2)
|
|
12955
|
+
* - `device-ops.router.ts` (compat layer — deleted; device-provider ops absorbed here)
|
|
12956
|
+
*
|
|
12957
|
+
* All device provider addons (rtsp, onvif, frigate, …) are hub-local: they may
|
|
12958
|
+
* fork into separate processes but never run on remote cluster agents. Therefore:
|
|
12959
|
+
* - No nodeId routing needed — this is a pure hub singleton.
|
|
12960
|
+
* - The hub's DeviceRegistry is the single source of truth for all live devices.
|
|
12961
|
+
* - No shadow registry or cross-node aggregation required.
|
|
12962
|
+
*
|
|
12963
|
+
* Forked workers register devices back to the hub via `ctx.devices`
|
|
12964
|
+
* (DeviceManagerApi → ctx.api.deviceManager.registerDevice), same as today.
|
|
12965
|
+
*/
|
|
12948
12966
|
/** One child-placement directive on a container's `childLayout`. Structurally
|
|
12949
12967
|
* identical to `ChildLayoutEntry` in `device-management.ts` — the cap wire
|
|
12950
12968
|
* shape for the same field. The child is identified by its re-sync-stable
|
|
@@ -13043,7 +13061,12 @@ var ConfigEntrySchema = object({
|
|
|
13043
13061
|
value: unknown(),
|
|
13044
13062
|
description: string().optional()
|
|
13045
13063
|
});
|
|
13046
|
-
|
|
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"]);
|
|
13047
13070
|
/** One resolved linked device — the compact projection consumers need. */
|
|
13048
13071
|
var LinkedDeviceSchema = object({
|
|
13049
13072
|
deviceId: number(),
|
|
@@ -18671,6 +18694,9 @@ var RetrainStatusSchema = _enum([
|
|
|
18671
18694
|
*
|
|
18672
18695
|
* `debug` does NOT pin; it is attention, not durability.
|
|
18673
18696
|
*
|
|
18697
|
+
* `debugNote` is the operator's own words about WHAT should be checked on this
|
|
18698
|
+
* track, and it lives and dies with `debug` — see {@link MAX_TRACK_DEBUG_NOTE_LEN}.
|
|
18699
|
+
*
|
|
18674
18700
|
* `favourited` IS a BOOLEAN pin (durability bit), not a retrain lifecycle.
|
|
18675
18701
|
* A favourited track is skipped by retention the same way `staging` is, but
|
|
18676
18702
|
* it does not enter `none|staging|trained` and has no staging budget.
|
|
@@ -18681,6 +18707,21 @@ var TrackFlagFields = {
|
|
|
18681
18707
|
markForTrain: boolean().optional(),
|
|
18682
18708
|
/** Operator marked this track for diagnostic attention. */
|
|
18683
18709
|
debug: boolean().optional(),
|
|
18710
|
+
/**
|
|
18711
|
+
* What the operator wants CHECKED on this track, in their own words.
|
|
18712
|
+
*
|
|
18713
|
+
* The flag alone says "look at this" and nothing about what for; a debug set
|
|
18714
|
+
* reviewed hours later is a list of tracks with no question attached. Bound
|
|
18715
|
+
* to `debug` on the WRITE side — the body clears it when `debug` goes off,
|
|
18716
|
+
* and refuses it on a track whose debug is off — so a note can never outlive
|
|
18717
|
+
* the attention it belongs to.
|
|
18718
|
+
*
|
|
18719
|
+
* `''` is a real value ("marked, nothing to add"); the ABSENT key means
|
|
18720
|
+
* "leave whatever is stored alone", which is what lets a surface turn debug
|
|
18721
|
+
* on without a note (a cancelled prompt) and what lets it edit the note
|
|
18722
|
+
* without touching the flag.
|
|
18723
|
+
*/
|
|
18724
|
+
debugNote: string().max(500).optional(),
|
|
18684
18725
|
/** Operator favourited this track. Pins it against pruning. */
|
|
18685
18726
|
favourited: boolean().optional()
|
|
18686
18727
|
};
|
|
@@ -18707,6 +18748,12 @@ var TrackFlagsSchema = object({
|
|
|
18707
18748
|
trackId: string(),
|
|
18708
18749
|
markForTrain: boolean(),
|
|
18709
18750
|
debug: boolean(),
|
|
18751
|
+
/** The note as it STANDS after the write — never absent here (a track with no
|
|
18752
|
+
* note reports `''`), for the same reason the booleans are required: a
|
|
18753
|
+
* surface that has just written must be able to render the note it now owns
|
|
18754
|
+
* without a re-fetch, and an absent key would read as "unchanged" on a
|
|
18755
|
+
* screen that has no previous value to keep. */
|
|
18756
|
+
debugNote: string(),
|
|
18710
18757
|
favourited: boolean(),
|
|
18711
18758
|
/** The lifecycle state the boolean was derived from. Required here (unlike on
|
|
18712
18759
|
* a track row) because this shape is only ever produced by the write body,
|
|
@@ -47469,6 +47516,29 @@ var TerminalInstanceReconcileCoordinator = class {
|
|
|
47469
47516
|
return this.running;
|
|
47470
47517
|
}
|
|
47471
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
|
+
}
|
|
47472
47542
|
//#endregion
|
|
47473
47543
|
//#region src/profiles.ts
|
|
47474
47544
|
/**
|
|
@@ -47997,6 +48067,8 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
47997
48067
|
/** Prevent repeat writes when a legacy device stays live after migration. */
|
|
47998
48068
|
migratedTerminalCameraConfigIds = /* @__PURE__ */ new Set();
|
|
47999
48069
|
terminalCameraTombstones = /* @__PURE__ */ new Set();
|
|
48070
|
+
/** Instance ids whose node was absent from the last topology read (D49 streak). */
|
|
48071
|
+
instancesOnAbsentNodes = /* @__PURE__ */ new Set();
|
|
48000
48072
|
instanceMutationQueue = new TerminalInstanceMutationQueue();
|
|
48001
48073
|
terminalReconcileCoordinator = new TerminalInstanceReconcileCoordinator(this.instanceMutationQueue);
|
|
48002
48074
|
glancesPythonPath = "";
|
|
@@ -48177,6 +48249,16 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
48177
48249
|
const nodes = topology.filter((node) => typeof node.id === "string" && node.id.length > 0);
|
|
48178
48250
|
const nodeIds = new Set(nodes.map((node) => node.id));
|
|
48179
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
|
+
}
|
|
48180
48262
|
const unavailableNodeIds = /* @__PURE__ */ new Set();
|
|
48181
48263
|
await Promise.all(nodes.map(async (node) => {
|
|
48182
48264
|
try {
|
package/dist/addon.mjs
CHANGED
|
@@ -12922,6 +12922,24 @@ method(_void(), _void(), { kind: "mutation" }), method(_void(), _void(), { kind:
|
|
|
12922
12922
|
kind: "mutation",
|
|
12923
12923
|
auth: "admin"
|
|
12924
12924
|
});
|
|
12925
|
+
/**
|
|
12926
|
+
* Device Manager capability — hub-side singleton that unifies device persistence,
|
|
12927
|
+
* live registry access, and all management operations into a single tRPC surface.
|
|
12928
|
+
*
|
|
12929
|
+
* Replaces:
|
|
12930
|
+
* - `device-persistence` capability (persistence methods absorbed here)
|
|
12931
|
+
* - `device-management.router.ts` (deleted in Phase 2)
|
|
12932
|
+
* - `device-ops.router.ts` (compat layer — deleted; device-provider ops absorbed here)
|
|
12933
|
+
*
|
|
12934
|
+
* All device provider addons (rtsp, onvif, frigate, …) are hub-local: they may
|
|
12935
|
+
* fork into separate processes but never run on remote cluster agents. Therefore:
|
|
12936
|
+
* - No nodeId routing needed — this is a pure hub singleton.
|
|
12937
|
+
* - The hub's DeviceRegistry is the single source of truth for all live devices.
|
|
12938
|
+
* - No shadow registry or cross-node aggregation required.
|
|
12939
|
+
*
|
|
12940
|
+
* Forked workers register devices back to the hub via `ctx.devices`
|
|
12941
|
+
* (DeviceManagerApi → ctx.api.deviceManager.registerDevice), same as today.
|
|
12942
|
+
*/
|
|
12925
12943
|
/** One child-placement directive on a container's `childLayout`. Structurally
|
|
12926
12944
|
* identical to `ChildLayoutEntry` in `device-management.ts` — the cap wire
|
|
12927
12945
|
* shape for the same field. The child is identified by its re-sync-stable
|
|
@@ -13020,7 +13038,12 @@ var ConfigEntrySchema = object({
|
|
|
13020
13038
|
value: unknown(),
|
|
13021
13039
|
description: string().optional()
|
|
13022
13040
|
});
|
|
13023
|
-
|
|
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"]);
|
|
13024
13047
|
/** One resolved linked device — the compact projection consumers need. */
|
|
13025
13048
|
var LinkedDeviceSchema = object({
|
|
13026
13049
|
deviceId: number(),
|
|
@@ -18648,6 +18671,9 @@ var RetrainStatusSchema = _enum([
|
|
|
18648
18671
|
*
|
|
18649
18672
|
* `debug` does NOT pin; it is attention, not durability.
|
|
18650
18673
|
*
|
|
18674
|
+
* `debugNote` is the operator's own words about WHAT should be checked on this
|
|
18675
|
+
* track, and it lives and dies with `debug` — see {@link MAX_TRACK_DEBUG_NOTE_LEN}.
|
|
18676
|
+
*
|
|
18651
18677
|
* `favourited` IS a BOOLEAN pin (durability bit), not a retrain lifecycle.
|
|
18652
18678
|
* A favourited track is skipped by retention the same way `staging` is, but
|
|
18653
18679
|
* it does not enter `none|staging|trained` and has no staging budget.
|
|
@@ -18658,6 +18684,21 @@ var TrackFlagFields = {
|
|
|
18658
18684
|
markForTrain: boolean().optional(),
|
|
18659
18685
|
/** Operator marked this track for diagnostic attention. */
|
|
18660
18686
|
debug: boolean().optional(),
|
|
18687
|
+
/**
|
|
18688
|
+
* What the operator wants CHECKED on this track, in their own words.
|
|
18689
|
+
*
|
|
18690
|
+
* The flag alone says "look at this" and nothing about what for; a debug set
|
|
18691
|
+
* reviewed hours later is a list of tracks with no question attached. Bound
|
|
18692
|
+
* to `debug` on the WRITE side — the body clears it when `debug` goes off,
|
|
18693
|
+
* and refuses it on a track whose debug is off — so a note can never outlive
|
|
18694
|
+
* the attention it belongs to.
|
|
18695
|
+
*
|
|
18696
|
+
* `''` is a real value ("marked, nothing to add"); the ABSENT key means
|
|
18697
|
+
* "leave whatever is stored alone", which is what lets a surface turn debug
|
|
18698
|
+
* on without a note (a cancelled prompt) and what lets it edit the note
|
|
18699
|
+
* without touching the flag.
|
|
18700
|
+
*/
|
|
18701
|
+
debugNote: string().max(500).optional(),
|
|
18661
18702
|
/** Operator favourited this track. Pins it against pruning. */
|
|
18662
18703
|
favourited: boolean().optional()
|
|
18663
18704
|
};
|
|
@@ -18684,6 +18725,12 @@ var TrackFlagsSchema = object({
|
|
|
18684
18725
|
trackId: string(),
|
|
18685
18726
|
markForTrain: boolean(),
|
|
18686
18727
|
debug: boolean(),
|
|
18728
|
+
/** The note as it STANDS after the write — never absent here (a track with no
|
|
18729
|
+
* note reports `''`), for the same reason the booleans are required: a
|
|
18730
|
+
* surface that has just written must be able to render the note it now owns
|
|
18731
|
+
* without a re-fetch, and an absent key would read as "unchanged" on a
|
|
18732
|
+
* screen that has no previous value to keep. */
|
|
18733
|
+
debugNote: string(),
|
|
18687
18734
|
favourited: boolean(),
|
|
18688
18735
|
/** The lifecycle state the boolean was derived from. Required here (unlike on
|
|
18689
18736
|
* a track row) because this shape is only ever produced by the write body,
|
|
@@ -47446,6 +47493,29 @@ var TerminalInstanceReconcileCoordinator = class {
|
|
|
47446
47493
|
return this.running;
|
|
47447
47494
|
}
|
|
47448
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
|
+
}
|
|
47449
47519
|
//#endregion
|
|
47450
47520
|
//#region src/profiles.ts
|
|
47451
47521
|
/**
|
|
@@ -47974,6 +48044,8 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
47974
48044
|
/** Prevent repeat writes when a legacy device stays live after migration. */
|
|
47975
48045
|
migratedTerminalCameraConfigIds = /* @__PURE__ */ new Set();
|
|
47976
48046
|
terminalCameraTombstones = /* @__PURE__ */ new Set();
|
|
48047
|
+
/** Instance ids whose node was absent from the last topology read (D49 streak). */
|
|
48048
|
+
instancesOnAbsentNodes = /* @__PURE__ */ new Set();
|
|
47977
48049
|
instanceMutationQueue = new TerminalInstanceMutationQueue();
|
|
47978
48050
|
terminalReconcileCoordinator = new TerminalInstanceReconcileCoordinator(this.instanceMutationQueue);
|
|
47979
48051
|
glancesPythonPath = "";
|
|
@@ -48154,6 +48226,16 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
48154
48226
|
const nodes = topology.filter((node) => typeof node.id === "string" && node.id.length > 0);
|
|
48155
48227
|
const nodeIds = new Set(nodes.map((node) => node.id));
|
|
48156
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
|
+
}
|
|
48157
48239
|
const unavailableNodeIds = /* @__PURE__ */ new Set();
|
|
48158
48240
|
await Promise.all(nodes.map(async (node) => {
|
|
48159
48241
|
try {
|