From 793570f4a1805f39d79b138b69d0e6d2c765cd8c Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Thu, 7 May 2026 02:20:06 +0300 Subject: [PATCH] feat(stats): inline 30-min sparklines on container CPU + memory bars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Always-visible trend lines next to the existing bars on every running instance and static-site card so the user can spot a slow drift or recent spike at a glance, without expanding the full history chart. Implemented as a tiny pure-SVG component (no ECharts on hot list paths) — values are fetched once per 30s alongside the current snapshot via a 30m history query that the collector already serves. - Sparkline.svelte: pure-SVG polyline + optional area fill, normalises values to [0,100] and clamps out-of-range points - ContainerStats: parallel fetch of stats + 30m history, two new $derived arrays for CPU% and memory%, sparklines slotted between the bar and the numeric readout --- web/src/lib/components/ContainerStats.svelte | 39 +++++++++-- web/src/lib/components/Sparkline.svelte | 71 ++++++++++++++++++++ 2 files changed, 104 insertions(+), 6 deletions(-) create mode 100644 web/src/lib/components/Sparkline.svelte diff --git a/web/src/lib/components/ContainerStats.svelte b/web/src/lib/components/ContainerStats.svelte index 555cc07..f56ce9f 100644 --- a/web/src/lib/components/ContainerStats.svelte +++ b/web/src/lib/components/ContainerStats.svelte @@ -9,6 +9,7 @@ import { t } from '$lib/i18n'; import { statsInterval } from '$lib/stores/statsInterval'; import ResourceChart from './ResourceChart.svelte'; + import Sparkline from './Sparkline.svelte'; import type { EChartsOption } from 'echarts'; export type StatsSource = @@ -24,6 +25,11 @@ let stats = $state(null); let history = $state([]); + // Short rolling window for the always-visible sparklines. Kept separate + // from `history` (which is sized to `historyWindow` and only loaded on + // expand) so the inline trend doesn't change zoom every time the user + // switches the chart range. + let sparkSamples = $state([]); let error = $state(false); let expanded = $state(false); @@ -34,17 +40,17 @@ return api.fetchStaticSiteStats(source.siteId, signal); } - async function fetchHistory(signal: AbortSignal): Promise { + async function fetchHistory(signal: AbortSignal, win: string = historyWindow): Promise { if (source.kind === 'instance') { return api.fetchInstanceStatsHistory( source.projectId, source.stageId, source.instanceId, - historyWindow, + win, signal ); } - return api.fetchStaticSiteStatsHistory(source.siteId, historyWindow, signal); + return api.fetchStaticSiteStatsHistory(source.siteId, win, signal); } $effect(() => { @@ -54,8 +60,15 @@ controller.abort(); controller = new AbortController(); try { - const result = await fetchStats(controller.signal); + const [result, spark] = await Promise.all([ + fetchStats(controller.signal), + // Always pull a 30-minute rolling window for the inline + // sparkline. Cheap query and gives the user a trend hint + // without forcing them to expand the full history chart. + fetchHistory(controller.signal, '30m').catch(() => [] as ContainerStatsSample[]) + ]); stats = result; + sparkSamples = spark; error = false; if (expanded) { history = await fetchHistory(controller.signal); @@ -91,6 +104,14 @@ return 'bg-blue-500'; }); + // CPU samples normalize to absolute percent (already 0-100 from the + // collector). Memory samples need percent-of-limit so the sparkline + // matches the bar's denominator. + const cpuSpark = $derived(sparkSamples.map((s) => s.cpu_percent)); + const memSpark = $derived( + sparkSamples.map((s) => (s.memory_limit > 0 ? (s.memory_usage / s.memory_limit) * 100 : 0)) + ); + const historyOption = $derived({ animation: false, grid: { top: 8, right: 10, bottom: 24, left: 40 }, @@ -139,7 +160,7 @@ {#if stats}
- +
{$t('stats.cpu')}
@@ -148,11 +169,14 @@ style="width: {Math.min(stats.cpu_percent, 100)}%" >
+ {stats.cpu_percent.toFixed(1)}%
- +
{$t('stats.mem')}
@@ -161,6 +185,9 @@ style="width: {Math.min(stats.memory_percent, 100)}%" >
+ {formatBytes(stats.memory_usage)} / {formatBytes(stats.memory_limit)} diff --git a/web/src/lib/components/Sparkline.svelte b/web/src/lib/components/Sparkline.svelte new file mode 100644 index 0000000..c8471df --- /dev/null +++ b/web/src/lib/components/Sparkline.svelte @@ -0,0 +1,71 @@ + + + + + {#if values.length > 0} + {#if fill !== 'transparent'} + + {/if} + + {/if} +