Migrate harness from in-memory stores to CloudNativePG
Replace all in-memory Map-backed stores (credentials, models, agents, tasks, iterations, usage) with Drizzle ORM queries against the homelab-pg PostgreSQL cluster. All store functions are now async. - Add 6 harness_* tables to @homelab/db schema - Generate and apply initial Drizzle migration - Add lazy DB connection proxy to avoid build-time errors - Wire DATABASE_URL from sealed secret into harness deployment - Update all API routes, orchestrator, executor, and boot to await async store operations
This commit is contained in:
@@ -2,14 +2,6 @@ import { NextResponse } from "next/server";
|
||||
import { getAllAgentConfigs, AGENT_RUNTIMES, AgentConfig } from "@/lib/agents";
|
||||
import { getRawCredentialsByProvider, Provider } from "@/lib/credentials";
|
||||
|
||||
const PROVIDER_ENV_VARS: Record<string, string> = {
|
||||
anthropic: "ANTHROPIC_API_KEY",
|
||||
openai: "OPENAI_API_KEY",
|
||||
google: "GOOGLE_API_KEY",
|
||||
openrouter: "OPENROUTER_API_KEY",
|
||||
"opencode-zen": "OPENCODE_ZEN_API_KEY",
|
||||
};
|
||||
|
||||
const PROVIDER_VALIDATION: Record<string, (token: string, baseUrl?: string) => Promise<boolean>> = {
|
||||
async anthropic(token) {
|
||||
const res = await fetch("https://api.anthropic.com/v1/models", {
|
||||
@@ -44,8 +36,8 @@ export interface AgentHealthStatus {
|
||||
provider: string;
|
||||
modelId: string;
|
||||
credentialConfigured: boolean;
|
||||
credentialValid: boolean | null; // null = not checked (no credential)
|
||||
cliInstalled: boolean | null; // null = not checked
|
||||
credentialValid: boolean | null;
|
||||
cliInstalled: boolean | null;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
@@ -72,22 +64,17 @@ async function checkAgent(config: AgentConfig): Promise<AgentHealthStatus> {
|
||||
cliInstalled: null,
|
||||
};
|
||||
|
||||
// Check CLI
|
||||
try {
|
||||
status.cliInstalled = await checkCliInstalled(runtime.cliCommand);
|
||||
} catch {
|
||||
status.cliInstalled = false;
|
||||
}
|
||||
|
||||
// Check credential exists
|
||||
const creds = getRawCredentialsByProvider(config.provider as Provider);
|
||||
const creds = await getRawCredentialsByProvider(config.provider as Provider);
|
||||
status.credentialConfigured = creds.length > 0;
|
||||
|
||||
if (!status.credentialConfigured) {
|
||||
return status;
|
||||
}
|
||||
if (!status.credentialConfigured) return status;
|
||||
|
||||
// Validate credential against provider API
|
||||
const validator = PROVIDER_VALIDATION[config.provider];
|
||||
if (validator) {
|
||||
try {
|
||||
@@ -96,16 +83,13 @@ async function checkAgent(config: AgentConfig): Promise<AgentHealthStatus> {
|
||||
status.credentialValid = false;
|
||||
status.error = err instanceof Error ? err.message : "Validation failed";
|
||||
}
|
||||
} else {
|
||||
// No validator for this provider (e.g. opencode-zen) — just confirm credential exists
|
||||
status.credentialValid = null;
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
const configs = getAllAgentConfigs();
|
||||
const configs = await getAllAgentConfigs();
|
||||
|
||||
if (configs.length === 0) {
|
||||
return NextResponse.json({
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json({
|
||||
configs: getAllAgentConfigs(),
|
||||
configs: await getAllAgentConfigs(),
|
||||
runtimes: Object.values(AGENT_RUNTIMES),
|
||||
});
|
||||
}
|
||||
@@ -41,12 +41,12 @@ export async function POST(request: NextRequest) {
|
||||
env: body.env,
|
||||
};
|
||||
|
||||
return NextResponse.json(upsertAgentConfig(config), { status: 201 });
|
||||
return NextResponse.json(await upsertAgentConfig(config), { status: 201 });
|
||||
}
|
||||
|
||||
export async function DELETE(request: NextRequest) {
|
||||
const id = request.nextUrl.searchParams.get("id");
|
||||
if (!id) return NextResponse.json({ error: "id required" }, { status: 400 });
|
||||
deleteAgentConfig(id);
|
||||
await deleteAgentConfig(id);
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
|
||||
@@ -11,20 +11,20 @@ import {
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const enabledOnly = request.nextUrl.searchParams.get("enabled") === "true";
|
||||
return NextResponse.json(enabledOnly ? getEnabledModels() : getCuratedModels());
|
||||
return NextResponse.json(enabledOnly ? await getEnabledModels() : await getCuratedModels());
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const body = await request.json();
|
||||
|
||||
if (body.action === "toggle" && body.id) {
|
||||
const result = toggleModelEnabled(body.id);
|
||||
const result = await toggleModelEnabled(body.id);
|
||||
if (!result) return NextResponse.json({ error: "not found" }, { status: 404 });
|
||||
return NextResponse.json(result);
|
||||
}
|
||||
|
||||
if (body.action === "update-cost" && body.id) {
|
||||
const result = updateModelCost(body.id, body.costPer1kInput, body.costPer1kOutput);
|
||||
const result = await updateModelCost(body.id, body.costPer1kInput, body.costPer1kOutput);
|
||||
if (!result) return NextResponse.json({ error: "not found" }, { status: 404 });
|
||||
return NextResponse.json(result);
|
||||
}
|
||||
@@ -43,12 +43,12 @@ export async function POST(request: NextRequest) {
|
||||
costPer1kOutput: body.costPer1kOutput,
|
||||
};
|
||||
|
||||
return NextResponse.json(upsertCuratedModel(model), { status: 201 });
|
||||
return NextResponse.json(await upsertCuratedModel(model), { status: 201 });
|
||||
}
|
||||
|
||||
export async function DELETE(request: NextRequest) {
|
||||
const id = request.nextUrl.searchParams.get("id");
|
||||
if (!id) return NextResponse.json({ error: "id required" }, { status: 400 });
|
||||
removeCuratedModel(id);
|
||||
await removeCuratedModel(id);
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ import { NextResponse } from "next/server";
|
||||
import { getUsageSummary, getUsageLog } from "@/lib/model-store";
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json({
|
||||
summary: getUsageSummary(),
|
||||
log: getUsageLog(),
|
||||
});
|
||||
const [summary, log] = await Promise.all([getUsageSummary(), getUsageLog()]);
|
||||
return NextResponse.json({ summary, log });
|
||||
}
|
||||
|
||||
@@ -14,9 +14,9 @@ const VALID_PROVIDERS = [...GIT_PROVIDERS, ...AI_PROVIDERS];
|
||||
export async function GET(request: NextRequest) {
|
||||
const kind = request.nextUrl.searchParams.get("kind");
|
||||
if (kind === "git" || kind === "ai") {
|
||||
return NextResponse.json(getCredentialsByKind(kind));
|
||||
return NextResponse.json(await getCredentialsByKind(kind));
|
||||
}
|
||||
return NextResponse.json(getAllCredentials());
|
||||
return NextResponse.json(await getAllCredentials());
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
@@ -44,7 +44,7 @@ export async function POST(request: NextRequest) {
|
||||
baseUrl: body.baseUrl,
|
||||
};
|
||||
|
||||
const saved = upsertCredential(cred);
|
||||
const saved = await upsertCredential(cred);
|
||||
return NextResponse.json(saved, { status: 201 });
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ export async function DELETE(request: NextRequest) {
|
||||
return NextResponse.json({ error: "id is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const deleted = deleteCredential(id);
|
||||
const deleted = await deleteCredential(id);
|
||||
if (!deleted) {
|
||||
return NextResponse.json({ error: "not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ export async function GET(
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
const { id } = await params;
|
||||
const task = getTask(id);
|
||||
const task = await getTask(id);
|
||||
if (!task) {
|
||||
return NextResponse.json({ error: "Task not found" }, { status: 404 });
|
||||
}
|
||||
@@ -19,7 +19,7 @@ export async function PATCH(
|
||||
) {
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
const updated = updateTask(id, body);
|
||||
const updated = await updateTask(id, body);
|
||||
if (!updated) {
|
||||
return NextResponse.json({ error: "Task not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ export async function POST(
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
const { id } = await params;
|
||||
const task = getTask(id);
|
||||
const task = await getTask(id);
|
||||
|
||||
if (!task) {
|
||||
return NextResponse.json({ error: "Task not found" }, { status: 404 });
|
||||
@@ -20,7 +20,6 @@ export async function POST(
|
||||
);
|
||||
}
|
||||
|
||||
// Ensure orchestrator is running — it will pick up this task
|
||||
startOrchestrator();
|
||||
|
||||
return NextResponse.json({ ok: true, message: "Orchestrator started, task will be picked up" });
|
||||
|
||||
@@ -7,7 +7,7 @@ export async function POST(
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
const { id } = await params;
|
||||
const task = getTask(id);
|
||||
const task = await getTask(id);
|
||||
|
||||
if (!task) {
|
||||
return NextResponse.json({ error: "Task not found" }, { status: 404 });
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getAgentConfig } from "@/lib/agents";
|
||||
import { Task, TaskSpec } from "@/lib/types";
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json(getAllTasks());
|
||||
return NextResponse.json(await getAllTasks());
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
@@ -18,7 +18,7 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ error: "agentId is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const agentConfig = getAgentConfig(spec.agentId);
|
||||
const agentConfig = await getAgentConfig(spec.agentId);
|
||||
if (!agentConfig) {
|
||||
return NextResponse.json(
|
||||
{ error: `Agent config not found: ${spec.agentId}` },
|
||||
@@ -40,6 +40,6 @@ export async function POST(request: NextRequest) {
|
||||
spec,
|
||||
};
|
||||
|
||||
const created = createTask(task);
|
||||
const created = await createTask(task);
|
||||
return NextResponse.json(created, { status: 201 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user