feat(dashboard): add live observability overview #23

Merged
vince merged 2 commits from codex/issue-1-observability-dashboard into main 2026-06-19 03:57:29 +02:00
33 changed files with 1601 additions and 150 deletions

View file

@ -10,6 +10,7 @@
"ajv": "^8.20.0", "ajv": "^8.20.0",
"ajv-formats": "^3.0.1", "ajv-formats": "^3.0.1",
"drizzle-orm": "^0.45.2", "drizzle-orm": "^0.45.2",
"uplot": "^1.6.32",
}, },
"devDependencies": { "devDependencies": {
"@axe-core/playwright": "^4.11.3", "@axe-core/playwright": "^4.11.3",
@ -735,6 +736,8 @@
"until-async": ["until-async@3.0.2", "", {}, "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw=="], "until-async": ["until-async@3.0.2", "", {}, "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw=="],
"uplot": ["uplot@1.6.32", "", {}, "sha512-KIMVnG68zvu5XXUbC4LQEPnhwOxBuLyW1AHtpm6IKTXImkbLgkMy+jabjLgSLMasNuGGzQm/ep3tOkyTxpiQIw=="],
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
"vite": ["vite@8.0.16", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", "rolldown": "1.0.3", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw=="], "vite": ["vite@8.0.16", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", "rolldown": "1.0.3", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw=="],

View file

@ -22,7 +22,8 @@
"@sinclair/typebox": "^0.34.49", "@sinclair/typebox": "^0.34.49",
"ajv": "^8.20.0", "ajv": "^8.20.0",
"ajv-formats": "^3.0.1", "ajv-formats": "^3.0.1",
"drizzle-orm": "^0.45.2" "drizzle-orm": "^0.45.2",
"uplot": "^1.6.32"
}, },
"devDependencies": { "devDependencies": {
"@axe-core/playwright": "^4.11.3", "@axe-core/playwright": "^4.11.3",

View file

@ -18,7 +18,7 @@ export default defineConfig({
screenshot: "only-on-failure", screenshot: "only-on-failure",
}, },
webServer: { webServer: {
command: `bun run build && DATABASE_URL=${databaseUrl} HOST=127.0.0.1 PORT=${port} bun build/index.js`, command: `DISABLE_LIVE_DATASOURCES=1 bun run build && DISABLE_LIVE_DATASOURCES=1 DATABASE_URL=${databaseUrl} HOST=127.0.0.1 PORT=${port} bun build/index.js`,
url: baseURL, url: baseURL,
reuseExistingServer: false, reuseExistingServer: false,
timeout: 120_000, timeout: 120_000,

View file

@ -128,9 +128,11 @@ const verifiedSeedIconIds = new Set([
"mdi:pulse", "mdi:pulse",
"mdi:robot-outline", "mdi:robot-outline",
"mdi:router-network", "mdi:router-network",
"mdi:text-box-search",
"mdi:thermometer", "mdi:thermometer",
"mdi:web", "mdi:web",
"mdi:weather-sunny", "mdi:weather-sunny",
"simple-icons:adguard",
"simple-icons:adminer", "simple-icons:adminer",
"simple-icons:amazonwebservices", "simple-icons:amazonwebservices",
"simple-icons:cockpit", "simple-icons:cockpit",

View file

@ -36,11 +36,35 @@ interface ServiceSeed {
detail?: string; detail?: string;
} }
const REAL_FILESYSTEM_FILTER =
'fstype!~"tmpfs|overlay|squashfs|nsfs|tracefs|autofs|proc|sysfs|cgroup2|devtmpfs|securityfs|debugfs|pstore|bpf|configfs|selinuxfs|mqueue|hugetlbfs|fusectl|ramfs"';
const USER_MOUNT_FILTER =
'mountpoint=~"^/home($|/)|^/srv($|/)|^/data($|/)|^/mnt($|/)|^/media($|/)",mountpoint!~"^/(boot|boot/efi|efi|var|usr|opt|run|dev|proc|sys)($|/)"';
const SYSTEM_MOUNT_FILTER =
'mountpoint=~"^/$|^/boot($|/)|^/boot/efi$|^/var($|/)|^/usr($|/)|^/opt($|/)",mountpoint!~"^/home($|/)|^/srv($|/)|^/data($|/)|^/mnt($|/)|^/media($|/)"';
const diskUsedQuery = (mountFilter: string) => {
const selector = `job="node",${REAL_FILESYSTEM_FILTER},${mountFilter}`;
return `topk(1, max by (host, mountpoint) (100 * (1 - node_filesystem_avail_bytes{${selector}} / node_filesystem_size_bytes{${selector}})))`;
};
const ramQuery = (host: string) =>
`100 * (1 - node_memory_MemAvailable_bytes{job="node",host="${host}"} / node_memory_MemTotal_bytes{job="node",host="${host}"})`;
const gpuQuery = (name: string, metric: string) =>
`${metric}{job="node",name=~".*${name}.*"}`;
const gpuVramQuery = (name: string) =>
`100 * nvidia_gpu_memory_used_bytes{job="node",name=~".*${name}.*"} / nvidia_gpu_memory_total_bytes{job="node",name=~".*${name}.*"}`;
const hostCpuQuery =
'topk(1, 100 * (1 - avg by (host) (rate(node_cpu_seconds_total{job="node",mode="idle"}[5m]))))';
const topCpuQuery =
'topk(1, 100 * rate(podman_container_cpu_seconds_total{job=~"podman-.*"}[5m]) * on(host,id) group_left(name) podman_container_info{job=~"podman-.*"})';
const topRamQuery =
'topk(1, podman_container_mem_usage_bytes{job=~"podman-.*"} * on(host,id) group_left(name) podman_container_info{job=~"podman-.*"})';
export const dimensionLabDashboardFixture: DashboardDocument = { export const dimensionLabDashboardFixture: DashboardDocument = {
schemaVersion: DASHBOARD_SCHEMA_VERSION, schemaVersion: DASHBOARD_SCHEMA_VERSION,
metadata: { metadata: {
title: "System Overview", title: "System Overview",
subtitle: "Dimension Lab home infra, GPU, and automation surface", subtitle: "Capacity, noise & top consumers",
description: description:
"Initial Dimension Lab dashboard seed data. Runtime values are fallback values until datasource adapters are enabled.", "Initial Dimension Lab dashboard seed data. Runtime values are fallback values until datasource adapters are enabled.",
timezone: "Europe/Amsterdam", timezone: "Europe/Amsterdam",
@ -81,7 +105,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = {
severity: "ok", severity: "ok",
thresholds: percentThresholds(), thresholds: percentThresholds(),
datasource: prometheus( datasource: prometheus(
'100 * (1 - node_memory_MemAvailable_bytes{instance="linux-infra"} / node_memory_MemTotal_bytes{instance="linux-infra"})', ramQuery("linux-infra"),
), ),
sparkline: [18, 18, 19, 19, 18, 19], sparkline: [18, 18, 19, 19, 18, 19],
}), }),
@ -95,7 +119,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = {
severity: "ok", severity: "ok",
thresholds: percentThresholds(), thresholds: percentThresholds(),
datasource: prometheus( datasource: prometheus(
'100 * (1 - node_memory_MemAvailable_bytes{instance="linux-gpu"} / node_memory_MemTotal_bytes{instance="linux-gpu"})', ramQuery("linux"),
), ),
sparkline: [16, 16, 17, 17, 17, 17], sparkline: [16, 16, 17, 17, 17, 17],
}), }),
@ -109,7 +133,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = {
severity: "ok", severity: "ok",
thresholds: percentThresholds(), thresholds: percentThresholds(),
datasource: prometheus( datasource: prometheus(
'100 * (1 - node_memory_MemAvailable_bytes{instance="network-core"} / node_memory_MemTotal_bytes{instance="network-core"})', ramQuery("network-core"),
), ),
sparkline: [5, 5, 5, 6, 5, 5], sparkline: [5, 5, 5, 6, 5, 5],
}), }),
@ -123,7 +147,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = {
severity: "ok", severity: "ok",
thresholds: percentThresholds(80, 92), thresholds: percentThresholds(80, 92),
datasource: prometheus( datasource: prometheus(
'100 * (1 - node_filesystem_avail_bytes{mountpoint="/home"} / node_filesystem_size_bytes{mountpoint="/home"})', diskUsedQuery(USER_MOUNT_FILTER),
), ),
sparkline: [27, 27, 28, 29, 29, 29], sparkline: [27, 27, 28, 29, 29, 29],
}), }),
@ -137,7 +161,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = {
severity: "ok", severity: "ok",
thresholds: percentThresholds(80, 92), thresholds: percentThresholds(80, 92),
datasource: prometheus( datasource: prometheus(
'100 * (1 - node_filesystem_avail_bytes{mountpoint="/boot"} / node_filesystem_size_bytes{mountpoint="/boot"})', diskUsedQuery(SYSTEM_MOUNT_FILTER),
), ),
sparkline: [48, 48, 49, 49, 49, 49], sparkline: [48, 48, 49, 49, 49, 49],
}), }),
@ -151,7 +175,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = {
severity: "ok", severity: "ok",
thresholds: percentThresholds(75, 90), thresholds: percentThresholds(75, 90),
datasource: prometheus( datasource: prometheus(
'100 - (avg by(instance) (rate(node_cpu_seconds_total{instance="linux-infra",mode="idle"}[5m])) * 100)', hostCpuQuery,
), ),
sparkline: [2, 3, 3, 4, 3, 3], sparkline: [2, 3, 3, 4, 3, 3],
}), }),
@ -165,9 +189,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = {
detail: "fallback - top container CPU pending label mapping", detail: "fallback - top container CPU pending label mapping",
severity: "ok", severity: "ok",
thresholds: percentThresholds(70, 90), thresholds: percentThresholds(70, 90),
datasource: placeholder( datasource: prometheus(topCpuQuery),
"fallback top-container CPU query pending container label normalization",
),
sparkline: [6.1, 6.4, 7.0, 7.5, 7.2, 7.9], sparkline: [6.1, 6.4, 7.0, 7.5, 7.2, 7.9],
}), }),
metric({ metric({
@ -180,9 +202,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = {
detail: "fallback - top container RAM pending label mapping", detail: "fallback - top container RAM pending label mapping",
severity: "danger", severity: "danger",
thresholds: { warning: 3 * 1024 ** 3, danger: 5 * 1024 ** 3 }, thresholds: { warning: 3 * 1024 ** 3, danger: 5 * 1024 ** 3 },
datasource: placeholder( datasource: prometheus(topRamQuery),
"fallback top-container memory query pending container label normalization",
),
sparkline: [ sparkline: [
3.1 * 1024 ** 3, 3.1 * 1024 ** 3,
3.5 * 1024 ** 3, 3.5 * 1024 ** 3,
@ -201,7 +221,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = {
detail: "fallback - RTX 3060 GPU utilization", detail: "fallback - RTX 3060 GPU utilization",
severity: "ok", severity: "ok",
thresholds: percentThresholds(85, 95), thresholds: percentThresholds(85, 95),
datasource: prometheus('DCGM_FI_DEV_GPU_UTIL{gpu="rtx-3060"}'), datasource: prometheus(gpuQuery("3060", "nvidia_gpu_utilization_percent")),
sparkline: [0, 0, 0, 0, 0, 0], sparkline: [0, 0, 0, 0, 0, 0],
}), }),
metric({ metric({
@ -214,7 +234,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = {
severity: "ok", severity: "ok",
thresholds: percentThresholds(80, 92), thresholds: percentThresholds(80, 92),
datasource: prometheus( datasource: prometheus(
'100 * DCGM_FI_DEV_FB_USED{gpu="rtx-3060"} / (DCGM_FI_DEV_FB_USED{gpu="rtx-3060"} + DCGM_FI_DEV_FB_FREE{gpu="rtx-3060"})', gpuVramQuery("3060"),
), ),
sparkline: [0, 0, 0, 0, 0, 0], sparkline: [0, 0, 0, 0, 0, 0],
}), }),
@ -227,7 +247,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = {
detail: "fallback - RTX 3060 temperature", detail: "fallback - RTX 3060 temperature",
severity: "ok", severity: "ok",
thresholds: { warning: 75, danger: 85 }, thresholds: { warning: 75, danger: 85 },
datasource: prometheus('DCGM_FI_DEV_GPU_TEMP{gpu="rtx-3060"}'), datasource: prometheus(gpuQuery("3060", "nvidia_gpu_temperature_celsius")),
sparkline: [52, 53, 54, 54, 53, 54], sparkline: [52, 53, 54, 54, 53, 54],
}), }),
metric({ metric({
@ -239,7 +259,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = {
detail: "fallback - RTX 3060 fan duty", detail: "fallback - RTX 3060 fan duty",
severity: "ok", severity: "ok",
thresholds: percentThresholds(80, 95), thresholds: percentThresholds(80, 95),
datasource: prometheus('DCGM_FI_DEV_FAN_SPEED{gpu="rtx-3060"}'), datasource: prometheus(gpuQuery("3060", "nvidia_gpu_fan_speed_percent")),
sparkline: [0, 0, 0, 0, 0, 0], sparkline: [0, 0, 0, 0, 0, 0],
}), }),
metric({ metric({
@ -251,7 +271,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = {
detail: "fallback - RTX 3090 GPU utilization", detail: "fallback - RTX 3090 GPU utilization",
severity: "ok", severity: "ok",
thresholds: percentThresholds(85, 95), thresholds: percentThresholds(85, 95),
datasource: prometheus('DCGM_FI_DEV_GPU_UTIL{gpu="rtx-3090"}'), datasource: prometheus(gpuQuery("3090", "nvidia_gpu_utilization_percent")),
sparkline: [0, 0, 0, 0, 0, 0], sparkline: [0, 0, 0, 0, 0, 0],
}), }),
metric({ metric({
@ -264,7 +284,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = {
severity: "warning", severity: "warning",
thresholds: percentThresholds(70, 90), thresholds: percentThresholds(70, 90),
datasource: prometheus( datasource: prometheus(
'100 * DCGM_FI_DEV_FB_USED{gpu="rtx-3090"} / (DCGM_FI_DEV_FB_USED{gpu="rtx-3090"} + DCGM_FI_DEV_FB_FREE{gpu="rtx-3090"})', gpuVramQuery("3090"),
), ),
sparkline: [68, 70, 72, 74, 73, 74], sparkline: [68, 70, 72, 74, 73, 74],
}), }),
@ -277,7 +297,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = {
detail: "fallback - RTX 3090 temperature", detail: "fallback - RTX 3090 temperature",
severity: "ok", severity: "ok",
thresholds: { warning: 75, danger: 85 }, thresholds: { warning: 75, danger: 85 },
datasource: prometheus('DCGM_FI_DEV_GPU_TEMP{gpu="rtx-3090"}'), datasource: prometheus(gpuQuery("3090", "nvidia_gpu_temperature_celsius")),
sparkline: [49, 50, 50, 51, 50, 50], sparkline: [49, 50, 50, 51, 50, 50],
}), }),
metric({ metric({
@ -289,7 +309,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = {
detail: "fallback - RTX 3090 fan duty", detail: "fallback - RTX 3090 fan duty",
severity: "ok", severity: "ok",
thresholds: percentThresholds(80, 95), thresholds: percentThresholds(80, 95),
datasource: prometheus('DCGM_FI_DEV_FAN_SPEED{gpu="rtx-3090"}'), datasource: prometheus(gpuQuery("3090", "nvidia_gpu_fan_speed_percent")),
sparkline: [0, 0, 0, 0, 0, 0], sparkline: [0, 0, 0, 0, 0, 0],
}), }),
], ],
@ -301,7 +321,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = {
description: "Password manager", description: "Password manager",
icon: "simple-icons:vaultwarden", icon: "simple-icons:vaultwarden",
href: "https://vault.dimensionlab.net", href: "https://vault.dimensionlab.net",
datasource: httpStatus("https://vault.dimensionlab.net/alive"), datasource: uptimeMonitor(1),
}), }),
service({ service({
id: "forgejo", id: "forgejo",
@ -309,7 +329,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = {
description: "Git repositories", description: "Git repositories",
icon: "simple-icons:forgejo", icon: "simple-icons:forgejo",
href: "https://git.dimensionlab.net", href: "https://git.dimensionlab.net",
datasource: httpStatus("https://git.dimensionlab.net/api/healthz"), datasource: uptimeMonitor(2),
}), }),
service({ service({
id: "wiki", id: "wiki",
@ -317,7 +337,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = {
description: "Internal documentation", description: "Internal documentation",
icon: "simple-icons:wikidotjs", icon: "simple-icons:wikidotjs",
href: "https://wiki.dimensionlab.net", href: "https://wiki.dimensionlab.net",
datasource: httpStatus("https://wiki.dimensionlab.net"), datasource: uptimeMonitor(3),
}), }),
service({ service({
id: "aws-start", id: "aws-start",
@ -325,7 +345,23 @@ export const dimensionLabDashboardFixture: DashboardDocument = {
description: "AWS access portal", description: "AWS access portal",
icon: "simple-icons:amazonwebservices", icon: "simple-icons:amazonwebservices",
href: "https://dimensionlab.awsapps.com/start", href: "https://dimensionlab.awsapps.com/start",
datasource: httpStatus("https://dimensionlab.awsapps.com/start"), datasource: uptimeMonitor(21),
}),
service({
id: "adguard-primary",
label: "AdGuard Primary",
description: "DNS filtering and DHCP",
icon: "simple-icons:adguard",
href: "https://control.dimensionlab.net",
datasource: uptimeMonitor(14),
}),
service({
id: "adguard-secondary",
label: "AdGuard Secondary",
description: "Fallback DNS",
icon: "simple-icons:adguard",
href: "https://control-secondary.dimensionlab.net",
datasource: uptimeMonitor(18),
}), }),
]), ]),
group("monitoring", "Monitoring", [ group("monitoring", "Monitoring", [
@ -335,7 +371,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = {
description: "Capacity and noise dashboard", description: "Capacity and noise dashboard",
icon: "simple-icons:grafana", icon: "simple-icons:grafana",
href: "https://grafana.dimensionlab.net", href: "https://grafana.dimensionlab.net",
datasource: httpStatus("https://grafana.dimensionlab.net/api/health"), datasource: uptimeMonitor(11),
}), }),
service({ service({
id: "uptime-kuma", id: "uptime-kuma",
@ -343,7 +379,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = {
description: "Service uptime checks", description: "Service uptime checks",
icon: "simple-icons:uptimekuma", icon: "simple-icons:uptimekuma",
href: "https://uptime.dimensionlab.net", href: "https://uptime.dimensionlab.net",
datasource: httpStatus("https://uptime.dimensionlab.net/status/dimensionlab"), datasource: uptimeMonitor(10),
}), }),
service({ service({
id: "prometheus", id: "prometheus",
@ -351,7 +387,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = {
description: "Metrics database", description: "Metrics database",
icon: "simple-icons:prometheus", icon: "simple-icons:prometheus",
href: "https://prometheus.dimensionlab.net", href: "https://prometheus.dimensionlab.net",
datasource: httpStatus("https://prometheus.dimensionlab.net/-/ready"), datasource: uptimeMonitor(12),
}), }),
service({ service({
id: "backrest", id: "backrest",
@ -359,7 +395,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = {
description: "Restic backup manager", description: "Restic backup manager",
icon: "mdi:backup-restore", icon: "mdi:backup-restore",
href: "https://backups.dimensionlab.net", href: "https://backups.dimensionlab.net",
datasource: httpStatus("https://backups.dimensionlab.net"), datasource: uptimeMonitor(13),
}), }),
]), ]),
group("ai-automation", "AI & Automation", [ group("ai-automation", "AI & Automation", [
@ -369,7 +405,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = {
description: "Workflow automation", description: "Workflow automation",
icon: "simple-icons:n8n", icon: "simple-icons:n8n",
href: "https://workflows.dimensionlab.net", href: "https://workflows.dimensionlab.net",
datasource: httpStatus("https://workflows.dimensionlab.net/healthz"), datasource: uptimeMonitor(4),
}), }),
service({ service({
id: "open-webui", id: "open-webui",
@ -377,7 +413,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = {
description: "Chat and model interface", description: "Chat and model interface",
icon: "mdi:web", icon: "mdi:web",
href: "https://webui.dimensionlab.net", href: "https://webui.dimensionlab.net",
datasource: httpStatus("https://webui.dimensionlab.net/health"), datasource: uptimeMonitor(5),
}), }),
service({ service({
id: "comfyui", id: "comfyui",
@ -385,7 +421,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = {
description: "Image generation workflows", description: "Image generation workflows",
icon: "mdi:image-edit-outline", icon: "mdi:image-edit-outline",
href: "https://comfy.dimensionlab.net", href: "https://comfy.dimensionlab.net",
datasource: httpStatus("https://comfy.dimensionlab.net"), datasource: uptimeMonitor(6),
}), }),
service({ service({
id: "models", id: "models",
@ -393,7 +429,15 @@ export const dimensionLabDashboardFixture: DashboardDocument = {
description: "Local model management", description: "Local model management",
icon: "mdi:brain", icon: "mdi:brain",
href: "https://models.dimensionlab.net", href: "https://models.dimensionlab.net",
datasource: httpStatus("https://models.dimensionlab.net/api/tags"), datasource: uptimeMonitor(7),
}),
service({
id: "prompt-registry",
label: "Prompt Registry",
description: "Shared prompts, traces, evals",
icon: "mdi:text-box-search",
href: "https://prompts.dimensionlab.net",
datasource: uptimeMonitor(20),
}), }),
]), ]),
group("systems", "Systems", [ group("systems", "Systems", [
@ -403,7 +447,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = {
description: "PostgreSQL database browser", description: "PostgreSQL database browser",
icon: "simple-icons:adminer", icon: "simple-icons:adminer",
href: "https://db.dimensionlab.net", href: "https://db.dimensionlab.net",
datasource: httpStatus("https://db.dimensionlab.net"), datasource: uptimeMonitor(17),
}), }),
service({ service({
id: "assistant", id: "assistant",
@ -411,7 +455,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = {
description: "Personal AI agent gateway", description: "Personal AI agent gateway",
icon: "mdi:robot-outline", icon: "mdi:robot-outline",
href: "https://assistant.dimensionlab.net", href: "https://assistant.dimensionlab.net",
datasource: httpStatus("https://assistant.dimensionlab.net/health"), datasource: uptimeMonitor(8),
}), }),
service({ service({
id: "suna", id: "suna",
@ -419,7 +463,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = {
description: "AI command center", description: "AI command center",
icon: "mdi:account-hard-hat-outline", icon: "mdi:account-hard-hat-outline",
href: "https://suna.dimensionlab.net", href: "https://suna.dimensionlab.net",
datasource: httpStatus("https://suna.dimensionlab.net"), datasource: uptimeMonitor(9),
}), }),
service({ service({
id: "cockpit-infra", id: "cockpit-infra",
@ -427,7 +471,23 @@ export const dimensionLabDashboardFixture: DashboardDocument = {
description: "linux-infra server console", description: "linux-infra server console",
icon: "simple-icons:cockpit", icon: "simple-icons:cockpit",
href: "https://infra-cockpit.dimensionlab.net", href: "https://infra-cockpit.dimensionlab.net",
datasource: httpStatus("https://infra-cockpit.dimensionlab.net"), datasource: uptimeMonitor(15),
}),
service({
id: "cockpit-gpu",
label: "Cockpit GPU",
description: "Linux GPU server console",
icon: "simple-icons:cockpit",
href: "https://linux-cockpit.dimensionlab.net",
datasource: uptimeMonitor(16),
}),
service({
id: "cockpit-network-core",
label: "Cockpit Network Core",
description: "i3 NUC DNS/DHCP console",
icon: "simple-icons:cockpit",
href: "https://network-core.dimensionlab.net",
datasource: uptimeMonitor(19),
}), }),
]), ]),
group("runtime-health", "Runtime Health", [ group("runtime-health", "Runtime Health", [
@ -437,7 +497,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = {
description: "Public Git SSH relay", description: "Public Git SSH relay",
icon: "simple-icons:forgejo", icon: "simple-icons:forgejo",
href: "https://uptime.dimensionlab.net/status/dimensionlab", href: "https://uptime.dimensionlab.net/status/dimensionlab",
datasource: custom("tcp:git.dimensionlab.net:22"), datasource: uptimeMonitor(28),
detail: "fallback - uptime monitor pending", detail: "fallback - uptime monitor pending",
}), }),
service({ service({
@ -445,7 +505,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = {
label: "PostgreSQL", label: "PostgreSQL",
description: "Shared application database", description: "Shared application database",
icon: "simple-icons:postgresql", icon: "simple-icons:postgresql",
datasource: prometheus('pg_up{cluster="dimensionlab"}'), datasource: uptimeMonitor(22),
detail: "fallback - postgres exporter pending", detail: "fallback - postgres exporter pending",
}), }),
service({ service({
@ -453,7 +513,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = {
label: "Ollama API", label: "Ollama API",
description: "Local model API", description: "Local model API",
icon: "simple-icons:ollama", icon: "simple-icons:ollama",
datasource: httpStatus("http://linux-gpu:11434/api/tags"), datasource: uptimeMonitor(23),
detail: "fallback - internal health check pending", detail: "fallback - internal health check pending",
}), }),
service({ service({
@ -461,10 +521,34 @@ export const dimensionLabDashboardFixture: DashboardDocument = {
label: "Node Exporter", label: "Node Exporter",
description: "Host metrics exporter", description: "Host metrics exporter",
icon: "simple-icons:prometheus", icon: "simple-icons:prometheus",
datasource: prometheus('up{job="node-exporter"}'), datasource: uptimeMonitor(24),
detail: "fallback - exporter health from Uptime Kuma",
}),
service({
id: "podman-user-exporter",
label: "Podman User Exporter",
description: "Rootless container metrics",
icon: "simple-icons:prometheus",
datasource: uptimeMonitor(25),
detail: "fallback - exporter health from Uptime Kuma",
}),
service({
id: "podman-system-exporter",
label: "Podman System Exporter",
description: "System container metrics",
icon: "simple-icons:prometheus",
datasource: uptimeMonitor(26),
detail: "fallback - exporter health from Uptime Kuma",
}),
service({
id: "network-core-node-exporter",
label: "Network Core Node Exporter",
description: "i3 DNS/DHCP metrics",
icon: "simple-icons:prometheus",
datasource: uptimeMonitor(27),
detail: "fallback - exporter health from Prometheus", detail: "fallback - exporter health from Prometheus",
}), }),
]), ], "grid"),
], ],
statusStrips: [ statusStrips: [
{ {
@ -474,6 +558,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = {
{ id: "last-sync", label: "Last Sync", value: "fallback: 2 minutes ago", severity: "stale" }, { id: "last-sync", label: "Last Sync", value: "fallback: 2 minutes ago", severity: "stale" },
{ id: "uptime", label: "Uptime", value: "fallback: 28d 14h 32m", severity: "ok" }, { id: "uptime", label: "Uptime", value: "fallback: 28d 14h 32m", severity: "ok" },
{ id: "load-avg", label: "Load Avg", value: "fallback: 0.47 0.53 0.59", severity: "neutral" }, { id: "load-avg", label: "Load Avg", value: "fallback: 0.47 0.53 0.59", severity: "neutral" },
{ id: "auto-refresh", label: "Auto Refresh", value: "fallback: 15s", severity: "neutral" },
], ],
}, },
], ],
@ -550,12 +635,17 @@ function service(seed: ServiceSeed): ServiceEntry {
}; };
} }
function group(id: string, title: string, services: ServiceEntry[]): ServiceGroup { function group(
id: string,
title: string,
services: ServiceEntry[],
layout: ServiceGroup["layout"] = "list",
): ServiceGroup {
return { return {
id, id,
title, layout,
layout: "list",
services, services,
title,
}; };
} }
@ -579,17 +669,13 @@ function httpStatus(url: string): DatasourceReference {
}; };
} }
function custom(reference: string): DatasourceReference {
return {
type: "external",
adapter: "custom",
reference,
};
}
function placeholder(reason: string): DatasourceReference { function placeholder(reason: string): DatasourceReference {
return { return {
type: "placeholder", type: "placeholder",
reason, reason,
}; };
} }
function uptimeMonitor(id: number): DatasourceReference {
return httpStatus(`https://uptime.dimensionlab.net/_homepage-badge/${id}`);
}

View file

@ -47,6 +47,68 @@ describe("dashboard runtime loader", () => {
expect(store.listRevisions()).toHaveLength(1); expect(store.listRevisions()).toHaveLength(1);
}); });
test("refreshes an existing initial seed when the bundled seed changes", async () => {
const store = await createTestStore();
const oldSeed = olderDimensionLabSeed();
store.seedDashboardIfEmpty(oldSeed, {
actor: "initial-seed",
message: "load initial dashboard document",
});
const runtime = loadDashboardRuntime(store, {
refreshSeedDocument: true,
seedDocument: dimensionLabDashboardFixture,
seedIfEmpty: true,
});
expect(runtime.state).toBe("ready");
if (runtime.state !== "ready") throw new Error("expected ready dashboard");
expect(runtime.document.statusStrips[0]?.items.map((item) => item.id)).toContain(
"auto-refresh",
);
expect(runtime.document.serviceGroups.flatMap((group) => group.services).map((service) => service.id)).toContain(
"prompt-registry",
);
expect(store.listRevisions()).toHaveLength(2);
expect(store.getActiveDashboard()?.revision.actor).toBe("initial-seed");
});
test("does not refresh a dashboard after a user-authored revision", async () => {
const store = await createTestStore();
const oldSeed = olderDimensionLabSeed();
store.seedDashboardIfEmpty(oldSeed, {
actor: "initial-seed",
message: "load initial dashboard document",
});
store.commitDashboard(
{
...oldSeed,
metadata: {
...oldSeed.metadata,
title: "Custom Dashboard",
},
},
{
actor: "agent",
message: "customize dashboard",
},
);
const runtime = loadDashboardRuntime(store, {
refreshSeedDocument: true,
seedDocument: dimensionLabDashboardFixture,
seedIfEmpty: true,
});
expect(runtime.state).toBe("ready");
if (runtime.state !== "ready") throw new Error("expected ready dashboard");
expect(runtime.document.metadata.title).toBe("Custom Dashboard");
expect(runtime.document.statusStrips[0]?.items.map((item) => item.id)).not.toContain(
"auto-refresh",
);
expect(store.listRevisions()).toHaveLength(2);
});
test("returns empty state when no dashboard is active and seeding is disabled", async () => { test("returns empty state when no dashboard is active and seeding is disabled", async () => {
const store = await createTestStore(); const store = await createTestStore();
@ -69,3 +131,20 @@ async function createTestStore() {
return store; return store;
} }
function olderDimensionLabSeed() {
const document = structuredClone(dimensionLabDashboardFixture);
document.statusStrips = document.statusStrips.map((strip) => ({
...strip,
items: strip.items.filter((item) => item.id !== "auto-refresh"),
}));
document.serviceGroups = document.serviceGroups.map((group) =>
group.id === "ai-automation"
? {
...group,
services: group.services.filter((service) => service.id !== "prompt-registry"),
}
: group,
);
return document;
}

View file

@ -43,6 +43,7 @@ export interface DashboardRuntimeInvalid {
} }
export interface DashboardRuntimeOptions { export interface DashboardRuntimeOptions {
refreshSeedDocument?: boolean;
seedIfEmpty?: boolean; seedIfEmpty?: boolean;
seedDocument?: DashboardDocument; seedDocument?: DashboardDocument;
} }
@ -54,8 +55,20 @@ export function loadDashboardRuntime(
const dashboardStore = store || createDashboardStore(); const dashboardStore = store || createDashboardStore();
try { try {
const seedDocument = options.seedDocument || dimensionLabDashboardFixture;
const active = dashboardStore.getActiveDashboard(); const active = dashboardStore.getActiveDashboard();
if (active) { if (active) {
if (
options.refreshSeedDocument &&
shouldRefreshSeedDashboard(active, seedDocument)
) {
const refreshed = dashboardStore.commitDashboard(seedDocument, {
actor: "initial-seed",
message: "refresh bundled dashboard document",
});
return readyRuntimeState(refreshed.document, refreshed.id);
}
return readyRuntimeState(active.document, active.currentRevisionId); return readyRuntimeState(active.document, active.currentRevisionId);
} }
@ -69,7 +82,7 @@ export function loadDashboardRuntime(
} }
const seeded = dashboardStore.seedDashboardIfEmpty( const seeded = dashboardStore.seedDashboardIfEmpty(
options.seedDocument || dimensionLabDashboardFixture, seedDocument,
{ {
actor: "initial-seed", actor: "initial-seed",
message: "load initial dashboard document", message: "load initial dashboard document",
@ -113,3 +126,22 @@ function invalidRuntimeState(errors: string[]): DashboardRuntimeInvalid {
errors, errors,
}; };
} }
function shouldRefreshSeedDashboard(
active: { document: DashboardDocument; revision: { actor: string } },
seedDocument: DashboardDocument,
): boolean {
if (active.revision.actor !== "initial-seed") return false;
if (!isBundledDimensionLabSeed(active.document, seedDocument)) return false;
return JSON.stringify(active.document) !== JSON.stringify(seedDocument);
}
function isBundledDimensionLabSeed(
document: DashboardDocument,
seedDocument: DashboardDocument,
): boolean {
return (
document.metadata.title === seedDocument.metadata.title &&
document.metadata.description === seedDocument.metadata.description
);
}

View file

@ -0,0 +1,240 @@
import { describe, expect, test, vi } from "vitest";
import {
DASHBOARD_SCHEMA_VERSION,
type DashboardDocument,
} from "$lib/model";
import { resolveDashboardDatasources } from ".";
describe("dashboard datasource resolution", () => {
test("hydrates telemetry, service health, weather, and summary data from live adapters", async () => {
const fetch = vi.fn(async (input: RequestInfo | URL) => {
const url = String(input);
if (url.startsWith("https://prometheus.example/api/v1/query_range")) {
return jsonResponse({
status: "success",
data: {
result: [
{
metric: { host: "linux-infra" },
values: [
[1771430000, "10"],
[1771430060, "40"],
[1771430120, "30"],
],
},
{
metric: { host: "linux-gpu" },
values: [
[1771430000, "12"],
[1771430060, "24"],
[1771430120, "36"],
],
},
],
},
});
}
if (url.startsWith("https://prometheus.example/api/v1/query")) {
const query = new URL(url).searchParams.get("query") || "";
if (query.includes("node_boot_time_seconds")) {
return prometheusVector("90061");
}
if (query.includes("node_load15")) return prometheusVector("0.40");
if (query.includes("node_load5")) return prometheusVector("0.46");
if (query.includes("node_load1")) return prometheusVector("0.34");
return jsonResponse({
status: "success",
data: {
result: [
{
metric: { host: "linux-infra", mountpoint: "/home" },
value: [1771430400, "88"],
},
],
},
});
}
if (url.startsWith("https://api.open-meteo.com/v1/forecast")) {
return jsonResponse({
current: {
apparent_temperature: 20.9,
temperature_2m: 21.4,
weather_code: 0,
wind_speed_10m: 12,
},
});
}
if (url === "https://service.example/health") {
return jsonResponse({
status: "UP",
ping: 42,
});
}
throw new Error(`Unhandled test request: ${url}`);
});
const resolved = await resolveDashboardDatasources(testDashboard(), {
fetch,
prometheusBaseUrl: "https://prometheus.example",
});
expect(fetch).toHaveBeenCalledWith(
expect.stringContaining("/api/v1/query?"),
expect.objectContaining({ cache: "no-store" }),
);
expect(fetch).toHaveBeenCalledWith(
expect.stringContaining("/api/v1/query_range?"),
expect.objectContaining({ cache: "no-store" }),
);
const telemetry = resolved.telemetry[0];
expect(telemetry.value).toEqual({ kind: "percent", value: 88 });
expect(telemetry.severity).toBe("warning");
expect(telemetry.detail).toBe("linux-infra /home");
expect(telemetry.sparkline).toEqual([12, 40, 36]);
const service = resolved.serviceGroups[0].services[0];
expect(service.severity).toBe("ok");
expect(service.detail).toBe("42 ms");
const weather = resolved.modules?.find((module) => module.id === "weather-amsterdam");
expect(weather?.value).toBe("21.4 C");
expect(weather?.detail).toBe("Clear - feels 20.9 C - wind 12 km/h");
expect(weather?.severity).toBe("ok");
const summary = resolved.modules?.find((module) => module.id === "runtime-health-summary");
expect(summary?.value).toBe("all systems operational");
expect(summary?.detail).toBe("1 service ok");
expect(summary?.severity).toBe("ok");
expect(resolved.statusStrips[0].items).toEqual([
{ id: "system-status", label: "System Status", value: "All systems operational", severity: "ok" },
{ id: "last-sync", label: "Last Sync", value: "just now", severity: "ok" },
{ id: "uptime", label: "Uptime", value: "1d 1h 1m", severity: "ok" },
{ id: "load-avg", label: "Load Avg", value: "0.34 0.46 0.40", severity: "neutral" },
{ id: "auto-refresh", label: "Auto Refresh", value: "15s", severity: "neutral" },
]);
expect(resolved).not.toBe(testDocument);
expect(testDocument.telemetry[0].value.value).toBe(1);
});
});
const testDocument = testDashboard();
function testDashboard(): DashboardDocument {
return {
schemaVersion: DASHBOARD_SCHEMA_VERSION,
metadata: {
title: "System Overview",
refreshIntervalSeconds: 15,
},
layout: {
telemetry: ["cpu"],
serviceGroups: ["services"],
statusStrips: ["footer"],
modules: ["weather-amsterdam", "runtime-health-summary"],
},
telemetry: [
{
id: "cpu",
label: "CPU",
value: { kind: "percent", value: 1 },
detail: "fallback",
severity: "stale",
thresholds: { warning: 70, danger: 90 },
datasource: {
type: "external",
adapter: "prometheus",
reference: "fixture_cpu_query",
},
sparkline: [1],
},
],
serviceGroups: [
{
id: "services",
title: "Services",
services: [
{
id: "api",
label: "API",
description: "Example API",
severity: "stale",
detail: "fallback",
datasource: {
type: "external",
adapter: "http-status",
reference: "GET https://service.example/health",
},
},
],
},
],
statusStrips: [
{
id: "footer",
items: [
{ id: "system-status", label: "System Status", value: "fallback", severity: "ok" },
{ id: "last-sync", label: "Last Sync", value: "fallback", severity: "stale" },
{ id: "uptime", label: "Uptime", value: "fallback", severity: "ok" },
{ id: "load-avg", label: "Load Avg", value: "fallback", severity: "neutral" },
{ id: "auto-refresh", label: "Auto Refresh", value: "fallback", severity: "neutral" },
],
},
],
modules: [
{
id: "weather-amsterdam",
kind: "weather",
title: "Amsterdam",
value: "fallback",
detail: "fallback",
severity: "stale",
datasource: {
type: "external",
adapter: "weather",
reference: "open-meteo:latitude=52.3676&longitude=4.9041",
},
},
{
id: "runtime-health-summary",
kind: "summary",
title: "Runtime Health",
value: "fallback",
detail: "fallback",
severity: "stale",
datasource: {
type: "placeholder",
reason: "summary pending live health aggregation",
},
},
],
};
}
function jsonResponse(payload: unknown): Response {
return new Response(JSON.stringify(payload), {
headers: { "content-type": "application/json" },
});
}
function prometheusVector(value: string): Response {
return jsonResponse({
status: "success",
data: {
result: [
{
metric: { host: "linux-infra" },
value: [1771430400, value],
},
],
},
});
}

View file

@ -0,0 +1,675 @@
import type {
DashboardDocument,
DashboardModule,
MetricValue,
ServiceEntry,
ServiceGroup,
Severity,
StatusItem,
StatusStrip,
TelemetryCard,
} from "$lib/model";
export interface DatasourceResolutionOptions {
fetch?: DatasourceFetch;
prometheusBaseUrl?: string;
prometheusRangeSeconds?: number;
prometheusStepSeconds?: number;
requestTimeoutMs?: number;
}
export async function resolveDashboardDatasources(
document: DashboardDocument,
options: DatasourceResolutionOptions = {},
): Promise<DashboardDocument> {
const context = datasourceContext(options);
const telemetry = await Promise.all(
document.telemetry.map((card) => resolveTelemetryCard(card, context)),
);
const serviceGroups = await Promise.all(
document.serviceGroups.map((group) => resolveServiceGroup(group, context)),
);
const modules = await resolveModules(document.modules || [], serviceGroups, context);
const statusStrips = await resolveStatusStrips(
document.statusStrips,
document.metadata.refreshIntervalSeconds,
serviceGroups,
context,
);
return {
...structuredClone(document),
telemetry,
serviceGroups,
modules,
statusStrips,
};
}
interface DatasourceContext {
fetch: DatasourceFetch;
prometheusBaseUrl: string;
prometheusRangeSeconds: number;
prometheusStepSeconds: number;
requestTimeoutMs: number;
}
type DatasourceFetch = (input: string, init?: RequestInit) => Promise<Response>;
interface PrometheusVectorResult {
metric?: Record<string, string>;
value?: [number, string];
}
interface PrometheusMatrixResult {
metric?: Record<string, string>;
values?: Array<[number, string]>;
}
function datasourceContext(options: DatasourceResolutionOptions): DatasourceContext {
return {
fetch: options.fetch || globalThis.fetch,
prometheusBaseUrl:
options.prometheusBaseUrl ||
process.env.PROMETHEUS_BASE_URL ||
"https://prometheus.dimensionlab.net",
prometheusRangeSeconds: options.prometheusRangeSeconds || 60 * 60,
prometheusStepSeconds: options.prometheusStepSeconds || 120,
requestTimeoutMs: options.requestTimeoutMs || 2_500,
};
}
async function resolveTelemetryCard(
card: TelemetryCard,
context: DatasourceContext,
): Promise<TelemetryCard> {
if (card.datasource?.type !== "external" || card.datasource.adapter !== "prometheus") {
return structuredClone(card);
}
try {
const [instant, range] = await Promise.all([
prometheusQuery(card.datasource.reference, context),
prometheusRangeQuery(card.datasource.reference, context),
]);
const current = pickMaxVectorResult(instant);
const currentValue = Number(current?.value?.[1]);
const sparkline = matrixPoints(range);
if (!Number.isFinite(currentValue)) return markStale(card, "no telemetry data");
return {
...structuredClone(card),
value: metricValueWithLiveNumber(card.value, currentValue),
severity: severityForValue(currentValue, card.thresholds),
detail: prometheusMetricDetail(current?.metric || {}, card.detail),
sparkline: sparkline.length ? sparkline : card.sparkline,
};
} catch {
return markStale(card, card.detail || "telemetry unavailable");
}
}
async function resolveServiceGroup(
group: ServiceGroup,
context: DatasourceContext,
): Promise<ServiceGroup> {
return {
...structuredClone(group),
services: await Promise.all(
group.services.map((service) => resolveService(service, context)),
),
};
}
async function resolveService(
service: ServiceEntry,
context: DatasourceContext,
): Promise<ServiceEntry> {
const datasource = service.datasource;
if (datasource?.type !== "external") return structuredClone(service);
if (datasource.adapter === "http-status") {
return resolveHttpStatusService(service, datasource.reference, context);
}
if (datasource.adapter === "prometheus") {
return resolvePrometheusService(service, datasource.reference, context);
}
return structuredClone(service);
}
async function resolveHttpStatusService(
service: ServiceEntry,
reference: string,
context: DatasourceContext,
): Promise<ServiceEntry> {
const request = parseHttpStatusReference(reference);
if (!request) return structuredClone(service);
try {
const startedAt = Date.now();
const response = await fetchWithTimeout(
context.fetch,
request.url,
{
cache: "no-store",
method: request.method,
},
context.requestTimeoutMs,
);
const elapsedMs = Math.max(0, Math.round(Date.now() - startedAt));
const badge = await uptimeBadge(response);
if (badge) {
const ping = badge.ping;
const status = badge.status || "UNKNOWN";
return {
...structuredClone(service),
severity: uptimeBadgeSeverity(status),
detail: Number.isFinite(ping)
? `${Math.round(ping as number)} ms`
: status.toLowerCase(),
};
}
return {
...structuredClone(service),
severity: response.ok ? "ok" : response.status >= 500 ? "danger" : "warning",
detail: response.ok ? `${elapsedMs} ms` : `HTTP ${response.status}`,
};
} catch {
return {
...structuredClone(service),
severity: "unavailable",
detail: "unavailable",
};
}
}
async function resolvePrometheusService(
service: ServiceEntry,
reference: string,
context: DatasourceContext,
): Promise<ServiceEntry> {
try {
const result = pickMaxVectorResult(await prometheusQuery(reference, context));
const value = Number(result?.value?.[1]);
const ok = Number.isFinite(value) && value > 0;
return {
...structuredClone(service),
severity: ok ? "ok" : "unavailable",
detail: ok ? "up" : "down",
};
} catch {
return {
...structuredClone(service),
severity: "unavailable",
detail: "unavailable",
};
}
}
async function resolveModules(
modules: DashboardModule[],
serviceGroups: ServiceGroup[],
context: DatasourceContext,
): Promise<DashboardModule[]> {
const resolved = await Promise.all(
modules.map((module) => resolveModule(module, context)),
);
return resolved.map((module) =>
module.id === "runtime-health-summary"
? runtimeHealthSummary(module, serviceGroups)
: module,
);
}
async function resolveModule(
module: DashboardModule,
context: DatasourceContext,
): Promise<DashboardModule> {
if (module.datasource?.type !== "external" || module.datasource.adapter !== "weather") {
return structuredClone(module);
}
try {
const weather = await requestJson<OpenMeteoResponse>(
context.fetch,
openMeteoUrl(module.datasource.reference),
context.requestTimeoutMs,
);
const current = weather.current;
const temperature = Number(current?.temperature_2m);
if (!Number.isFinite(temperature)) {
return {
...structuredClone(module),
severity: "stale",
detail: "weather unavailable",
};
}
const apparent = Number(current?.apparent_temperature);
const wind = Number(current?.wind_speed_10m);
const condition = weatherCondition(Number(current?.weather_code));
return {
...structuredClone(module),
value: `${temperature.toFixed(1)} C`,
detail: [
condition,
Number.isFinite(apparent) ? `feels ${apparent.toFixed(1)} C` : "",
Number.isFinite(wind) ? `wind ${Math.round(wind)} km/h` : "",
].filter(Boolean).join(" - "),
severity: "ok",
};
} catch {
return {
...structuredClone(module),
severity: "stale",
detail: "weather unavailable",
};
}
}
function runtimeHealthSummary(
module: DashboardModule,
serviceGroups: ServiceGroup[],
): DashboardModule {
const services = serviceGroups.flatMap((group) => group.services);
const down = services.filter((service) =>
service.severity === "danger" || service.severity === "unavailable"
).length;
const warning = services.filter((service) => service.severity === "warning").length;
const ok = services.filter((service) => service.severity === "ok").length;
if (down > 0) {
return {
...structuredClone(module),
value: `${down} service${down === 1 ? "" : "s"} down`,
detail: `${warning} warning${warning === 1 ? "" : "s"} - ${ok} service${ok === 1 ? "" : "s"} ok`,
severity: "danger",
};
}
if (warning > 0) {
return {
...structuredClone(module),
value: `${warning} service${warning === 1 ? "" : "s"} warning`,
detail: `${ok} service${ok === 1 ? "" : "s"} ok`,
severity: "warning",
};
}
return {
...structuredClone(module),
value: "all systems operational",
detail: `${ok} service${ok === 1 ? "" : "s"} ok`,
severity: "ok",
};
}
async function resolveStatusStrips(
strips: StatusStrip[],
refreshIntervalSeconds: number | undefined,
serviceGroups: ServiceGroup[],
context: DatasourceContext,
): Promise<StatusStrip[]> {
const [uptime, loadAverage] = await Promise.all([
prometheusScalar(
'time() - node_boot_time_seconds{job="node",host="linux-infra"}',
context,
).catch(() => null),
prometheusLoadAverage(context).catch(() => null),
]);
const health = serviceHealthSummary(serviceGroups);
return strips.map((strip) => ({
...structuredClone(strip),
items: strip.items.map((item) =>
resolveStatusItem(item, {
health,
loadAverage,
refreshIntervalSeconds,
uptime,
}),
),
}));
}
function resolveStatusItem(
item: StatusItem,
values: {
health: { severity: Severity; value: string };
loadAverage: string | null;
refreshIntervalSeconds?: number;
uptime: number | null;
},
): StatusItem {
if (item.id === "system-status") {
return {
...structuredClone(item),
value: values.health.value,
severity: values.health.severity,
};
}
if (item.id === "last-sync") {
return {
...structuredClone(item),
value: "just now",
severity: "ok",
};
}
if (item.id === "uptime" && values.uptime !== null) {
return {
...structuredClone(item),
value: formatDuration(values.uptime),
severity: "ok",
};
}
if (item.id === "load-avg" && values.loadAverage) {
return {
...structuredClone(item),
value: values.loadAverage,
severity: "neutral",
};
}
if (item.id === "auto-refresh" && values.refreshIntervalSeconds) {
return {
...structuredClone(item),
value: `${values.refreshIntervalSeconds}s`,
severity: "neutral",
};
}
return structuredClone(item);
}
function serviceHealthSummary(serviceGroups: ServiceGroup[]): {
severity: Severity;
value: string;
} {
const services = serviceGroups.flatMap((group) => group.services);
const down = services.filter((service) =>
service.severity === "danger" || service.severity === "unavailable"
).length;
const warning = services.filter((service) => service.severity === "warning").length;
if (down > 0) {
return {
severity: "danger",
value: `${down} service${down === 1 ? "" : "s"} down`,
};
}
if (warning > 0) {
return {
severity: "warning",
value: `${warning} service${warning === 1 ? "" : "s"} warning`,
};
}
return {
severity: "ok",
value: "All systems operational",
};
}
async function prometheusQuery(
query: string,
context: DatasourceContext,
): Promise<PrometheusVectorResult[]> {
const url = prometheusApiUrl(context.prometheusBaseUrl, "/api/v1/query", {
query,
});
const payload = await requestJson<PrometheusResponse<PrometheusVectorResult>>(
context.fetch,
url,
context.requestTimeoutMs,
);
return payload.status === "success" ? payload.data.result || [] : [];
}
async function prometheusRangeQuery(
query: string,
context: DatasourceContext,
): Promise<PrometheusMatrixResult[]> {
const end = Math.floor(Date.now() / 1000);
const start = end - context.prometheusRangeSeconds;
const url = prometheusApiUrl(context.prometheusBaseUrl, "/api/v1/query_range", {
query,
start: String(start),
end: String(end),
step: String(context.prometheusStepSeconds),
});
const payload = await requestJson<PrometheusResponse<PrometheusMatrixResult>>(
context.fetch,
url,
context.requestTimeoutMs,
);
return payload.status === "success" ? payload.data.result || [] : [];
}
async function prometheusScalar(
query: string,
context: DatasourceContext,
): Promise<number | null> {
const result = pickMaxVectorResult(await prometheusQuery(query, context));
const value = Number(result?.value?.[1]);
return Number.isFinite(value) ? value : null;
}
async function prometheusLoadAverage(context: DatasourceContext): Promise<string | null> {
const [one, five, fifteen] = await Promise.all([
prometheusScalar('node_load1{job="node",host="linux-infra"}', context),
prometheusScalar('node_load5{job="node",host="linux-infra"}', context),
prometheusScalar('node_load15{job="node",host="linux-infra"}', context),
]);
if (one === null || five === null || fifteen === null) return null;
return [one, five, fifteen].map((value) => value.toFixed(2)).join(" ");
}
async function requestJson<T>(
fetch: DatasourceFetch,
url: string,
timeoutMs: number,
): Promise<T> {
const response = await fetchWithTimeout(
fetch,
url,
{ cache: "no-store" },
timeoutMs,
);
if (!response.ok) throw new Error(`request failed: ${response.status}`);
return response.json() as Promise<T>;
}
async function fetchWithTimeout(
fetch: DatasourceFetch,
url: string,
init: RequestInit,
timeoutMs: number,
): Promise<Response> {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
return await fetch(url, {
...init,
signal: controller.signal,
});
} finally {
clearTimeout(timeout);
}
}
function prometheusApiUrl(
baseUrl: string,
pathname: string,
params: Record<string, string>,
): string {
const url = new URL(pathname, ensureTrailingSlash(baseUrl));
Object.entries(params).forEach(([key, value]) => url.searchParams.set(key, value));
return url.toString();
}
function ensureTrailingSlash(value: string): string {
return value.endsWith("/") ? value : `${value}/`;
}
function pickMaxVectorResult(
results: PrometheusVectorResult[],
): PrometheusVectorResult | null {
return results.reduce<PrometheusVectorResult | null>((winner, item) => {
if (!winner) return item;
return Number(item.value?.[1]) > Number(winner.value?.[1]) ? item : winner;
}, null);
}
function matrixPoints(results: PrometheusMatrixResult[]): number[] {
const byTimestamp = new Map<number, number>();
for (const series of results) {
for (const [timestamp, value] of series.values || []) {
const numeric = Number(value);
if (!Number.isFinite(numeric)) continue;
const previous = byTimestamp.get(timestamp);
if (previous === undefined || numeric > previous) {
byTimestamp.set(timestamp, numeric);
}
}
}
return [...byTimestamp.entries()]
.sort(([left], [right]) => left - right)
.map(([, value]) => value);
}
function metricValueWithLiveNumber(value: MetricValue, nextValue: number): MetricValue {
if (value.kind === "text") return value;
return {
...value,
value: value.kind === "percent" ? clamp(nextValue, 0, 100) : Math.max(0, nextValue),
};
}
function severityForValue(
value: number,
thresholds: TelemetryCard["thresholds"],
): Severity {
if (thresholds?.danger !== undefined && value >= thresholds.danger) return "danger";
if (thresholds?.warning !== undefined && value >= thresholds.warning) return "warning";
return "ok";
}
function prometheusMetricDetail(
metric: Record<string, string>,
fallback = "",
): string {
const host = metric.host || metric.instance || metric.job;
const detail = metric.mountpoint || metric.name || metric.container || metric.id;
const parts = [host, detail].filter(Boolean);
if (parts.length) return parts.join(" ");
return fallback.replace(/^fallback\s*-\s*/i, "") || "telemetry";
}
function markStale(card: TelemetryCard, detail: string): TelemetryCard {
return {
...structuredClone(card),
severity: "stale",
detail,
};
}
function parseHttpStatusReference(reference: string): { method: string; url: string } | null {
const match = /^(GET|HEAD|POST)\s+(.+)$/i.exec(reference.trim());
if (!match) return null;
return { method: match[1].toUpperCase(), url: match[2] };
}
interface PrometheusResponse<T> {
status: string;
data: {
result?: T[];
};
}
interface OpenMeteoResponse {
current?: {
apparent_temperature?: number;
temperature_2m?: number;
weather_code?: number;
wind_speed_10m?: number;
};
}
interface UptimeBadgeResponse {
status?: string;
ping?: number;
}
async function uptimeBadge(response: Response): Promise<UptimeBadgeResponse | null> {
const contentType = response.headers.get("content-type") || "";
if (!contentType.includes("json")) return null;
try {
const payload = await response.clone().json() as UptimeBadgeResponse;
return typeof payload.status === "string" ? payload : null;
} catch {
return null;
}
}
function uptimeBadgeSeverity(status = ""): Severity {
const normalized = status.toUpperCase();
if (normalized === "UP") return "ok";
if (normalized === "PENDING" || normalized === "MAINTENANCE") return "warning";
return "unavailable";
}
function openMeteoUrl(reference: string): string {
const params = new URLSearchParams();
const serialized = reference.startsWith("open-meteo:")
? reference.slice("open-meteo:".length)
: reference;
new URLSearchParams(serialized).forEach((value, key) => {
params.set(key, value);
});
params.set(
"current",
"temperature_2m,apparent_temperature,weather_code,wind_speed_10m",
);
params.set("timezone", "Europe/Amsterdam");
return `https://api.open-meteo.com/v1/forecast?${params.toString()}`;
}
function weatherCondition(code: number): string {
if (code === 0) return "Clear";
if ([1, 2].includes(code)) return "Partly cloudy";
if (code === 3) return "Cloudy";
if ([45, 48].includes(code)) return "Fog";
if (code >= 51 && code <= 67) return "Rain";
if (code >= 71 && code <= 77) return "Snow";
if (code >= 80 && code <= 82) return "Showers";
if (code >= 95) return "Thunderstorm";
return "Mixed";
}
function clamp(value: number, min: number, max: number): number {
return Math.max(min, Math.min(max, value));
}
function formatDuration(totalSeconds: number): string {
const seconds = Math.max(0, Math.floor(totalSeconds));
const days = Math.floor(seconds / 86_400);
const hours = Math.floor((seconds % 86_400) / 3_600);
const minutes = Math.floor((seconds % 3_600) / 60);
return `${days}d ${hours}h ${minutes}m`;
}

View file

@ -50,9 +50,13 @@
<style> <style>
.dashboard-frame { .dashboard-frame {
display: grid; display: grid;
box-sizing: border-box;
height: 100vh;
min-height: 100vh; min-height: 100vh;
gap: var(--ui-space-3); gap: 0.36rem;
padding: clamp(0.75rem, 1.3vw, 1.25rem); grid-template-rows: auto auto minmax(0, 1fr) auto;
overflow: hidden;
padding: clamp(0.42rem, 0.65vw, 0.62rem);
background: background:
linear-gradient(90deg, transparent 0 49%, rgba(255, 255, 255, 0.08) 50%, transparent 51%), linear-gradient(90deg, transparent 0 49%, rgba(255, 255, 255, 0.08) 50%, transparent 51%),
rgba(0, 0, 0, 0.18); rgba(0, 0, 0, 0.18);
@ -60,11 +64,12 @@
.dashboard-frame__header { .dashboard-frame__header {
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr) auto; grid-template-columns: minmax(30rem, 1fr) minmax(30rem, 0.9fr);
gap: var(--ui-space-4); gap: 0.5rem;
align-items: start; align-items: start;
border-bottom: var(--ui-border); border-bottom: var(--ui-border);
padding-bottom: var(--ui-space-3); min-height: 5.35rem;
padding-bottom: 0.34rem;
} }
.dashboard-frame__header p, .dashboard-frame__header p,
@ -85,32 +90,65 @@
max-width: 100%; max-width: 100%;
overflow-wrap: anywhere; overflow-wrap: anywhere;
font-family: var(--ui-font-display); font-family: var(--ui-font-display);
font-size: clamp(2.45rem, 5vw, 4.2rem); font-size: clamp(2.35rem, 3.65vw, 3.25rem);
font-weight: 850; font-weight: 850;
letter-spacing: 0; letter-spacing: 0;
line-height: 0.85; line-height: 0.78;
white-space: nowrap;
} }
.dashboard-frame__modules { .dashboard-frame__modules {
display: flex; display: grid;
flex-wrap: wrap; grid-template-columns: minmax(15rem, 0.66fr) minmax(17rem, 1fr);
justify-content: end; justify-content: end;
gap: var(--ui-space-3); gap: 0.5rem;
} }
.dashboard-frame__panels { .dashboard-frame__panels {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr)); grid-template-columns: repeat(4, minmax(0, 1fr));
gap: var(--ui-space-3); align-content: start;
gap: 0.36rem;
min-height: 0;
overflow: hidden;
}
.dashboard-frame__panels :global(.service-panel[data-layout="grid"]) {
grid-column: 1 / -1;
}
.dashboard-frame__panels
:global(.service-panel[data-layout="grid"] .panel__body) {
grid-template-columns: repeat(4, minmax(0, 1fr));
} }
@media (max-width: 780px) { @media (max-width: 780px) {
.dashboard-frame {
height: auto;
min-height: 100vh;
overflow: visible;
}
.dashboard-frame__header { .dashboard-frame__header {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.dashboard-frame__modules { .dashboard-frame__modules {
justify-content: stretch; grid-template-columns: 1fr;
}
h1 {
white-space: normal;
}
.dashboard-frame__panels {
grid-template-columns: 1fr;
overflow: visible;
}
.dashboard-frame__panels
:global(.service-panel[data-layout="grid"] .panel__body) {
grid-template-columns: 1fr;
} }
} }
</style> </style>

View file

@ -34,7 +34,7 @@
.footer-cell { .footer-cell {
display: grid; display: grid;
grid-template-columns: auto minmax(0, 1fr); grid-template-columns: auto minmax(0, 1fr);
min-height: 2.65rem; min-height: 1.9rem;
align-items: center; align-items: center;
background: rgba(2, 3, 2, 0.92); background: rgba(2, 3, 2, 0.92);
color: inherit; color: inherit;
@ -44,7 +44,7 @@
span, span,
strong { strong {
min-width: 0; min-width: 0;
padding: var(--ui-space-2) var(--ui-space-3); padding: 0.34rem 0.52rem;
overflow-wrap: anywhere; overflow-wrap: anywhere;
text-transform: uppercase; text-transform: uppercase;
} }
@ -53,13 +53,13 @@
height: 100%; height: 100%;
border-right: var(--ui-border); border-right: var(--ui-border);
color: var(--ui-color-muted); color: var(--ui-color-muted);
font-size: 0.68rem; font-size: 0.52rem;
font-weight: 800; font-weight: 800;
} }
strong { strong {
color: var(--ui-color-text); color: var(--ui-color-text);
font-size: 0.76rem; font-size: 0.58rem;
} }
.footer-cell[data-severity="ok"] strong { .footer-cell[data-severity="ok"] strong {

View file

@ -27,8 +27,8 @@
<style> <style>
.icon-glyph { .icon-glyph {
display: inline-grid; display: inline-grid;
width: 2.35rem; width: 1.55rem;
height: 2.35rem; height: 1.55rem;
place-items: center; place-items: center;
border: var(--ui-border); border: var(--ui-border);
background: #050605; background: #050605;
@ -37,17 +37,17 @@
} }
.icon-glyph :global(svg) { .icon-glyph :global(svg) {
width: 1.35rem; width: 0.95rem;
height: 1.35rem; height: 0.95rem;
} }
.icon-glyph[data-size="sm"] { .icon-glyph[data-size="sm"] {
width: 1.75rem; width: 1.1rem;
height: 1.75rem; height: 1.1rem;
} }
.icon-glyph[data-size="lg"] { .icon-glyph[data-size="lg"] {
width: 3rem; width: 2.35rem;
height: 3rem; height: 2.35rem;
} }
</style> </style>

View file

@ -0,0 +1,147 @@
<script lang="ts">
import { onDestroy, onMount } from "svelte";
import "uplot/dist/uPlot.min.css";
import type { UiSeverity } from "../types";
let {
values = [],
severity = "neutral",
label = "Telemetry trend",
}: {
values?: number[];
severity?: UiSeverity;
label?: string;
} = $props();
let chartElement: HTMLDivElement;
let chart: ChartInstance | null = null;
let resizeObserver: ResizeObserver | null = null;
onMount(async () => {
const module = await import("uplot");
chart = new module.default(chartOptions(chartElement, severity), chartData(values), chartElement);
resizeObserver = new ResizeObserver(() => {
chart?.setSize(chartSize(chartElement));
});
resizeObserver.observe(chartElement);
});
$effect(() => {
chart?.setData(chartData(values));
chart?.setSize(chartSize(chartElement));
});
onDestroy(() => {
resizeObserver?.disconnect();
chart?.destroy();
});
</script>
<div
class="line-chart"
data-chart-library="uplot"
data-severity={severity}
role="img"
aria-label={label}
>
<div class="line-chart__canvas" bind:this={chartElement} aria-hidden="true"></div>
</div>
<style>
.line-chart {
position: relative;
min-width: 0;
height: 1rem;
color: var(--ui-color-accent);
}
.line-chart[data-severity="warning"] {
color: var(--ui-color-warning);
}
.line-chart[data-severity="danger"] {
color: var(--ui-color-danger);
}
.line-chart[data-severity="stale"],
.line-chart[data-severity="unavailable"] {
color: var(--ui-color-stale);
}
.line-chart__canvas {
width: 100%;
height: 100%;
}
.line-chart__canvas :global(.uplot) {
width: 100% !important;
height: 100% !important;
background: transparent;
font-family: var(--ui-font-mono);
}
.line-chart__canvas :global(.u-over),
.line-chart__canvas :global(.u-under) {
overflow: visible;
}
</style>
<script lang="ts" module>
type ChartInstance = {
destroy(): void;
setData(data: import("uplot").AlignedData): void;
setSize(size: { width: number; height: number }): void;
};
function chartData(values: number[]): import("uplot").AlignedData {
const normalized = values.length ? values : [0, 0];
return [
normalized.map((_, index) => index),
normalized.map((value) => Math.max(0, Number(value) || 0)),
];
}
function chartOptions(
element: HTMLElement,
severity: import("../types").UiSeverity,
): import("uplot").Options {
return {
...chartSize(element),
cursor: { show: false },
legend: { show: false },
padding: [2, 0, 2, 0],
scales: {
x: { time: false },
y: { auto: true },
},
axes: [
{ show: false },
{ show: false },
],
series: [
{},
{
stroke: chartStroke(element, severity),
width: 2,
points: { show: false },
},
],
};
}
function chartSize(element?: HTMLElement): { width: number; height: number } {
return {
width: Math.max(80, Math.round(element?.clientWidth || 120)),
height: Math.max(20, Math.round(element?.clientHeight || 24)),
};
}
function chartStroke(element: HTMLElement, severity: import("../types").UiSeverity): string {
const styles = window.getComputedStyle(element.closest(".line-chart") || element);
const currentColor = styles.color;
if (currentColor) return currentColor;
if (severity === "danger") return "#ff1744";
if (severity === "warning") return "#ffb020";
return "#d7ff00";
}
</script>

View file

@ -37,12 +37,13 @@
.module-card { .module-card {
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr) auto; grid-template-columns: minmax(0, 1fr) auto;
gap: var(--ui-space-4); gap: 0.5rem;
align-items: start; align-items: start;
min-width: min(100%, 18rem); min-width: 0;
min-height: 4.15rem;
border: var(--ui-border); border: var(--ui-border);
background: rgba(6, 8, 7, 0.82); background: rgba(6, 8, 7, 0.82);
padding: var(--ui-space-3); padding: 0.5rem;
} }
h2, h2,
@ -52,23 +53,29 @@
h2 { h2 {
color: var(--ui-color-muted); color: var(--ui-color-muted);
font-size: 0.82rem; font-size: 0.6rem;
line-height: 1;
text-transform: uppercase; text-transform: uppercase;
} }
strong { strong {
display: block; display: block;
margin-top: var(--ui-space-1); margin-top: 0.15rem;
font-family: var(--ui-font-display); font-family: var(--ui-font-display);
font-size: 2.25rem; font-size: clamp(1.45rem, 2vw, 2rem);
line-height: 0.9; line-height: 0.82;
overflow-wrap: anywhere;
} }
p { p {
margin-top: var(--ui-space-1); margin-top: 0.2rem;
color: var(--ui-color-muted); color: var(--ui-color-muted);
font-size: 0.72rem; font-size: 0.52rem;
line-height: 1.05;
overflow: hidden;
text-overflow: ellipsis;
text-transform: uppercase; text-transform: uppercase;
white-space: nowrap;
} }
.module-card[data-severity="ok"] :global(.icon-glyph) { .module-card[data-severity="ok"] :global(.icon-glyph) {

View file

@ -56,13 +56,13 @@
} }
.panel__header { .panel__header {
padding: var(--ui-space-3) var(--ui-space-3) 0; padding: 0.42rem 0.55rem 0;
} }
h2 { h2 {
margin: 0; margin: 0;
font-family: var(--ui-font-display); font-family: var(--ui-font-display);
font-size: clamp(1.75rem, 3vw, 2.5rem); font-size: clamp(1.45rem, 2.05vw, 1.95rem);
font-weight: 800; font-weight: 800;
letter-spacing: 0; letter-spacing: 0;
line-height: 0.9; line-height: 0.9;
@ -71,8 +71,8 @@
.panel__body { .panel__body {
display: grid; display: grid;
gap: var(--ui-space-2); gap: 0.25rem;
padding: var(--ui-space-3); padding: 0.42rem 0.55rem 0.55rem;
} }
.panel[data-density="compact"] .panel__body { .panel[data-density="compact"] .panel__body {

View file

@ -7,7 +7,7 @@
let { group }: { group: UiServiceGroup } = $props(); let { group }: { group: UiServiceGroup } = $props();
</script> </script>
<div class="service-panel" data-model-id={group.id}> <div class="service-panel" data-layout={group.layout || "list"} data-model-id={group.id}>
<Panel title={group.title}> <Panel title={group.title}>
{#if group.summary?.length} {#if group.summary?.length}
<StatusStrip id={`${group.id}:summary`} items={group.summary} compact /> <StatusStrip id={`${group.id}:summary`} items={group.summary} compact />

View file

@ -42,13 +42,13 @@
.service-row { .service-row {
display: grid; display: grid;
grid-template-columns: auto minmax(0, 1fr) auto; grid-template-columns: auto minmax(0, 1fr) auto;
gap: var(--ui-space-3); gap: 0.42rem;
align-items: center; align-items: center;
min-height: 3.75rem; min-height: 2.35rem;
border: var(--ui-border); border: var(--ui-border);
background: rgba(12, 13, 12, 0.72); background: rgba(12, 13, 12, 0.72);
color: inherit; color: inherit;
padding: var(--ui-space-2); padding: 0.3rem 0.38rem;
text-decoration: none; text-decoration: none;
} }
@ -79,19 +79,25 @@
} }
h3 { h3 {
overflow-wrap: anywhere; overflow: hidden;
font-size: 0.88rem; font-size: 0.66rem;
font-weight: 850; font-weight: 850;
letter-spacing: 0; letter-spacing: 0;
line-height: 1.05;
text-overflow: ellipsis;
text-transform: uppercase; text-transform: uppercase;
white-space: nowrap;
} }
p { p {
margin-top: 0.22rem; margin-top: 0.16rem;
overflow: hidden;
color: var(--ui-color-muted); color: var(--ui-color-muted);
font-size: 0.68rem; font-size: 0.5rem;
line-height: 1.25; line-height: 1.08;
text-overflow: ellipsis;
text-transform: uppercase; text-transform: uppercase;
white-space: nowrap;
} }
@media (max-width: 580px) { @media (max-width: 580px) {

View file

@ -15,15 +15,16 @@
<style> <style>
.status-badge { .status-badge {
display: inline-grid; display: inline-grid;
min-height: 1.6rem; min-height: 1.12rem;
align-items: center; align-items: center;
border: var(--ui-border); border: var(--ui-border);
padding: 0 var(--ui-space-2); padding: 0 0.34rem;
color: var(--ui-color-muted); color: var(--ui-color-muted);
font-size: 0.72rem; font-size: 0.52rem;
font-weight: 800; font-weight: 800;
letter-spacing: 0; letter-spacing: 0;
text-transform: uppercase; text-transform: uppercase;
white-space: nowrap;
} }
.status-badge[data-severity="ok"] { .status-badge[data-severity="ok"] {

View file

@ -22,13 +22,13 @@
<style> <style>
.status-strip { .status-strip {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
gap: 1px; gap: 1px;
border: var(--ui-border); border: var(--ui-border);
background: var(--ui-color-line); background: var(--ui-color-line);
} }
.status-strip[data-compact="true"] { .status-strip[data-compact="true"] {
grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr)); grid-template-columns: repeat(auto-fit, minmax(6rem, 1fr));
} }
</style> </style>

View file

@ -2,6 +2,7 @@
import { formatMetricValue } from "../format"; import { formatMetricValue } from "../format";
import type { UiTelemetryCard } from "../types"; import type { UiTelemetryCard } from "../types";
import IconGlyph from "./IconGlyph.svelte"; import IconGlyph from "./IconGlyph.svelte";
import LineChart from "./LineChart.svelte";
let { card }: { card: UiTelemetryCard } = $props(); let { card }: { card: UiTelemetryCard } = $props();
@ -23,9 +24,7 @@
</div> </div>
{/if} {/if}
{#if card.sparkline?.length} {#if card.sparkline?.length}
<svg class="telemetry-card__sparkline" viewBox="0 0 100 24" aria-hidden="true"> <LineChart values={card.sparkline} severity={card.severity} label={`${card.label} trend`} />
<polyline points={sparklinePoints(card.sparkline)} />
</svg>
{/if} {/if}
{#if card.detail || card.description} {#if card.detail || card.description}
<p>{card.detail || card.description}</p> <p>{card.detail || card.description}</p>
@ -35,12 +34,12 @@
<style> <style>
.telemetry-card { .telemetry-card {
display: grid; display: grid;
min-height: 7.15rem; min-height: 5.15rem;
border: var(--ui-border); border: var(--ui-border);
background: linear-gradient(180deg, rgba(13, 15, 14, 0.82), rgba(2, 3, 2, 0.92)); background: linear-gradient(180deg, rgba(13, 15, 14, 0.82), rgba(2, 3, 2, 0.92));
padding: var(--ui-space-3); padding: 0.45rem 0.55rem 0.42rem;
color: var(--ui-color-text); color: var(--ui-color-text);
gap: var(--ui-space-2); gap: 0.25rem;
} }
.telemetry-card[data-severity="warning"] { .telemetry-card[data-severity="warning"] {
@ -69,7 +68,11 @@
display: flex; display: flex;
min-width: 0; min-width: 0;
align-items: center; align-items: center;
gap: var(--ui-space-2); gap: 0.35rem;
}
header :global(.icon-glyph) {
display: none;
} }
h3, h3,
@ -80,29 +83,34 @@
h3 { h3 {
overflow-wrap: anywhere; overflow-wrap: anywhere;
color: inherit; color: inherit;
font-size: 0.78rem; font-size: 0.62rem;
font-weight: 800; font-weight: 800;
letter-spacing: 0; letter-spacing: 0;
line-height: 1.02;
text-transform: uppercase; text-transform: uppercase;
} }
strong { strong {
font-family: var(--ui-font-display); font-family: var(--ui-font-display);
font-size: clamp(2rem, 4vw, 3rem); font-size: clamp(1.75rem, 2.75vw, 2.4rem);
font-weight: 800; font-weight: 800;
letter-spacing: 0; letter-spacing: 0;
line-height: 0.82; line-height: 0.76;
white-space: nowrap;
} }
p { p {
overflow: hidden;
color: var(--ui-color-muted); color: var(--ui-color-muted);
font-size: 0.67rem; font-size: 0.53rem;
line-height: 1.2; line-height: 1.05;
text-overflow: ellipsis;
text-transform: uppercase; text-transform: uppercase;
white-space: nowrap;
} }
.telemetry-card__bar { .telemetry-card__bar {
height: 0.35rem; height: 0.25rem;
border: var(--ui-border); border: var(--ui-border);
background: #030403; background: #030403;
} }
@ -114,19 +122,6 @@
background: currentColor; background: currentColor;
} }
.telemetry-card__sparkline {
width: 100%;
height: 1.5rem;
color: var(--ui-color-accent);
}
.telemetry-card__sparkline polyline {
fill: none;
stroke: currentColor;
stroke-linecap: square;
stroke-linejoin: miter;
stroke-width: 2;
}
</style> </style>
<script lang="ts" module> <script lang="ts" module>
@ -142,17 +137,4 @@
const progress = Number(card.value.value); const progress = Number(card.value.value);
return Number.isFinite(progress) ? clampPercent(progress) : null; return Number.isFinite(progress) ? clampPercent(progress) : null;
} }
function sparklinePoints(values: number[]): string {
const max = Math.max(...values, 1);
const step = values.length > 1 ? 100 / (values.length - 1) : 100;
return values
.map((value, index) => {
const x = index * step;
const y = 24 - (Math.max(0, value) / max) * 22;
return `${x.toFixed(2)},${y.toFixed(2)}`;
})
.join(" ");
}
</script> </script>

View file

@ -14,9 +14,21 @@
<style> <style>
.telemetry-grid { .telemetry-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(var(--ui-density-card-min), 1fr)); grid-template-columns: repeat(8, minmax(0, 1fr));
gap: 1px; gap: 1px;
border: var(--ui-border); border: var(--ui-border);
background: var(--ui-color-line); background: var(--ui-color-line);
} }
@media (max-width: 1100px) {
.telemetry-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
@media (max-width: 700px) {
.telemetry-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
</style> </style>

View file

@ -108,6 +108,22 @@ describe("dashboard UI components", () => {
expect(withProgress.body).toContain("--metric-progress: 42%"); expect(withProgress.body).toContain("--metric-progress: 42%");
}); });
test("renders telemetry trends through the uPlot chart surface", () => {
const { body } = render(TelemetryCard, {
props: {
card: {
id: "trend-card",
label: "Trend Card",
value: { kind: "percent", value: 64 },
sparkline: [18, 24, 64],
severity: "ok",
},
},
});
expect(body).toContain('data-chart-library="uplot"');
});
test("base button controls forward native attributes", () => { test("base button controls forward native attributes", () => {
const button = render(Button, { const button = render(Button, {
props: { props: {

View file

@ -10,6 +10,8 @@ export function formatMetricValue(metric: UiMetricValue): string {
const unit = metric.unit ?? defaultUnit(metric.kind); const unit = metric.unit ?? defaultUnit(metric.kind);
if (metric.kind === "bytes") return formatBytes(value, precision); if (metric.kind === "bytes") return formatBytes(value, precision);
if (metric.kind === "percent") return `${value.toFixed(precision)}%`;
if (metric.kind === "temperature") return `${value.toFixed(precision)}°C`;
return `${value.toFixed(precision)}${unit ? ` ${unit}` : ""}`; return `${value.toFixed(precision)}${unit ? ` ${unit}` : ""}`;
} }

View file

@ -9,6 +9,7 @@ export { default as FooterStatusCell } from "./components/FooterStatusCell.svelt
export { default as GridFrame } from "./components/GridFrame.svelte"; export { default as GridFrame } from "./components/GridFrame.svelte";
export { default as IconGlyph } from "./components/IconGlyph.svelte"; export { default as IconGlyph } from "./components/IconGlyph.svelte";
export { default as IconButton } from "./components/IconButton.svelte"; export { default as IconButton } from "./components/IconButton.svelte";
export { default as LineChart } from "./components/LineChart.svelte";
export { default as ModuleCard } from "./components/ModuleCard.svelte"; export { default as ModuleCard } from "./components/ModuleCard.svelte";
export { default as Panel } from "./components/Panel.svelte"; export { default as Panel } from "./components/Panel.svelte";
export { default as ProgressMeter } from "./components/ProgressMeter.svelte"; export { default as ProgressMeter } from "./components/ProgressMeter.svelte";

View file

@ -25,6 +25,9 @@ describe("dashboard model renderer", () => {
expect(dashboard.serviceGroups.map((group) => group.id)).toEqual( expect(dashboard.serviceGroups.map((group) => group.id)).toEqual(
dimensionLabDashboardFixture.layout.serviceGroups, dimensionLabDashboardFixture.layout.serviceGroups,
); );
expect(dashboard.serviceGroups.find((group) => group.id === "runtime-health")?.layout).toBe(
"grid",
);
expect(dashboard.modules.map((module) => module.id)).toEqual([ expect(dashboard.modules.map((module) => module.id)).toEqual([
"weather-amsterdam", "weather-amsterdam",
"runtime-health-summary", "runtime-health-summary",
@ -34,6 +37,7 @@ describe("dashboard model renderer", () => {
"footer-status:last-sync", "footer-status:last-sync",
"footer-status:uptime", "footer-status:uptime",
"footer-status:load-avg", "footer-status:load-avg",
"footer-status:auto-refresh",
]); ]);
expect(dashboard.statusStripId).toBe("footer-status"); expect(dashboard.statusStripId).toBe("footer-status");
}); });

View file

@ -58,6 +58,7 @@ function telemetryToUi(card: TelemetryCard): UiTelemetryCard {
function serviceGroupToUi(group: ServiceGroup): UiServiceGroup { function serviceGroupToUi(group: ServiceGroup): UiServiceGroup {
return { return {
id: group.id, id: group.id,
layout: group.layout,
title: group.title, title: group.title,
services: group.services.map(serviceToUi), services: group.services.map(serviceToUi),
}; };

View file

@ -0,0 +1,13 @@
<script module lang="ts">
import { defineMeta } from "@storybook/addon-svelte-csf";
import LineChart from "../components/LineChart.svelte";
const { Story } = defineMeta({
title: "Dashboard Primitives/LineChart",
component: LineChart,
});
</script>
<Story name="Neutral Trend" args={{ values: [12, 18, 15, 28, 24, 32] }} />
<Story name="Warning Trend" args={{ values: [22, 34, 48, 62, 74, 70], severity: "warning" }} />
<Story name="Danger Trend" args={{ values: [20, 40, 64, 82, 90, 94], severity: "danger" }} />

View file

@ -52,6 +52,7 @@ export interface UiServiceRow {
export interface UiServiceGroup { export interface UiServiceGroup {
id: string; id: string;
layout?: "grid" | "list";
title: string; title: string;
services: UiServiceRow[]; services: UiServiceRow[];
summary?: UiStatusItem[]; summary?: UiStatusItem[];

View file

@ -1,7 +1,24 @@
import { loadDashboardRuntime } from "$lib/server/dashboard"; import { loadDashboardRuntime } from "$lib/server/dashboard";
import { resolveDashboardDatasources } from "$lib/server/datasources";
export async function load() {
const dashboard = loadDashboardRuntime(undefined, {
refreshSeedDocument: true,
seedIfEmpty: true,
});
if (dashboard.state !== "ready") {
return { dashboard };
}
if (process.env.DISABLE_LIVE_DATASOURCES === "1") {
return { dashboard };
}
export function load() {
return { return {
dashboard: loadDashboardRuntime(undefined, { seedIfEmpty: true }), dashboard: {
...dashboard,
document: await resolveDashboardDatasources(dashboard.document),
},
}; };
} }

View file

@ -1,9 +1,12 @@
<script lang="ts"> <script lang="ts">
import { browser } from "$app/environment";
import { invalidateAll } from "$app/navigation";
import { import {
DashboardFrame, DashboardFrame,
SystemState, SystemState,
dashboardDocumentToUiDashboard, dashboardDocumentToUiDashboard,
} from "$lib/ui"; } from "$lib/ui";
import { onDestroy } from "svelte";
import type { PageData } from "./$types"; import type { PageData } from "./$types";
let { data }: { data: PageData } = $props(); let { data }: { data: PageData } = $props();
@ -29,6 +32,30 @@
let pageDescription = $derived( let pageDescription = $derived(
dashboard?.subtitle || stateSubtitle || stateMessage, dashboard?.subtitle || stateSubtitle || stateMessage,
); );
let refreshTimer: number | undefined;
$effect(() => {
if (!browser) return;
if (refreshTimer) {
window.clearInterval(refreshTimer);
refreshTimer = undefined;
}
const refreshIntervalSeconds =
data.dashboard.state === "ready"
? data.dashboard.document.metadata.refreshIntervalSeconds
: undefined;
if (!refreshIntervalSeconds) return;
refreshTimer = window.setInterval(() => {
void invalidateAll();
}, refreshIntervalSeconds * 1000);
});
onDestroy(() => {
if (refreshTimer) window.clearInterval(refreshTimer);
});
</script> </script>
<svelte:head> <svelte:head>

View file

@ -6,6 +6,8 @@ const linkedServiceIds = [
"forgejo", "forgejo",
"wiki", "wiki",
"aws-start", "aws-start",
"adguard-primary",
"adguard-secondary",
"grafana", "grafana",
"uptime-kuma", "uptime-kuma",
"prometheus", "prometheus",
@ -14,10 +16,13 @@ const linkedServiceIds = [
"open-webui", "open-webui",
"comfyui", "comfyui",
"models", "models",
"prompt-registry",
"adminer", "adminer",
"assistant", "assistant",
"suna", "suna",
"cockpit-infra", "cockpit-infra",
"cockpit-gpu",
"cockpit-network-core",
"forgejo-ssh-relay", "forgejo-ssh-relay",
]; ];
@ -48,6 +53,59 @@ test.describe("dashboard page QA gate", () => {
}); });
}); });
test("fits the operational dashboard into a 1470 by 956 viewport", async ({
page,
}, testInfo) => {
test.skip(testInfo.project.name !== "chromium-desktop");
await page.setViewportSize({ width: 1470, height: 956 });
await page.goto("/");
const metrics = await page.evaluate(() => {
const footer = document.querySelector("[data-model-id='footer-status']");
const runtime = document.querySelector("[data-model-id='runtime-health']");
const visibleItems = [
...document.querySelectorAll(".telemetry-card"),
...document.querySelectorAll(".service-row"),
...document.querySelectorAll(".footer-cell"),
].map((element) => {
const rect = element.getBoundingClientRect();
return {
bottom: rect.bottom,
height: rect.height,
id: element.getAttribute("data-model-id"),
top: rect.top,
width: rect.width,
};
});
return {
clippedItems: visibleItems.filter((item) =>
item.top < 0 ||
item.bottom > window.innerHeight ||
item.width <= 0 ||
item.height <= 0
),
footerCellCount: document.querySelectorAll(".footer-cell").length,
scrollWidth: document.documentElement.scrollWidth,
scrollHeight: document.documentElement.scrollHeight,
serviceRowCount: document.querySelectorAll(".service-row").length,
telemetryCardCount: document.querySelectorAll(".telemetry-card").length,
footerBottom: footer?.getBoundingClientRect().bottom ?? 0,
runtimeBottom: runtime?.getBoundingClientRect().bottom ?? 0,
};
});
expect(metrics.scrollWidth).toBeLessThanOrEqual(1470);
expect(metrics.scrollHeight).toBeLessThanOrEqual(956);
expect(metrics.runtimeBottom).toBeLessThanOrEqual(956);
expect(metrics.footerBottom).toBeLessThanOrEqual(956);
expect(metrics.telemetryCardCount).toBe(16);
expect(metrics.serviceRowCount).toBe(28);
expect(metrics.footerCellCount).toBe(5);
expect(metrics.clippedItems).toEqual([]);
});
test("keeps the first screen usable on mobile", async ({ page }, testInfo) => { test("keeps the first screen usable on mobile", async ({ page }, testInfo) => {
test.skip(testInfo.project.name !== "chromium-mobile"); test.skip(testInfo.project.name !== "chromium-mobile");

Binary file not shown.

Before

Width:  |  Height:  |  Size: 379 KiB

After

Width:  |  Height:  |  Size: 238 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 351 KiB

After

Width:  |  Height:  |  Size: 272 KiB

Before After
Before After