Files
ampelos-dashboard/src/app/api/agents/status/route.ts
T
Ryan acdc25c797 Ampelos dashboard: the web face, and the owner of the schema
Split out of the single Ampelos repository. Next.js app, Drizzle schema and
migrations, brand art, planning notes.

What left: scripts/, which was the agent's job library misfiled under web/ and
imported nothing from src/; and deploy/truenas, whose broadcast posts to the
scan listener on :3427 -- an agent script -- so it belongs beside the thing it
talks to.

This repository keeps the schema. The agent speaks raw SQL against the same
tables and holds no copy of it, so a rename here can break it silently where it
used to be one commit. The README says so, and the agent carries a snapshot to
check against.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 12:12:08 +02:00

57 lines
2.0 KiB
TypeScript

import { db } from "@/db/client";
import { storageTiers } from "@/db/schema";
import { getAgentStatuses, AGENT_TIMEOUT_SECONDS } from "@/lib/agents";
import { timingSafeEqual } from "node:crypto";
export const dynamic = "force-dynamic";
// Read-only view of agent liveness, consumed by the local mount daemon
// (agent/ampelos-mountd.sh) so it can decide whether a tier's storage host is
// up. Deliberately read-only: the daemon holds the privilege, this endpoint
// never performs a mount.
function authorize(request: Request) {
const expected = process.env.AMPELOS_AGENT_TOKEN;
if (!expected) return { ok: false, status: 503, error: "AMPELOS_AGENT_TOKEN is not configured" };
const header = request.headers.get("authorization") ?? "";
const provided = header.startsWith("Bearer ") ? header.slice(7) : "";
const a = Buffer.from(provided);
const b = Buffer.from(expected);
if (!provided || a.length !== b.length || !timingSafeEqual(a, b)) {
return { ok: false, status: 401, error: "unauthorized" };
}
return { ok: true as const };
}
export async function GET(request: Request) {
const auth = authorize(request);
if (!auth.ok) return Response.json({ error: auth.error }, { status: auth.status });
const [agents, tiers] = await Promise.all([
getAgentStatuses(),
db.select().from(storageTiers),
]);
const agentByName = new Map(agents.map((agent) => [agent.agent, agent]));
return Response.json({
timeoutSeconds: AGENT_TIMEOUT_SECONDS,
agents: agents.map((agent) => ({
agent: agent.agent,
online: agent.online,
secondsSinceSeen: agent.secondsSinceSeen,
hostname: agent.hostname,
})),
tiers: tiers.map((tier) => ({
tier: tier.tier,
basePath: tier.basePath,
agentName: tier.agentName,
alwaysOnline: tier.alwaysOnline,
// null when the tier names no agent: availability is then unmanaged.
agentOnline: tier.agentName ? Boolean(agentByName.get(tier.agentName)?.online) : null,
})),
});
}