feat(admin): ask the panel to restart, from verdande's own settings
Release / Build and publish image (push) Failing after 10m2s
Release / Build and publish image (push) Failing after 10m2s
A container cannot replace its own image from the inside, so "update" is really "ask whoever owns the container to recreate it" — Yggdrasil pulls `:latest` on the way, which is what makes a restart an update at all. This removes the second browser tab, not the panel's authority: the panel still does the work and still records who asked. Three settings, needed together, and the page names whichever are missing — an operator reading "not configured" should not have to go and find out which of three. The token is never sent to a browser; the page is told only whether it is set. Answers 202 rather than waiting. The panel stops this container as part of answering, so the reply may never arrive, and a request cut off mid-flight is the successful case wearing a failure's clothes. The interface believes neither answer and polls /healthz until it responds. Administrators only, and sessions only. A leaked API token that could restart the server is a denial of service in one request. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
19aa02e80c
commit
2b45aa33e1
@@ -89,6 +89,55 @@
|
||||
|
||||
let version = $state(null);
|
||||
|
||||
// --- restarting from here ---------------------------------------------------
|
||||
|
||||
let panel = $state(null);
|
||||
let restarting = $state(false);
|
||||
let restartNote = $state('');
|
||||
|
||||
$effect(() => {
|
||||
api
|
||||
.panelStatus()
|
||||
.then((p) => (panel = p))
|
||||
.catch(() => {});
|
||||
});
|
||||
|
||||
/**
|
||||
* Asks the panel to recreate this container, then waits for it to come back.
|
||||
*
|
||||
* The reply may never arrive — the panel stops this container as part of
|
||||
* answering — so the answer is not believed either way. `/healthz` is polled
|
||||
* until it responds, which is the only thing that actually says "it is up".
|
||||
*/
|
||||
async function restart() {
|
||||
if (!confirm(t('update.restartQuestion'))) return;
|
||||
restarting = true;
|
||||
restartNote = t('update.asking');
|
||||
try {
|
||||
await api.restartFromPanel();
|
||||
} catch (e) {
|
||||
// A request cut off mid-flight is the successful case wearing a
|
||||
// failure's clothes. The poll below decides.
|
||||
restartNote = humanMessage(e);
|
||||
}
|
||||
|
||||
restartNote = t('update.waiting');
|
||||
for (let i = 0; i < 60; i++) {
|
||||
await new Promise((r) => setTimeout(r, 2000));
|
||||
try {
|
||||
const response = await fetch('/healthz', { cache: 'no-store' });
|
||||
if (response.ok) {
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// Still down, which is expected for the first few seconds.
|
||||
}
|
||||
}
|
||||
restarting = false;
|
||||
restartNote = t('update.stillDown');
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
api.version().then((v) => (version = v)).catch(() => {});
|
||||
});
|
||||
@@ -182,6 +231,20 @@
|
||||
{/if}
|
||||
</p>
|
||||
|
||||
<!-- Restarting is what makes a new version arrive: a container cannot replace
|
||||
its own image, and the panel pulls `:latest` when it recreates one. The
|
||||
button is here rather than in another browser tab. -->
|
||||
{#if panel?.configured}
|
||||
<div class="row">
|
||||
<button class="secondary" onclick={restart} disabled={restarting}>
|
||||
{restarting ? t('update.restarting') : t('update.restart')}
|
||||
</button>
|
||||
{#if restartNote}<span class="saved">{restartNote}</span>{/if}
|
||||
</div>
|
||||
{:else if panel}
|
||||
<p class="hint">{t('update.notConfigured', { missing: panel.missing.join(', ') })}</p>
|
||||
{/if}
|
||||
|
||||
{#if version.update_available}
|
||||
{#if version.notes}
|
||||
<p class="notes">{version.notes}</p>
|
||||
|
||||
Reference in New Issue
Block a user