Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions dashboard/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"test:e2e:ui": "playwright test --ui"
},
"dependencies": {
"@fontsource/inter": "^5.2.8",
"@fontsource/jetbrains-mono": "^5.2.8",
"@sveltejs/kit": "^2.15.0",
"svelte": "^5.0.0",
"uplot": "^1.6.31"
Expand Down
76 changes: 51 additions & 25 deletions dashboard/src/app.css
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
@import '@fontsource/inter/400.css';
@import '@fontsource/inter/500.css';
@import '@fontsource/inter/600.css';
@import '@fontsource/inter/700.css';
@import '@fontsource/jetbrains-mono/500.css';
@import '@fontsource/jetbrains-mono/600.css';
@import '@fontsource/jetbrains-mono/700.css';

/* Default tokens — Catppuccin Mocha (dark). Overridden at runtime by theme.svelte.ts. */
:root {
--color-bg: #ffffff;
--color-text: #1a1a1a;
--color-muted: #475569; /* slate-600, 6.8:1 on white — WCAG AA safe */
--color-border: #e5e7eb;
--color-accent: #3b82f6;
--color-accent-faint: rgba(59, 130, 246, 0.08);
--color-link: #1d4ed8; /* blue-700, 6.1:1 on white — WCAG AA safe for text links */
--font-mono: 'SFMono-Regular', 'Consolas', 'Liberation Mono', monospace;
}
--bg: #181825;
--card: #1e1e2e;
--elevated: #313244;
--text: #cdd6f4;
--muted: #a6adc8;
--border: #313244;
--primary: #89b4fa;
--primary-fg: #11111b;
--link: #89b4fa;
--ring: rgba(137,180,250,0.3);
--accent-fill: rgba(137,180,250,0.14);
--chart-0: #89b4fa;
--chart-1: #cba6f7;
--chart-2: #94e2d5;

@media (prefers-color-scheme: dark) {
:root {
--color-bg: #0f172a;
--color-text: #f1f5f9;
--color-muted: #94a3b8; /* 6.6:1 on #0f172a — WCAG AA safe */
--color-border: #1e293b;
--color-accent: #60a5fa;
--color-accent-faint: rgba(96, 165, 250, 0.12);
--color-link: #93c5fd; /* blue-300, 9:1 on #0f172a — WCAG AA safe for text links */
}
--radius: 0.625rem;
--font-mono: 'JetBrains Mono', 'SFMono-Regular', 'Consolas', monospace;
--font-body: 'Inter', system-ui, sans-serif;
}

*,
Expand All @@ -28,20 +35,39 @@
}

html {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-family: var(--font-body);
font-size: 16px;
background: var(--color-bg);
color: var(--color-text);
background: var(--bg);
color: var(--text);
/* Smooth theme transitions */
transition: background 0.25s, color 0.25s;
}


body {
margin: 0;
}

/* Transition color/border/shadow properties for all elements */
*,
*::before,
*::after {
transition-property: background-color, border-color, color, box-shadow, opacity;
transition-duration: 0.25s;
transition-timing-function: ease;
}

/* But keep interactive hover transitions snappy */
button,
a,
input {
transition-duration: 0.15s;
}

code {
font-family: var(--font-mono);
font-size: 0.875em;
background: var(--color-border);
padding: 0.1em 0.3em;
border-radius: 0.25rem;
background: var(--elevated);
padding: 2px 7px;
border-radius: 5px;
}
39 changes: 28 additions & 11 deletions dashboard/src/lib/components/SparkLine.svelte
Original file line number Diff line number Diff line change
@@ -1,38 +1,55 @@
<script lang="ts">
import { onMount } from 'svelte';
import uPlot from 'uplot';
import 'uplot/dist/uPlot.min.css';

let { timestamps, values }: { timestamps: number[]; values: number[] } = $props();
let {
timestamps,
values,
color,
}: { timestamps: number[]; values: number[]; color: string } = $props();

let container: HTMLDivElement;
let chart: uPlot | null = null;

function hexAlpha(hex: string, alpha: number): string {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return `rgba(${r},${g},${b},${alpha})`;
}

onMount(() => {
if (timestamps.length < 2) return;
function buildChart(c: string) {
chart?.destroy();
if (timestamps.length < 2 || !container) { chart = null; return; }

const chart = new uPlot(
chart = new uPlot(
{
width: 160,
height: 48,
width: 150,
height: 44,
padding: [4, 0, 4, 0],
axes: [],
axes: [{ show: false, size: 0 }, { show: false, size: 0 }],
scales: { x: { time: true } },
legend: { show: false },
cursor: { show: false },
select: { show: false },
series: [
{},
{
stroke: 'var(--color-accent)',
fill: 'var(--color-accent-faint)',
stroke: c,
fill: hexAlpha(c, 0.2),
width: 1.5,
points: { show: false },
},
],
},
[timestamps, values],
container,
);
}

return () => chart.destroy();
$effect(() => {
buildChart(color);
return () => { chart?.destroy(); chart = null; };
});
</script>

Expand Down
140 changes: 125 additions & 15 deletions dashboard/src/lib/components/TrendChart.svelte
Original file line number Diff line number Diff line change
@@ -1,52 +1,157 @@
<script lang="ts">
import { onMount } from 'svelte';
import uPlot from 'uplot';
import 'uplot/dist/uPlot.min.css';
import type { MetricPoint } from '../types';

let { data, metric, unit }: { data: MetricPoint[]; metric: string; unit: string } = $props();
let {
data,
metric,
unit,
color,
borderColor,
mutedColor,
textColor,
}: {
data: MetricPoint[];
metric: string;
unit: string;
color: string;
borderColor: string;
mutedColor: string;
textColor: string;
} = $props();

let container: HTMLDivElement;
let chart: uPlot | null = null;

function hexAlpha(hex: string, alpha: number): string {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return `rgba(${r},${g},${b},${alpha})`;
}

function gradientFill(c: string) {
return (u: uPlot) => {
const grad = u.ctx.createLinearGradient(0, u.bbox.top, 0, u.bbox.top + u.bbox.height);
grad.addColorStop(0, hexAlpha(c, 0.28));
grad.addColorStop(1, hexAlpha(c, 0.02));
return grad;
};
}

onMount(() => {
if (data.length === 0) return;
// Draw a dot + vertical dashed guide at the last data point
function lastPointPlugin(c: string, bc: string) {
return {
hooks: {
draw: [
(u: uPlot) => {
const { ctx, data: d } = u;
const lastIdx = (d[0] as number[]).length - 1;
if (lastIdx < 0) return;

const cx = Math.round(u.valToPos((d[0] as number[])[lastIdx], 'x', true));
const cy = Math.round(u.valToPos((d[1] as (number | null)[])[lastIdx]!, 'y', true));

// Dashed vertical guide
ctx.save();
ctx.setLineDash([4, 4]);
ctx.strokeStyle = hexAlpha(bc, 0.6);
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(cx, u.bbox.top);
ctx.lineTo(cx, u.bbox.top + u.bbox.height);
ctx.stroke();
ctx.restore();

// Dot
ctx.save();
ctx.beginPath();
ctx.arc(cx, cy, 4, 0, Math.PI * 2);
ctx.fillStyle = c;
ctx.fill();
ctx.strokeStyle = hexAlpha(c, 0.3);
ctx.lineWidth = 3;
ctx.stroke();
ctx.restore();
},
],
},
};
}

function buildChart() {
chart?.destroy();
if (data.length === 0 || !container) { chart = null; return; }

const timestamps = data.map((p) => new Date(p.recorded_at).getTime() / 1000);
const values = data.map((p) => p.value);
const yLabel = unit === '%' ? `${metric} (%)` : metric;

const chart = new uPlot(
chart = new uPlot(
{
width: container.clientWidth,
height: 280,
height: 220,
padding: [12, 12, 0, 0],
scales: { x: { time: true } },
axes: [
{ label: 'Date', size: 36 },
{ label: yLabel, size: 48 },
{
stroke: mutedColor,
ticks: { stroke: borderColor, width: 1, size: 4 },
border: { show: false },
grid: { show: false },
font: `12px 'JetBrains Mono', monospace`,
},
{
stroke: mutedColor,
ticks: { show: false },
border: { show: false },
grid: { stroke: borderColor, width: 1, dash: [] },
size: 52,
font: `12px 'JetBrains Mono', monospace`,
values: (_u: uPlot, vals: number[]) =>
vals.map((v) => (v !== null ? `${v.toFixed(1)}${unit}` : '')),
},
],
series: [
{},
{
label: metric,
stroke: 'var(--color-accent)',
fill: 'var(--color-accent-faint)',
stroke: color,
fill: gradientFill(color),
width: 2,
points: { show: false },
},
],
legend: { show: true },
cursor: { show: true },
legend: { show: false },
cursor: { show: false },
plugins: [lastPointPlugin(color, borderColor)],
},
[timestamps, values],
container,
);
}

$effect(() => {
// React to any of these changing; also depends on container via buildChart()
void data;
void color;
void borderColor;
void mutedColor;
void textColor;
buildChart();

if (!container) return;
const observer = new ResizeObserver(() => {
chart.setSize({ width: container.clientWidth, height: 280 });
if (chart && container) {
chart.setSize({ width: container.clientWidth, height: 220 });
}
});
observer.observe(container);

return () => {
chart.destroy();
observer.disconnect();
chart?.destroy();
chart = null;
};
});
</script>
Expand All @@ -57,4 +162,9 @@
.trend-chart {
width: 100%;
}

/* Override uPlot default white background */
.trend-chart :global(.u-wrap) {
background: transparent;
}
</style>
Loading
Loading