feat(frontend): SystemSheet §9.11 — security and scheduled sheets (PR 2/3) (#961)

* feat(frontend): add SystemSheet primitive and migrate mesh sheets to §9.11 chrome

DESIGN.md §9.11 codifies one canonical right-side detail-sheet shell (cyan
rail, mono crumb, italic serif name, mono meta, ESC chip + close glyph,
fixed three-slot toolbar, cyan-underline tabs, ScrollArea body, footer
freshness band). Today the 16 sheet consumers each render their own
header chrome with stock shadcn SheetHeader/SheetTitle.

Introduce <SystemSheet> + <SheetSection> in
frontend/src/components/ui/system-sheet.tsx, composing the existing
<Sheet>/<SheetContent> primitive. Add a backward-compatible showClose
prop to SheetContent so SystemSheet can render its own ESC chip + close
glyph instead of the stock cyan square close.

Migrate the four mesh sheets as the first batch:
* MeshActivitySheet: crumb Fleet › Mesh › Activity, footer freshness from
  most-recent event timestamp.
* MeshOptInSheet: crumb Fleet › Mesh › {nodeName}, meta of opted-in
  count, drops the redundant bottom Close button (ESC chip dismisses).
* MeshDiagnosticsSheet: removes the icon-prefixed title (forbidden by
  §9.11), lifts Refresh/Restart buttons from the body into the toolbar
  band, three SheetSection blocks for sidecar status, streams, cache.
* MeshRouteDetailSheet: adds Overview/Events/Raw tabs, lifts Test probe
  into the toolbar primary slot, footer surfaces last probe latency.

* feat(frontend): migrate security and scheduled sheets to §9.11 chrome

PR 2 of the System Sheet (§9.11) rollout, stacked on the SystemSheet
primitive PR. Migrates six more sheet consumers and merges the Stack
alert + auto-heal sheets into a single tabbed sheet per audit §17.

Sheets migrated:
* NodeUpdatesSheet: crumb Fleet > Updates, Recheck primary, Update-all
  secondary when applicable. Stat tiles and node rows lose their
  card-in-sheet wrapping (forbidden by §9.11) for flat dividers.
* StackAlertSheet (now the merged stack monitor): tabs Alerts /
  Auto-heal, crumb Stack > {name} > Monitor. New initialTab prop lets
  callers open directly to either tab. Auto-heal tab is hidden entirely
  for Community-tier users (matches the existing tier-gating on the
  context menu trigger and keyboard shortcut).
* StackAutoHealSheet.tsx: deleted. Its body became the Auto-heal tab
  inside the merged sheet.
* VulnerabilityScanSheet: removed the icon-prefixed title (forbidden by
  §9.11). Re-scan, Compare, CSV, SARIF lifted from body cards into the
  toolbar band. Tabs Vulnerabilities | Secrets | Misconfigs (counts on
  the tab labels). SBOM dropdown stays in the body summary section
  pending a primitive enhancement for dropdown-attached toolbar actions.
* ScanComparisonSheet: crumb Security > Scans > Compare, name Diff,
  meta with the +added/-removed delta.
* SecurityHistoryView: the inner sheet only. Crumb Security > Scan
  history. Compare (paid + 2 selected) and Refresh in toolbar.
* ScheduledOperationsView run-history sheet (lines ~847-935 only):
  crumb Schedules > {taskName} > Runs, Download CSV in toolbar, footer
  surfaces next-run timestamp.

Hook refactor:
* useOverlayState replaces three separate state vars (alertSheetOpen,
  alertSheetStack, autoHealStackName) with one stackMonitor object
  carrying { stackName, tab }. New helpers openAlertSheet(stackName),
  openAutoHeal(stackName), closeStackMonitor(). Tests rewritten and
  pass (11/11).
* useSidebarContextMenu and ShellOverlays updated for the new API. The
  three other call sites (useStackMenuItems, useStackKeyboardShortcuts)
  already use openAlertSheet/openAutoHeal and need no change.
This commit is contained in:
Anso
2026-05-06 23:19:16 -04:00
committed by GitHub
parent 4d9617a5c6
commit 3ec0a45ff0
11 changed files with 1307 additions and 1507 deletions
@@ -5,7 +5,6 @@ import { PolicyBlockDialog } from '../stack/PolicyBlockDialog';
import { DeleteStackDialog } from './DeleteStackDialog';
import { UnsavedChangesDialog } from './UnsavedChangesDialog';
import { StackAlertSheet } from '../StackAlertSheet';
import { StackAutoHealSheet } from '@/components/StackAutoHealSheet';
import { GitSourcePanel } from '../stack/GitSourcePanel';
import { LogViewer } from '../LogViewer';
import { VulnerabilityScanSheet } from '../VulnerabilityScanSheet';
@@ -54,9 +53,8 @@ export function ShellOverlays({
pendingUnsavedLoad,
bashModalOpen, selectedContainer,
logViewerOpen, logContainer,
alertSheetOpen, closeAlertSheet, alertSheetStack,
stackMonitor, closeStackMonitor,
policyBlock, setPolicyBlock, policyBypassing,
autoHealStackName, setAutoHealStackName,
stackMisconfigScanId, setStackMisconfigScanId,
diffPreview, setDiffPreview, diffPreviewConfirming, setDiffPreviewConfirming,
} = overlayState;
@@ -96,11 +94,12 @@ export function ShellOverlays({
/>
)}
{/* Stack Alert Sheet */}
{/* Stack monitor (alerts + auto-heal as tabs) */}
<StackAlertSheet
isOpen={alertSheetOpen}
onClose={closeAlertSheet}
stackName={alertSheetStack}
open={stackMonitor !== null}
onOpenChange={(open) => { if (!open) closeStackMonitor(); }}
stackName={stackMonitor?.stackName ?? ''}
initialTab={stackMonitor?.tab ?? 'alerts'}
/>
{/* Pre-deploy policy block */}
@@ -114,13 +113,6 @@ export function ShellOverlays({
onBypass={stackActions.bypassPolicyAndDeploy}
/>
{/* Stack Auto-Heal Sheet */}
<StackAutoHealSheet
stackName={autoHealStackName ?? ''}
open={autoHealStackName !== null}
onOpenChange={(open) => { if (!open) setAutoHealStackName(null); }}
/>
{/* Git Source Panel */}
{stackName && (
<GitSourcePanel
@@ -14,9 +14,7 @@ describe('useOverlayState', () => {
expect(result.current.selectedContainer).toBeNull();
expect(result.current.logViewerOpen).toBe(false);
expect(result.current.logContainer).toBeNull();
expect(result.current.alertSheetOpen).toBe(false);
expect(result.current.alertSheetStack).toBe('');
expect(result.current.autoHealStackName).toBeNull();
expect(result.current.stackMonitor).toBeNull();
expect(result.current.policyBlock).toBeNull();
expect(result.current.policyBypassing).toBe(false);
expect(result.current.stackMisconfigScanId).toBeNull();
@@ -69,32 +67,29 @@ describe('useOverlayState', () => {
expect(result.current.logContainer).toBeNull();
});
it('openAlertSheet sets sheet state', () => {
it('openAlertSheet opens stack monitor on the alerts tab', () => {
const { result } = renderHook(() => useOverlayState());
act(() => result.current.openAlertSheet('web-stack'));
expect(result.current.alertSheetOpen).toBe(true);
expect(result.current.alertSheetStack).toBe('web-stack');
expect(result.current.stackMonitor).toEqual({ stackName: 'web-stack', tab: 'alerts' });
});
it('openAlertSheet with autoHeal sets autoHealStackName', () => {
it('openAutoHeal opens stack monitor on the auto-heal tab', () => {
const { result } = renderHook(() => useOverlayState());
act(() => result.current.openAlertSheet('web-stack', 'web-stack'));
expect(result.current.alertSheetOpen).toBe(true);
expect(result.current.alertSheetStack).toBe('web-stack');
expect(result.current.autoHealStackName).toBe('web-stack');
act(() => result.current.openAutoHeal('web-stack'));
expect(result.current.stackMonitor).toEqual({ stackName: 'web-stack', tab: 'auto-heal' });
});
it('openAlertSheet without autoHeal leaves autoHealStackName null', () => {
it('openAutoHeal after openAlertSheet switches to the auto-heal tab', () => {
const { result } = renderHook(() => useOverlayState());
act(() => result.current.openAlertSheet('web-stack'));
expect(result.current.alertSheetOpen).toBe(true);
expect(result.current.autoHealStackName).toBeNull();
act(() => result.current.openAutoHeal('web-stack'));
expect(result.current.stackMonitor).toEqual({ stackName: 'web-stack', tab: 'auto-heal' });
});
it('closeAlertSheet sets alertSheetOpen to false', () => {
it('closeStackMonitor clears the stack monitor state', () => {
const { result } = renderHook(() => useOverlayState());
act(() => result.current.openAlertSheet('web-stack'));
act(() => result.current.closeAlertSheet());
expect(result.current.alertSheetOpen).toBe(false);
act(() => result.current.closeStackMonitor());
expect(result.current.stackMonitor).toBeNull();
});
});
@@ -66,15 +66,14 @@ export function useOverlayState() {
return () => window.removeEventListener(SENCHO_OPEN_LOGS_EVENT, handler);
}, [openLogViewer]); // openLogViewer is stable (useCallback with empty deps)
const [alertSheetOpen, setAlertSheetOpen] = useState(false);
const [alertSheetStack, setAlertSheetStack] = useState('');
const [autoHealStackName, setAutoHealStackName] = useState<string | null>(null);
const openAlertSheet = useCallback((stackName: string, autoHeal?: string | null) => {
setAlertSheetStack(stackName);
setAutoHealStackName(autoHeal ?? null);
setAlertSheetOpen(true);
const [stackMonitor, setStackMonitor] = useState<{ stackName: string; tab: 'alerts' | 'auto-heal' } | null>(null);
const openAlertSheet = useCallback((stackName: string) => {
setStackMonitor({ stackName, tab: 'alerts' });
}, []);
const closeAlertSheet = useCallback(() => setAlertSheetOpen(false), []);
const openAutoHeal = useCallback((stackName: string) => {
setStackMonitor({ stackName, tab: 'auto-heal' });
}, []);
const closeStackMonitor = useCallback(() => setStackMonitor(null), []);
const [policyBlock, setPolicyBlock] = useState<PolicyBlock | null>(null);
const [policyBypassing, setPolicyBypassing] = useState(false);
@@ -91,8 +90,7 @@ export function useOverlayState() {
pendingUnsavedNode, setPendingUnsavedNode,
bashModalOpen, selectedContainer, openBashModal, closeBashModal,
logViewerOpen, logContainer, openLogViewer, closeLogViewer,
alertSheetOpen, alertSheetStack, autoHealStackName, openAlertSheet, closeAlertSheet,
setAutoHealStackName,
stackMonitor, openAlertSheet, openAutoHeal, closeStackMonitor,
policyBlock, setPolicyBlock, policyBypassing, setPolicyBypassing,
stackMisconfigScanId, setStackMisconfigScanId,
diffPreview, setDiffPreview, diffPreviewConfirming, setDiffPreviewConfirming,
@@ -49,7 +49,7 @@ export function useSidebarContextMenu({
menuVisibility: stackActions.getStackMenuVisibility(file),
autoUpdateEnabled: stackListState.autoUpdateSettings[sName] ?? true,
openAlertSheet: () => overlayState.openAlertSheet(file),
openAutoHeal: () => overlayState.setAutoHealStackName(file),
openAutoHeal: () => overlayState.openAutoHeal(file),
checkUpdates: () => stackActions.checkUpdatesForStack(),
openStackApp: () => stackActions.openStackApp(file),
deploy: () => stackActions.executeStackActionByFile(file, 'deploy', 'deploy'),