mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-11 03:06:54 +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:
@@ -386,6 +386,69 @@ class DockerController {
|
||||
return { success: true, reclaimedBytes };
|
||||
}
|
||||
|
||||
/**
|
||||
* Non-destructive sibling of `pruneManagedOnly` used by the dry-run path and
|
||||
* the `/api/system/prune/estimate` route. Walks the same filter rules but
|
||||
* does not call `.remove()`. Kept structurally parallel to the destructive
|
||||
* method so the two stay in lockstep when the enumeration logic changes.
|
||||
*/
|
||||
public async estimateManagedReclaim(
|
||||
target: 'images' | 'volumes' | 'networks',
|
||||
knownStackNames: string[],
|
||||
): Promise<{ reclaimableBytes: number }> {
|
||||
const knownSet = new Set(knownStackNames);
|
||||
const projectToStack = await DockerController.resolveProjectNameMap(knownStackNames);
|
||||
let reclaimableBytes = 0;
|
||||
|
||||
if (target === 'volumes') {
|
||||
const rawVolumeData = await this.docker.listVolumes();
|
||||
const rawVolumes: any[] = (this.validateApiData<any>(rawVolumeData)).Volumes || [];
|
||||
const prunable = rawVolumes.filter((v: any) => {
|
||||
return !!DockerController.resolveProjectLabel(v.Labels?.['com.docker.compose.project'], knownSet, projectToStack)
|
||||
&& (v.UsageData?.RefCount ?? 1) === 0;
|
||||
});
|
||||
for (const vol of prunable) reclaimableBytes += vol.UsageData?.Size ?? 0;
|
||||
} else if (target === 'networks') {
|
||||
// Networks have no on-disk size; the dry-run still reports 0 so the
|
||||
// shape matches the destructive path.
|
||||
} else if (target === 'images') {
|
||||
const allContainers = await this.docker.listContainers({ all: true });
|
||||
const resolvedBase = path.resolve(COMPOSE_DIR);
|
||||
const absDirToStack = DockerController.buildAbsDirMap(knownStackNames);
|
||||
const unmanagedImageIds = new Set<string>();
|
||||
for (const c of allContainers as any[]) {
|
||||
const stack = DockerController.resolveContainerStack(
|
||||
c.Labels, projectToStack, knownSet, absDirToStack, resolvedBase,
|
||||
);
|
||||
if (!stack) unmanagedImageIds.add(c.ImageID);
|
||||
}
|
||||
const rawImages = await this.docker.listImages({ all: false });
|
||||
const prunable = (rawImages as any[]).filter((img: any) =>
|
||||
img.Containers === 0 && !unmanagedImageIds.has(img.Id),
|
||||
);
|
||||
for (const img of prunable) reclaimableBytes += img.Size ?? 0;
|
||||
}
|
||||
|
||||
return { reclaimableBytes };
|
||||
}
|
||||
|
||||
/**
|
||||
* Non-destructive estimate for the `all` (system) prune scope. Reuses the
|
||||
* same disk-usage source as the resources page so the readout matches what
|
||||
* the operator already sees there.
|
||||
*/
|
||||
public async estimateSystemReclaim(
|
||||
target: 'containers' | 'images' | 'networks' | 'volumes',
|
||||
knownStackNames: string[],
|
||||
): Promise<{ reclaimableBytes: number }> {
|
||||
const df = await this.getDiskUsageClassified(knownStackNames);
|
||||
if (target === 'images') return { reclaimableBytes: df.reclaimableImages };
|
||||
if (target === 'containers') return { reclaimableBytes: df.reclaimableContainers };
|
||||
if (target === 'volumes') return { reclaimableBytes: df.reclaimableVolumes };
|
||||
// Networks have no on-disk size.
|
||||
return { reclaimableBytes: 0 };
|
||||
}
|
||||
|
||||
public async getDiskUsageClassified(knownStackNames: string[]): Promise<{
|
||||
reclaimableImages: number;
|
||||
reclaimableContainers: number;
|
||||
|
||||
Reference in New Issue
Block a user