mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-20 15:22:59 +00:00
fix(mesh): surface stopped-stack opt-ins on routing node cards (#1098)
Add `currentlyResolvable: boolean` per entry in `MeshNodeStatus.optedInStacks`, derived from the existing alias cache so the new field stays consistent with `/api/mesh/aliases` without any extra Dockerode or cross-node inspect on the status path. The Routing tab renders an amber `suspended` pill for entries whose stack is opted in but currently has no running services, plus a single explanatory caption below the suspended list. Resolves the contradictory `Mesh stacks: 1 / Aliases: 0 / No mesh services on this node yet` copy on the node card when a meshed stack's container has been stopped; the misleading line is now only shown when the node truly has no opt-ins. The opt-in itself remains sticky: when the stack starts again, its aliases reappear automatically on the next refresh. Defensive de-dup in the UI filters suspended entries against the live alias snapshot to handle the transient gap where `/mesh/status` and `/mesh/aliases` return slightly inconsistent views from their separate fetches. Tests: - new `mesh-status-resolvability.test.ts` (6 cases) locks the resolvable / suspended / mixed / empty / per-node-scoping / stale-alias-no-phantom invariants for `getStatus`. - `mesh-topology-layout.test.ts` gains a `stacksKey` resolvability-flip case and a `meshNodeStateEqual` case asserting a resolvability flip on an otherwise identical stack registers as a state change so the topology layout re-runs. Operator docs gain one new troubleshooting accordion in `/docs/features/sencho-mesh.mdx` explaining the suspended state.
This commit is contained in:
@@ -190,7 +190,18 @@ export interface MeshNodeStatus {
|
||||
reachableReason: string | null;
|
||||
/** Peer→central reverse path state. `not_applicable` for non-proxy peers. */
|
||||
reverseCallbackStatus: MeshReverseCallbackStatus;
|
||||
optedInStacks: string[];
|
||||
/**
|
||||
* Stacks opted into the mesh on this node, with a per-stack resolvability
|
||||
* flag. `currentlyResolvable` is `true` iff the alias cache currently
|
||||
* carries at least one alias for that (nodeId, stackName) pair, i.e. the
|
||||
* stack's services were inspectable and exposed at least one port the
|
||||
* last time `refreshAliasCache()` ran (every 60 s on a timer, plus on
|
||||
* opt-in / opt-out and pilot reconnect). A suspended opt-in (stack
|
||||
* stopped, services not running) reports `currentlyResolvable: false` so
|
||||
* the Routing tab can surface the asymmetry between the persistent
|
||||
* registry and the live alias list.
|
||||
*/
|
||||
optedInStacks: Array<{ stackName: string; currentlyResolvable: boolean }>;
|
||||
activeStreamCount: number;
|
||||
}
|
||||
|
||||
@@ -2131,6 +2142,15 @@ export class MeshService extends EventEmitter implements MeshForwarderHost {
|
||||
const localListening = this.isLocalForwarderActive();
|
||||
const ptm = PilotTunnelManager.getInstance();
|
||||
const dialer = MeshProxyTunnelDialer.getInstance();
|
||||
// Precompute the (nodeId, stackName) pairs that currently have at least
|
||||
// one alias in the cache. Reading the cache (refreshed every 60 s and
|
||||
// on opt-in/opt-out) keeps the new `currentlyResolvable` field aligned
|
||||
// with what `/api/mesh/aliases` reports, without paying the cost of a
|
||||
// fresh Dockerode/cross-node inspect per status poll.
|
||||
const resolvableKeys = new Set<string>();
|
||||
for (const alias of this.aliasCache.values()) {
|
||||
resolvableKeys.add(`${alias.nodeId}:${alias.stackName}`);
|
||||
}
|
||||
return nodes.map((node) => {
|
||||
const reach = this.computeReachable(node, localNodeId);
|
||||
const meshEnabled = db.getNodeMeshEnabled(node.id);
|
||||
@@ -2148,7 +2168,10 @@ export class MeshService extends EventEmitter implements MeshForwarderHost {
|
||||
reachableMode: reach.mode,
|
||||
reachableReason: reach.reason,
|
||||
reverseCallbackStatus: this.computeReverseCallbackStatus(node, meshEnabled, dialer),
|
||||
optedInStacks: db.listMeshStacks(node.id).map((s) => s.stack_name),
|
||||
optedInStacks: db.listMeshStacks(node.id).map((s) => ({
|
||||
stackName: s.stack_name,
|
||||
currentlyResolvable: resolvableKeys.has(`${node.id}:${s.stack_name}`),
|
||||
})),
|
||||
activeStreamCount: this.activeStreams.size,
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user