#795: Updates aus der WebUI ueber den Instanzmanager
Das Update-Angebot gab es nur im Desktop-Fenster. Der Manager prueft jetzt selbst und spielt ein: erst alle Instanzen geordnet anhalten, dann - ohne systemd: update-agent mit --wait-for-pid und --restart, - unter systemd: clawddotnet-update.service anstossen (Vorlage unter deploy/systemd), damit der agent keinen zweiten Manager neben dem Dienst startet (DC SETUP_INTEGRATION_GUIDE 4A). Fehlt die Unit, wird nichts angehalten; scheitert die Uebergabe, kommen die Instanzen wieder hoch. Anleitung in InstanceSetupGuide Abschnitt 8. Nebenbei: Sprachpaket wird vor dem Kopf geladen (rohe Schluessel im Kopf). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
4e41fc12d7
commit
d4b7f34200
@@ -439,6 +439,21 @@ internal static class ManagerEndpoints
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Updates (#795) ───
|
||||
|
||||
verwaltung.MapGet("/update", async (ManagerUpdates updates, HttpContext context, CancellationToken ct) =>
|
||||
Results.Ok(context.Request.Query.ContainsKey("refresh") || updates.Status.CheckedAt is null
|
||||
? await updates.CheckAsync(ct)
|
||||
: updates.Status));
|
||||
|
||||
verwaltung.MapPost("/update/install", async (ManagerUpdates updates) =>
|
||||
{
|
||||
var result = await updates.InstallAsync();
|
||||
return result.Ok
|
||||
? Results.Ok(new OutcomeDto(true, result.Message))
|
||||
: Results.BadRequest(new OutcomeDto(false, result.Message));
|
||||
});
|
||||
|
||||
// ─── License and manager settings (#764) ───
|
||||
|
||||
verwaltung.MapGet("/license", (ManagerCore manager) =>
|
||||
|
||||
@@ -54,7 +54,7 @@ public sealed class ManagerWebHost : IAsyncDisposable
|
||||
/// man ihn braucht: Er beaufsichtigt dann auch keine Instanzen mehr. Was schiefging,
|
||||
/// steht im Protokoll.</para>
|
||||
/// </summary>
|
||||
public static ManagerWebHost? TryStart(ManagerCore manager, ManagerBackups backups, ILoggerFactory loggerFactory)
|
||||
public static ManagerWebHost? TryStart(ManagerCore manager, ManagerBackups backups, ILoggerFactory loggerFactory, ManagerUpdates? updates = null)
|
||||
{
|
||||
var logger = loggerFactory.CreateLogger("ClawdDotNet.Manager.Web");
|
||||
var settings = manager.Settings;
|
||||
@@ -82,7 +82,7 @@ public sealed class ManagerWebHost : IAsyncDisposable
|
||||
|
||||
try
|
||||
{
|
||||
var app = Build(manager, backups, loggerFactory, url, token, forwarding);
|
||||
var app = Build(manager, backups, updates, loggerFactory, url, token, forwarding);
|
||||
|
||||
app.StartAsync().GetAwaiter().GetResult();
|
||||
|
||||
@@ -105,6 +105,7 @@ public sealed class ManagerWebHost : IAsyncDisposable
|
||||
private static WebApplication Build(
|
||||
ManagerCore manager,
|
||||
ManagerBackups backups,
|
||||
ManagerUpdates? updates,
|
||||
ILoggerFactory loggerFactory,
|
||||
string url,
|
||||
string token,
|
||||
@@ -130,6 +131,8 @@ public sealed class ManagerWebHost : IAsyncDisposable
|
||||
builder.Services.AddSingleton(manager);
|
||||
builder.Services.AddSingleton(manager.Instances);
|
||||
builder.Services.AddSingleton(backups);
|
||||
// #795: Updates from the web interface. Without one given (tests), one that knows no version.
|
||||
builder.Services.AddSingleton(updates ?? new ManagerUpdates(manager, "0.0.0", loggerFactory.CreateLogger("ClawdDotNet.Manager.Update")));
|
||||
builder.Services.AddSingleton(AccessToken.For(token));
|
||||
builder.Services.AddSingleton(forwarding);
|
||||
|
||||
|
||||
@@ -367,6 +367,13 @@ async function aktualisiereStagingZaehler() {
|
||||
async function starteHauptAnwendung(instanzen) {
|
||||
versteckeAnmeldung();
|
||||
|
||||
// #661: language packs before the first render. #764: Before the header as well - it stood
|
||||
// below, so the instance list and the header status were drawn first and showed raw keys
|
||||
// ("header.instanceOffline", "status-running") until something drew them again.
|
||||
await loadLanguage();
|
||||
translateDocument();
|
||||
if (spracheSchalter) spracheSchalter.value = currentLanguage();
|
||||
|
||||
// Instanzenliste initialisieren
|
||||
await aktualisiereInstanzenListe(Array.isArray(instanzen) ? instanzen : null);
|
||||
|
||||
@@ -394,11 +401,6 @@ async function starteHauptAnwendung(instanzen) {
|
||||
}
|
||||
});
|
||||
|
||||
// #661: language packs before the first render
|
||||
await loadLanguage();
|
||||
translateDocument();
|
||||
if (spracheSchalter) spracheSchalter.value = currentLanguage();
|
||||
|
||||
// Router initialisieren
|
||||
initialisiereRouter();
|
||||
}
|
||||
|
||||
@@ -669,6 +669,19 @@
|
||||
"logs.empty": "Noch keine Protokolleinträge vorhanden.",
|
||||
|
||||
"settings.title": "Einstellungen & Systemarchitektur",
|
||||
"settings.updateCard": "Update",
|
||||
"settings.updateInstalled": "Installiert",
|
||||
"settings.updateLatest": "Neueste",
|
||||
"settings.updateState": "Zustand",
|
||||
"settings.updateCurrent": "Aktuell",
|
||||
"settings.updateAvailable": "Update verfügbar",
|
||||
"settings.updateCritical": "Wichtiges Update",
|
||||
"settings.updateProblem": "Nicht prüfbar",
|
||||
"settings.updateCheckNow": "Jetzt prüfen",
|
||||
"settings.updateInstall": "Einspielen",
|
||||
"settings.updateConfirm": "Version {version} einspielen? Zuerst werden alle Instanzen angehalten; der Instanzmanager ist ein bis zwei Minuten nicht erreichbar.",
|
||||
"settings.updateHint": "Der update-agent spielt das Release ein und startet den Instanzmanager wieder. Instanzen mit Autostart kommen mit ihm zurück.",
|
||||
"settings.updateHintSystemd": "Läuft als systemd-Dienst: Das Update geht über clawddotnet-update.service (siehe docs/InstanceSetupGuide.md, Abschnitt 8). Instanzen mit Autostart kommen mit dem Manager zurück.",
|
||||
"settings.telegramCard": "Telegram-Client",
|
||||
"settings.telegramState.Connecting": "Verbindet",
|
||||
"settings.telegramState.NeedsCode": "Code nötig",
|
||||
|
||||
@@ -669,6 +669,19 @@
|
||||
"logs.empty": "No log messages yet.",
|
||||
|
||||
"settings.title": "Settings & System Architecture",
|
||||
"settings.updateCard": "Update",
|
||||
"settings.updateInstalled": "Installed",
|
||||
"settings.updateLatest": "Latest",
|
||||
"settings.updateState": "State",
|
||||
"settings.updateCurrent": "Up to date",
|
||||
"settings.updateAvailable": "Update available",
|
||||
"settings.updateCritical": "Important update",
|
||||
"settings.updateProblem": "Not checkable",
|
||||
"settings.updateCheckNow": "Check now",
|
||||
"settings.updateInstall": "Install",
|
||||
"settings.updateConfirm": "Install version {version}? All instances are stopped first; the instance manager is unreachable for a minute or two.",
|
||||
"settings.updateHint": "The update agent installs the release and starts the instance manager again. Instances with autostart come back with it.",
|
||||
"settings.updateHintSystemd": "Runs as a systemd service: the update goes through clawddotnet-update.service (see docs/InstanceSetupGuide.md, section 8). Instances with autostart come back with the manager.",
|
||||
"settings.telegramCard": "Telegram client",
|
||||
"settings.telegramState.Connecting": "Connecting",
|
||||
"settings.telegramState.NeedsCode": "Code needed",
|
||||
|
||||
@@ -129,14 +129,16 @@ async function managerSection() {
|
||||
const el = section("sektion-manager", PFADE.server, t("settings.managerLevel"),
|
||||
[erstellePlakette(t("settings.managerBadge"), "warnung")], t("settings.managerIntro"));
|
||||
|
||||
const [license, manager] = await Promise.all([
|
||||
const [license, manager, update] = await Promise.all([
|
||||
api("/verwaltung/license").catch(() => null),
|
||||
api("/verwaltung/settings").catch(() => null)
|
||||
api("/verwaltung/settings").catch(() => null),
|
||||
api("/verwaltung/update").catch(() => null)
|
||||
]);
|
||||
|
||||
const grid = erstelleElement("div", { klasse: "settings-grid" });
|
||||
grid.append(licenseCard(license));
|
||||
if (manager) grid.append(managerCard(manager));
|
||||
if (update) grid.append(updateCard(update));
|
||||
el.append(grid);
|
||||
return el;
|
||||
}
|
||||
@@ -420,3 +422,81 @@ function telegramCard(initial) {
|
||||
render(initial);
|
||||
return card;
|
||||
}
|
||||
|
||||
// ─── Updates (#795) ───
|
||||
//
|
||||
// Installing stops every instance first; the manager then hands over to the update agent (or,
|
||||
// under systemd, to clawddotnet-update.service) and goes away for a while. The page waits for
|
||||
// it to answer again and reloads.
|
||||
|
||||
function updateCard(status) {
|
||||
const body = erstelleElement("div", { klasse: "formular-raster" });
|
||||
const card = erstelleKarte(t("settings.updateCard"), [body]);
|
||||
|
||||
function render(s) {
|
||||
const state = s.problem
|
||||
? erstellePlakette(t("settings.updateProblem"), "warnung")
|
||||
: s.available
|
||||
? erstellePlakette(s.critical ? t("settings.updateCritical") : t("settings.updateAvailable"), s.critical ? "gefahr" : "warnung")
|
||||
: erstellePlakette(t("settings.updateCurrent"), "erfolg");
|
||||
|
||||
body.replaceChildren(dataList([
|
||||
[t("settings.updateInstalled"), s.installedVersion],
|
||||
[t("settings.updateLatest"), s.latestVersion ?? "—"],
|
||||
[t("settings.updateState"), state]
|
||||
]));
|
||||
if (s.problem) body.append(erstelleElement("p", { klasse: "gedaempft text-klein", text: s.problem }));
|
||||
if (s.available && s.notes) body.append(erstelleElement("pre", { klasse: "text-klein", text: s.notes }));
|
||||
|
||||
const check = erstelleElement("button", {
|
||||
type: "button", klasse: "schaltflaeche flach klein", text: t("settings.updateCheckNow"),
|
||||
onclick: async () => {
|
||||
check.disabled = true;
|
||||
try { render(await api("/verwaltung/update?refresh=1")); } catch (err) { zeigeMeldung(err.message, "gefahr"); check.disabled = false; }
|
||||
}
|
||||
});
|
||||
const actions = erstelleElement("div", { klasse: "gruppe-horizontal" }, check);
|
||||
if (s.available) {
|
||||
actions.append(erstelleElement("button", {
|
||||
type: "button", klasse: "schaltflaeche primaer klein", text: t("settings.updateInstall"),
|
||||
onclick: () => install(s)
|
||||
}));
|
||||
}
|
||||
body.append(actions,
|
||||
erstelleElement("p", { klasse: "gedaempft text-klein", text: s.underSystemd ? t("settings.updateHintSystemd") : t("settings.updateHint") }));
|
||||
}
|
||||
|
||||
async function install(s) {
|
||||
if (!confirm(t("settings.updateConfirm", { version: s.latestVersion ?? "?" }))) return;
|
||||
try {
|
||||
const result = await api("/verwaltung/update/install", { method: "POST" });
|
||||
body.replaceChildren(erstelleElement("p", { klasse: "hinweis", text: result?.meldung ?? result?.message ?? "" }));
|
||||
waitForManager();
|
||||
} catch (err) {
|
||||
zeigeMeldung(err.message, "gefahr", 10000);
|
||||
}
|
||||
}
|
||||
|
||||
// The manager is gone for a while; ask until it answers again, then show the new state.
|
||||
// Reload only after it was gone once — otherwise the old manager, still shutting down,
|
||||
// would answer and the page would show the old version.
|
||||
function waitForManager() {
|
||||
let tries = 0;
|
||||
let wasDown = false;
|
||||
const timer = setInterval(async () => {
|
||||
tries++;
|
||||
if (!card.isConnected || tries > 120) { clearInterval(timer); return; }
|
||||
try {
|
||||
await api("/verwaltung/update");
|
||||
if (!wasDown) return;
|
||||
clearInterval(timer);
|
||||
location.reload();
|
||||
} catch {
|
||||
wasDown = true;
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
render(status);
|
||||
return card;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user