mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-13 12:17:34 +00:00
feat(dashboard): status masthead, unified gauges, stack health sparklines (#676)
* feat(dashboard): status masthead, unified gauges, stack health sparklines Rework the home dashboard around a single cyan-railed status masthead that carries the health state word, node meta, and reasons inline. The resource block collapses into one strip with a CPU hero sparkline, memory and disk gauge bars, and a network tile whose sparkline is built from per-container byte-counter deltas. Stack health becomes an 8-column mono table with row tinting, an uptime column sourced from the oldest running container's creation time, and a per-stack 10-minute CPU sparkline. Historical charts pick up a cyan gradient and an amber peak marker. A shared Sparkline primitive backs all of the above. * docs(dashboard): refresh screenshot for phase B layout * fix(dashboard): anchor sparkline bucketing to latest metric timestamp The cpuHistory, netHistory, and cpuPeakLabel memos called Date.now() inside useMemo, which violates react-hooks/purity: the rule fires because re-renders can produce different bucket boundaries from the same inputs. Derive a historyEndAt anchor from the newest metric sample in the polled series and thread it through to ResourceGauges.
This commit is contained in:
@@ -4376,7 +4376,7 @@ app.get('/api/stacks/statuses', async (req: Request, res: Response) => {
|
||||
const dockerController = DockerController.getInstance(req.nodeId);
|
||||
const bulkInfo = await dockerController.getBulkStackStatuses(stackNames);
|
||||
// Map back to filenames to match frontend expectations
|
||||
const data: Record<string, { status: 'running' | 'exited' | 'unknown'; mainPort?: number }> = {};
|
||||
const data: Record<string, { status: 'running' | 'exited' | 'unknown'; mainPort?: number; runningSince?: number }> = {};
|
||||
for (const stack of stacks) {
|
||||
const name = stack.replace(/\.(yml|yaml)$/, '');
|
||||
data[stack] = bulkInfo[name] ?? { status: 'unknown' };
|
||||
|
||||
@@ -29,6 +29,8 @@ const IGNORE_PORTS = [1900, 53, 22];
|
||||
export interface BulkStackInfo {
|
||||
status: 'running' | 'exited' | 'unknown';
|
||||
mainPort?: number;
|
||||
/** Unix seconds of the oldest running container (approximates stack uptime). */
|
||||
runningSince?: number;
|
||||
}
|
||||
|
||||
export interface ClassifiedImage {
|
||||
@@ -660,6 +662,18 @@ class DockerController {
|
||||
if (container.State === 'running') {
|
||||
result[stackDir].status = 'running';
|
||||
|
||||
// Track the oldest running container's creation time as a proxy for
|
||||
// stack uptime. Docker's listContainers payload exposes Created (unix
|
||||
// seconds) but not StartedAt; for compose stacks the gap is small
|
||||
// enough to treat as uptime without paying for a per-container inspect.
|
||||
const created = typeof container.Created === 'number' ? container.Created : undefined;
|
||||
if (created !== undefined) {
|
||||
const existing = result[stackDir].runningSince;
|
||||
if (existing === undefined || created < existing) {
|
||||
result[stackDir].runningSince = created;
|
||||
}
|
||||
}
|
||||
|
||||
// Detect main web port (first running container with a matchable port wins)
|
||||
if (result[stackDir].mainPort === undefined && Array.isArray(container.Ports) && container.Ports.length > 0) {
|
||||
const ports = container.Ports as { PrivatePort?: number; PublicPort?: number }[];
|
||||
|
||||
Reference in New Issue
Block a user