Migrate harness from in-memory stores to CloudNativePG
Some checks failed
CI / lint-and-test (push) Successful in 22s
Deploy Production / deploy (push) Failing after 21s
CI / build (push) Failing after 1m51s

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:
Julia McGhee
2026-03-21 20:17:00 +00:00
parent df351439d6
commit 3fe75a8e04
28 changed files with 1245 additions and 304 deletions

View File

@@ -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({

View File

@@ -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 });
}

View File

@@ -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 });
}

View File

@@ -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 });
}

View File

@@ -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 });
}

View File

@@ -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 });
}

View File

@@ -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" });

View File

@@ -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 });

View File

@@ -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 });
}