diff --git a/bun.lock b/bun.lock index db0b7d3..c120138 100644 --- a/bun.lock +++ b/bun.lock @@ -10,7 +10,6 @@ "ajv": "^8.20.0", "ajv-formats": "^3.0.1", "drizzle-orm": "^0.45.2", - "uplot": "^1.6.32", }, "devDependencies": { "@axe-core/playwright": "^4.11.3", @@ -736,8 +735,6 @@ "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=="], "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=="], diff --git a/package.json b/package.json index aade3b6..fdb14dc 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,7 @@ "@sinclair/typebox": "^0.34.49", "ajv": "^8.20.0", "ajv-formats": "^3.0.1", - "drizzle-orm": "^0.45.2", - "uplot": "^1.6.32" + "drizzle-orm": "^0.45.2" }, "devDependencies": { "@axe-core/playwright": "^4.11.3", diff --git a/playwright.config.ts b/playwright.config.ts index 2785c33..6ca14cd 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -18,7 +18,7 @@ export default defineConfig({ screenshot: "only-on-failure", }, webServer: { - 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`, + command: `bun run build && DATABASE_URL=${databaseUrl} HOST=127.0.0.1 PORT=${port} bun build/index.js`, url: baseURL, reuseExistingServer: false, timeout: 120_000, diff --git a/src/lib/model/fixtures/dimensionlab.test.ts b/src/lib/model/fixtures/dimensionlab.test.ts index 38b2ac6..ae13cbf 100644 --- a/src/lib/model/fixtures/dimensionlab.test.ts +++ b/src/lib/model/fixtures/dimensionlab.test.ts @@ -128,11 +128,9 @@ const verifiedSeedIconIds = new Set([ "mdi:pulse", "mdi:robot-outline", "mdi:router-network", - "mdi:text-box-search", "mdi:thermometer", "mdi:web", "mdi:weather-sunny", - "simple-icons:adguard", "simple-icons:adminer", "simple-icons:amazonwebservices", "simple-icons:cockpit", diff --git a/src/lib/model/fixtures/dimensionlab.ts b/src/lib/model/fixtures/dimensionlab.ts index c2b4808..9890fef 100644 --- a/src/lib/model/fixtures/dimensionlab.ts +++ b/src/lib/model/fixtures/dimensionlab.ts @@ -36,35 +36,11 @@ interface ServiceSeed { 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 = { schemaVersion: DASHBOARD_SCHEMA_VERSION, metadata: { title: "System Overview", - subtitle: "Capacity, noise & top consumers", + subtitle: "Dimension Lab home infra, GPU, and automation surface", description: "Initial Dimension Lab dashboard seed data. Runtime values are fallback values until datasource adapters are enabled.", timezone: "Europe/Amsterdam", @@ -105,7 +81,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = { severity: "ok", thresholds: percentThresholds(), datasource: prometheus( - ramQuery("linux-infra"), + '100 * (1 - node_memory_MemAvailable_bytes{instance="linux-infra"} / node_memory_MemTotal_bytes{instance="linux-infra"})', ), sparkline: [18, 18, 19, 19, 18, 19], }), @@ -119,7 +95,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = { severity: "ok", thresholds: percentThresholds(), datasource: prometheus( - ramQuery("linux"), + '100 * (1 - node_memory_MemAvailable_bytes{instance="linux-gpu"} / node_memory_MemTotal_bytes{instance="linux-gpu"})', ), sparkline: [16, 16, 17, 17, 17, 17], }), @@ -133,7 +109,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = { severity: "ok", thresholds: percentThresholds(), datasource: prometheus( - ramQuery("network-core"), + '100 * (1 - node_memory_MemAvailable_bytes{instance="network-core"} / node_memory_MemTotal_bytes{instance="network-core"})', ), sparkline: [5, 5, 5, 6, 5, 5], }), @@ -147,7 +123,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = { severity: "ok", thresholds: percentThresholds(80, 92), datasource: prometheus( - diskUsedQuery(USER_MOUNT_FILTER), + '100 * (1 - node_filesystem_avail_bytes{mountpoint="/home"} / node_filesystem_size_bytes{mountpoint="/home"})', ), sparkline: [27, 27, 28, 29, 29, 29], }), @@ -161,7 +137,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = { severity: "ok", thresholds: percentThresholds(80, 92), datasource: prometheus( - diskUsedQuery(SYSTEM_MOUNT_FILTER), + '100 * (1 - node_filesystem_avail_bytes{mountpoint="/boot"} / node_filesystem_size_bytes{mountpoint="/boot"})', ), sparkline: [48, 48, 49, 49, 49, 49], }), @@ -175,7 +151,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = { severity: "ok", thresholds: percentThresholds(75, 90), datasource: prometheus( - hostCpuQuery, + '100 - (avg by(instance) (rate(node_cpu_seconds_total{instance="linux-infra",mode="idle"}[5m])) * 100)', ), sparkline: [2, 3, 3, 4, 3, 3], }), @@ -189,7 +165,9 @@ export const dimensionLabDashboardFixture: DashboardDocument = { detail: "fallback - top container CPU pending label mapping", severity: "ok", thresholds: percentThresholds(70, 90), - datasource: prometheus(topCpuQuery), + datasource: placeholder( + "fallback top-container CPU query pending container label normalization", + ), sparkline: [6.1, 6.4, 7.0, 7.5, 7.2, 7.9], }), metric({ @@ -202,7 +180,9 @@ export const dimensionLabDashboardFixture: DashboardDocument = { detail: "fallback - top container RAM pending label mapping", severity: "danger", thresholds: { warning: 3 * 1024 ** 3, danger: 5 * 1024 ** 3 }, - datasource: prometheus(topRamQuery), + datasource: placeholder( + "fallback top-container memory query pending container label normalization", + ), sparkline: [ 3.1 * 1024 ** 3, 3.5 * 1024 ** 3, @@ -221,7 +201,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = { detail: "fallback - RTX 3060 GPU utilization", severity: "ok", thresholds: percentThresholds(85, 95), - datasource: prometheus(gpuQuery("3060", "nvidia_gpu_utilization_percent")), + datasource: prometheus('DCGM_FI_DEV_GPU_UTIL{gpu="rtx-3060"}'), sparkline: [0, 0, 0, 0, 0, 0], }), metric({ @@ -234,7 +214,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = { severity: "ok", thresholds: percentThresholds(80, 92), datasource: prometheus( - gpuVramQuery("3060"), + '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"})', ), sparkline: [0, 0, 0, 0, 0, 0], }), @@ -247,7 +227,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = { detail: "fallback - RTX 3060 temperature", severity: "ok", thresholds: { warning: 75, danger: 85 }, - datasource: prometheus(gpuQuery("3060", "nvidia_gpu_temperature_celsius")), + datasource: prometheus('DCGM_FI_DEV_GPU_TEMP{gpu="rtx-3060"}'), sparkline: [52, 53, 54, 54, 53, 54], }), metric({ @@ -259,7 +239,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = { detail: "fallback - RTX 3060 fan duty", severity: "ok", thresholds: percentThresholds(80, 95), - datasource: prometheus(gpuQuery("3060", "nvidia_gpu_fan_speed_percent")), + datasource: prometheus('DCGM_FI_DEV_FAN_SPEED{gpu="rtx-3060"}'), sparkline: [0, 0, 0, 0, 0, 0], }), metric({ @@ -271,7 +251,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = { detail: "fallback - RTX 3090 GPU utilization", severity: "ok", thresholds: percentThresholds(85, 95), - datasource: prometheus(gpuQuery("3090", "nvidia_gpu_utilization_percent")), + datasource: prometheus('DCGM_FI_DEV_GPU_UTIL{gpu="rtx-3090"}'), sparkline: [0, 0, 0, 0, 0, 0], }), metric({ @@ -284,7 +264,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = { severity: "warning", thresholds: percentThresholds(70, 90), datasource: prometheus( - gpuVramQuery("3090"), + '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"})', ), sparkline: [68, 70, 72, 74, 73, 74], }), @@ -297,7 +277,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = { detail: "fallback - RTX 3090 temperature", severity: "ok", thresholds: { warning: 75, danger: 85 }, - datasource: prometheus(gpuQuery("3090", "nvidia_gpu_temperature_celsius")), + datasource: prometheus('DCGM_FI_DEV_GPU_TEMP{gpu="rtx-3090"}'), sparkline: [49, 50, 50, 51, 50, 50], }), metric({ @@ -309,7 +289,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = { detail: "fallback - RTX 3090 fan duty", severity: "ok", thresholds: percentThresholds(80, 95), - datasource: prometheus(gpuQuery("3090", "nvidia_gpu_fan_speed_percent")), + datasource: prometheus('DCGM_FI_DEV_FAN_SPEED{gpu="rtx-3090"}'), sparkline: [0, 0, 0, 0, 0, 0], }), ], @@ -321,7 +301,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = { description: "Password manager", icon: "simple-icons:vaultwarden", href: "https://vault.dimensionlab.net", - datasource: uptimeMonitor(1), + datasource: httpStatus("https://vault.dimensionlab.net/alive"), }), service({ id: "forgejo", @@ -329,7 +309,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = { description: "Git repositories", icon: "simple-icons:forgejo", href: "https://git.dimensionlab.net", - datasource: uptimeMonitor(2), + datasource: httpStatus("https://git.dimensionlab.net/api/healthz"), }), service({ id: "wiki", @@ -337,7 +317,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = { description: "Internal documentation", icon: "simple-icons:wikidotjs", href: "https://wiki.dimensionlab.net", - datasource: uptimeMonitor(3), + datasource: httpStatus("https://wiki.dimensionlab.net"), }), service({ id: "aws-start", @@ -345,23 +325,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = { description: "AWS access portal", icon: "simple-icons:amazonwebservices", href: "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), + datasource: httpStatus("https://dimensionlab.awsapps.com/start"), }), ]), group("monitoring", "Monitoring", [ @@ -371,7 +335,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = { description: "Capacity and noise dashboard", icon: "simple-icons:grafana", href: "https://grafana.dimensionlab.net", - datasource: uptimeMonitor(11), + datasource: httpStatus("https://grafana.dimensionlab.net/api/health"), }), service({ id: "uptime-kuma", @@ -379,7 +343,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = { description: "Service uptime checks", icon: "simple-icons:uptimekuma", href: "https://uptime.dimensionlab.net", - datasource: uptimeMonitor(10), + datasource: httpStatus("https://uptime.dimensionlab.net/status/dimensionlab"), }), service({ id: "prometheus", @@ -387,7 +351,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = { description: "Metrics database", icon: "simple-icons:prometheus", href: "https://prometheus.dimensionlab.net", - datasource: uptimeMonitor(12), + datasource: httpStatus("https://prometheus.dimensionlab.net/-/ready"), }), service({ id: "backrest", @@ -395,7 +359,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = { description: "Restic backup manager", icon: "mdi:backup-restore", href: "https://backups.dimensionlab.net", - datasource: uptimeMonitor(13), + datasource: httpStatus("https://backups.dimensionlab.net"), }), ]), group("ai-automation", "AI & Automation", [ @@ -405,7 +369,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = { description: "Workflow automation", icon: "simple-icons:n8n", href: "https://workflows.dimensionlab.net", - datasource: uptimeMonitor(4), + datasource: httpStatus("https://workflows.dimensionlab.net/healthz"), }), service({ id: "open-webui", @@ -413,7 +377,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = { description: "Chat and model interface", icon: "mdi:web", href: "https://webui.dimensionlab.net", - datasource: uptimeMonitor(5), + datasource: httpStatus("https://webui.dimensionlab.net/health"), }), service({ id: "comfyui", @@ -421,7 +385,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = { description: "Image generation workflows", icon: "mdi:image-edit-outline", href: "https://comfy.dimensionlab.net", - datasource: uptimeMonitor(6), + datasource: httpStatus("https://comfy.dimensionlab.net"), }), service({ id: "models", @@ -429,15 +393,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = { description: "Local model management", icon: "mdi:brain", href: "https://models.dimensionlab.net", - 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), + datasource: httpStatus("https://models.dimensionlab.net/api/tags"), }), ]), group("systems", "Systems", [ @@ -447,7 +403,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = { description: "PostgreSQL database browser", icon: "simple-icons:adminer", href: "https://db.dimensionlab.net", - datasource: uptimeMonitor(17), + datasource: httpStatus("https://db.dimensionlab.net"), }), service({ id: "assistant", @@ -455,7 +411,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = { description: "Personal AI agent gateway", icon: "mdi:robot-outline", href: "https://assistant.dimensionlab.net", - datasource: uptimeMonitor(8), + datasource: httpStatus("https://assistant.dimensionlab.net/health"), }), service({ id: "suna", @@ -463,7 +419,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = { description: "AI command center", icon: "mdi:account-hard-hat-outline", href: "https://suna.dimensionlab.net", - datasource: uptimeMonitor(9), + datasource: httpStatus("https://suna.dimensionlab.net"), }), service({ id: "cockpit-infra", @@ -471,23 +427,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = { description: "linux-infra server console", icon: "simple-icons:cockpit", href: "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), + datasource: httpStatus("https://infra-cockpit.dimensionlab.net"), }), ]), group("runtime-health", "Runtime Health", [ @@ -497,7 +437,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = { description: "Public Git SSH relay", icon: "simple-icons:forgejo", href: "https://uptime.dimensionlab.net/status/dimensionlab", - datasource: uptimeMonitor(28), + datasource: custom("tcp:git.dimensionlab.net:22"), detail: "fallback - uptime monitor pending", }), service({ @@ -505,7 +445,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = { label: "PostgreSQL", description: "Shared application database", icon: "simple-icons:postgresql", - datasource: uptimeMonitor(22), + datasource: prometheus('pg_up{cluster="dimensionlab"}'), detail: "fallback - postgres exporter pending", }), service({ @@ -513,7 +453,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = { label: "Ollama API", description: "Local model API", icon: "simple-icons:ollama", - datasource: uptimeMonitor(23), + datasource: httpStatus("http://linux-gpu:11434/api/tags"), detail: "fallback - internal health check pending", }), service({ @@ -521,34 +461,10 @@ export const dimensionLabDashboardFixture: DashboardDocument = { label: "Node Exporter", description: "Host metrics exporter", icon: "simple-icons:prometheus", - 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), + datasource: prometheus('up{job="node-exporter"}'), detail: "fallback - exporter health from Prometheus", }), - ], "grid"), + ]), ], statusStrips: [ { @@ -558,7 +474,6 @@ export const dimensionLabDashboardFixture: DashboardDocument = { { 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: "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" }, ], }, ], @@ -635,17 +550,12 @@ function service(seed: ServiceSeed): ServiceEntry { }; } -function group( - id: string, - title: string, - services: ServiceEntry[], - layout: ServiceGroup["layout"] = "list", -): ServiceGroup { +function group(id: string, title: string, services: ServiceEntry[]): ServiceGroup { return { id, - layout, - services, title, + layout: "list", + services, }; } @@ -669,13 +579,17 @@ function httpStatus(url: string): DatasourceReference { }; } +function custom(reference: string): DatasourceReference { + return { + type: "external", + adapter: "custom", + reference, + }; +} + function placeholder(reason: string): DatasourceReference { return { type: "placeholder", reason, }; } - -function uptimeMonitor(id: number): DatasourceReference { - return httpStatus(`https://uptime.dimensionlab.net/_homepage-badge/${id}`); -} diff --git a/src/lib/server/dashboard.test.ts b/src/lib/server/dashboard.test.ts index b15fa6c..06c45b8 100644 --- a/src/lib/server/dashboard.test.ts +++ b/src/lib/server/dashboard.test.ts @@ -47,68 +47,6 @@ describe("dashboard runtime loader", () => { 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 () => { const store = await createTestStore(); @@ -131,20 +69,3 @@ async function createTestStore() { 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; -} diff --git a/src/lib/server/dashboard.ts b/src/lib/server/dashboard.ts index 7821ee0..d2bfc39 100644 --- a/src/lib/server/dashboard.ts +++ b/src/lib/server/dashboard.ts @@ -43,7 +43,6 @@ export interface DashboardRuntimeInvalid { } export interface DashboardRuntimeOptions { - refreshSeedDocument?: boolean; seedIfEmpty?: boolean; seedDocument?: DashboardDocument; } @@ -55,20 +54,8 @@ export function loadDashboardRuntime( const dashboardStore = store || createDashboardStore(); try { - const seedDocument = options.seedDocument || dimensionLabDashboardFixture; const active = dashboardStore.getActiveDashboard(); 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); } @@ -82,7 +69,7 @@ export function loadDashboardRuntime( } const seeded = dashboardStore.seedDashboardIfEmpty( - seedDocument, + options.seedDocument || dimensionLabDashboardFixture, { actor: "initial-seed", message: "load initial dashboard document", @@ -126,22 +113,3 @@ function invalidRuntimeState(errors: string[]): DashboardRuntimeInvalid { 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 - ); -} diff --git a/src/lib/server/datasources/dashboard-datasources.test.ts b/src/lib/server/datasources/dashboard-datasources.test.ts deleted file mode 100644 index 19082a2..0000000 --- a/src/lib/server/datasources/dashboard-datasources.test.ts +++ /dev/null @@ -1,240 +0,0 @@ -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], - }, - ], - }, - }); -} diff --git a/src/lib/server/datasources/index.ts b/src/lib/server/datasources/index.ts deleted file mode 100644 index 804fbb5..0000000 --- a/src/lib/server/datasources/index.ts +++ /dev/null @@ -1,675 +0,0 @@ -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 { - 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; - -interface PrometheusVectorResult { - metric?: Record; - value?: [number, string]; -} - -interface PrometheusMatrixResult { - metric?: Record; - 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 { - 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 { - return { - ...structuredClone(group), - services: await Promise.all( - group.services.map((service) => resolveService(service, context)), - ), - }; -} - -async function resolveService( - service: ServiceEntry, - context: DatasourceContext, -): Promise { - 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 { - 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 { - 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 { - 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 { - if (module.datasource?.type !== "external" || module.datasource.adapter !== "weather") { - return structuredClone(module); - } - - try { - const weather = await requestJson( - 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 { - 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 { - const url = prometheusApiUrl(context.prometheusBaseUrl, "/api/v1/query", { - query, - }); - const payload = await requestJson>( - context.fetch, - url, - context.requestTimeoutMs, - ); - return payload.status === "success" ? payload.data.result || [] : []; -} - -async function prometheusRangeQuery( - query: string, - context: DatasourceContext, -): Promise { - 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>( - context.fetch, - url, - context.requestTimeoutMs, - ); - return payload.status === "success" ? payload.data.result || [] : []; -} - -async function prometheusScalar( - query: string, - context: DatasourceContext, -): Promise { - 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 { - 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( - fetch: DatasourceFetch, - url: string, - timeoutMs: number, -): Promise { - 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; -} - -async function fetchWithTimeout( - fetch: DatasourceFetch, - url: string, - init: RequestInit, - timeoutMs: number, -): Promise { - 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 { - 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((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(); - - 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, - 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 { - 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 { - 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`; -} diff --git a/src/lib/ui/components/DashboardFrame.svelte b/src/lib/ui/components/DashboardFrame.svelte index 5f1f0e4..4a4fff8 100644 --- a/src/lib/ui/components/DashboardFrame.svelte +++ b/src/lib/ui/components/DashboardFrame.svelte @@ -50,13 +50,9 @@ diff --git a/src/lib/ui/components/FooterCell.svelte b/src/lib/ui/components/FooterCell.svelte index 3093e36..b71cc66 100644 --- a/src/lib/ui/components/FooterCell.svelte +++ b/src/lib/ui/components/FooterCell.svelte @@ -34,7 +34,7 @@ .footer-cell { display: grid; grid-template-columns: auto minmax(0, 1fr); - min-height: 1.9rem; + min-height: 2.65rem; align-items: center; background: rgba(2, 3, 2, 0.92); color: inherit; @@ -44,7 +44,7 @@ span, strong { min-width: 0; - padding: 0.34rem 0.52rem; + padding: var(--ui-space-2) var(--ui-space-3); overflow-wrap: anywhere; text-transform: uppercase; } @@ -53,13 +53,13 @@ height: 100%; border-right: var(--ui-border); color: var(--ui-color-muted); - font-size: 0.52rem; + font-size: 0.68rem; font-weight: 800; } strong { color: var(--ui-color-text); - font-size: 0.58rem; + font-size: 0.76rem; } .footer-cell[data-severity="ok"] strong { diff --git a/src/lib/ui/components/IconGlyph.svelte b/src/lib/ui/components/IconGlyph.svelte index a231f6d..81318d6 100644 --- a/src/lib/ui/components/IconGlyph.svelte +++ b/src/lib/ui/components/IconGlyph.svelte @@ -27,8 +27,8 @@ diff --git a/src/lib/ui/components/LineChart.svelte b/src/lib/ui/components/LineChart.svelte deleted file mode 100644 index f8cac24..0000000 --- a/src/lib/ui/components/LineChart.svelte +++ /dev/null @@ -1,147 +0,0 @@ - - - - - - - diff --git a/src/lib/ui/components/ModuleCard.svelte b/src/lib/ui/components/ModuleCard.svelte index 852af2d..2afeedf 100644 --- a/src/lib/ui/components/ModuleCard.svelte +++ b/src/lib/ui/components/ModuleCard.svelte @@ -37,13 +37,12 @@ .module-card { display: grid; grid-template-columns: minmax(0, 1fr) auto; - gap: 0.5rem; + gap: var(--ui-space-4); align-items: start; - min-width: 0; - min-height: 4.15rem; + min-width: min(100%, 18rem); border: var(--ui-border); background: rgba(6, 8, 7, 0.82); - padding: 0.5rem; + padding: var(--ui-space-3); } h2, @@ -53,29 +52,23 @@ h2 { color: var(--ui-color-muted); - font-size: 0.6rem; - line-height: 1; + font-size: 0.82rem; text-transform: uppercase; } strong { display: block; - margin-top: 0.15rem; + margin-top: var(--ui-space-1); font-family: var(--ui-font-display); - font-size: clamp(1.45rem, 2vw, 2rem); - line-height: 0.82; - overflow-wrap: anywhere; + font-size: 2.25rem; + line-height: 0.9; } p { - margin-top: 0.2rem; + margin-top: var(--ui-space-1); color: var(--ui-color-muted); - font-size: 0.52rem; - line-height: 1.05; - overflow: hidden; - text-overflow: ellipsis; + font-size: 0.72rem; text-transform: uppercase; - white-space: nowrap; } .module-card[data-severity="ok"] :global(.icon-glyph) { diff --git a/src/lib/ui/components/Panel.svelte b/src/lib/ui/components/Panel.svelte index 776e59d..cbc68e3 100644 --- a/src/lib/ui/components/Panel.svelte +++ b/src/lib/ui/components/Panel.svelte @@ -56,13 +56,13 @@ } .panel__header { - padding: 0.42rem 0.55rem 0; + padding: var(--ui-space-3) var(--ui-space-3) 0; } h2 { margin: 0; font-family: var(--ui-font-display); - font-size: clamp(1.45rem, 2.05vw, 1.95rem); + font-size: clamp(1.75rem, 3vw, 2.5rem); font-weight: 800; letter-spacing: 0; line-height: 0.9; @@ -71,8 +71,8 @@ .panel__body { display: grid; - gap: 0.25rem; - padding: 0.42rem 0.55rem 0.55rem; + gap: var(--ui-space-2); + padding: var(--ui-space-3); } .panel[data-density="compact"] .panel__body { diff --git a/src/lib/ui/components/ServicePanel.svelte b/src/lib/ui/components/ServicePanel.svelte index 864e09a..0b997d7 100644 --- a/src/lib/ui/components/ServicePanel.svelte +++ b/src/lib/ui/components/ServicePanel.svelte @@ -7,7 +7,7 @@ let { group }: { group: UiServiceGroup } = $props(); -
+
{#if group.summary?.length} diff --git a/src/lib/ui/components/ServiceRow.svelte b/src/lib/ui/components/ServiceRow.svelte index 91d6028..9a8c9b6 100644 --- a/src/lib/ui/components/ServiceRow.svelte +++ b/src/lib/ui/components/ServiceRow.svelte @@ -42,13 +42,13 @@ .service-row { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; - gap: 0.42rem; + gap: var(--ui-space-3); align-items: center; - min-height: 2.35rem; + min-height: 3.75rem; border: var(--ui-border); background: rgba(12, 13, 12, 0.72); color: inherit; - padding: 0.3rem 0.38rem; + padding: var(--ui-space-2); text-decoration: none; } @@ -79,25 +79,19 @@ } h3 { - overflow: hidden; - font-size: 0.66rem; + overflow-wrap: anywhere; + font-size: 0.88rem; font-weight: 850; letter-spacing: 0; - line-height: 1.05; - text-overflow: ellipsis; text-transform: uppercase; - white-space: nowrap; } p { - margin-top: 0.16rem; - overflow: hidden; + margin-top: 0.22rem; color: var(--ui-color-muted); - font-size: 0.5rem; - line-height: 1.08; - text-overflow: ellipsis; + font-size: 0.68rem; + line-height: 1.25; text-transform: uppercase; - white-space: nowrap; } @media (max-width: 580px) { diff --git a/src/lib/ui/components/StatusBadge.svelte b/src/lib/ui/components/StatusBadge.svelte index ba37351..a0fa058 100644 --- a/src/lib/ui/components/StatusBadge.svelte +++ b/src/lib/ui/components/StatusBadge.svelte @@ -15,16 +15,15 @@ diff --git a/src/lib/ui/components/TelemetryCard.svelte b/src/lib/ui/components/TelemetryCard.svelte index eb16e7e..f621709 100644 --- a/src/lib/ui/components/TelemetryCard.svelte +++ b/src/lib/ui/components/TelemetryCard.svelte @@ -2,7 +2,6 @@ import { formatMetricValue } from "../format"; import type { UiTelemetryCard } from "../types"; import IconGlyph from "./IconGlyph.svelte"; - import LineChart from "./LineChart.svelte"; let { card }: { card: UiTelemetryCard } = $props(); @@ -24,7 +23,9 @@
{/if} {#if card.sparkline?.length} - + {/if} {#if card.detail || card.description}

{card.detail || card.description}

@@ -34,12 +35,12 @@ diff --git a/src/lib/ui/components/TelemetryGrid.svelte b/src/lib/ui/components/TelemetryGrid.svelte index 388d0c9..aead537 100644 --- a/src/lib/ui/components/TelemetryGrid.svelte +++ b/src/lib/ui/components/TelemetryGrid.svelte @@ -14,21 +14,9 @@ diff --git a/src/lib/ui/components/render.test.ts b/src/lib/ui/components/render.test.ts index 4a8a145..fa55ecb 100644 --- a/src/lib/ui/components/render.test.ts +++ b/src/lib/ui/components/render.test.ts @@ -108,22 +108,6 @@ describe("dashboard UI components", () => { 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", () => { const button = render(Button, { props: { diff --git a/src/lib/ui/format.ts b/src/lib/ui/format.ts index 9a10434..4a8a5f1 100644 --- a/src/lib/ui/format.ts +++ b/src/lib/ui/format.ts @@ -10,8 +10,6 @@ export function formatMetricValue(metric: UiMetricValue): string { const unit = metric.unit ?? defaultUnit(metric.kind); 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}` : ""}`; } diff --git a/src/lib/ui/index.ts b/src/lib/ui/index.ts index 1281a76..398bcbe 100644 --- a/src/lib/ui/index.ts +++ b/src/lib/ui/index.ts @@ -9,7 +9,6 @@ export { default as FooterStatusCell } from "./components/FooterStatusCell.svelt export { default as GridFrame } from "./components/GridFrame.svelte"; export { default as IconGlyph } from "./components/IconGlyph.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 Panel } from "./components/Panel.svelte"; export { default as ProgressMeter } from "./components/ProgressMeter.svelte"; diff --git a/src/lib/ui/model-renderer.test.ts b/src/lib/ui/model-renderer.test.ts index 62942ee..def9d50 100644 --- a/src/lib/ui/model-renderer.test.ts +++ b/src/lib/ui/model-renderer.test.ts @@ -25,9 +25,6 @@ describe("dashboard model renderer", () => { expect(dashboard.serviceGroups.map((group) => group.id)).toEqual( dimensionLabDashboardFixture.layout.serviceGroups, ); - expect(dashboard.serviceGroups.find((group) => group.id === "runtime-health")?.layout).toBe( - "grid", - ); expect(dashboard.modules.map((module) => module.id)).toEqual([ "weather-amsterdam", "runtime-health-summary", @@ -37,7 +34,6 @@ describe("dashboard model renderer", () => { "footer-status:last-sync", "footer-status:uptime", "footer-status:load-avg", - "footer-status:auto-refresh", ]); expect(dashboard.statusStripId).toBe("footer-status"); }); diff --git a/src/lib/ui/model-renderer.ts b/src/lib/ui/model-renderer.ts index d5f3647..96e58f3 100644 --- a/src/lib/ui/model-renderer.ts +++ b/src/lib/ui/model-renderer.ts @@ -58,7 +58,6 @@ function telemetryToUi(card: TelemetryCard): UiTelemetryCard { function serviceGroupToUi(group: ServiceGroup): UiServiceGroup { return { id: group.id, - layout: group.layout, title: group.title, services: group.services.map(serviceToUi), }; diff --git a/src/lib/ui/stories/LineChart.stories.svelte b/src/lib/ui/stories/LineChart.stories.svelte deleted file mode 100644 index ef1ad22..0000000 --- a/src/lib/ui/stories/LineChart.stories.svelte +++ /dev/null @@ -1,13 +0,0 @@ - - - - - diff --git a/src/lib/ui/types.ts b/src/lib/ui/types.ts index 792a24b..55d8aee 100644 --- a/src/lib/ui/types.ts +++ b/src/lib/ui/types.ts @@ -52,7 +52,6 @@ export interface UiServiceRow { export interface UiServiceGroup { id: string; - layout?: "grid" | "list"; title: string; services: UiServiceRow[]; summary?: UiStatusItem[]; diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts index 36e913d..c779342 100644 --- a/src/routes/+page.server.ts +++ b/src/routes/+page.server.ts @@ -1,24 +1,7 @@ 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 { - dashboard: { - ...dashboard, - document: await resolveDashboardDatasources(dashboard.document), - }, + dashboard: loadDashboardRuntime(undefined, { seedIfEmpty: true }), }; } diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 29e61c9..b4c4f11 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,12 +1,9 @@ diff --git a/tests/e2e/dashboard.spec.ts b/tests/e2e/dashboard.spec.ts index c0d65f6..e7da1ff 100644 --- a/tests/e2e/dashboard.spec.ts +++ b/tests/e2e/dashboard.spec.ts @@ -6,8 +6,6 @@ const linkedServiceIds = [ "forgejo", "wiki", "aws-start", - "adguard-primary", - "adguard-secondary", "grafana", "uptime-kuma", "prometheus", @@ -16,13 +14,10 @@ const linkedServiceIds = [ "open-webui", "comfyui", "models", - "prompt-registry", "adminer", "assistant", "suna", "cockpit-infra", - "cockpit-gpu", - "cockpit-network-core", "forgejo-ssh-relay", ]; @@ -53,59 +48,6 @@ 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.skip(testInfo.project.name !== "chromium-mobile"); diff --git a/tests/e2e/dashboard.spec.ts-snapshots/dashboard-desktop-chromium-desktop-linux.png b/tests/e2e/dashboard.spec.ts-snapshots/dashboard-desktop-chromium-desktop-linux.png index 464f6e2..1fdca53 100644 Binary files a/tests/e2e/dashboard.spec.ts-snapshots/dashboard-desktop-chromium-desktop-linux.png and b/tests/e2e/dashboard.spec.ts-snapshots/dashboard-desktop-chromium-desktop-linux.png differ diff --git a/tests/e2e/dashboard.spec.ts-snapshots/dashboard-mobile-chromium-mobile-linux.png b/tests/e2e/dashboard.spec.ts-snapshots/dashboard-mobile-chromium-mobile-linux.png index 49cc902..282fee7 100644 Binary files a/tests/e2e/dashboard.spec.ts-snapshots/dashboard-mobile-chromium-mobile-linux.png and b/tests/e2e/dashboard.spec.ts-snapshots/dashboard-mobile-chromium-mobile-linux.png differ