mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-27 18:57:09 +00:00
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:
@@ -49,15 +49,37 @@ systemMaintenanceRouter.post('/prune/orphans', async (req: Request, res: Respons
|
||||
systemMaintenanceRouter.post('/prune/system', async (req: Request, res: Response) => {
|
||||
if (!requireAdmin(req, res)) return;
|
||||
try {
|
||||
const { target, scope } = req.body as { target: string; scope?: string };
|
||||
const { target, scope, dryRun } = req.body as { target: string; scope?: string; dryRun?: boolean };
|
||||
if (!['containers', 'images', 'networks', 'volumes'].includes(target)) {
|
||||
return res.status(400).json({ error: 'Invalid prune target' });
|
||||
}
|
||||
|
||||
const pruneScope = scope === 'managed' ? 'managed' : 'all';
|
||||
console.log(`[Resources] System prune: ${target} (scope: ${pruneScope})`);
|
||||
const isDryRun = dryRun === true;
|
||||
const dockerController = DockerController.getInstance(req.nodeId);
|
||||
|
||||
if (isDryRun) {
|
||||
// Rehearse the destructive path: same scope resolution, same Docker
|
||||
// enumeration, no remove calls. Containers have no managed estimate
|
||||
// helper because pruneManagedOnly does not handle them.
|
||||
const knownStacks = await FileSystemService.getInstance(req.nodeId).getStacks();
|
||||
let estimate: { reclaimableBytes: number };
|
||||
if (pruneScope === 'managed' && target !== 'containers') {
|
||||
estimate = await dockerController.estimateManagedReclaim(
|
||||
target as 'images' | 'volumes' | 'networks',
|
||||
knownStacks,
|
||||
);
|
||||
} else {
|
||||
estimate = await dockerController.estimateSystemReclaim(
|
||||
target as 'containers' | 'images' | 'networks' | 'volumes',
|
||||
knownStacks,
|
||||
);
|
||||
}
|
||||
res.json({ message: 'Dry run', success: true, dryRun: true, reclaimedBytes: estimate.reclaimableBytes });
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[Resources] System prune: ${target} (scope: ${pruneScope})`);
|
||||
let result: { success: boolean; reclaimedBytes: number };
|
||||
if (pruneScope === 'managed' && target !== 'containers') {
|
||||
const knownStacks = await FileSystemService.getInstance(req.nodeId).getStacks();
|
||||
@@ -80,6 +102,41 @@ systemMaintenanceRouter.post('/prune/system', async (req: Request, res: Response
|
||||
}
|
||||
});
|
||||
|
||||
// Non-destructive size estimate for a prune target/scope. The Fleet Actions
|
||||
// "Prune fleet-wide" card calls this on each remote node to populate its live
|
||||
// blast-radius readout before the operator confirms. Reuses the same Docker
|
||||
// enumeration as `/prune/system` so the estimate matches what the destructive
|
||||
// path would reclaim.
|
||||
systemMaintenanceRouter.post('/prune/estimate', async (req: Request, res: Response) => {
|
||||
if (!requireAdmin(req, res)) return;
|
||||
try {
|
||||
const { target, scope } = req.body as { target: string; scope?: string };
|
||||
if (!['containers', 'images', 'networks', 'volumes'].includes(target)) {
|
||||
return res.status(400).json({ error: 'Invalid prune target' });
|
||||
}
|
||||
const pruneScope = scope === 'managed' ? 'managed' : 'all';
|
||||
const dockerController = DockerController.getInstance(req.nodeId);
|
||||
const knownStacks = await FileSystemService.getInstance(req.nodeId).getStacks();
|
||||
|
||||
let result: { reclaimableBytes: number };
|
||||
if (pruneScope === 'managed' && target !== 'containers') {
|
||||
result = await dockerController.estimateManagedReclaim(
|
||||
target as 'images' | 'volumes' | 'networks',
|
||||
knownStacks,
|
||||
);
|
||||
} else {
|
||||
result = await dockerController.estimateSystemReclaim(
|
||||
target as 'containers' | 'images' | 'networks' | 'volumes',
|
||||
knownStacks,
|
||||
);
|
||||
}
|
||||
res.json({ reclaimableBytes: result.reclaimableBytes });
|
||||
} catch (error: unknown) {
|
||||
console.error('Prune estimate error:', error);
|
||||
res.status(500).json({ error: 'Failed to estimate reclaimable bytes' });
|
||||
}
|
||||
});
|
||||
|
||||
systemMaintenanceRouter.get('/docker-df', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const knownStacks = await FileSystemService.getInstance(req.nodeId).getStacks();
|
||||
|
||||
Reference in New Issue
Block a user