feat(fleet): redesign Fleet Action cards to the System Sheet recipe (#1137)

Lift <FleetActionCard> primitive from audit §20 / DESIGN §9.12. Migrate
the three v1 cards (stop-by-label, bulk-label-assign, prune-fleet-wide)
to consume it: cyan rail on every card, action class as a chip, blast
radius as a live readout in the toolbar, preview section replaces the
warning banner, footer carries reversibility plus freshness. Drop the
per-card tone rail, the icon prop, and cards/tone.ts.

Add /fleet/labels/match-preview and /fleet/prune/estimate for the live
blast readouts (chrome falls back to "preview unavailable" if either
404s). Add dryRun: true to the existing fleet-stop and fleet-prune
endpoints so the Dry run button rehearses the full code path (locks,
per-node fan-out, remote propagation) without firing the destructive
leaf call. Result flows into the existing ResultsList.

Extend <SheetSection> with optional meta. Add --action-transformative
semantic token.
This commit is contained in:
Anso
2026-05-21 11:45:52 -04:00
committed by GitHub
parent d0e140444a
commit 6f301e005a
13 changed files with 1471 additions and 423 deletions
+13 -6
View File
@@ -172,12 +172,13 @@ labelsRouter.post('/:id/action', authMiddleware, async (req: Request, res: Respo
try {
const id = parseIntParam(req, res, 'id', 'label ID');
if (id === null) return;
const { action } = req.body;
const { action, dryRun } = req.body;
const validActions = ['deploy', 'stop', 'restart'];
if (!action || !validActions.includes(action)) {
res.status(400).json({ error: `action must be one of: ${validActions.join(', ')}` });
return;
}
const isDryRun = dryRun === true;
const nodeId = req.nodeId ?? 0;
@@ -200,11 +201,17 @@ labelsRouter.post('/:id/action', authMiddleware, async (req: Request, res: Respo
const fsStackNames = new Set(fsStacks);
const validStacks = stackNames.filter(name => fsStackNames.has(name));
if (isDebugEnabled()) console.debug('[Labels:debug] Bulk action start:', { id, action, nodeId, totalLabeled: stackNames.length, validStacks: validStacks.length });
if (isDebugEnabled()) console.debug('[Labels:debug] Bulk action start:', { id, action, nodeId, totalLabeled: stackNames.length, validStacks: validStacks.length, dryRun: isDryRun });
const results: { stackName: string; success: boolean; error?: string }[] = [];
const results: { stackName: string; success: boolean; error?: string; dryRun?: boolean }[] = [];
for (const stackName of validStacks) {
if (isDryRun) {
// Rehearse the action under the same lock + label resolution + fs
// intersection. Skip the destructive leaf call.
results.push({ stackName, success: true, dryRun: true });
continue;
}
try {
if (action === 'deploy') {
const gate = await enforcePolicyPreDeploy(
@@ -235,10 +242,10 @@ labelsRouter.post('/:id/action', authMiddleware, async (req: Request, res: Respo
const succeeded = results.filter(r => r.success).length;
const failed = results.length - succeeded;
console.log(`[Labels] Bulk ${sanitizeForLog(action)} on label ${id}: ${validStacks.length} stacks (${succeeded} succeeded, ${failed} failed)`);
if (isDebugEnabled()) console.debug('[Labels:debug] Bulk action complete:', { id, action, total: results.length, succeeded, failed });
console.log(`[Labels] Bulk ${sanitizeForLog(action)}${isDryRun ? ' (dry run)' : ''} on label ${id}: ${validStacks.length} stacks (${succeeded} succeeded, ${failed} failed)`);
if (isDebugEnabled()) console.debug('[Labels:debug] Bulk action complete:', { id, action, total: results.length, succeeded, failed, dryRun: isDryRun });
if (succeeded > 0) {
if (succeeded > 0 && !isDryRun) {
invalidateNodeCaches(req.nodeId);
}
res.json({ results });