mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-09 10:21:03 +00:00
feat(stack-view): identity header with health state and action hierarchy (#688)
* feat(stack-view): identity header with health state and action hierarchy Redesign the stack view header around three questions: what is this, is it healthy, what does it do. Surface Docker healthcheck state, primary image tag, and image digest; group actions by frequency. - Backend: extend /api/stacks/:name/containers with healthStatus (healthy / unhealthy / starting / none), Image, and ImageID by inspecting each container in parallel. - Frontend: replace flat CardHeader with breadcrumb, italic serif title, colored state pill with pulse, and a mono image/digest line with a one-click copy button for the full digest. - Frontend: action hierarchy - primary cyan Restart/Start, outline Stop and Update, and an overflow menu for Rollback, Scan config, and Delete. - Docs: new Stack header section and updated controlling-a-running-stack tables showing primary/secondary/overflow grouping. * test(e2e): open overflow menu to reach stack Delete action Destructive actions now live under the stack toolbar overflow menu rather than as a flat top-level button, so the delete flow must click More actions before selecting the Delete menu item.
This commit is contained in:
@@ -801,7 +801,7 @@ class DockerController {
|
||||
// Map to frontend's expected interface
|
||||
// Note: docker compose ps returns Name (singular), but frontend expects Names (array)
|
||||
// Dockerode returns Names with leading slash, so we add it for compatibility
|
||||
return containers.map((c) => {
|
||||
const mapped = containers.map((c) => {
|
||||
let Ports: { PrivatePort: number, PublicPort: number }[] = [];
|
||||
if (c.Publishers && Array.isArray(c.Publishers)) {
|
||||
Ports = c.Publishers
|
||||
@@ -817,11 +817,12 @@ class DockerController {
|
||||
Ports
|
||||
};
|
||||
});
|
||||
return await this.enrichContainers(mapped);
|
||||
}
|
||||
|
||||
// SMART FALLBACK: Trigger when docker compose ps returns empty
|
||||
// This handles legacy containers with incorrect project labels
|
||||
return await this.smartFallback(stackName, stackDir);
|
||||
return await this.enrichContainers(await this.smartFallback(stackName, stackDir));
|
||||
|
||||
} catch (error) {
|
||||
// If command fails (e.g., stack not deployed, invalid YAML, missing env_file)
|
||||
@@ -829,10 +830,30 @@ class DockerController {
|
||||
console.error(`Docker Compose Error for ${stackName}:`, execError.stderr || execError.message);
|
||||
|
||||
// Try smart fallback even on error
|
||||
return await this.smartFallback(stackName, stackDir);
|
||||
return await this.enrichContainers(await this.smartFallback(stackName, stackDir));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspect each container to attach healthcheck status, image tag, and image digest.
|
||||
* Each mapper catches its own errors, so Promise.all never rejects (allSettled adds ceremony with no behavior change).
|
||||
*/
|
||||
private async enrichContainers<T extends { Id?: string }>(list: T[]): Promise<Array<T & { healthStatus: 'healthy' | 'unhealthy' | 'starting' | 'none'; Image?: string; ImageID?: string }>> {
|
||||
return Promise.all(list.map(async (c) => {
|
||||
const base = { ...c, healthStatus: 'none' as const };
|
||||
if (!c.Id) return base;
|
||||
try {
|
||||
const info = await this.docker.getContainer(c.Id).inspect();
|
||||
const health = info.State?.Health?.Status;
|
||||
const healthStatus: 'healthy' | 'unhealthy' | 'starting' | 'none' =
|
||||
health === 'healthy' || health === 'unhealthy' || health === 'starting' ? health : 'none';
|
||||
return { ...c, healthStatus, Image: info.Config?.Image, ImageID: info.Image };
|
||||
} catch {
|
||||
return base;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Smart Fallback: Find legacy containers by parsing compose YAML definitions.
|
||||
* This handles containers that were deployed with incorrect project labels
|
||||
|
||||
Reference in New Issue
Block a user