feat(agent): Bash-Agent mit Installer, systemd-Timer und Spool (#005)

Produkterkennung fuer PVE, PBS und PMG, JSON-Erzeugung ohne jq, Retry mit
Backoff, Nachsenden aus dem Spool, Update und Uninstall. Das Dashboard liefert
install.sh und agent.sh mit eingesetzter Adresse aus. 38 Tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-17 20:44:38 +02:00
co-authored by Claude Opus 5
parent d16115de66
commit d9455db311
13 changed files with 1004 additions and 25 deletions
+9
View File
@@ -0,0 +1,9 @@
import { basisUrl, skriptAntwort, skriptLaden } from "@/server/agent/skripte";
export const dynamic = "force-dynamic";
/** Aktuelles Agent-Skript, wird vom Installer und vom Update-Mechanismus geladen. */
export async function GET(request: Request) {
const inhalt = await skriptLaden("proxledger-agent.sh", basisUrl(request));
return skriptAntwort(inhalt, "proxledger-agent.sh");
}
+9
View File
@@ -0,0 +1,9 @@
import { basisUrl, skriptAntwort, skriptLaden } from "@/server/agent/skripte";
export const dynamic = "force-dynamic";
/** Installationsskript für den One-Liner `curl -fsSL <url>/install.sh | bash -s -- --token …`. */
export async function GET(request: Request) {
const inhalt = await skriptLaden("install.sh", basisUrl(request));
return skriptAntwort(inhalt, "install.sh");
}
+31
View File
@@ -0,0 +1,31 @@
import { readFile } from "node:fs/promises";
import path from "node:path";
import { AGENT_VERSION } from "./http";
/**
* Liefert die Agent-Skripte aus dem Verzeichnis `agent/` aus und setzt dabei
* Dashboard-Adresse und Agent-Version ein.
*/
export async function skriptLaden(datei: "install.sh" | "proxledger-agent.sh", basisUrl: string) {
const pfad = path.join(process.cwd(), "agent", datei);
const inhalt = await readFile(pfad, "utf8");
return inhalt
.replaceAll("@@DASHBOARD_URL@@", basisUrl.replace(/\/$/, ""))
.replaceAll("@@AGENT_VERSION@@", AGENT_VERSION);
}
/** Bevorzugt die konfigurierte Adresse; sonst die Herkunft der Anfrage. */
export function basisUrl(request: Request): string {
if (process.env.APP_URL) return process.env.APP_URL;
return new URL(request.url).origin;
}
export function skriptAntwort(inhalt: string, dateiname: string) {
return new Response(inhalt, {
headers: {
"Content-Type": "text/x-shellscript; charset=utf-8",
"Content-Disposition": `inline; filename="${dateiname}"`,
"Cache-Control": "no-store",
},
});
}