Add temporary danger restart mode
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 12s

This commit is contained in:
Kroonk
2026-05-21 17:21:52 +02:00
parent d62224366b
commit fddb2e3544
4 changed files with 143 additions and 10 deletions

View File

@@ -20,6 +20,8 @@ const resetLocalButton = document.querySelector('#resetLocalButton');
const downloadConfigButton = document.querySelector('#downloadConfigButton');
const refreshOpsButton = document.querySelector('#refreshOpsButton');
const runWatchtowerButton = document.querySelector('#runWatchtowerButton');
const dangerModeButton = document.querySelector('#dangerModeButton');
const dangerState = document.querySelector('#dangerState');
const diskStatus = document.querySelector('#diskStatus');
const watchtowerStatus = document.querySelector('#watchtowerStatus');
const containerStatus = document.querySelector('#containerStatus');
@@ -29,6 +31,10 @@ let tokenSet = null;
let opsRefreshTimer = null;
let latestWatchtower = null;
let lastManualWatchtowerRun = null;
let latestContainers = [];
let dangerToken = null;
let dangerExpiresAt = 0;
let dangerTimer = null;
function base64UrlEncode(buffer) {
return btoa(String.fromCharCode(...new Uint8Array(buffer)))
@@ -352,6 +358,9 @@ function renderContainers(containers) {
return;
}
latestContainers = containers;
const dangerActive = Date.now() < dangerExpiresAt && Boolean(dangerToken);
const important = [
'mischlabs',
'watchtower',
@@ -374,7 +383,7 @@ function renderContainers(containers) {
<div class="ops-line container-line" data-container="${container.name}">
<strong>${container.name}</strong>
<span>${container.status}</span>
<button class="ghost-button mini restart-container" type="button" title="${container.restartable ? 'Container neu starten' : (container.restartBlockReason || 'Neustart gesperrt')}" ${container.restartable ? '' : 'disabled'}>
<button class="ghost-button mini restart-container ${dangerActive && !container.restartable ? 'is-danger-enabled' : ''}" type="button" title="${container.restartable ? 'Container neu starten' : (dangerActive ? 'Danger-Modus aktiv: Neustart erlaubt' : (container.restartBlockReason || 'Neustart gesperrt'))}" ${(container.restartable || dangerActive) ? '' : 'disabled'}>
Neustarten
</button>
</div>
@@ -388,8 +397,9 @@ function renderContainers(containers) {
async function restartContainer(row) {
const name = row?.dataset.container;
if (!name) return;
const dangerActive = Date.now() < dangerExpiresAt && Boolean(dangerToken);
const confirmed = window.confirm(`${name} jetzt neu starten? Der Dienst ist kurz nicht erreichbar.`);
const confirmed = window.confirm(`${name} jetzt neu starten? Der Dienst ist kurz nicht erreichbar.${dangerActive ? '\n\nDanger-Modus ist aktiv.' : ''}`);
if (!confirmed) return;
const button = row.querySelector('.restart-container');
@@ -403,7 +413,8 @@ async function restartContainer(row) {
const response = await fetch(`/api/containers/${encodeURIComponent(name)}/restart`, {
method: 'POST',
headers: {
Authorization: `Bearer ${tokenSet.id_token}`
Authorization: `Bearer ${tokenSet.id_token}`,
...(dangerActive ? { 'X-MischLabs-Danger-Token': dangerToken } : {})
}
});
const payload = await response.json().catch(() => ({}));
@@ -421,6 +432,57 @@ async function restartContainer(row) {
}
}
function updateDangerState() {
const remaining = Math.max(0, dangerExpiresAt - Date.now());
if (!dangerToken || remaining <= 0) {
dangerToken = null;
dangerExpiresAt = 0;
dangerState.hidden = true;
dangerState.textContent = '';
dangerModeButton.textContent = 'Danger entsperren';
dangerModeButton.classList.remove('is-active-danger');
window.clearInterval(dangerTimer);
dangerTimer = null;
if (latestContainers.length) renderContainers(latestContainers);
return;
}
const seconds = Math.ceil(remaining / 1000);
dangerState.hidden = false;
dangerState.textContent = `Danger-Modus aktiv: geschuetzte Neustarts noch ${seconds}s erlaubt.`;
dangerModeButton.textContent = `Danger aktiv ${seconds}s`;
dangerModeButton.classList.add('is-active-danger');
}
async function unlockDangerMode() {
const confirmed = window.confirm('Danger-Modus fuer 2 Minuten aktivieren?\n\nDamit koennen auch geschuetzte Dienste wie Watchtower, Gitea, Keycloak oder Datenbanken neu gestartet werden. Nutze das nur bewusst.');
if (!confirmed) return;
dangerModeButton.disabled = true;
try {
const response = await fetch('/api/danger/unlock', {
method: 'POST',
headers: {
Authorization: `Bearer ${tokenSet.id_token}`
}
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(payload.error || `HTTP ${response.status}`);
dangerToken = payload.token;
dangerExpiresAt = new Date(payload.expiresAt).getTime();
updateDangerState();
if (latestContainers.length) renderContainers(latestContainers);
window.clearInterval(dangerTimer);
dangerTimer = window.setInterval(updateDangerState, 1000);
} catch (error) {
dangerState.hidden = false;
dangerState.textContent = `Danger-Modus konnte nicht aktiviert werden: ${error.message}`;
} finally {
dangerModeButton.disabled = false;
}
}
async function runWatchtowerOnce() {
const confirmed = window.confirm('Watchtower jetzt manuell starten? Je nach Updates koennen Dienste kurz neu starten.');
if (!confirmed) return;
@@ -562,6 +624,7 @@ resetLocalButton.addEventListener('click', () => {
downloadConfigButton.addEventListener('click', downloadConfig);
refreshOpsButton.addEventListener('click', refreshOps);
runWatchtowerButton.addEventListener('click', runWatchtowerOnce);
dangerModeButton.addEventListener('click', unlockDangerMode);
boot().catch((error) => {
loginMessage.textContent = `Adminseite konnte nicht starten: ${error.message}`;