feat(db): HostMetric model for host-load sampling

Adds the per-minute snapshot table that backs the new admin host-load
chart above /admin/system-status. One row per minute, captured by the
pg-boss sampler in the next commit, with 7-day retention enforced
inside the worker so the table stays under a few thousand rows.

Schema mirrors what the chart needs:
  - load_avg_1/5/15 (DOUBLE PRECISION) — os.loadavg() output
  - mem_used_bytes / mem_total_bytes — derived from os.totalmem() and
    os.freemem(), kept as raw bytes so the API can compute either a
    percent or a MiB delta downstream
  - disk_read_bytes / disk_write_bytes — nullable cumulative byte
    counters from /proc/diskstats. NULL on macOS/Windows dev hosts;
    Linux production picks them up automatically. The API endpoint
    converts deltas to bytes-per-second between samples

Indexed by captured_at so the "last 2 hours" query stays index-only.

Migration 0032_host_metric. integration setup.ts adds host_metrics to
the truncate list so testcontainers stays clean across runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 committed 2026-05-09 23:46:56 +02:00
1 parent 27f3933344
commit 5d1ece1fd5
3 files changed
+58

No files matched your search

@@ -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");
+26
View File
@@ -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")
}
+1
View File
@@ -43,6 +43,7 @@ export async function truncateAllTables(client: PrismaClient): Promise<void> {
"data_backups",
"devices",
"feedback",
"host_metrics",
"idempotency_keys",
"integration_statuses",
"measurements",