diff --git a/prisma/migrations/0032_host_metric/migration.sql b/prisma/migrations/0032_host_metric/migration.sql new file mode 100644 index 000000000..e76902ec3 --- /dev/null +++ b/prisma/migrations/0032_host_metric/migration.sql @@ -0,0 +1,31 @@ +-- v1.4.16 phase B3: per-minute host-load snapshot table. +-- +-- The admin /system-status section now renders a 2-hour host-load chart +-- above the existing facts grid (Marc: "über dem Systemstatus vielleicht +-- nochmal so ein Graf der Auslastung des Hosts der letzten zwei Stunden"). +-- +-- A pg-boss `host-metric-sample` job inserts one row per minute carrying +-- `os.loadavg()` (1/5/15-minute) + `os.totalmem() - os.freemem()` and, +-- on Linux, a rolling cumulative byte counter from /proc/diskstats. +-- Disk fields are nullable so non-Linux dev hosts keep working. +-- +-- Retention: 7 days, enforced inside the sampler at the end of each +-- tick. Indexed by `captured_at` so the API's `WHERE captured_at >= +-- now() - 2h ORDER BY captured_at` query is index-only. + +CREATE TABLE "host_metrics" ( + "id" TEXT NOT NULL, + "captured_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "load_avg_1" DOUBLE PRECISION NOT NULL, + "load_avg_5" DOUBLE PRECISION NOT NULL, + "load_avg_15" DOUBLE PRECISION NOT NULL, + "mem_used_bytes" BIGINT NOT NULL, + "mem_total_bytes" BIGINT NOT NULL, + "disk_read_bytes" BIGINT, + "disk_write_bytes" BIGINT, + + CONSTRAINT "host_metrics_pkey" PRIMARY KEY ("id") +); + +CREATE INDEX "host_metrics_captured_at_idx" + ON "host_metrics" ("captured_at"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0b9aea443..7aa78a626 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -757,3 +757,29 @@ model UserAchievement { @@index([userId]) @@map("user_achievements") } + +// ─── Host Metrics (admin system-status host-load chart) ────── + +/// Per-minute snapshot of host-process load + memory captured by the +/// pg-boss `host-metric-sample` job. Read by `/api/admin/host-metrics` +/// to render the host-load trend above the system-status section. +/// 7-day retention is enforced inside the sampler — we keep the table +/// small enough that a single `findMany` over the last 2 hours stays +/// well under 200 rows. +model HostMetric { + id String @id @default(cuid()) + capturedAt DateTime @default(now()) @map("captured_at") + loadAvg1 Float @map("load_avg_1") + loadAvg5 Float @map("load_avg_5") + loadAvg15 Float @map("load_avg_15") + memUsedBytes BigInt @map("mem_used_bytes") + memTotalBytes BigInt @map("mem_total_bytes") + // Optional rolling-window byte counters from /proc/diskstats. Null + // outside Linux (macOS dev machines, Windows). The API endpoint + // converts deltas into bytes-per-second client-side. + diskReadBytes BigInt? @map("disk_read_bytes") + diskWriteBytes BigInt? @map("disk_write_bytes") + + @@index([capturedAt]) + @@map("host_metrics") +} diff --git a/tests/integration/setup.ts b/tests/integration/setup.ts index 846c6200b..5a5c0b1ba 100644 --- a/tests/integration/setup.ts +++ b/tests/integration/setup.ts @@ -43,6 +43,7 @@ export async function truncateAllTables(client: PrismaClient): Promise { "data_backups", "devices", "feedback", + "host_metrics", "idempotency_keys", "integration_statuses", "measurements",