Turns the admin panel into a real monitoring tool so production bugs are visible instead of silent. - Error & crash tracking: error_events table (migration 0010) + logError() helper + /api/errors ingest; global-error.tsx and (app)/error.tsx report crashes automatically; /admin/errors viewer (recent + grouped, filters). - Full audit-log viewer at /admin/audit over the existing audit_log (all actions, not just auth) with action/resource/family/user/text filters. - AI observability at /admin/ai over ai_usage: per-intent latency (avg/p95), tokens, cost, daily trend, slowest calls, medical-redirect count. - System health at /admin/health: DB latency, migration status, recent error volume, and integration config presence. - Sidebar updated with Health / Errors / Audit Log / AI Usage. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
77 lines
3.4 KiB
TypeScript
77 lines
3.4 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { requireAdmin } from "@/lib/admin-auth";
|
|
import { sql } from "@/db";
|
|
|
|
type Check = { name: string; status: "ok" | "warn" | "down"; detail: string };
|
|
|
|
// System health snapshot: DB connectivity + latency, migration status, recent
|
|
// error volume, and which integrations are configured. Read-only and cheap —
|
|
// no external round-trips, just config presence + DB queries.
|
|
export async function GET(request: Request) {
|
|
const auth = await requireAdmin(request);
|
|
if (!auth.success) return NextResponse.json({ error: auth.error }, { status: auth.status });
|
|
|
|
const checks: Check[] = [];
|
|
|
|
// 1. Database connectivity + latency
|
|
let dbOk = false;
|
|
try {
|
|
const t0 = Date.now();
|
|
await sql`SELECT 1`;
|
|
const ms = Date.now() - t0;
|
|
dbOk = true;
|
|
checks.push({ name: "Database", status: ms < 500 ? "ok" : "warn", detail: `Connected — ${ms}ms` });
|
|
} catch (e) {
|
|
checks.push({ name: "Database", status: "down", detail: String(e).slice(0, 200) });
|
|
}
|
|
|
|
// 2. Migrations applied (drizzle stores its journal in drizzle.__drizzle_migrations)
|
|
if (dbOk) {
|
|
try {
|
|
const rows = await sql`
|
|
SELECT COUNT(*)::int AS count, MAX(created_at) AS latest
|
|
FROM drizzle.__drizzle_migrations
|
|
`;
|
|
const count = Number(rows[0]?.count) || 0;
|
|
const latest = rows[0]?.latest ? new Date(Number(rows[0].latest)).toISOString().split("T")[0] : "—";
|
|
checks.push({ name: "Migrations", status: count > 0 ? "ok" : "warn", detail: `${count} applied (latest ${latest})` });
|
|
} catch {
|
|
checks.push({ name: "Migrations", status: "warn", detail: "Could not read migration journal" });
|
|
}
|
|
}
|
|
|
|
// 3. Recent error volume (from the error tracker)
|
|
let recentErrors = { last24h: 0, last1h: 0 };
|
|
if (dbOk) {
|
|
try {
|
|
const rows = await sql`
|
|
SELECT
|
|
COUNT(*) FILTER (WHERE created_at > NOW() - INTERVAL '24 hours')::int AS last24h,
|
|
COUNT(*) FILTER (WHERE created_at > NOW() - INTERVAL '1 hour')::int AS last1h
|
|
FROM error_events
|
|
`;
|
|
recentErrors = { last24h: Number(rows[0]?.last24h) || 0, last1h: Number(rows[0]?.last1h) || 0 };
|
|
checks.push({
|
|
name: "Errors (24h)",
|
|
status: recentErrors.last1h > 5 ? "down" : recentErrors.last24h > 0 ? "warn" : "ok",
|
|
detail: `${recentErrors.last24h} in 24h · ${recentErrors.last1h} in last hour`,
|
|
});
|
|
} catch {
|
|
checks.push({ name: "Errors (24h)", status: "warn", detail: "error_events table unavailable" });
|
|
}
|
|
}
|
|
|
|
// 4. Integration config presence (no live calls — just whether env is wired)
|
|
const configCheck = (name: string, present: boolean, missingHint: string): Check =>
|
|
({ name, status: present ? "ok" : "warn", detail: present ? "Configured" : missingHint });
|
|
|
|
checks.push(configCheck("AI Gateway", !!(process.env.LITELLM_BASE_URL && process.env.LITELLM_API_KEY), "LITELLM_BASE_URL / LITELLM_API_KEY not set"));
|
|
checks.push(configCheck("R2 Storage", !!(process.env.R2_ACCOUNT_ID && process.env.R2_ACCESS_KEY_ID && process.env.R2_BUCKET_NAME), "R2_* env vars incomplete"));
|
|
checks.push(configCheck("Email (Resend)", !!process.env.RESEND_API_KEY, "RESEND_API_KEY not set"));
|
|
|
|
const overall: "ok" | "warn" | "down" = checks.some(c => c.status === "down")
|
|
? "down"
|
|
: checks.some(c => c.status === "warn") ? "warn" : "ok";
|
|
|
|
return NextResponse.json({ overall, checks, recentErrors, checkedAt: new Date().toISOString() });
|
|
}
|