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:
Anso
2026-04-18 01:36:12 -04:00
committed by GitHub
parent 7ec189dc35
commit 748ba46669
12 changed files with 914 additions and 359 deletions
+1 -1
View File
@@ -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' };
+14
View File
@@ -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 }[];