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>
This commit is contained in:
@@ -163,6 +163,12 @@ Create `C:\ProgramData\VFXReviewWorker\config.json` (§7.8):
|
||||
}
|
||||
```
|
||||
|
||||
There is no `workerVersion` key: the version is stamped into the exe at build time and
|
||||
read back from the assembly, so a machine can never report a version it is not running.
|
||||
`publish.ps1` appends the git commit, giving `1.1.0+feb1c7b`. Bump `<Version>` in
|
||||
`VFXReviewWorker.csproj` when publishing a change; the worker reports it on startup and
|
||||
**Pipeline → Machines** flags any machine still on an older one.
|
||||
|
||||
`pathMappings` translate the manifest's canonical UNC paths to this machine's drive
|
||||
mappings. `plateSearchRoots` is optional: it is only consulted when a manifest carries no
|
||||
timing plate (older panel versions), to find the shot's plates for the timecode check.
|
||||
|
||||
@@ -176,6 +176,36 @@ public sealed class ApiClient
|
||||
public Task<string?> UploadLogAsync(string jobId, string machineId, string logFilePath, CancellationToken ct) =>
|
||||
UploadArtifactAsync(jobId, machineId, "log", logFilePath, "text/plain", ct);
|
||||
|
||||
/// <summary>
|
||||
/// Reports the deployed worker version so the web UI can show what is
|
||||
/// running where. Fire-and-forget — never worth failing a startup over.
|
||||
/// </summary>
|
||||
public async Task ReportVersionAsync(WorkerOptions o, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _http.PostAsJsonAsync("api/ext/tools/ping", new
|
||||
{
|
||||
tool = "RENDER_WORKER",
|
||||
machineName = o.MachineName,
|
||||
version = o.WorkerVersion,
|
||||
hostUser = Environment.UserName,
|
||||
hostApp = $"{Environment.OSVersion.VersionString} ({Dns.GetHostName()})",
|
||||
details = new
|
||||
{
|
||||
aeVersion = o.AeVersion,
|
||||
aerenderPath = o.AerenderPath,
|
||||
ffmpeg = o.FfmpegPath is not null,
|
||||
plateSearchRoots = o.PlateSearchRoots,
|
||||
},
|
||||
}, Json, ct);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.LogDebug(ex, "Version ping failed (non-fatal)");
|
||||
}
|
||||
}
|
||||
|
||||
// E3 — used by crash-recovery reconcile (§7.5)
|
||||
public async Task<ExportDetail?> GetExportAsync(string exportId, CancellationToken ct)
|
||||
{
|
||||
|
||||
@@ -31,6 +31,7 @@ public static class Preflight
|
||||
return 1;
|
||||
}
|
||||
|
||||
Console.WriteLine($" worker version : {options.WorkerVersion}");
|
||||
Console.WriteLine($" machine name : {options.MachineName}");
|
||||
Console.WriteLine($" server : {options.ServerUrl}");
|
||||
Console.WriteLine($" api key : {(string.IsNullOrWhiteSpace(options.ApiKey) ? "(missing)" : "set")}");
|
||||
|
||||
@@ -8,6 +8,14 @@
|
||||
<AssemblyName>VFXReviewWorker</AssemblyName>
|
||||
<!-- Single self-contained exe for render nodes: dotnet publish -r win-x64 (see README) -->
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
<!--
|
||||
The version is a property of the build, not of the machine it lands on:
|
||||
it is stamped here and read back from the assembly at runtime, so a
|
||||
workstation cannot report a version it is not actually running. Bump on
|
||||
every change that gets published. publish.ps1 appends the git SHA via
|
||||
SourceRevisionId, giving "1.1.0+feb1c7b".
|
||||
-->
|
||||
<Version>1.1.0</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
@@ -27,7 +28,18 @@ public sealed class WorkerOptions
|
||||
/// Leave empty and old manifests simply fall through to the API fallback.
|
||||
/// </summary>
|
||||
public List<string> PlateSearchRoots { get; set; } = new();
|
||||
public string WorkerVersion { get; set; } = "1.0.0";
|
||||
/// <summary>
|
||||
/// Read from the assembly, not from config.json: a hand-maintained version
|
||||
/// string is exactly the thing that goes stale, and a machine reporting a
|
||||
/// version it is not running is worse than no version at all.
|
||||
/// Includes the git SHA when publish.ps1 stamped one ("1.1.0+feb1c7b").
|
||||
/// </summary>
|
||||
public string WorkerVersion =>
|
||||
typeof(WorkerOptions).Assembly
|
||||
.GetCustomAttribute<System.Reflection.AssemblyInformationalVersionAttribute>()?.InformationalVersion
|
||||
?? typeof(WorkerOptions).Assembly.GetName().Version?.ToString()
|
||||
?? "unknown";
|
||||
|
||||
public string? AeVersion { get; set; }
|
||||
|
||||
public static string DataDir =>
|
||||
|
||||
@@ -40,6 +40,7 @@ public sealed class WorkerService : BackgroundService
|
||||
_ = Task.Run(() => _spooler.RunAsync(stoppingToken), stoppingToken);
|
||||
|
||||
await RegisterWithRetryAsync(stoppingToken);
|
||||
await _api.ReportVersionAsync(_options, stoppingToken);
|
||||
await _executor.ReconcileAsync(_machineId!, stoppingToken);
|
||||
|
||||
_ = Task.Run(() => HeartbeatLoopAsync(stoppingToken), stoppingToken);
|
||||
|
||||
@@ -20,9 +20,22 @@ $ErrorActionPreference = "Stop"
|
||||
$root = Split-Path $PSScriptRoot -Parent
|
||||
if (-not $OutDir) { $OutDir = Join-Path $root "publish\win-x64" }
|
||||
|
||||
# Stamp the commit into the version so a deployed exe can be traced back to
|
||||
# source ("1.1.0+feb1c7b"). Absent git, the plain version still publishes.
|
||||
$sha = ""
|
||||
try { $sha = (& git -C $root rev-parse --short HEAD 2>$null) } catch { }
|
||||
if ($LASTEXITCODE -ne 0) { $sha = "" }
|
||||
|
||||
Write-Host "Publishing self-contained worker..." -ForegroundColor Cyan
|
||||
& dotnet publish (Join-Path $root "VFXReviewWorker\VFXReviewWorker.csproj") `
|
||||
-c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -o $OutDir
|
||||
$publishArgs = @(
|
||||
(Join-Path $root "VFXReviewWorker\VFXReviewWorker.csproj")
|
||||
"-c"; "Release"; "-r"; "win-x64"
|
||||
"--self-contained"; "true"
|
||||
"-p:PublishSingleFile=true"
|
||||
"-o"; $OutDir
|
||||
)
|
||||
if ($sha) { $publishArgs += "-p:SourceRevisionId=$sha" }
|
||||
& dotnet publish @publishArgs
|
||||
if ($LASTEXITCODE -ne 0) { throw "dotnet publish failed" }
|
||||
|
||||
# The exe reads scripts\vfxr_build_preview.jsx from its own folder at runtime.
|
||||
|
||||
+51
-1
@@ -18,6 +18,11 @@ var CONFIG = {
|
||||
var SETTINGS_GROUP = "VFXReviewConnector";
|
||||
var SETTINGS_KEY = "cfg";
|
||||
|
||||
// Bump on every change deployed to a workstation. The panel reports this to
|
||||
// the server on load, so Pipeline → Machines shows which version each machine
|
||||
// is actually running and flags the ones left behind.
|
||||
var PANEL_VERSION = "2.1.0";
|
||||
|
||||
(function VFXReviewConnector(thisObj) {
|
||||
var uiState = {
|
||||
projectDropdown: null,
|
||||
@@ -103,6 +108,48 @@ var SETTINGS_KEY = "cfg";
|
||||
return Folder.userData.fsName + "/VFXReviewConnector/connector.log";
|
||||
}
|
||||
|
||||
// ExtendScript exposes no hostname directly; $.getenv reaches the shell's.
|
||||
function getHostName() {
|
||||
var name = "";
|
||||
|
||||
try {
|
||||
name = $.getenv("COMPUTERNAME") || $.getenv("HOSTNAME") || "";
|
||||
} catch (envError) {
|
||||
name = "";
|
||||
}
|
||||
return trimText(name) || "unknown-machine";
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports this panel's version to the server so Pipeline → Machines shows
|
||||
* what is deployed where. Fire-and-forget: a failure here must never stop
|
||||
* the panel loading, so it is logged and dropped.
|
||||
*/
|
||||
function reportPanelVersion() {
|
||||
var response;
|
||||
|
||||
if (!trimText(CONFIG.baseUrl) || !trimText(CONFIG.token)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
response = postJSON(CONFIG.baseUrl + "/api/ext/tools/ping", {
|
||||
tool: "AE_PANEL",
|
||||
machineName: getHostName(),
|
||||
version: PANEL_VERSION,
|
||||
hostUser: CONFIG.artistEmail || "",
|
||||
hostApp: "After Effects " + app.version,
|
||||
details: {
|
||||
os: String($.os).substring(0, 120),
|
||||
project: CONFIG.lastProjectCode || ""
|
||||
}
|
||||
});
|
||||
diagLog("version ping: HTTP " + response.status);
|
||||
} catch (pingError) {
|
||||
diagLog("version ping failed: " + describeError(pingError));
|
||||
}
|
||||
}
|
||||
|
||||
function padTwo(value) {
|
||||
return ("0" + value).slice(-2);
|
||||
}
|
||||
@@ -181,6 +228,7 @@ var SETTINGS_KEY = "cfg";
|
||||
DIAG.lastFailure = null;
|
||||
diagWriteToFile("");
|
||||
diagLog("=== " + action + " ===");
|
||||
diagLog("panel v" + PANEL_VERSION + " | " + getHostName());
|
||||
diagLog("AE " + app.version + " | " + $.os + " | project " +
|
||||
(app.project && app.project.file ? app.project.file.fsName : "(unsaved)"));
|
||||
diagLog("config: " + describeConfig());
|
||||
@@ -4052,7 +4100,7 @@ var SETTINGS_KEY = "cfg";
|
||||
}
|
||||
}
|
||||
|
||||
title = scrollContent.add("statictext", undefined, "VFXReview Connector");
|
||||
title = scrollContent.add("statictext", undefined, "VFXReview Connector v" + PANEL_VERSION);
|
||||
title.alignment = ["fill", "top"];
|
||||
|
||||
shotBuilderPanel = scrollContent.add("panel", undefined, "Shot Builder");
|
||||
@@ -4359,6 +4407,8 @@ var SETTINGS_KEY = "cfg";
|
||||
var panel = buildUI(thisObj);
|
||||
refreshSelection();
|
||||
populateProjectDropdown();
|
||||
diagLog("panel v" + PANEL_VERSION + " loaded on " + getHostName());
|
||||
reportPanelVersion();
|
||||
|
||||
if (panel instanceof Window) {
|
||||
panel.center();
|
||||
|
||||
@@ -434,6 +434,24 @@ Below them are two buttons:
|
||||
|
||||
---
|
||||
|
||||
## Versions
|
||||
|
||||
The panel's version is the `PANEL_VERSION` constant at the top of
|
||||
`VFXReviewConnector.jsx`. It appears in the panel title, in every diagnostics
|
||||
report, and is sent to the server when the panel loads.
|
||||
|
||||
**Pipeline → Machines → Deployed tool versions** then shows which version each
|
||||
workstation is running, alongside the render workers. Anything behind the newest
|
||||
version seen for that tool is flagged `→ rebuild to vX` — so updating one
|
||||
machine is what surfaces the ones still to do. No expected-version list to keep
|
||||
in sync; the newest thing reporting in is the benchmark.
|
||||
|
||||
**Bump `PANEL_VERSION` whenever you copy the .jsx to a workstation.** If two
|
||||
machines report the same version but behave differently, one of them was copied
|
||||
without the bump.
|
||||
|
||||
---
|
||||
|
||||
## Diagnostics and logging
|
||||
|
||||
Every button press is traced: what it tried, in what order, with the paths and
|
||||
|
||||
@@ -201,6 +201,77 @@ export function MachinesClient() {
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ToolVersions />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ToolRow {
|
||||
id: string;
|
||||
tool: string;
|
||||
toolLabel: string;
|
||||
machineName: string;
|
||||
version: string;
|
||||
buildRef: string | null;
|
||||
hostUser: string | null;
|
||||
hostApp: string | null;
|
||||
lastSeenAt: string;
|
||||
newestVersion: string;
|
||||
outdated: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* What is deployed where. Each tool reports its own version on startup, and
|
||||
* anything behind the newest version seen for that tool is flagged — so
|
||||
* updating one machine is what surfaces the ones still to do.
|
||||
*/
|
||||
function ToolVersions() {
|
||||
const { data } = useQuery({
|
||||
queryKey: ["tool-versions"],
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/tools");
|
||||
if (!res.ok) throw new Error("Failed to load tool versions");
|
||||
return res.json() as Promise<{ tools: ToolRow[] }>;
|
||||
},
|
||||
refetchInterval: 30000,
|
||||
});
|
||||
|
||||
const tools = data?.tools ?? [];
|
||||
const stale = tools.filter((t) => t.outdated).length;
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-zinc-800 bg-zinc-900/40">
|
||||
<div className="px-4 py-2.5 border-b border-zinc-800 text-sm font-medium text-white flex items-center gap-3">
|
||||
Deployed tool versions
|
||||
{stale > 0 && (
|
||||
<span className="text-xs text-amber-400">
|
||||
{stale} machine{stale > 1 ? "s" : ""} behind
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="divide-y divide-zinc-800/60">
|
||||
{tools.map((t) => (
|
||||
<div key={t.id} className="px-4 py-2.5 text-sm flex items-center gap-3 flex-wrap">
|
||||
<span className="w-44 shrink-0 text-zinc-300">{t.toolLabel}</span>
|
||||
<span className="w-40 shrink-0 text-zinc-400 text-xs font-mono">{t.machineName}</span>
|
||||
<span className={cn("text-xs font-mono", t.outdated ? "text-amber-400" : "text-emerald-400")}>
|
||||
v{t.version}
|
||||
</span>
|
||||
{t.outdated && <span className="text-xs text-amber-400/80">→ rebuild to v{t.newestVersion}</span>}
|
||||
{t.buildRef && <span className="text-xs text-zinc-600 font-mono">{t.buildRef}</span>}
|
||||
{t.hostUser && <span className="text-xs text-zinc-600">{t.hostUser}</span>}
|
||||
<span className="ml-auto text-xs text-zinc-600">
|
||||
seen {new Date(t.lastSeenAt).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{tools.length === 0 && (
|
||||
<div className="p-4 text-sm text-zinc-500">
|
||||
Nothing has reported yet. Versions appear as each panel and worker starts up.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { isExtAuthorized } from "@/lib/render-pipeline/ext-auth";
|
||||
import { toErrorResponse } from "@/lib/render-pipeline/errors";
|
||||
import { recordToolVersion, toolPingSchema } from "@/lib/render-pipeline/tools";
|
||||
|
||||
// ── POST /api/ext/tools/ping ─────────────────────────────────────────────────
|
||||
//
|
||||
// A studio tool reporting its own version on startup. Fire-and-forget by
|
||||
// design: callers must not fail if this errors.
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
if (!isExtAuthorized(req)) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
let raw: unknown;
|
||||
try {
|
||||
raw = await req.json();
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
|
||||
}
|
||||
|
||||
const parsed = toolPingSchema.safeParse(raw);
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json({ error: "Invalid tool ping", details: parsed.error.issues }, { status: 422 });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await recordToolVersion(parsed.data);
|
||||
return NextResponse.json(result);
|
||||
} catch (err) {
|
||||
const { body, status } = toErrorResponse(err);
|
||||
return NextResponse.json(body, { status });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { toErrorResponse } from "@/lib/render-pipeline/errors";
|
||||
import { requirePipelineUser } from "@/lib/render-pipeline/session-auth";
|
||||
import { listToolVersions } from "@/lib/render-pipeline/tools";
|
||||
|
||||
// ── GET /api/tools ───────────────────────────────────────────────────────────
|
||||
//
|
||||
// What version of each studio tool is deployed where, with anything behind the
|
||||
// newest seen version flagged.
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
await requirePipelineUser();
|
||||
const result = await listToolVersions();
|
||||
return NextResponse.json(result);
|
||||
} catch (err) {
|
||||
const { body, status } = toErrorResponse(err);
|
||||
return NextResponse.json(body, { status });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
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,
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
-- Deployed tool versions, reported by the tools themselves on startup.
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "tool_versions" (
|
||||
"id" TEXT NOT NULL,
|
||||
"tool" TEXT NOT NULL,
|
||||
"machineName" TEXT NOT NULL,
|
||||
"version" TEXT NOT NULL,
|
||||
"buildRef" TEXT,
|
||||
"hostUser" TEXT,
|
||||
"hostApp" TEXT,
|
||||
"details" JSONB,
|
||||
"firstSeenAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"lastSeenAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "tool_versions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "tool_versions_tool_idx" ON "tool_versions"("tool");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "tool_versions_tool_machineName_key" ON "tool_versions"("tool", "machineName");
|
||||
@@ -988,6 +988,26 @@ model WorkerHeartbeat {
|
||||
@@map("worker_heartbeats")
|
||||
}
|
||||
|
||||
/// What version of each studio tool is running where. Written by the tool
|
||||
/// itself on startup, so it reflects what is *deployed* rather than what
|
||||
/// someone remembered to write down. One row per (tool, machine).
|
||||
model ToolVersion {
|
||||
id String @id @default(cuid())
|
||||
tool String // "AE_PANEL" | "RENDER_WORKER"
|
||||
machineName String
|
||||
version String // "2.1.0"
|
||||
buildRef String? // git sha / build stamp, when the tool can report one
|
||||
hostUser String? // artist email, or the worker's service account
|
||||
hostApp String? // "After Effects 24.6" / OS build
|
||||
details Json?
|
||||
firstSeenAt DateTime @default(now())
|
||||
lastSeenAt DateTime @default(now())
|
||||
|
||||
@@unique([tool, machineName])
|
||||
@@index([tool])
|
||||
@@map("tool_versions")
|
||||
}
|
||||
|
||||
/// One row per check per validation run, so the UI shows exactly what failed
|
||||
/// and history survives retries (§5.4).
|
||||
model ValidationResult {
|
||||
|
||||
Reference in New Issue
Block a user