Skip to content

Live dashboard

On mount, data-init opens a GET stream that never closes on its own: the endpoint emits one patch-signals event per tick with an id. If the connection drops, the client reconnects with Last-Event-ID and the server resumes the tick counter from there. The bars animate towards each new value, thresholds switch classes, and a rolling history is maintained on the client by data-on-signal-patch.

Live
edge-cluster-01 live · tick ·
CPU
Memory
Requests / s
Error rate
CPU, last 30 ticks
<div class="ex dash" data-signals="{ metrics: { cpu: 0, mem: 0, rps: 0, errors: 0 }, tick: 0, updatedAt: 0, history: [] }"
data-init="@get('/api/metrics', { reconnect: true })"
data-on-signal-patch:metrics.cpu="$history.push($metrics.cpu); if ($history.length > 30) $history.shift()">
<header class="bar">
<div><b>edge-cluster-01</b><small class="muted"><span class="pulse"></span> live · tick <span class="num" data-text="$tick"></span> · <span data-text="$updatedAt ? @intl('datetime', new Date($updatedAt), { timeStyle: 'medium' }) : '–'"></span></small></div>
<span class="chip" data-class="{ good: $metrics.errors <= 2, warn: $metrics.errors > 2 && $metrics.errors <= 5, bad: $metrics.errors > 5 }" data-text="$metrics.errors > 5 ? 'degraded' : $metrics.errors > 2 ? 'watch' : 'healthy'"></span>
<button class="btn ghost" type="button" data-on:click="@get('/api/metrics', { reconnect: true })">Reconnect</button>
</header>
<div class="tiles">
<div class="tile card" data-class="{ warn: $metrics.cpu > 60, bad: $metrics.cpu > 85 }">
<span class="label">CPU</span><b class="num" data-text="$metrics.cpu + '%'"></b>
<div class="track"><i data-animate:width__duration.700ms="$metrics.cpu + '%'"></i></div>
</div>
<div class="tile card" data-class:warn="$metrics.mem > 80">
<span class="label">Memory</span><b class="num" data-text="$metrics.mem + '%'"></b>
<div class="track"><i data-animate:width__duration.700ms="$metrics.mem + '%'"></i></div>
</div>
<div class="tile card">
<span class="label">Requests / s</span><b class="num" data-text="@intl('number', $metrics.rps)"></b>
<div class="track"><i data-animate:width__duration.700ms="@fit($metrics.rps, 0, 900, 0, 100, true) + '%'"></i></div>
</div>
<div class="tile card" data-class="{ warn: $metrics.errors > 2, bad: $metrics.errors > 5 }">
<span class="label">Error rate</span><b class="num" data-text="@intl('number', $metrics.errors / 100, { style: 'percent', maximumFractionDigits: 1 })"></b>
<div class="track"><i data-animate:width__duration.700ms="@fit($metrics.errors, 0, 8, 0, 100, true) + '%'"></i></div>
</div>
</div>
<div class="card spark">
<div class="spark-head"><span class="label">CPU, last 30 ticks</span><span class="muted num" data-text="$history.length ? 'min ' + Math.min(...$history) + '% · max ' + Math.max(...$history) + '%' : 'collecting…'"></span></div>
<svg viewBox="0 0 300 60" preserveAspectRatio="none" aria-hidden="true">
<defs><linearGradient id="spark-fill" x1="0" x2="0" y1="0" y2="1"><stop offset="0" stop-color="var(--ex-accent)" stop-opacity=".45"/><stop offset="1" stop-color="var(--ex-accent)" stop-opacity="0"/></linearGradient></defs>
<polygon fill="url(#spark-fill)" data-attr:points="'0,60 ' + $history.map((v, i) => (i * 300 / 29).toFixed(1) + ',' + (60 - v * .58).toFixed(1)).join(' ') + ' ' + ((($history.length - 1) * 300 / 29) || 0).toFixed(1) + ',60'"></polygon>
<polyline fill="none" stroke="var(--ex-accent)" stroke-width="2" stroke-linejoin="round" data-attr:points="$history.map((v, i) => (i * 300 / 29).toFixed(1) + ',' + (60 - v * .58).toFixed(1)).join(' ')"></polyline>
</svg>
</div>
</div>
<style is:global>
.dash { padding: 1rem; display: grid; gap: .8rem; }
.dash .bar { display: flex; align-items: center; gap: .8rem; }
.dash .bar b { display: block; font-family: var(--sl-font-mono); }
.dash .bar small { display: flex; align-items: center; gap: .4rem; font-size: .75rem; }
.dash .bar .btn { margin-left: auto; }
.dash .tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr)); gap: .8rem; }
.dash .tile { padding: .8rem .9rem; display: grid; gap: .3rem; }
.dash .label { font-size: .7rem; letter-spacing: .08em; text-transform: uppercase; color: var(--ex-muted); }
.dash .tile b { font-size: 1.7rem; line-height: 1.1; letter-spacing: -.02em; }
.dash .track { height: .45rem; border-radius: 999px; background: var(--ex-surface-2); overflow: hidden; }
.dash .track i { display: block; height: 100%; width: 0; border-radius: 999px; background: linear-gradient(90deg, var(--ex-accent), #7c5cff); }
.dash .tile.warn .track i { background: linear-gradient(90deg, var(--ex-warm), #ff8a3d); }
.dash .tile.bad .track i { background: linear-gradient(90deg, var(--ex-bad), #ff6b8b); }
.dash .tile.bad b { color: var(--ex-bad); }
.dash .spark { padding: .7rem .9rem .4rem; }
.dash .spark-head { display: flex; justify-content: space-between; font-size: .75rem; margin-bottom: .3rem; }
.dash svg { width: 100%; height: 3.6rem; display: block; }
</style>
  • data-init="@get('/api/metrics', { reconnect: true })" for a stream that outlives any single request
  • id: on every event and Last-Event-ID on reconnect (see the endpoint’s first line)
  • data-animate:width__duration.700ms for the gauges, @fit to map ranges onto percentages
  • data-class with an object of thresholds, @intl for numbers, percentages and times
  • data-on-signal-patch:metrics.cpu keeping a bounded history array with push and shift
  • the stream ends when the client disconnects (stream.closed) or after a bounded number of ticks
import { sseStream } from '@sigmx/astro/server';
import type { APIRoute } from 'astro';
export const prerender = false;
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
const drift = (v: number, min: number, max: number, step: number) =>
Math.min(max, Math.max(min, v + (Math.random() - 0.5) * step));
export const GET: APIRoute = ({ request }) => {
const lastId = Number(request.headers.get('last-event-id') ?? 0);
let cpu = 35,
mem = 62,
rps = 240,
errors = 0.4;
return sseStream(async (s) => {
for (let tick = lastId + 1; tick < lastId + 400 && !s.closed; tick++) {
cpu = drift(cpu, 5, 100, 30);
mem = drift(mem, 30, 95, 6);
rps = drift(rps, 50, 900, 120);
errors = drift(errors, 0, 8, 1.2);
s.patchSignals(
{
metrics: {
cpu: Math.round(cpu),
mem: Math.round(mem),
rps: Math.round(rps),
errors: Math.round(errors * 10) / 10,
},
tick,
updatedAt: Date.now(),
},
{ id: String(tick) },
);
await sleep(800);
}
});
};

Browsers throttle timers and animation frames in background tabs; the stream keeps flowing and the gauges catch up when you return.