);
const shellOverlaysEl = (
{
if (isMobile) {
navigateMobileAware('fleet');
} else {
setFleetUpdatesIntent({ tab: 'nodes' });
handleNavigate('fleet');
}
}}
/>
);
// Bespoke, masthead-led mobile screens. When showing one, the TopBar is
// dropped and the screen renders its own masthead (with notifications +
// more-menu rehomed into the right slot).
const bespokeContent = mobileSurface === 'content' && BESPOKE_MOBILE_VIEWS.has(activeView);
// Shared lazy-chunk fallback for the code-split bespoke phone screens.
const lazyFallback = (
);
const renderMobileBespoke = () => {
switch (activeView) {
case 'dashboard':
return (
openSettings('nodes')}
/>
);
case 'fleet':
// Never mount the mobile fleet view without node:read: the redirect
// effect bounces the deep-link to the dashboard, but MobileFleet is a
// static (non-lazy) render, so without this guard it would fire one
// /fleet/overview (now 403) before the redirect unmounts it.
if (!can('node:read')) return null;
return (
);
case 'scheduled-ops':
return ;
case 'settings':
return (
);
case 'security':
return (
)}
>
);
case 'auto-updates':
// Same gates as the desktop content path (ViewRouter): hub-only +
// the auto-updates capability, preserved on the phone surface.
return (
);
case 'templates':
return (
{ refreshStacks(); void stackActions.loadFile(sName); }}
headerActions={mobileMastheadActions}
/>
);
case 'audit-log':
return (
);
case 'resources':
return (
);
case 'networking':
return (
);
case 'global-observability':
// Hub-only, like the desktop content path (ViewRouter); no capability gate.
return (
);
default:
return workspaceEl;
}
};
// The mobile Stacks list leads with the status masthead (no TopBar): the
// node switcher is its kicker chip, the serif word summarizes stack
// health, and notifications + the more-menu sit in the right slot.
// up counts 'running' and down counts 'exited'; any other status (or the
// window before statuses load) is neither, so "All running" must require
// every stack to be up rather than just no stack being down.
const { all: stacksAll, up: stacksUp, down: stacksDown, updates: stacksUpdates } = filterCounts;
let stacksState = 'All running';
let stacksTone: Tone = 'success';
if (stacksAll === 0) {
stacksState = 'No stacks';
} else if (stacksDown > 0) {
stacksState = `${stacksDown} down`;
stacksTone = 'destructive';
} else if (stacksUpdates > 0) {
stacksState = `${stacksUpdates} update${stacksUpdates === 1 ? '' : 's'}`;
stacksTone = 'warning';
} else if (stacksUp !== stacksAll) {
stacksState = `${stacksUp}/${stacksAll} up`;
stacksTone = 'brand';
}
const stacksMasthead = (
openSettings('nodes')} />}
state={stacksState}
stateTone={stacksTone}
live={stacksDown > 0}
meta={`${stacksAll} ${stacksAll === 1 ? 'stack' : 'stacks'} · ${stacksUp} up · ${stacksDown} down`}
right={mobileMastheadActions}
/>
);
if (isMobile) {
return (
// h-dvh tracks the visible viewport as the address bar shows and hides, so
// the bottom tab bar stays on screen (100vh would push it behind the bar).
);
})()}
);
}
// Optimistic stack-detail placeholder shown on mobile the instant a row is
// tapped, until loadFile resolves and the real EditorView mounts. Keeps the tap
// feeling immediate on slow networks.
function MobileDetailLoading({ name, onBack, headerActions }: { name: string; onBack: () => void; headerActions?: ReactNode }) {
return (