diff --git a/README.md b/README.md index 9981182..c49bbd4 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,15 @@ Stored dashboard documents pass through a version migration boundary before reads or writes; the MVP supports `dashboard.v1` and fails unsupported versions with an explicit migration error. +## Seed Data + +The initial Dimension Lab dashboard lives in +`src/lib/model/fixtures/dimensionlab.ts` as validated model data. It includes +the first-screen telemetry, service groups, status strip, weather module, +Iconify icon identifiers, links, and datasource references. Values that are not +live yet are labeled as fallback values in the data so later datasource adapters +can replace them without changing presentation components. + ## Runtime Shape The SvelteKit build uses the Node adapter, and the current persistence runtime diff --git a/src/lib/model/fixtures/dimensionlab.test.ts b/src/lib/model/fixtures/dimensionlab.test.ts new file mode 100644 index 0000000..ae13cbf --- /dev/null +++ b/src/lib/model/fixtures/dimensionlab.test.ts @@ -0,0 +1,146 @@ +import { describe, expect, test } from "vitest"; +import { + dimensionLabDashboardFixture, +} from "./dimensionlab"; +import type { + DashboardDocument, + DatasourceReference, + ServiceEntry, + TelemetryCard, +} from "../schema"; + +describe("Dimension Lab dashboard seed", () => { + test("defines the primary first-screen sections as model data", () => { + expect(dimensionLabDashboardFixture.metadata.title).toBe("System Overview"); + expect(dimensionLabDashboardFixture.layout.telemetry).toHaveLength(16); + expect(dimensionLabDashboardFixture.layout.serviceGroups).toEqual([ + "essentials", + "monitoring", + "ai-automation", + "systems", + "runtime-health", + ]); + expect(dimensionLabDashboardFixture.layout.statusStrips).toEqual([ + "footer-status", + ]); + expect(dimensionLabDashboardFixture.layout.modules).toEqual([ + "weather-amsterdam", + "runtime-health-summary", + ]); + }); + + test("keeps icon, link, and datasource references in seed data", () => { + const iconIds = [ + ...dimensionLabDashboardFixture.telemetry.map((card) => card.icon), + ...allServices(dimensionLabDashboardFixture).map((service) => service.icon), + ...(dimensionLabDashboardFixture.modules || []).map((module) => module.icon), + ].filter((icon): icon is string => Boolean(icon)); + + expect( + dimensionLabDashboardFixture.telemetry.every( + (card) => card.icon && card.datasource, + ), + ).toBe(true); + expect( + dimensionLabDashboardFixture.telemetry.some( + (card) => card.datasource?.type === "external" && + card.datasource.adapter === "prometheus", + ), + ).toBe(true); + expect( + allServices(dimensionLabDashboardFixture).some( + (service) => service.datasource?.type === "external" && + service.datasource.adapter === "http-status", + ), + ).toBe(true); + + for (const service of allServices(dimensionLabDashboardFixture)) { + expect(service.icon, `${service.id} must declare an Iconify id`).toBeTruthy(); + expect(verifiedSeedIconIds.has(service.icon || ""), `${service.id} icon must be verified`).toBe(true); + expect(service.datasource, `${service.id} must declare health source`).toBeTruthy(); + if (service.link) { + expect(service.link.label, `${service.id} link needs an accessible label`).toBeTruthy(); + expect(service.link.external).toBe(true); + } + } + + for (const icon of iconIds) { + expect(verifiedSeedIconIds.has(icon), `${icon} must be verified`).toBe(true); + } + }); + + test("labels fallback values and unresolved adapters explicitly", () => { + const placeholders = collectDatasources(dimensionLabDashboardFixture).filter( + (datasource) => datasource.type === "placeholder", + ); + const fallbackText = [ + ...dimensionLabDashboardFixture.telemetry.map((card) => card.detail || ""), + ...allServices(dimensionLabDashboardFixture).map((service) => service.detail || ""), + ...dimensionLabDashboardFixture.statusStrips.flatMap((strip) => + strip.items.map((item) => item.value), + ), + ...(dimensionLabDashboardFixture.modules || []).map((module) => module.detail || ""), + ].join("\n").toLowerCase(); + + expect(fallbackText).toContain("fallback"); + for (const service of allServices(dimensionLabDashboardFixture)) { + expect(service.detail?.toLowerCase(), `${service.id} detail must label fallback state`).toContain("fallback"); + } + for (const statusItem of dimensionLabDashboardFixture.statusStrips.flatMap((strip) => strip.items)) { + expect(statusItem.value.toLowerCase(), `${statusItem.id} value must label fallback state`).toContain("fallback"); + } + expect(placeholders.length).toBeGreaterThan(0); + for (const datasource of placeholders) { + expect(datasource.reason.toLowerCase()).toMatch(/fallback|pending|unresolved/); + } + }); +}); + +function allServices(document: DashboardDocument): ServiceEntry[] { + return document.serviceGroups.flatMap((group) => group.services); +} + +function collectDatasources(document: DashboardDocument): DatasourceReference[] { + const telemetry = document.telemetry + .map((card: TelemetryCard) => card.datasource) + .filter((datasource): datasource is DatasourceReference => Boolean(datasource)); + const services = allServices(document) + .map((service) => service.datasource) + .filter((datasource): datasource is DatasourceReference => Boolean(datasource)); + const modules = (document.modules || []) + .map((module) => module.datasource) + .filter((datasource): datasource is DatasourceReference => Boolean(datasource)); + + return [...telemetry, ...services, ...modules]; +} + +const verifiedSeedIconIds = new Set([ + "mdi:account-hard-hat-outline", + "mdi:backup-restore", + "mdi:brain", + "mdi:cpu-64-bit", + "mdi:docker", + "mdi:expansion-card", + "mdi:fan", + "mdi:harddisk", + "mdi:image-edit-outline", + "mdi:memory", + "mdi:pulse", + "mdi:robot-outline", + "mdi:router-network", + "mdi:thermometer", + "mdi:web", + "mdi:weather-sunny", + "simple-icons:adminer", + "simple-icons:amazonwebservices", + "simple-icons:cockpit", + "simple-icons:forgejo", + "simple-icons:grafana", + "simple-icons:n8n", + "simple-icons:ollama", + "simple-icons:postgresql", + "simple-icons:prometheus", + "simple-icons:uptimekuma", + "simple-icons:vaultwarden", + "simple-icons:wikidotjs", +]); diff --git a/src/lib/model/fixtures/dimensionlab.ts b/src/lib/model/fixtures/dimensionlab.ts index be23b02..9890fef 100644 --- a/src/lib/model/fixtures/dimensionlab.ts +++ b/src/lib/model/fixtures/dimensionlab.ts @@ -1,14 +1,48 @@ import { DASHBOARD_SCHEMA_VERSION, type DashboardDocument, + type DatasourceReference, + type MetricValue, + type ServiceEntry, + type ServiceGroup, + type Severity, + type TelemetryCard, } from "../schema"; +type NumericValueKind = "percent" | "bytes" | "temperature" | "latency" | "number"; + +interface MetricSeed { + id: string; + label: string; + icon: string; + kind: NumericValueKind; + value: number; + detail: string; + severity: Severity; + datasource: DatasourceReference; + thresholds?: TelemetryCard["thresholds"]; + sparkline?: number[]; + precision?: number; +} + +interface ServiceSeed { + id: string; + label: string; + description: string; + icon: string; + datasource: DatasourceReference; + href?: string; + severity?: Severity; + detail?: string; +} + export const dimensionLabDashboardFixture: DashboardDocument = { schemaVersion: DASHBOARD_SCHEMA_VERSION, metadata: { title: "System Overview", - subtitle: "Capacity, noise & top consumers", - description: "Dimension Lab operational dashboard seed data.", + 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", refreshIntervalSeconds: 15, }, @@ -34,130 +68,528 @@ export const dimensionLabDashboardFixture: DashboardDocument = { ], serviceGroups: ["essentials", "monitoring", "ai-automation", "systems", "runtime-health"], statusStrips: ["footer-status"], - modules: ["weather"], + modules: ["weather-amsterdam", "runtime-health-summary"], }, telemetry: [ - metric("infra-ram", "Infra RAM", "percent", 19, "linux-infra", "ok"), - metric("gpu-host-ram", "GPU Host RAM", "percent", 17, "linux", "ok"), - metric("network-ram", "Network RAM", "percent", 5, "network-core", "ok"), - metric("user-disk-peak", "User Disk Peak", "percent", 29, "linux /home", "ok"), - metric("system-disk-peak", "System Disk Peak", "percent", 49, "linux /boot", "ok"), - metric("peak-cpu-busy", "Peak CPU Busy", "percent", 3, "linux-infra", "ok"), - metric("top-cpu-container", "Top CPU Container", "percent", 7.9, "container placeholder", "ok"), - metric("top-ram-container", "Top RAM Container", "bytes", 5.2 * 1024 ** 3, "container placeholder", "danger"), - metric("gpu-3060-load", "3060 GPU Load", "percent", 0, "RTX 3060", "ok"), - metric("gpu-3060-vram", "3060 VRAM", "percent", 0, "RTX 3060", "ok"), - metric("gpu-3060-temp", "3060 Temp", "temperature", 54, "RTX 3060", "ok"), - metric("gpu-3060-fan", "3060 Fan Spin", "percent", 0, "RTX 3060", "ok"), - metric("gpu-3090-load", "3090 GPU Load", "percent", 0, "RTX 3090", "ok"), - metric("gpu-3090-vram", "3090 VRAM", "percent", 74, "RTX 3090", "warning"), - metric("gpu-3090-temp", "3090 Temp", "temperature", 50, "RTX 3090", "ok"), - metric("gpu-3090-fan", "3090 Fan Spin", "percent", 0, "RTX 3090", "ok"), + metric({ + id: "infra-ram", + label: "Infra RAM", + icon: "mdi:memory", + kind: "percent", + value: 19, + detail: "fallback - linux-infra memory used", + severity: "ok", + thresholds: percentThresholds(), + datasource: prometheus( + '100 * (1 - node_memory_MemAvailable_bytes{instance="linux-infra"} / node_memory_MemTotal_bytes{instance="linux-infra"})', + ), + sparkline: [18, 18, 19, 19, 18, 19], + }), + metric({ + id: "gpu-host-ram", + label: "GPU Host RAM", + icon: "mdi:memory", + kind: "percent", + value: 17, + detail: "fallback - GPU host memory used", + severity: "ok", + thresholds: percentThresholds(), + datasource: prometheus( + '100 * (1 - node_memory_MemAvailable_bytes{instance="linux-gpu"} / node_memory_MemTotal_bytes{instance="linux-gpu"})', + ), + sparkline: [16, 16, 17, 17, 17, 17], + }), + metric({ + id: "network-ram", + label: "Network RAM", + icon: "mdi:router-network", + kind: "percent", + value: 5, + detail: "fallback - network core memory used", + severity: "ok", + thresholds: percentThresholds(), + datasource: prometheus( + '100 * (1 - node_memory_MemAvailable_bytes{instance="network-core"} / node_memory_MemTotal_bytes{instance="network-core"})', + ), + sparkline: [5, 5, 5, 6, 5, 5], + }), + metric({ + id: "user-disk-peak", + label: "User Disk Peak", + icon: "mdi:harddisk", + kind: "percent", + value: 29, + detail: "fallback - /home peak usage", + severity: "ok", + thresholds: percentThresholds(80, 92), + datasource: prometheus( + '100 * (1 - node_filesystem_avail_bytes{mountpoint="/home"} / node_filesystem_size_bytes{mountpoint="/home"})', + ), + sparkline: [27, 27, 28, 29, 29, 29], + }), + metric({ + id: "system-disk-peak", + label: "System Disk Peak", + icon: "mdi:harddisk", + kind: "percent", + value: 49, + detail: "fallback - /boot peak usage", + severity: "ok", + thresholds: percentThresholds(80, 92), + datasource: prometheus( + '100 * (1 - node_filesystem_avail_bytes{mountpoint="/boot"} / node_filesystem_size_bytes{mountpoint="/boot"})', + ), + sparkline: [48, 48, 49, 49, 49, 49], + }), + metric({ + id: "peak-cpu-busy", + label: "Peak CPU Busy", + icon: "mdi:cpu-64-bit", + kind: "percent", + value: 3, + detail: "fallback - linux-infra 5m CPU busy", + severity: "ok", + thresholds: percentThresholds(75, 90), + datasource: prometheus( + '100 - (avg by(instance) (rate(node_cpu_seconds_total{instance="linux-infra",mode="idle"}[5m])) * 100)', + ), + sparkline: [2, 3, 3, 4, 3, 3], + }), + metric({ + id: "top-cpu-container", + label: "Top CPU Container", + icon: "mdi:docker", + kind: "percent", + value: 7.9, + precision: 1, + detail: "fallback - top container CPU pending label mapping", + severity: "ok", + thresholds: percentThresholds(70, 90), + 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({ + id: "top-ram-container", + label: "Top RAM Container", + icon: "mdi:docker", + kind: "bytes", + value: Math.round(5.2 * 1024 ** 3), + precision: 1, + detail: "fallback - top container RAM pending label mapping", + severity: "danger", + thresholds: { warning: 3 * 1024 ** 3, danger: 5 * 1024 ** 3 }, + datasource: placeholder( + "fallback top-container memory query pending container label normalization", + ), + sparkline: [ + 3.1 * 1024 ** 3, + 3.5 * 1024 ** 3, + 4.1 * 1024 ** 3, + 4.8 * 1024 ** 3, + 5.0 * 1024 ** 3, + 5.2 * 1024 ** 3, + ], + }), + metric({ + id: "gpu-3060-load", + label: "3060 GPU Load", + icon: "mdi:expansion-card", + kind: "percent", + value: 0, + detail: "fallback - RTX 3060 GPU utilization", + severity: "ok", + thresholds: percentThresholds(85, 95), + datasource: prometheus('DCGM_FI_DEV_GPU_UTIL{gpu="rtx-3060"}'), + sparkline: [0, 0, 0, 0, 0, 0], + }), + metric({ + id: "gpu-3060-vram", + label: "3060 VRAM", + icon: "mdi:memory", + kind: "percent", + value: 0, + detail: "fallback - RTX 3060 VRAM used", + severity: "ok", + thresholds: percentThresholds(80, 92), + 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"})', + ), + sparkline: [0, 0, 0, 0, 0, 0], + }), + metric({ + id: "gpu-3060-temp", + label: "3060 Temp", + icon: "mdi:thermometer", + kind: "temperature", + value: 54, + detail: "fallback - RTX 3060 temperature", + severity: "ok", + thresholds: { warning: 75, danger: 85 }, + datasource: prometheus('DCGM_FI_DEV_GPU_TEMP{gpu="rtx-3060"}'), + sparkline: [52, 53, 54, 54, 53, 54], + }), + metric({ + id: "gpu-3060-fan", + label: "3060 Fan Spin", + icon: "mdi:fan", + kind: "percent", + value: 0, + detail: "fallback - RTX 3060 fan duty", + severity: "ok", + thresholds: percentThresholds(80, 95), + datasource: prometheus('DCGM_FI_DEV_FAN_SPEED{gpu="rtx-3060"}'), + sparkline: [0, 0, 0, 0, 0, 0], + }), + metric({ + id: "gpu-3090-load", + label: "3090 GPU Load", + icon: "mdi:expansion-card", + kind: "percent", + value: 0, + detail: "fallback - RTX 3090 GPU utilization", + severity: "ok", + thresholds: percentThresholds(85, 95), + datasource: prometheus('DCGM_FI_DEV_GPU_UTIL{gpu="rtx-3090"}'), + sparkline: [0, 0, 0, 0, 0, 0], + }), + metric({ + id: "gpu-3090-vram", + label: "3090 VRAM", + icon: "mdi:memory", + kind: "percent", + value: 74, + detail: "fallback - RTX 3090 VRAM used", + severity: "warning", + thresholds: percentThresholds(70, 90), + 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"})', + ), + sparkline: [68, 70, 72, 74, 73, 74], + }), + metric({ + id: "gpu-3090-temp", + label: "3090 Temp", + icon: "mdi:thermometer", + kind: "temperature", + value: 50, + detail: "fallback - RTX 3090 temperature", + severity: "ok", + thresholds: { warning: 75, danger: 85 }, + datasource: prometheus('DCGM_FI_DEV_GPU_TEMP{gpu="rtx-3090"}'), + sparkline: [49, 50, 50, 51, 50, 50], + }), + metric({ + id: "gpu-3090-fan", + label: "3090 Fan Spin", + icon: "mdi:fan", + kind: "percent", + value: 0, + detail: "fallback - RTX 3090 fan duty", + severity: "ok", + thresholds: percentThresholds(80, 95), + datasource: prometheus('DCGM_FI_DEV_FAN_SPEED{gpu="rtx-3090"}'), + sparkline: [0, 0, 0, 0, 0, 0], + }), ], serviceGroups: [ group("essentials", "Essentials", [ - service("vaultwarden", "Vaultwarden", "Password manager", "simple-icons:vaultwarden", "https://vault.dimensionlab.net"), - service("forgejo", "Forgejo", "Git repositories", "simple-icons:forgejo", "https://git.dimensionlab.net"), - service("wiki", "Wiki", "Internal documentation", "simple-icons:wikidotjs", "https://wiki.dimensionlab.net"), - service("aws-start", "AWS Start", "AWS access portal", "simple-icons:amazonaws", "https://dimensionlab.awsapps.com/start"), + service({ + id: "vaultwarden", + label: "Vaultwarden", + description: "Password manager", + icon: "simple-icons:vaultwarden", + href: "https://vault.dimensionlab.net", + datasource: httpStatus("https://vault.dimensionlab.net/alive"), + }), + service({ + id: "forgejo", + label: "Forgejo", + description: "Git repositories", + icon: "simple-icons:forgejo", + href: "https://git.dimensionlab.net", + datasource: httpStatus("https://git.dimensionlab.net/api/healthz"), + }), + service({ + id: "wiki", + label: "Wiki", + description: "Internal documentation", + icon: "simple-icons:wikidotjs", + href: "https://wiki.dimensionlab.net", + datasource: httpStatus("https://wiki.dimensionlab.net"), + }), + service({ + id: "aws-start", + label: "AWS Start", + description: "AWS access portal", + icon: "simple-icons:amazonwebservices", + href: "https://dimensionlab.awsapps.com/start", + datasource: httpStatus("https://dimensionlab.awsapps.com/start"), + }), ]), group("monitoring", "Monitoring", [ - service("grafana", "Grafana", "Capacity and noise dashboard", "simple-icons:grafana", "https://grafana.dimensionlab.net"), - service("uptime-kuma", "Uptime Kuma", "Service uptime checks", "simple-icons:uptimekuma", "https://uptime.dimensionlab.net"), - service("prometheus", "Prometheus", "Metrics database", "simple-icons:prometheus", "https://prometheus.dimensionlab.net"), - service("backrest", "Backrest", "Restic backup manager", "mdi:backup-restore", "https://backups.dimensionlab.net"), + service({ + id: "grafana", + label: "Grafana", + description: "Capacity and noise dashboard", + icon: "simple-icons:grafana", + href: "https://grafana.dimensionlab.net", + datasource: httpStatus("https://grafana.dimensionlab.net/api/health"), + }), + service({ + id: "uptime-kuma", + label: "Uptime Kuma", + description: "Service uptime checks", + icon: "simple-icons:uptimekuma", + href: "https://uptime.dimensionlab.net", + datasource: httpStatus("https://uptime.dimensionlab.net/status/dimensionlab"), + }), + service({ + id: "prometheus", + label: "Prometheus", + description: "Metrics database", + icon: "simple-icons:prometheus", + href: "https://prometheus.dimensionlab.net", + datasource: httpStatus("https://prometheus.dimensionlab.net/-/ready"), + }), + service({ + id: "backrest", + label: "Backrest", + description: "Restic backup manager", + icon: "mdi:backup-restore", + href: "https://backups.dimensionlab.net", + datasource: httpStatus("https://backups.dimensionlab.net"), + }), ]), group("ai-automation", "AI & Automation", [ - service("n8n", "n8n", "Workflow automation", "simple-icons:n8n", "https://workflows.dimensionlab.net"), - service("open-webui", "Open WebUI", "Chat and model interface", "simple-icons:openwebui", "https://webui.dimensionlab.net"), - service("comfyui", "ComfyUI", "Image generation workflows", "mdi:image-edit-outline", "https://comfy.dimensionlab.net"), - service("models", "Models", "Local model management", "mdi:brain", "https://models.dimensionlab.net"), + service({ + id: "n8n", + label: "n8n", + description: "Workflow automation", + icon: "simple-icons:n8n", + href: "https://workflows.dimensionlab.net", + datasource: httpStatus("https://workflows.dimensionlab.net/healthz"), + }), + service({ + id: "open-webui", + label: "Open WebUI", + description: "Chat and model interface", + icon: "mdi:web", + href: "https://webui.dimensionlab.net", + datasource: httpStatus("https://webui.dimensionlab.net/health"), + }), + service({ + id: "comfyui", + label: "ComfyUI", + description: "Image generation workflows", + icon: "mdi:image-edit-outline", + href: "https://comfy.dimensionlab.net", + datasource: httpStatus("https://comfy.dimensionlab.net"), + }), + service({ + id: "models", + label: "Models", + description: "Local model management", + icon: "mdi:brain", + href: "https://models.dimensionlab.net", + datasource: httpStatus("https://models.dimensionlab.net/api/tags"), + }), ]), group("systems", "Systems", [ - service("adminer", "Adminer", "PostgreSQL database browser", "simple-icons:adminer", "https://db.dimensionlab.net"), - service("assistant", "Assistant", "Personal AI agent gateway", "mdi:robot-outline", "https://assistant.dimensionlab.net"), - service("suna", "Suna", "AI command center", "mdi:account-hard-hat-outline", "https://suna.dimensionlab.net"), - service("cockpit-infra", "Cockpit Infra", "linux-infra server console", "simple-icons:cockpit", "https://infra-cockpit.dimensionlab.net"), + service({ + id: "adminer", + label: "Adminer", + description: "PostgreSQL database browser", + icon: "simple-icons:adminer", + href: "https://db.dimensionlab.net", + datasource: httpStatus("https://db.dimensionlab.net"), + }), + service({ + id: "assistant", + label: "Assistant", + description: "Personal AI agent gateway", + icon: "mdi:robot-outline", + href: "https://assistant.dimensionlab.net", + datasource: httpStatus("https://assistant.dimensionlab.net/health"), + }), + service({ + id: "suna", + label: "Suna", + description: "AI command center", + icon: "mdi:account-hard-hat-outline", + href: "https://suna.dimensionlab.net", + datasource: httpStatus("https://suna.dimensionlab.net"), + }), + service({ + id: "cockpit-infra", + label: "Cockpit Infra", + description: "linux-infra server console", + icon: "simple-icons:cockpit", + href: "https://infra-cockpit.dimensionlab.net", + datasource: httpStatus("https://infra-cockpit.dimensionlab.net"), + }), ]), group("runtime-health", "Runtime Health", [ - service("forgejo-ssh-relay", "Forgejo SSH Relay", "Public Git SSH relay", "simple-icons:forgejo", "https://uptime.dimensionlab.net/status/dimensionlab"), - service("postgresql", "PostgreSQL", "Shared application database", "simple-icons:postgresql"), - service("ollama-api", "Ollama API", "Local model API", "simple-icons:ollama"), - service("node-exporter", "Node Exporter", "Host metrics exporter", "simple-icons:prometheus"), + service({ + id: "forgejo-ssh-relay", + label: "Forgejo SSH Relay", + description: "Public Git SSH relay", + icon: "simple-icons:forgejo", + href: "https://uptime.dimensionlab.net/status/dimensionlab", + datasource: custom("tcp:git.dimensionlab.net:22"), + detail: "fallback - uptime monitor pending", + }), + service({ + id: "postgresql", + label: "PostgreSQL", + description: "Shared application database", + icon: "simple-icons:postgresql", + datasource: prometheus('pg_up{cluster="dimensionlab"}'), + detail: "fallback - postgres exporter pending", + }), + service({ + id: "ollama-api", + label: "Ollama API", + description: "Local model API", + icon: "simple-icons:ollama", + datasource: httpStatus("http://linux-gpu:11434/api/tags"), + detail: "fallback - internal health check pending", + }), + service({ + id: "node-exporter", + label: "Node Exporter", + description: "Host metrics exporter", + icon: "simple-icons:prometheus", + datasource: prometheus('up{job="node-exporter"}'), + detail: "fallback - exporter health from Prometheus", + }), ]), ], statusStrips: [ { id: "footer-status", items: [ - { id: "system-status", label: "System Status", value: "All systems operational", severity: "ok" }, - { id: "last-sync", label: "Last Sync", value: "2 minutes ago", severity: "stale" }, - { id: "uptime", label: "Uptime", value: "28d 14h 32m", severity: "ok" }, - { id: "load-avg", label: "Load Avg", value: "0.47 0.53 0.59", severity: "neutral" }, + { id: "system-status", label: "System Status", value: "Fallback operational", severity: "ok" }, + { 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" }, ], }, ], modules: [ { - id: "weather", + id: "weather-amsterdam", kind: "weather", title: "Amsterdam", value: "28.3 C", - detail: "Clear", + detail: "fallback - weather adapter pending", icon: "mdi:weather-sunny", severity: "ok", - datasource: { type: "placeholder", reason: "weather adapter pending" }, + 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 - health adapters pending", + icon: "mdi:pulse", + severity: "stale", + datasource: placeholder("summary pending live health aggregation"), }, ], }; -type NumericValueKind = "percent" | "bytes" | "temperature" | "latency" | "number"; -type Severity = "neutral" | "ok" | "warning" | "danger" | "stale" | "unavailable"; +function metric(seed: MetricSeed): TelemetryCard { + return { + id: seed.id, + label: seed.label, + icon: seed.icon, + value: metricValue(seed.kind, seed.value, seed.precision), + detail: seed.detail, + severity: seed.severity, + thresholds: seed.thresholds, + datasource: seed.datasource, + sparkline: seed.sparkline, + }; +} -function metric( - id: string, - label: string, +function metricValue( kind: NumericValueKind, value: number, - detail: string, - severity: Severity, -) { + precision?: number, +): MetricValue { + return precision === undefined ? { kind, value } : { kind, value, precision }; +} + +function service(seed: ServiceSeed): ServiceEntry { + const entry: ServiceEntry = { + id: seed.id, + label: seed.label, + description: seed.description, + icon: seed.icon, + severity: seed.severity || "ok", + detail: seed.detail || "fallback - health check pending", + datasource: seed.datasource, + }; + + if (!seed.href) return entry; + return { - id, - label, - value: { kind, value }, - detail, - severity, - datasource: { type: "placeholder" as const, reason: "live datasource pending" }, - sparkline: [10, 18, 15, 28, 24, 32], + ...entry, + link: { + href: seed.href, + label: `Open ${seed.label}`, + external: true, + }, }; } -function service( - id: string, - label: string, - description: string, - icon: string, - href?: string, -) { - const entry = { - id, - label, - description, - icon, - severity: "ok" as const, - detail: "ready", - datasource: { type: "placeholder" as const, reason: "service health adapter pending" }, - }; - - return href ? { ...entry, link: { href, external: true } } : entry; -} - -function group(id: string, title: string, services: ReturnType[]) { +function group(id: string, title: string, services: ServiceEntry[]): ServiceGroup { return { id, title, - layout: "list" as const, + layout: "list", services, }; } + +function percentThresholds(warning = 80, danger = 92) { + return { warning, danger }; +} + +function prometheus(reference: string): DatasourceReference { + return { + type: "external", + adapter: "prometheus", + reference, + }; +} + +function httpStatus(url: string): DatasourceReference { + return { + type: "external", + adapter: "http-status", + reference: `GET ${url}`, + }; +} + +function custom(reference: string): DatasourceReference { + return { + type: "external", + adapter: "custom", + reference, + }; +} + +function placeholder(reason: string): DatasourceReference { + return { + type: "placeholder", + reason, + }; +} diff --git a/src/lib/server/dashboard.ts b/src/lib/server/dashboard.ts index 244e97b..56380a4 100644 --- a/src/lib/server/dashboard.ts +++ b/src/lib/server/dashboard.ts @@ -17,8 +17,8 @@ export interface PlaceholderDashboard { export function loadPlaceholderDashboard(): PlaceholderDashboard { return { - title: "System Overview", - subtitle: "Dashboard runtime scaffold", + title: "Dashboard Runtime", + subtitle: "Runtime scaffold", status: "building", message: "SvelteKit is running. Later issues will replace this placeholder with the validated dashboard model.", @@ -30,8 +30,8 @@ export function loadDashboardRuntime(store?: DashboardStore): PlaceholderDashboa try { const seeded = dashboardStore.seedDashboardIfEmpty(dimensionLabDashboardFixture, { - actor: "dimensionlab-seed", - message: "load initial dashboard seed", + actor: "initial-seed", + message: "load initial dashboard document", }); const active = dashboardStore.getActiveDashboard(); const document = active?.document || seeded.document;