Files
vfxreview/lib/render-pipeline/tools.ts
twotalesanimation a4f8226f3f feat(pipeline): report deployed tool versions from the tools themselves
"What is running where" was answerable only by asking someone. Each tool
now reports its own version on startup into a ToolVersion registry, shown
on Pipeline → Machines.

The worker's version moves out of config.json and into the assembly:
a hand-maintained string on each machine is exactly the thing that goes
stale, and a machine reporting a version it is not running is worse than
no version at all. publish.ps1 stamps the git SHA via SourceRevisionId,
so a deployed exe traces back to a commit (1.1.0+feb1c7b).

The panel gains PANEL_VERSION, shown in its title and every diagnostics
report, and pings on load.

Staleness is derived rather than declared: the newest version seen for a
tool is the benchmark, so updating one machine is what flags the rest.
There is no expected-version list to keep in sync — which would have been
one more thing to forget.

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

105 lines
3.7 KiB
TypeScript

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<string, string> = {
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<typeof toolPingSchema>;
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<string, string>();
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,
})),
};
}