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]');
|
||||
|
||||
@@ -513,6 +513,17 @@ input:focus, textarea:focus, select:focus { border-color: var(--accent); box-sha
|
||||
.software-update-button { min-height: 44px; border-radius: 9px; box-shadow: 0 10px 22px rgba(37,99,235,.16); }
|
||||
.software-update-button i { font-style: normal; transition: transform .2s ease; }
|
||||
.software-update-button:hover i { transform: translate(2px,-2px); }
|
||||
.container-restart-panel { display: grid; width: min(930px,100%); grid-template-columns: 44px minmax(0,1fr) auto; gap: 14px; align-items: center; margin-bottom: 12px; }
|
||||
.container-restart-icon { display: grid; width: 42px; height: 42px; place-items: center; border: 1px solid #c8d3df; border-radius: 12px; background: linear-gradient(145deg,#f7f9fb,#edf1f5); color: #526174; font-family: var(--mono); font-size: 18px; font-weight: 900; }
|
||||
.container-restart-copy h2 { margin-bottom: 3px; }
|
||||
.container-restart-copy p { max-width: 650px; margin: 0; color: var(--muted); font-size: 9px; line-height: 1.55; }
|
||||
.container-restart-panel form { position: relative; z-index: 1; }
|
||||
.container-restart-overlay .software-update-dialog { width: min(500px,100%); }
|
||||
.container-restart-progress { height: 7px; overflow: hidden; margin-top: 18px; border: 1px solid rgba(147,197,253,.08); border-radius: 99px; background: #202f44; box-shadow: inset 0 1px 2px rgba(0,0,0,.2); }
|
||||
.container-restart-progress i { display: block; width: 38%; height: 100%; border-radius: inherit; background: linear-gradient(90deg,#2563eb,#60a5fa,#2dd4bf); box-shadow: 0 0 17px rgba(96,165,250,.52); animation: container-restart-progress 1.35s ease-in-out infinite; }
|
||||
.container-restart-overlay[data-update-state="complete"] .container-restart-progress i { width: 100%; background: #34d399; animation: none; }
|
||||
.container-restart-overlay[data-update-state="failed"] .container-restart-progress i { width: 100%; background: #e45c70; animation: none; }
|
||||
.container-restart-overlay [data-container-restart-close] { margin: 20px auto 0; border-color: #40516a; background: #1a2940; color: #dbe7f6; }
|
||||
.update-credential-panel { width: min(930px,100%); }
|
||||
.update-credential-panel.is-connected { overflow: hidden; padding: 0; border-color: #b8dcd5; background: linear-gradient(120deg,#fff 62%,#f0faf7); }
|
||||
.update-access-state { display: grid; grid-template-columns: 44px minmax(0,1fr) auto; gap: 14px; align-items: center; padding: 18px; }
|
||||
@@ -613,6 +624,7 @@ body.software-update-open { overflow: hidden; }
|
||||
@keyframes update-progress-shine { 0%,25% { transform: translateX(-110%); } 75%,100% { transform: translateX(110%); } }
|
||||
@keyframes update-phase-pulse { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-2px); } }
|
||||
@keyframes update-result-pop { 0% { transform: scale(.82) rotate(-5deg); } 70% { transform: scale(1.08) rotate(2deg); } 100% { transform: scale(1) rotate(0); } }
|
||||
@keyframes container-restart-progress { from { transform: translateX(-115%); } to { transform: translateX(305%); } }
|
||||
|
||||
.error-summary-grid { display: grid; grid-template-columns: repeat(4,minmax(0,1fr)); gap: 10px; margin-bottom: 12px; }
|
||||
.error-summary-grid article { display: flex; align-items: center; justify-content: space-between; padding: 13px 15px; border: 1px solid #efc7ce; border-radius: var(--radius-sm); background: #fff8f9; }
|
||||
@@ -709,6 +721,9 @@ body.software-update-open { overflow: hidden; }
|
||||
.software-version-visual { width: 46px; height: 46px; border-radius: 13px; }
|
||||
.software-version-action { grid-column: 1 / -1; }
|
||||
.software-version-action form, .software-version-action .button { width: 100%; }
|
||||
.container-restart-panel { grid-template-columns: 42px minmax(0,1fr); }
|
||||
.container-restart-panel form { grid-column: 1 / -1; }
|
||||
.container-restart-panel form, .container-restart-panel .button { width: 100%; }
|
||||
.update-access-state { grid-template-columns: 42px minmax(0,1fr); }
|
||||
.update-access-state > .status-pill { grid-column: 2; justify-self: start; }
|
||||
.admin-full-access-card { grid-template-columns: 42px minmax(0,1fr); }
|
||||
@@ -755,6 +770,7 @@ body.software-update-open { overflow: hidden; }
|
||||
.software-update-dialog p { min-height: 30px; margin-bottom: 12px; }
|
||||
.software-update-phases { margin-top: 14px; }
|
||||
.software-update-dialog [data-software-update-close] { margin-top: 14px; }
|
||||
.container-restart-overlay [data-container-restart-close] { margin-top: 14px; }
|
||||
.onboarding-intro { padding-block: 30px; }
|
||||
.onboarding-copy { margin-top: 28px; }
|
||||
.onboarding-copy h1 { margin-bottom: 15px; font-size: 44px; }
|
||||
|
||||
Reference in new issue
Block a user