feat(dashboard): add live observability overview

This commit is contained in:
vince 2026-06-19 03:38:38 +02:00
parent 2ff9f3c0ed
commit 2d2b905d18
27 changed files with 1417 additions and 144 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,7 +521,31 @@ 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",
}), }),
]), ]),
@ -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" },
], ],
}, },
], ],
@ -579,17 +664,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

@ -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-model-id="runtime-health"]) {
grid-column: 1 / -1;
}
.dashboard-frame__panels
:global(.service-panel[data-model-id="runtime-health"] .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-model-id="runtime-health"] .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

@ -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

@ -34,6 +34,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

@ -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

@ -1,7 +1,21 @@
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, { 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

@ -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,32 @@ 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']");
return {
scrollWidth: document.documentElement.scrollWidth,
scrollHeight: document.documentElement.scrollHeight,
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);
});
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