feat: add resilient container restart controls
This commit is contained in:
1 parent
1bbb14470c
commit
7bb33e528c
13 files changed
+747
-28
No files matched your search
@@ -478,6 +478,115 @@
|
||||
});
|
||||
}
|
||||
|
||||
const containerRestartForm = document.querySelector('[data-container-restart-form]');
|
||||
if (containerRestartForm) {
|
||||
const overlay = document.querySelector('[data-container-restart-overlay]');
|
||||
const title = overlay?.querySelector('[data-container-restart-title]');
|
||||
const message = overlay?.querySelector('[data-container-restart-message]');
|
||||
const close = overlay?.querySelector('[data-container-restart-close]');
|
||||
const submit = containerRestartForm.querySelector('button[type="submit"]');
|
||||
let polling = false;
|
||||
let startedAt = 0;
|
||||
let expectedInstance = containerRestartForm.dataset.instanceId || '';
|
||||
let consecutiveReady = 0;
|
||||
let sawUnavailable = false;
|
||||
|
||||
const setRestartState = (state, heading, text) => {
|
||||
if (!overlay) return;
|
||||
overlay.dataset.updateState = state;
|
||||
overlay.setAttribute('aria-busy', String(state !== 'complete' && state !== 'failed'));
|
||||
if (title) title.textContent = heading;
|
||||
if (message) message.textContent = text;
|
||||
};
|
||||
|
||||
const pollContainerRestart = async () => {
|
||||
if (!polling) return;
|
||||
if (Date.now() - startedAt > 90 * 1000) {
|
||||
polling = false;
|
||||
setRestartState('failed', 'Neustart nicht bestätigt', 'Die neue Container-Instanz wurde nicht rechtzeitig erreichbar. Prüfe den Containerstatus am Docker-Host.');
|
||||
if (close) close.hidden = false;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await fetch(`/healthz?restart-check=${Date.now()}`, {cache: 'no-store', credentials: 'same-origin'});
|
||||
if (!response.ok) throw new Error('healthcheck unavailable');
|
||||
const currentInstance = response.headers.get('X-Domain-Control-Instance') || '';
|
||||
const isNewInstance = expectedInstance
|
||||
? (currentInstance !== '' && currentInstance !== expectedInstance) || (currentInstance === '' && sawUnavailable)
|
||||
: sawUnavailable;
|
||||
if (isNewInstance) {
|
||||
consecutiveReady += 1;
|
||||
setRestartState('restarting', 'Neue Instanz wird geprüft', 'Der Container ist wieder erreichbar. Die Verbindung wird noch einmal bestätigt.');
|
||||
if (consecutiveReady >= 2) {
|
||||
polling = false;
|
||||
setRestartState('complete', 'Container neu gestartet', 'Die neue Container-Instanz ist bereit. Die Seite wird neu geladen.');
|
||||
window.setTimeout(() => window.location.assign('/system'), 900);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
consecutiveReady = 0;
|
||||
setRestartState('restarting', 'Container wird neu gestartet', 'Die bisherige Instanz wird geordnet beendet.');
|
||||
}
|
||||
} catch {
|
||||
sawUnavailable = true;
|
||||
consecutiveReady = 0;
|
||||
setRestartState('restarting', 'Verbindung wird aufgebaut', 'Der Container ist kurz nicht erreichbar und startet automatisch neu.');
|
||||
}
|
||||
window.setTimeout(pollContainerRestart, 700);
|
||||
};
|
||||
|
||||
containerRestartForm.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
if (polling || !overlay) return;
|
||||
if (!window.confirm('Den vollständigen DynDNS-Container jetzt neu starten? Der Dienst ist kurz nicht erreichbar.')) return;
|
||||
|
||||
const formData = new FormData(containerRestartForm);
|
||||
const requestBody = new URLSearchParams();
|
||||
for (const [name, value] of formData) {
|
||||
if (typeof value === 'string') requestBody.append(name, value);
|
||||
}
|
||||
polling = true;
|
||||
startedAt = Date.now();
|
||||
consecutiveReady = 0;
|
||||
sawUnavailable = false;
|
||||
overlay.hidden = false;
|
||||
if (close) close.hidden = true;
|
||||
if (submit) submit.disabled = true;
|
||||
document.body.classList.add('software-update-open');
|
||||
setRestartState('restarting', 'Neustart wird vorbereitet', 'Die Anwendung wird geordnet beendet und automatisch wieder gestartet.');
|
||||
|
||||
let responseReceived = false;
|
||||
try {
|
||||
const response = await fetch(containerRestartForm.action, {method: 'POST', body: requestBody, headers: {'Accept': 'application/json'}, credentials: 'same-origin'});
|
||||
responseReceived = true;
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
const result = contentType.includes('application/json')
|
||||
? await response.json().catch(() => ({}))
|
||||
: {message: (await response.text()).trim().slice(0, 240)};
|
||||
if (!response.ok || !result.accepted) throw new Error(result.message || 'Container-Neustart konnte nicht gestartet werden.');
|
||||
expectedInstance = result.instance_id || expectedInstance;
|
||||
} catch (error) {
|
||||
if (responseReceived) {
|
||||
polling = false;
|
||||
setRestartState('failed', 'Neustart nicht gestartet', error instanceof Error ? error.message : 'Container-Neustart konnte nicht gestartet werden.');
|
||||
if (close) close.hidden = false;
|
||||
if (submit) submit.disabled = false;
|
||||
return;
|
||||
}
|
||||
sawUnavailable = true;
|
||||
}
|
||||
window.setTimeout(pollContainerRestart, 350);
|
||||
});
|
||||
|
||||
close?.addEventListener('click', () => {
|
||||
polling = false;
|
||||
overlay.hidden = true;
|
||||
close.hidden = true;
|
||||
document.body.classList.remove('software-update-open');
|
||||
if (submit) submit.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
const countdowns = [...document.querySelectorAll('[data-countdown]')];
|
||||
const currentIPv4Fields = [...document.querySelectorAll('[data-current-ipv4-placeholder]')];
|
||||
const dashboardIPv4 = document.querySelector('[data-dashboard-ipv4]');
|
||||
|
||||
Reference in new issue
Block a user