feat: add resilient updates and access controls
Container-Image veröffentlichen / container (push) Has been cancelled

This commit is contained in:
admin committed 2026-08-28 14:02:07 +02:00
1 parent a000152423
commit f02ec9977c
41 files changed
+3277 -151

No files matched your search

+143 -8
View File
@@ -91,6 +91,32 @@
syncIPv6Settings();
}
const setupForm = document.querySelector('[data-setup-form]');
if (setupForm) {
const password = setupForm.querySelector('[data-setup-password]');
const confirmation = setupForm.querySelector('[data-setup-confirm]');
const lengthHint = setupForm.querySelector('[data-password-length]');
const matchHint = setupForm.querySelector('[data-password-match]');
const syncPasswordGuidance = () => {
const length = password ? [...password.value].length : 0;
const longEnough = length >= 15 && length <= 128;
const matches = Boolean(confirmation?.value) && confirmation.value === password?.value;
lengthHint?.classList.toggle('is-valid', longEnough);
matchHint?.classList.toggle('is-valid', matches);
lengthHint?.setAttribute('data-state', longEnough ? 'valid' : 'pending');
matchHint?.setAttribute('data-state', matches ? 'valid' : 'pending');
password?.classList.toggle('is-valid', longEnough);
confirmation?.classList.toggle('is-valid', matches);
setupForm.classList.toggle('is-password-ready', longEnough && matches);
confirmation?.setCustomValidity(confirmation.value && !matches ? 'Die Passphrasen stimmen nicht überein.' : '');
};
password?.addEventListener('input', syncPasswordGuidance);
confirmation?.addEventListener('input', syncPasswordGuidance);
syncPasswordGuidance();
}
for (const form of document.querySelectorAll('[data-target-form]')) {
const active = form.querySelector('[data-target-active]');
const staticFields = form.querySelector('[data-static-ip-fields]');
@@ -226,12 +252,107 @@
});
}
const countdowns = [...document.querySelectorAll('[data-countdown]')];
if (countdowns.length === 0) return;
const softwareUpdateForms = [...document.querySelectorAll('[data-software-update-form]')];
if (softwareUpdateForms.length > 0) {
const overlay = document.querySelector('[data-software-update-overlay]');
const message = overlay?.querySelector('[data-software-update-message]');
const close = overlay?.querySelector('[data-software-update-close]');
const phaseNodes = [...(overlay?.querySelectorAll('[data-update-phase]') || [])];
const phaseOrder = ['pulling', 'restarting', 'verifying'];
let polling = false;
let targetVersion = '';
let startedAt = 0;
let nextRun = Number(countdowns[0].dataset.countdown || 0);
let running = countdowns[0].dataset.running === 'true';
let enabled = countdowns[0].dataset.enabled !== 'false';
const setUpdatePhase = (phase, text) => {
if (!overlay) return;
overlay.dataset.updateState = phase;
if (message && text) message.textContent = text;
const currentIndex = phaseOrder.indexOf(phase);
for (const node of phaseNodes) {
const nodeIndex = phaseOrder.indexOf(node.dataset.updatePhase);
node.classList.toggle('is-active', nodeIndex === currentIndex);
node.classList.toggle('is-complete', currentIndex > nodeIndex || phase === 'complete');
}
};
const pollSoftwareUpdate = async () => {
if (!polling) return;
if (Date.now() - startedAt > 12 * 60 * 1000) {
polling = false;
setUpdatePhase('failed', 'Das Update dauert ungewöhnlich lange. Der DNS-Dienst läuft unabhängig davon weiter; prüfe den Status erneut.');
if (close) close.hidden = false;
return;
}
try {
const response = await fetch('/api/v1/software-update/status', {headers: {'Accept': 'application/json'}, cache: 'no-store'});
if (!response.ok) throw new Error('status unavailable');
const state = await response.json();
const phase = state.phase || 'queued';
const messages = {
queued: 'Update ist eingereiht.',
pulling: 'Das neue Container-Image wird geladen. Der laufende Dienst bleibt dabei online.',
restarting: 'Der Dienst wechselt kontrolliert auf die neue Version.',
verifying: 'Die neue Instanz durchläuft den Healthcheck.',
complete: `Version ${state.target_version || targetVersion} ist bereit.`,
failed: state.message || 'Update fehlgeschlagen. Die bisherige Version bleibt aktiv.',
};
setUpdatePhase(phase, messages[phase] || state.message || 'Update läuft.');
if (phase === 'complete') {
polling = false;
window.setTimeout(() => window.location.assign('/system'), 1400);
return;
}
if (phase === 'failed') {
polling = false;
if (close) close.hidden = false;
return;
}
} catch {
setUpdatePhase('reconnecting', 'Dienst startet neu – Verbindung wird automatisch wiederhergestellt.');
}
window.setTimeout(pollSoftwareUpdate, 1500);
};
for (const form of softwareUpdateForms) {
form.addEventListener('submit', async (event) => {
event.preventDefault();
if (polling || !overlay) return;
const formData = new FormData(form);
targetVersion = String(formData.get('version') || '');
overlay.hidden = false;
document.body.classList.add('software-update-open');
setUpdatePhase('queued', `Update auf ${targetVersion} wird vorbereitet.`);
for (const candidate of softwareUpdateForms) candidate.querySelector('button')?.setAttribute('disabled', '');
try {
const response = await fetch(form.action, {method: 'POST', body: formData, headers: {'Accept': 'application/json'}});
const result = await response.json().catch(() => ({}));
if (!response.ok || !result.accepted) throw new Error(result.message || 'Update konnte nicht gestartet werden.');
polling = true;
startedAt = Date.now();
window.setTimeout(pollSoftwareUpdate, 400);
} catch (error) {
setUpdatePhase('failed', error instanceof Error ? error.message : 'Update konnte nicht gestartet werden.');
if (close) close.hidden = false;
for (const candidate of softwareUpdateForms) candidate.querySelector('button')?.removeAttribute('disabled');
}
});
}
close?.addEventListener('click', () => {
overlay.hidden = true;
document.body.classList.remove('software-update-open');
close.hidden = true;
for (const form of softwareUpdateForms) form.querySelector('button')?.removeAttribute('disabled');
});
}
const countdowns = [...document.querySelectorAll('[data-countdown]')];
const currentIPv4Fields = [...document.querySelectorAll('[data-current-ipv4-placeholder]')];
if (countdowns.length === 0 && currentIPv4Fields.length === 0) return;
let nextRun = Number(countdowns[0]?.dataset.countdown || 0);
let running = countdowns[0]?.dataset.running === 'true';
let enabled = countdowns[0]?.dataset.enabled !== 'false';
let lastRefresh = 0;
const formatRemaining = (seconds) => {
@@ -243,6 +364,7 @@
};
const refreshSchedule = async () => {
if (document.hidden) return;
const now = Date.now();
if (now - lastRefresh < 2500) return;
lastRefresh = now;
@@ -253,12 +375,17 @@
nextRun = Number(state.next_run_unix || 0);
running = Boolean(state.running);
enabled = state.enabled !== false;
if (typeof state.last_ipv4 === 'string' && state.last_ipv4.trim()) {
for (const field of currentIPv4Fields) field.placeholder = state.last_ipv4.trim();
}
} catch {
// The visible countdown keeps its last known state when the status request fails.
// Visible scheduler data keeps its last known state when the request fails.
}
};
const tick = () => {
if (document.hidden) return;
if (countdowns.length === 0) return;
if (!enabled) {
for (const node of countdowns) {
node.textContent = 'Inaktiv';
@@ -276,8 +403,16 @@
if (running || nextRun === 0 || remaining === 0) refreshSchedule();
};
tick();
if (countdowns.length > 0) {
tick();
window.setInterval(tick, 1000);
}
refreshSchedule();
window.setInterval(tick, 1000);
window.setInterval(refreshSchedule, 30000);
document.addEventListener('visibilitychange', () => {
if (document.hidden) return;
lastRefresh = 0;
tick();
refreshSchedule();
});
})();