From 2a50c1f518ba4601f90e451185193e5dc6a7a5b5 Mon Sep 17 00:00:00 2001 From: Jonathan Date: Mon, 13 Jul 2026 00:16:43 +0200 Subject: [PATCH] feat(web): monitor config API route Implement GET and PATCH endpoints for monitor configuration, matching the existing discover config pattern. GET returns stored or default values; PATCH upserts known keys with allowlist filtering. Co-Authored-By: Claude Opus 4.8 (1M context) --- web/src/app/api/monitor/config/route.test.ts | 29 +++++++++++++++ web/src/app/api/monitor/config/route.ts | 38 ++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 web/src/app/api/monitor/config/route.test.ts create mode 100644 web/src/app/api/monitor/config/route.ts diff --git a/web/src/app/api/monitor/config/route.test.ts b/web/src/app/api/monitor/config/route.test.ts new file mode 100644 index 0000000..413199c --- /dev/null +++ b/web/src/app/api/monitor/config/route.test.ts @@ -0,0 +1,29 @@ +import { describe, it, expect } from "vitest"; +import { prisma } from "@/lib/db"; +import { GET, PATCH } from "./route"; + +function patch(body: unknown) { + return PATCH(new Request("http://localhost/api/monitor/config", { + method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify(body), + })); +} + +describe("monitor config API", () => { + it("GET returns defaults when nothing is stored", async () => { + const body = await (await GET()).json(); + expect(body).toMatchObject({ + "monitor.enabled": "false", "monitor.autoMonitorFuture": "false", + "monitor.pollIntervalHours": "24", "monitor.retryIntervalHours": "6", + "monitor.qualityCutoff": "2", "monitor.upgradeWindowDays": "14", + }); + }); + + it("PATCH upserts known keys and ignores unknown ones", async () => { + const res = await patch({ "monitor.enabled": "true", "monitor.pollIntervalHours": 12, "nope": "x" }); + expect((await res.json()).updated).toBe(2); + const body = await (await GET()).json(); + expect(body["monitor.enabled"]).toBe("true"); + expect(body["monitor.pollIntervalHours"]).toBe("12"); + expect(await prisma.config.findUnique({ where: { key: "nope" } })).toBeNull(); + }); +}); diff --git a/web/src/app/api/monitor/config/route.ts b/web/src/app/api/monitor/config/route.ts new file mode 100644 index 0000000..b8c83b0 --- /dev/null +++ b/web/src/app/api/monitor/config/route.ts @@ -0,0 +1,38 @@ +import { prisma } from "@/lib/db"; + +const DEFAULTS: Record = { + "monitor.enabled": "false", + "monitor.autoMonitorFuture": "false", + "monitor.pollIntervalHours": "24", + "monitor.retryIntervalHours": "6", + "monitor.qualityCutoff": "2", + "monitor.upgradeWindowDays": "14", +}; + +export async function GET() { + const rows = await prisma.config.findMany({ where: { key: { in: Object.keys(DEFAULTS) } } }); + const stored = Object.fromEntries(rows.map((r) => [r.key, r.value])); + return Response.json( + Object.fromEntries(Object.entries(DEFAULTS).map(([k, d]) => [k, stored[k] ?? d])), + ); +} + +export async function PATCH(request: Request) { + let body: unknown; + try { + body = await request.json(); + } catch { + return Response.json({ error: "invalid JSON" }, { status: 400 }); + } + const entries = Object.entries((body ?? {}) as Record).filter( + ([k]) => k in DEFAULTS, + ); + for (const [key, value] of entries) { + await prisma.config.upsert({ + where: { key }, + create: { key, value: String(value), secret: false }, + update: { value: String(value) }, + }); + } + return Response.json({ updated: entries.length }); +}