import { Prisma } from "@prisma/client"; import { z } from "zod"; import { db } from "@/lib/db"; /** * Deployed-tool registry. Every studio tool reports its own version on * startup, so "what is running where" is answered by the tools themselves * rather than by a spreadsheet someone has to remember to update. * * Deliberately self-maintaining: nothing here declares what the *current* * version ought to be. The newest version seen for a tool is treated as the * benchmark, so updating one machine is what flags the others as stale. */ export const TOOL_LABELS: Record = { AE_PANEL: "AE connector panel", RENDER_WORKER: "Render worker", }; export const toolPingSchema = z.object({ tool: z.enum(["AE_PANEL", "RENDER_WORKER"]), machineName: z.string().min(1).max(200), version: z.string().min(1).max(50), buildRef: z.string().max(100).optional().nullable(), hostUser: z.string().max(200).optional().nullable(), hostApp: z.string().max(200).optional().nullable(), details: z.record(z.string(), z.unknown()).optional(), }); export type ToolPing = z.infer; export async function recordToolVersion(input: ToolPing) { const now = new Date(); const row = await db.toolVersion.upsert({ where: { tool_machineName: { tool: input.tool, machineName: input.machineName } }, create: { tool: input.tool, machineName: input.machineName, version: input.version, buildRef: input.buildRef ?? null, hostUser: input.hostUser ?? null, hostApp: input.hostApp ?? null, details: (input.details ?? undefined) as Prisma.InputJsonValue | undefined, lastSeenAt: now, }, update: { version: input.version, buildRef: input.buildRef ?? null, hostUser: input.hostUser ?? null, hostApp: input.hostApp ?? null, details: (input.details ?? undefined) as Prisma.InputJsonValue | undefined, lastSeenAt: now, }, }); return { tool: row.tool, machineName: row.machineName, version: row.version, lastSeenAt: row.lastSeenAt }; } /** * Compares versions as dot-separated numbers, falling back to a string * compare for anything non-numeric. Good enough to answer "is this one * behind?", which is the only question being asked. */ export function compareVersions(a: string, b: string): number { const parse = (v: string) => v.split(/[.+-]/).map((p) => (/^\d+$/.test(p) ? Number(p) : p)); const pa = parse(a); const pb = parse(b); for (let i = 0; i < Math.max(pa.length, pb.length); i++) { const x = pa[i] ?? 0; const y = pb[i] ?? 0; if (x === y) continue; if (typeof x === "number" && typeof y === "number") return x < y ? -1 : 1; return String(x) < String(y) ? -1 : 1; } return 0; } export async function listToolVersions() { const rows = await db.toolVersion.findMany({ orderBy: [{ tool: "asc" }, { machineName: "asc" }] }); // Newest version seen per tool is the benchmark — update one machine and the // rest light up as stale, with no "expected version" to keep in sync. const newest = new Map(); for (const row of rows) { const current = newest.get(row.tool); if (!current || compareVersions(row.version, current) > 0) newest.set(row.tool, row.version); } return { tools: rows.map((row) => ({ id: row.id, tool: row.tool, toolLabel: TOOL_LABELS[row.tool] ?? row.tool, machineName: row.machineName, version: row.version, buildRef: row.buildRef, hostUser: row.hostUser, hostApp: row.hostApp, firstSeenAt: row.firstSeenAt, lastSeenAt: row.lastSeenAt, newestVersion: newest.get(row.tool) ?? row.version, outdated: compareVersions(row.version, newest.get(row.tool) ?? row.version) < 0, })), }; }