feat(agent): v0.3.9 sync, UI details, self-update, centralized version
- Add agent/server startup sync (sync/sync_response) - Centralize agent version in agent/VERSION + expose /api/agent/version - Display agent version, nodeId and server version in local UI - Add agent self-update detection/download/restart via helper scripts - Run start/stop/delete/reset handlers in goroutines to avoid WebSocket blocking - Update dashboard download links and SUIVI_VPN_ONDEMAND.md - Document Podman stays installer-managed, not agent-updated
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getAgentVersionInfo } from "@/lib/agent-version";
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json(getAgentVersionInfo());
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
const AGENT_VERSION = "0.3.4";
|
||||
const AGENT_BIN_NAME = "studioE5-agent";
|
||||
import { getAgentVersionInfo } from "@/lib/agent-version";
|
||||
|
||||
export async function GET() {
|
||||
const info = getAgentVersionInfo();
|
||||
return NextResponse.json({
|
||||
version: AGENT_VERSION,
|
||||
windows: `/${AGENT_BIN_NAME}-v${AGENT_VERSION}.exe`,
|
||||
linux: `/${AGENT_BIN_NAME}-v${AGENT_VERSION}`,
|
||||
mac: `/${AGENT_BIN_NAME}-v${AGENT_VERSION}-mac`,
|
||||
version: info.version,
|
||||
windows: info.downloadUrls.windows,
|
||||
linux: info.downloadUrls.linux,
|
||||
mac: info.downloadUrls.mac,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||
|
||||
const AGENT_VERSION = "0.3.8";
|
||||
const AGENT_BIN_NAME = "studioE5-agent";
|
||||
import { getAgentVersionInfo } from "@/lib/agent-version";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default function DownloadPage() {
|
||||
const info = getAgentVersionInfo();
|
||||
const { version, downloadUrls } = info;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-3xl font-bold">Téléchargements Agent</h1>
|
||||
<p className="text-sm text-muted-foreground">Version actuelle : <strong>{AGENT_VERSION}</strong></p>
|
||||
<p className="text-sm text-muted-foreground">Version actuelle : <strong>{version}</strong></p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -17,7 +18,7 @@ export default function DownloadPage() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground mb-4">Agent studioE5 pour Windows (64 bits). Nécessite Tailscale installé séparément ou les binaires dans <code>tailscale-bin/windows/</code>.</p>
|
||||
<a href={`/${AGENT_BIN_NAME}-v${AGENT_VERSION}.exe`} download className="inline-flex items-center justify-center rounded-md text-sm font-medium bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 w-full">Télécharger (.exe)</a>
|
||||
<a href={downloadUrls.windows} download className="inline-flex items-center justify-center rounded-md text-sm font-medium bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 w-full">Télécharger (.exe)</a>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -27,7 +28,7 @@ export default function DownloadPage() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground mb-4">Archive complète incluant l'agent, Tailscale et le README Windows.</p>
|
||||
<a href={`/${AGENT_BIN_NAME}-v${AGENT_VERSION}-windows.zip`} download className="inline-flex items-center justify-center rounded-md text-sm font-medium bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 w-full">Télécharger (.zip)</a>
|
||||
<a href={downloadUrls.windowsZip} download className="inline-flex items-center justify-center rounded-md text-sm font-medium bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 w-full">Télécharger (.zip)</a>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -37,7 +38,7 @@ export default function DownloadPage() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground mb-4">Agent studioE5 pour Linux (64 bits).</p>
|
||||
<a href={`/${AGENT_BIN_NAME}-v${AGENT_VERSION}`} download className="inline-flex items-center justify-center rounded-md text-sm font-medium bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 w-full">Télécharger (Linux)</a>
|
||||
<a href={downloadUrls.linux} download className="inline-flex items-center justify-center rounded-md text-sm font-medium bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 w-full">Télécharger (Linux)</a>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
const BIN_NAME = "studioE5-agent";
|
||||
|
||||
function findVersionFile(): string | null {
|
||||
// Try a few common paths relative to the server workspace and Next.js build output.
|
||||
const candidates = [
|
||||
path.join(process.cwd(), "..", "agent", "VERSION"),
|
||||
path.join(process.cwd(), "..", "..", "agent", "VERSION"),
|
||||
path.join(process.cwd(), "agent", "VERSION"),
|
||||
path.join(__dirname, "..", "..", "..", "agent", "VERSION"),
|
||||
path.join(__dirname, "..", "..", "agent", "VERSION"),
|
||||
"/app/agent-version",
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
if (fs.existsSync(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getAgentVersion(): string {
|
||||
const versionFile = findVersionFile();
|
||||
if (versionFile) {
|
||||
return fs.readFileSync(versionFile, "utf-8").trim();
|
||||
}
|
||||
// Fallback used when the agent workspace is not mounted (should not happen).
|
||||
return "0.3.9";
|
||||
}
|
||||
|
||||
export interface AgentDownloadUrls {
|
||||
windows: string;
|
||||
windowsZip: string;
|
||||
linux: string;
|
||||
mac: string;
|
||||
}
|
||||
|
||||
export function getAgentDownloadUrls(version: string): AgentDownloadUrls {
|
||||
return {
|
||||
windows: `/${BIN_NAME}-v${version}.exe`,
|
||||
windowsZip: `/${BIN_NAME}-v${version}-windows.zip`,
|
||||
linux: `/${BIN_NAME}-v${version}`,
|
||||
mac: `/${BIN_NAME}-v${version}-mac`,
|
||||
};
|
||||
}
|
||||
|
||||
export function getAgentVersionInfo() {
|
||||
const version = getAgentVersion();
|
||||
return {
|
||||
version,
|
||||
downloadUrls: getAgentDownloadUrls(version),
|
||||
};
|
||||
}
|
||||
+60
-1
@@ -3,6 +3,7 @@ import { randomBytes } from "crypto";
|
||||
import type { IncomingMessage } from "http";
|
||||
import { prisma } from "./prisma";
|
||||
import { createEphemeralPreAuthKey, getHeadscaleUserId } from "./headscale";
|
||||
import { getAgentVersion } from "./agent-version";
|
||||
|
||||
interface NodeMessage {
|
||||
action: string;
|
||||
@@ -12,10 +13,16 @@ interface NodeMessage {
|
||||
type?: string;
|
||||
port?: number;
|
||||
composeConfig?: string;
|
||||
initScript?: string;
|
||||
studentName?: string;
|
||||
error?: string;
|
||||
tailscaleIp?: string;
|
||||
token?: string;
|
||||
serverVersion?: string;
|
||||
instances?: Array<{ id: string; status: string; port: number; templateName?: string }>;
|
||||
toStart?: Array<{ id: string; type: string; port: number; composeConfig?: string; initScript?: string }>;
|
||||
toDelete?: string[];
|
||||
toStop?: string[];
|
||||
}
|
||||
|
||||
const nodes = new Map<string, WebSocket>();
|
||||
@@ -126,7 +133,7 @@ export function initWebSocketServer(wss: WebSocketServer) {
|
||||
create: { id, status: "online", lastSeen: new Date() },
|
||||
});
|
||||
}
|
||||
ws.send(JSON.stringify({ action: "registered" }));
|
||||
ws.send(JSON.stringify({ action: "registered", serverVersion: getAgentVersion() }));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -263,6 +270,58 @@ export function initWebSocketServer(wss: WebSocketServer) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.action === "sync" && msg.instances) {
|
||||
const serverInstances = await prisma.instance.findMany({
|
||||
where: { nodeId },
|
||||
include: { template: true },
|
||||
});
|
||||
|
||||
const localIds = new Set(msg.instances.map((i) => i.id));
|
||||
const serverIds = new Set(serverInstances.map((i) => i.id));
|
||||
|
||||
const toDelete = msg.instances
|
||||
.filter((i) => !serverIds.has(i.id))
|
||||
.map((i) => i.id);
|
||||
|
||||
const toStop = msg.instances
|
||||
.filter((i) => {
|
||||
const server = serverInstances.find((s) => s.id === i.id);
|
||||
return server && server.status === "stopped" && i.status === "running";
|
||||
})
|
||||
.map((i) => i.id);
|
||||
|
||||
const toStart = serverInstances
|
||||
.filter((s) => !localIds.has(s.id))
|
||||
.map((s) => ({
|
||||
id: s.id,
|
||||
type: s.template.type,
|
||||
port: s.port,
|
||||
composeConfig: s.template.composeConfig,
|
||||
initScript: s.template.initScript ?? undefined,
|
||||
}));
|
||||
|
||||
console.log(
|
||||
"[WS] Sync for",
|
||||
nodeId,
|
||||
"- toStart:",
|
||||
toStart.length,
|
||||
"toDelete:",
|
||||
toDelete.length,
|
||||
"toStop:",
|
||||
toStop.length
|
||||
);
|
||||
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
action: "sync_response",
|
||||
toStart,
|
||||
toDelete,
|
||||
toStop,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.action === "instance_started" && msg.instanceId) {
|
||||
const { count } = await prisma.instance.updateMany({
|
||||
where: { id: msg.instanceId },
|
||||
|
||||
Reference in New Issue
Block a user