fix(mesh): poll topology data and tighten stack-membership equality (#1059)

Adds a visibility-aware 30s poll on the Routing tab so the graph reflects
tunnel state, alias publishes, and remote-node disconnects without a
manual reload. Polling pauses while the tab is hidden and wakes
immediately on focus via the shared visibilityInterval helper.

Tightens nodeStateEqual so a stack swap on the same node (opt out A, opt
in B) is detected even when the count is unchanged. Extracts the helper,
the minimap colour mapping, and the minimap colour literals to
mesh-topology-layout so they can be unit-tested and to drop a now-unused
isOwnerView field from MeshNodeData.

Persists the Table/Graph and Tunnels/Aliases toggles to localStorage so a
Routing tab session keeps the operator's last-used view.

Adds a legend line to the per-stack topology sheet clarifying that
consumer edges mean meshed peers that could reach the aliases via DNS;
whether containers dial them depends on each consumer's own opt-in
stacks.

Adds unit tests for stacksKey, meshNodeStateEqual, and miniMapColorFor,
plus troubleshooting entries and a refresh-cadence note to the mesh docs.
This commit is contained in:
Anso
2026-05-15 09:52:10 -04:00
committed by GitHub
parent 13e4edc1e2
commit 3323a59003
6 changed files with 184 additions and 43 deletions
@@ -3,6 +3,13 @@ import {
buildTunnelsGraph,
buildAliasesGraph,
buildStackTopologyGraph,
meshNodeStateEqual,
miniMapColorFor,
stacksKey,
MINIMAP_BRAND,
MINIMAP_WARNING,
MINIMAP_MUTED,
MINIMAP_DESTRUCTIVE,
} from './mesh-topology-layout';
import type { MeshAlias, MeshNodeStatus } from '@/types/mesh';
@@ -31,6 +38,74 @@ function makeAlias(over: Partial<MeshAlias> & Pick<MeshAlias, 'host' | 'nodeId'>
};
}
describe('stacksKey', () => {
it('returns the same key for the same membership regardless of order', () => {
expect(stacksKey(['a', 'b', 'c'])).toBe(stacksKey(['c', 'a', 'b']));
});
it('differs when membership differs even with the same count', () => {
expect(stacksKey(['a', 'b'])).not.toBe(stacksKey(['a', 'c']));
});
it('returns empty string for empty list', () => {
expect(stacksKey([])).toBe('');
});
});
describe('meshNodeStateEqual', () => {
const base = makeNode({ nodeId: 1, nodeName: 'n', reachableMode: 'pilot', enabled: true, pilotConnected: true, optedInStacks: ['a', 'b'] });
it('returns true when scalar fields and stack membership match', () => {
const a = { ...base };
const b = { ...base, optedInStacks: ['b', 'a'] };
expect(meshNodeStateEqual(a, b)).toBe(true);
});
it('detects a stack swap that preserves the count', () => {
const a = { ...base };
const b = { ...base, optedInStacks: ['a', 'c'] };
expect(meshNodeStateEqual(a, b)).toBe(false);
});
it('detects a reachable-mode flip', () => {
const a = { ...base };
const b = { ...base, reachableMode: 'unreachable' as const };
expect(meshNodeStateEqual(a, b)).toBe(false);
});
it('detects a pilot disconnect', () => {
const a = { ...base };
const b = { ...base, pilotConnected: false };
expect(meshNodeStateEqual(a, b)).toBe(false);
});
});
describe('miniMapColorFor', () => {
it('returns destructive for unreachable', () => {
const node = makeNode({ nodeId: 1, nodeName: 'n', reachableMode: 'unreachable' });
expect(miniMapColorFor(node)).toBe(MINIMAP_DESTRUCTIVE);
});
it('returns warning for pilot that is not connected', () => {
const node = makeNode({ nodeId: 1, nodeName: 'n', reachableMode: 'pilot', enabled: true, pilotConnected: false });
expect(miniMapColorFor(node)).toBe(MINIMAP_WARNING);
});
it('returns muted for an enabled-false pilot that is connected', () => {
const node = makeNode({ nodeId: 1, nodeName: 'n', reachableMode: 'pilot', enabled: false, pilotConnected: true });
expect(miniMapColorFor(node)).toBe(MINIMAP_MUTED);
});
it('returns brand for a healthy meshed remote', () => {
const node = makeNode({ nodeId: 1, nodeName: 'n', reachableMode: 'pilot', enabled: true, pilotConnected: true });
expect(miniMapColorFor(node)).toBe(MINIMAP_BRAND);
});
it('returns muted when node is undefined', () => {
expect(miniMapColorFor(undefined)).toBe(MINIMAP_MUTED);
});
});
describe('buildTunnelsGraph', () => {
it('returns empty result when no nodes', () => {
const result = buildTunnelsGraph([]);
+30 -2
View File
@@ -5,7 +5,35 @@ import type { MeshAlias, MeshNodeStatus } from '@/types/mesh';
export interface MeshNodeData extends Record<string, unknown> {
node: MeshNodeStatus;
aliasCount: number;
isOwnerView: boolean;
}
// MiniMap colour literals. ReactFlow cannot resolve CSS vars inside inline
// SVG fills, so the minimap takes raw oklch strings.
export const MINIMAP_BRAND = 'oklch(0.78 0.11 195)';
export const MINIMAP_WARNING = 'oklch(0.75 0.14 75)';
export const MINIMAP_MUTED = 'oklch(0.55 0 0)';
export const MINIMAP_DESTRUCTIVE = 'oklch(0.65 0.2 28)';
export function miniMapColorFor(node: MeshNodeStatus | undefined): string {
if (!node) return MINIMAP_MUTED;
if (node.reachableMode === 'unreachable') return MINIMAP_DESTRUCTIVE;
if (node.reachableMode === 'pilot' && !node.pilotConnected) return MINIMAP_WARNING;
if (!node.enabled) return MINIMAP_MUTED;
return MINIMAP_BRAND;
}
export function stacksKey(stacks: readonly string[]): string {
return [...stacks].sort().join(' ');
}
export function meshNodeStateEqual(a: MeshNodeStatus, b: MeshNodeStatus): boolean {
return a.nodeId === b.nodeId
&& a.enabled === b.enabled
&& a.reachableMode === b.reachableMode
&& a.pilotConnected === b.pilotConnected
&& a.reachableReason === b.reachableReason
&& a.activeStreamCount === b.activeStreamCount
&& stacksKey(a.optedInStacks) === stacksKey(b.optedInStacks);
}
export interface MeshEdgeData extends Record<string, unknown> {
@@ -105,7 +133,7 @@ function makeNode(
id: String(node.nodeId),
type: 'meshNode',
position,
data: { node, aliasCount, isOwnerView: false } satisfies MeshNodeData,
data: { node, aliasCount } satisfies MeshNodeData,
draggable: true,
};
}