feat: add resilient container restart controls

This commit is contained in:
admin committed 2026-09-07 13:27:27 +02:00
1 parent 1bbb14470c
commit 7bb33e528c
13 files changed
+747 -28

No files matched your search

+109
View File
@@ -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]');