feat(mesh): refine the Routing tab and add per-route removal (#1311)

* feat(mesh): refine the Routing tab and add per-route removal

Tighten the Fleet > Routing tab so its node cards and diagnostics read
honestly for proxy-connected fleets, and remove a couple of dead ends.

- Node cards drop the "pilot connected/offline" line, which was meaningless
  for nodes that connect over the HTTP API proxy. The compact card drops the
  matching agent cell and now reads stacks / aliases / bridge.
- Enabling mesh on a proxy node shows a transient "Connecting" state and
  settles to meshed on its own, instead of flashing "Degraded" with a manual
  refresh while the bridge finishes dialing.
- The alias detail sheet gains a "Remove from mesh" action that opts the
  alias's owning stack out (the confirmation says how many aliases that
  drops), and a "Topology" tab. The standalone topology sheet and its
  opt-in-sheet shortcut are removed in favor of the tab.
- The "Add stack" affordance stays reachable on a meshed node, so a node
  that already has aliases is no longer a dead end.
- Diagnostics and the alias detail sheet show a transport-aware line
  ("local", "API proxy bridge", or "Pilot tunnel" with its state) instead of
  a fixed "Pilot tunnel" label.

Adds unit coverage for the new state derivation, transport descriptor, the
enable auto-converge, and the admin-gated removal, and updates the mesh docs.

* fix(mesh): harden Routing tab toggle and alias sheet against async races

Independent review surfaced two narrow async races in the new code.

- RoutingNodeCard: cancel the converge re-poll batch at the start of any
  toggle (so a slow disable can't let a prior enable's re-polls fire), and
  guard the toast, refresh, timer scheduling, and setToggling behind a mounted
  ref so nothing runs after the card unmounts while the enable POST is still
  in flight.
- MeshRouteDetailSheet: re-check the cancelled flag after reading the
  diagnostic body, not just before it. Reading the body is itself async, so a
  superseded alias's diagnostic could otherwise call setDiag and expose Remove
  for the wrong stack.

Adds tests for unmount-before-enable-resolves and the deferred-diagnostic
alias switch.
This commit is contained in:
Anso
2026-06-04 21:45:53 -04:00
committed by GitHub
parent 9d3055049f
commit 81858a0bb0
16 changed files with 731 additions and 256 deletions
@@ -16,7 +16,7 @@ function renderCard(overrides: Partial<RoutingNodeCardProps> = {}) {
crumb: ['Routing', 'Node', 'node-alpha'],
name: 'node-alpha',
nodeState: 'idle',
meta: { pilotConnected: true, reverseBridge: 'na', stacks: 0, aliases: 0 },
meta: { reverseBridge: 'na', stacks: 0, aliases: 0 },
aliases: [],
onToggleEnabled: vi.fn(),
onShowDiagnostics: vi.fn(),
@@ -49,7 +49,7 @@ describe('routing-node-card canManage gate', () => {
it('hides the add-stack CTA for a non-manager on a meshed node', () => {
renderCard({
nodeState: 'meshed',
meta: { pilotConnected: true, reverseBridge: 'up', stacks: 0, aliases: 0 },
meta: { reverseBridge: 'up', stacks: 0, aliases: 0 },
canManage: false,
});
expect(screen.queryByRole('switch')).not.toBeInTheDocument();
@@ -72,6 +72,36 @@ describe('routing-node-card canManage gate', () => {
});
});
describe('routing-node-card add-stack reachability and connecting state', () => {
const oneAlias = [{ host: 'web.api.node', port: 8080, kind: 'alias' as const }];
it('keeps an add-stack control for a manager on a meshed node that already has aliases', () => {
renderCard({
nodeState: 'meshed',
meta: { reverseBridge: 'up', stacks: 1, aliases: 1 },
aliases: oneAlias,
canManage: true,
});
expect(screen.getByRole('button', { name: /Add stack/i })).toBeInTheDocument();
});
it('hides the add-stack control for a non-manager on a meshed node with aliases', () => {
renderCard({
nodeState: 'meshed',
meta: { reverseBridge: 'up', stacks: 1, aliases: 1 },
aliases: oneAlias,
canManage: false,
});
expect(screen.queryByRole('button', { name: /Add stack/i })).not.toBeInTheDocument();
});
it('renders the connecting state as passive with no retry button', () => {
renderCard({ nodeState: 'connecting', canManage: true });
expect(screen.getByText(/Connecting to the mesh/i)).toBeInTheDocument();
expect(screen.queryByRole('button', { name: /Retry now/i })).not.toBeInTheDocument();
});
});
describe('routing-node-card canManage gate (compact density)', () => {
beforeEach(() => {
window.localStorage.setItem('sencho.appearance.density', 'compact');
@@ -83,7 +113,7 @@ describe('routing-node-card canManage gate (compact density)', () => {
it('hides the toggle for a non-manager on a meshed node', () => {
renderCard({
nodeState: 'meshed',
meta: { pilotConnected: true, reverseBridge: 'up', stacks: 0, aliases: 0 },
meta: { reverseBridge: 'up', stacks: 0, aliases: 0 },
canManage: false,
});
expect(screen.queryByRole('switch')).not.toBeInTheDocument();
@@ -92,7 +122,7 @@ describe('routing-node-card canManage gate (compact density)', () => {
it('shows the toggle for a manager on a meshed node', () => {
renderCard({
nodeState: 'meshed',
meta: { pilotConnected: true, reverseBridge: 'up', stacks: 0, aliases: 0 },
meta: { reverseBridge: 'up', stacks: 0, aliases: 0 },
canManage: true,
});
expect(screen.getByRole('switch')).toBeInTheDocument();
@@ -7,7 +7,7 @@ import { useDensity } from '@/hooks/use-density';
import { formatAgeShort } from '@/lib/relativeTime';
import { cn } from '@/lib/utils';
export type RoutingNodeState = 'meshed' | 'idle' | 'degraded' | 'offline';
export type RoutingNodeState = 'meshed' | 'idle' | 'connecting' | 'degraded' | 'offline';
export interface RoutingAliasRow {
host: string;
@@ -18,7 +18,6 @@ export interface RoutingAliasRow {
}
export interface RoutingNodeCardMeta {
pilotConnected: boolean;
reverseBridge: 'up' | 'unavailable' | 'na';
stacks: number;
aliases: number;
@@ -56,6 +55,7 @@ const KICKER = 'font-mono text-[10px] uppercase tracking-[0.18em]';
const RAIL_CLASS: Record<RoutingNodeState, string> = {
meshed: 'bg-brand',
idle: '',
connecting: 'bg-brand animate-pulse',
degraded: 'bg-warning',
offline: 'bg-destructive',
};
@@ -63,6 +63,7 @@ const RAIL_CLASS: Record<RoutingNodeState, string> = {
const RAIL_INLINE_STYLE: Record<RoutingNodeState, React.CSSProperties | undefined> = {
meshed: undefined,
idle: { background: 'oklch(0.28 0 0)' },
connecting: undefined,
degraded: undefined,
offline: undefined,
};
@@ -70,6 +71,7 @@ const RAIL_INLINE_STYLE: Record<RoutingNodeState, React.CSSProperties | undefine
const STATE_CHIP: Record<RoutingNodeState, { label: string; tone: string }> = {
meshed: { label: 'Meshed', tone: 'border-brand/40 bg-brand/10 text-brand' },
idle: { label: 'Idle', tone: 'border-card-border bg-card text-stat-subtitle' },
connecting: { label: 'Connecting', tone: 'border-brand/40 bg-brand/10 text-brand' },
degraded: { label: 'Degraded', tone: 'border-warning/40 bg-warning/10 text-warning' },
offline: { label: 'Offline', tone: 'border-destructive/40 bg-destructive/10 text-destructive' },
};
@@ -92,7 +94,7 @@ export function RoutingNodeCard(props: RoutingNodeCardProps) {
const toggleDisabled = nodeState === 'offline';
const diagnosticsDisabled = nodeState === 'offline';
const isEnabled = nodeState === 'meshed' || nodeState === 'degraded';
const isEnabled = nodeState === 'meshed' || nodeState === 'degraded' || nodeState === 'connecting';
const chip = STATE_CHIP[nodeState];
const railClass = RAIL_CLASS[nodeState];
@@ -119,7 +121,6 @@ export function RoutingNodeCard(props: RoutingNodeCardProps) {
onShowDiagnostics={onShowDiagnostics}
canManage={canManage}
footerContext={footerContext}
aliasesEmpty={aliases.length === 0}
onAddStack={onAddStack}
onRetry={onRetry}
/>
@@ -165,10 +166,6 @@ interface BodyChrome {
onRetry?: () => void;
}
interface CompactProps extends BodyChrome {
aliasesEmpty: boolean;
}
interface ComfortableProps extends BodyChrome {
crumb: string[];
aliases: RoutingAliasRow[];
@@ -207,8 +204,7 @@ function ComfortableBody(props: ComfortableProps) {
)}
</div>
<div className={cn(KICKER, 'mt-1.5 text-stat-subtitle leading-none tracking-[0.14em]')}>
pilot {meta.pilotConnected ? 'connected' : 'offline'}
{' · '}reverse {REVERSE_LABEL[meta.reverseBridge]}
reverse {REVERSE_LABEL[meta.reverseBridge]}
{' · '}{meta.stacks} stacks
{' · '}{meta.aliases} aliases
</div>
@@ -231,6 +227,8 @@ function ComfortableBody(props: ComfortableProps) {
aliases={aliases}
onShowAlias={onShowAlias}
onTestAlias={onTestAlias}
onAddStack={onAddStack}
canManage={canManage}
/>
: <EmptyState
nodeState={nodeState}
@@ -248,11 +246,11 @@ function ComfortableBody(props: ComfortableProps) {
);
}
function CompactBody(props: CompactProps) {
function CompactBody(props: BodyChrome) {
const {
name, isLocal, chip, meta, nodeState, isEnabled,
toggleDisabled, diagnosticsDisabled, onToggleEnabled, onShowDiagnostics,
footerContext, aliasesEmpty, onAddStack, onRetry, canManage,
footerContext, onAddStack, onRetry, canManage,
} = props;
return (
@@ -290,7 +288,7 @@ function CompactBody(props: CompactProps) {
</div>
</header>
<div className="grid grid-cols-4 divide-x divide-card-border/40 border-y border-card-border/40 bg-card/60 pl-[3px]">
<div className="grid grid-cols-3 divide-x divide-card-border/40 border-y border-card-border/40 bg-card/60 pl-[3px]">
<CompactCell
dot={meta.stacks > 0 ? 'success' : 'muted'}
value={String(meta.stacks)}
@@ -301,11 +299,6 @@ function CompactBody(props: CompactProps) {
value={String(meta.aliases)}
sub="published"
/>
<CompactCell
dot={meta.pilotConnected ? 'success' : 'warning'}
value={meta.pilotConnected ? 'on' : 'off'}
sub="agent"
/>
<CompactCell
dot={reverseDot(meta.reverseBridge)}
value={REVERSE_LABEL[meta.reverseBridge]}
@@ -317,7 +310,6 @@ function CompactBody(props: CompactProps) {
context={footerContext}
nodeState={nodeState}
name={name}
aliasesEmpty={aliasesEmpty}
onAddStack={onAddStack}
onRetry={onRetry}
onToggleEnabled={onToggleEnabled}
@@ -388,14 +380,28 @@ interface AliasListProps {
aliases: RoutingAliasRow[];
onShowAlias?: (alias: string) => void;
onTestAlias?: (alias: string) => void;
onAddStack?: () => void;
canManage: boolean;
}
function AliasList({ title, aliases, onShowAlias, onTestAlias }: AliasListProps) {
function AliasList({ title, aliases, onShowAlias, onTestAlias, onAddStack, canManage }: AliasListProps) {
return (
<section>
<h4 className={cn(KICKER, 'text-stat-subtitle leading-none mb-2 tracking-[0.22em]')}>
{title}
</h4>
<div className="mb-2 flex items-center justify-between gap-2">
<h4 className={cn(KICKER, 'text-stat-subtitle leading-none tracking-[0.22em]')}>
{title}
</h4>
{canManage && onAddStack && (
<Button
variant="ghost"
size="sm"
onClick={onAddStack}
className="h-6 -my-1 px-1.5 text-stat-subtitle hover:text-brand"
>
<Plus className="w-3 h-3 mr-1" /> Add stack
</Button>
)}
</div>
<div className="space-y-1">
{aliases.map((row) => (
<AliasRow
@@ -480,6 +486,9 @@ function EmptyState({ nodeState, name, offlineReason, onAddStack, onRetry, onTog
// the backend gates on the admin role, so a non-admin viewer sees a hint
// instead. The degraded/offline retry is a read-only refresh and stays.
const isManagementState = nodeState === 'idle' || nodeState === 'meshed';
// `connecting` is transient while the mesh bridge dials; show the headline
// only, with no retry button (it clears on its own once the bridge is up).
const isConnecting = nodeState === 'connecting';
const handleClick = () => {
if (nodeState === 'idle') onToggleEnabled(true);
@@ -487,6 +496,25 @@ function EmptyState({ nodeState, name, offlineReason, onAddStack, onRetry, onTog
else onRetry?.();
};
let action: React.ReactNode = (
<Button
variant="outline"
size="sm"
onClick={handleClick}
className={cn('mt-1', ctaToneFor(nodeState))}
>
{ctaIconFor(nodeState)}{cta}
</Button>
);
if (isConnecting) action = null;
else if (!canManage && isManagementState) {
action = (
<div className="font-mono text-[11px] leading-snug text-stat-subtitle">
Managing the mesh requires an administrator.
</div>
);
}
return (
<div className="flex flex-col items-start gap-2 py-3">
<div className="font-display italic text-[18px] leading-[24px] text-stat-value">
@@ -495,20 +523,7 @@ function EmptyState({ nodeState, name, offlineReason, onAddStack, onRetry, onTog
<div className="font-mono text-[11px] leading-snug text-stat-subtitle">
{sub}
</div>
{!canManage && isManagementState ? (
<div className="font-mono text-[11px] leading-snug text-stat-subtitle">
Managing the mesh requires an administrator.
</div>
) : (
<Button
variant="outline"
size="sm"
onClick={handleClick}
className={cn('mt-1', ctaToneFor(nodeState))}
>
{ctaIconFor(nodeState)}{cta}
</Button>
)}
{action}
</div>
);
}
@@ -522,6 +537,7 @@ function ctaIconFor(state: RoutingNodeState): React.ReactNode {
const CTA_TONE: Record<RoutingNodeState, string> = {
meshed: '',
idle: '',
connecting: '',
degraded: 'border-warning/40 text-warning hover:bg-warning/10',
offline: 'border-destructive/40 text-destructive hover:bg-destructive/10',
};
@@ -530,6 +546,16 @@ function ctaToneFor(state: RoutingNodeState): string {
return CTA_TONE[state];
}
// Management CTAs (enable mesh / add stack) need admin; the read-only retry on a
// degraded/offline node stays available to everyone. `meshed` always offers "add
// stack" to admins so a node with aliases is never a dead end, and the transient
// `connecting` state shows no CTA.
function shouldShowCta(state: RoutingNodeState, canManage: boolean): boolean {
if (state === 'connecting') return false;
if (state === 'idle' || state === 'meshed') return canManage;
return true;
}
function emptyStateCopy(state: RoutingNodeState, name: string, offlineReason?: string | null): {
headline: string; sub: string; cta: string;
} {
@@ -546,10 +572,16 @@ function emptyStateCopy(state: RoutingNodeState, name: string, offlineReason?: s
sub: 'Mesh is on. Opt a stack in to start publishing aliases.',
cta: 'Add stack to mesh',
};
case 'connecting':
return {
headline: 'Connecting to the mesh…',
sub: 'Bringing up the bridge to this node.',
cta: '',
};
case 'degraded':
return {
headline: 'Pilot tunnel disconnected.',
sub: 'Mesh traffic resumes when the agent reconnects.',
headline: 'Mesh bridge unavailable.',
sub: 'Central retries the connection automatically.',
cta: 'Retry now',
};
case 'offline':
@@ -558,6 +590,10 @@ function emptyStateCopy(state: RoutingNodeState, name: string, offlineReason?: s
sub: offlineReason || 'Connection refused.',
cta: 'Retry now',
};
default: {
const _exhaustive: never = state;
throw new Error(`Unhandled routing node state: ${String(_exhaustive)}`);
}
}
}
@@ -575,18 +611,14 @@ interface CompactFooterProps {
context: string;
nodeState: RoutingNodeState;
name: string;
aliasesEmpty: boolean;
onAddStack?: () => void;
onRetry?: () => void;
onToggleEnabled: (next: boolean) => void;
canManage: boolean;
}
function CompactFooter({ context, nodeState, name, aliasesEmpty, onAddStack, onRetry, onToggleEnabled, canManage }: CompactFooterProps) {
// Hide the management CTAs (enable mesh / add stack) for non-admins; the
// read-only retry on a degraded/offline node stays available.
const isManagementState = nodeState === 'idle' || nodeState === 'meshed';
const showCta = (nodeState !== 'meshed' || aliasesEmpty) && (canManage || !isManagementState);
function CompactFooter({ context, nodeState, name, onAddStack, onRetry, onToggleEnabled, canManage }: CompactFooterProps) {
const showCta = shouldShowCta(nodeState, canManage);
const { cta } = emptyStateCopy(nodeState, name);
const handleClick = () => {
if (nodeState === 'idle') onToggleEnabled(true);