acdc25c797
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>
57 lines
2.0 KiB
TypeScript
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,
|
|
})),
|
|
});
|
|
}
|