feat: implement update management web UI

Add complete update management interface to web UI:
- Update status display (current version, last status, timestamp, errors)
- Check for updates button with availability indicator
- Apply update button with confirmation dialog and backup warnings
- Rollback button with confirmation dialog
- Update logs viewer (collapsible, reverse chronological)
- Auto-refresh after update/rollback actions (5s delay)

Features:
- Uses existing Pico CSS framework for consistent styling
- Integrates with existing auth system (Bearer token/session cookies)
- Real-time feedback with loading states and error handling
- German UI language matching existing interface
- All API calls use existing api() helper function

Complete US_000029-033 and TASK_000030-034:
- US_000029: Display update status in web UI
- US_000030: Trigger update check from UI
- US_000031: Apply updates from UI
- US_000032: Display update logs in UI
- US_000033: Trigger rollback from UI

EPIC_000008 (Client-Side Update Mechanism) now fully complete.
Documentation updated per SOP (CHANGELOG, PROJECT_STATUS, stories/tasks).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-30 11:32:39 +01:00
parent 107cdabe8d
commit d799c0f042
13 changed files with 210 additions and 25 deletions

View File

@ -84,6 +84,27 @@
<div id="result" class="log"></div>
</section>
<section>
<h3>Update-Verwaltung</h3>
<div id="updateStatus" class="log" style="margin-bottom: 1rem; padding: 1rem; background: var(--pico-card-background-color); border-radius: var(--pico-border-radius);">
Lade Update-Status...
</div>
<div class="grid">
<button id="checkUpdateBtn" type="button">Nach Updates suchen</button>
<button id="applyUpdateBtn" type="button" disabled>Update installieren</button>
<button id="rollbackBtn" type="button">Rollback durchführen</button>
</div>
<div id="updateResult" class="log" style="margin-top: 1rem;"></div>
<details style="margin-top: 1.5rem;">
<summary>Update-Logs anzeigen</summary>
<button id="refreshLogsBtn" type="button" style="margin-top: 0.5rem;">Logs neu laden</button>
<div id="updateLogs" class="log" style="margin-top: 1rem; max-height: 400px; overflow-y: auto;"></div>
</details>
</section>
<script>
const statusDiv = document.getElementById('status');
const resultDiv = document.getElementById('result');
@ -221,6 +242,169 @@
checkSession();
checkOidcStatus();
// ==================== Update Management ====================
const updateStatusDiv = document.getElementById('updateStatus');
const updateResultDiv = document.getElementById('updateResult');
const updateLogsDiv = document.getElementById('updateLogs');
const checkUpdateBtn = document.getElementById('checkUpdateBtn');
const applyUpdateBtn = document.getElementById('applyUpdateBtn');
const rollbackBtn = document.getElementById('rollbackBtn');
const refreshLogsBtn = document.getElementById('refreshLogsBtn');
let latestUpdateCheck = null;
async function refreshUpdateStatus() {
try {
const data = await api('/update/status');
const statusText = `
Version: ${data.current_version}
Letzter Status: ${data.last_status || 'unbekannt'}
${data.last_error ? `Fehler: ${data.last_error}` : ''}
${data.last_timestamp ? `Zeitstempel: ${new Date(data.last_timestamp).toLocaleString('de-DE')}` : ''}
`.trim();
updateStatusDiv.textContent = statusText;
} catch (err) {
updateStatusDiv.textContent = `Fehler beim Laden des Update-Status: ${err.message}`;
}
}
checkUpdateBtn.addEventListener('click', async () => {
updateResultDiv.textContent = 'Prüfe auf Updates...';
checkUpdateBtn.disabled = true;
try {
const data = await api('/update/check', { method: 'POST' });
latestUpdateCheck = data;
if (data.available) {
updateResultDiv.textContent = `
✅ Update verfügbar!
Version: ${data.latest_version}
${data.message ? `Info: ${data.message}` : ''}
Klicke auf "Update installieren" um fortzufahren.
`.trim();
applyUpdateBtn.disabled = false;
} else {
updateResultDiv.textContent = `✓ Keine Updates verfügbar. Aktuelle Version ist aktuell.`;
applyUpdateBtn.disabled = true;
}
} catch (err) {
updateResultDiv.textContent = `❌ Fehler beim Update-Check: ${err.message}`;
applyUpdateBtn.disabled = true;
} finally {
checkUpdateBtn.disabled = false;
}
});
applyUpdateBtn.addEventListener('click', async () => {
if (!latestUpdateCheck || !latestUpdateCheck.available) {
updateResultDiv.textContent = '❌ Bitte zuerst nach Updates suchen.';
return;
}
const confirmed = confirm(
`Update auf Version ${latestUpdateCheck.latest_version} installieren?\n\n` +
`⚠️ WICHTIG:\n` +
`- Ein Backup wird automatisch erstellt\n` +
`- Der Service wird neu gestartet\n` +
`- Bei Fehlern erfolgt automatischer Rollback\n\n` +
`Fortfahren?`
);
if (!confirmed) return;
updateResultDiv.textContent = 'Update wird gestartet... (läuft im Hintergrund)';
applyUpdateBtn.disabled = true;
try {
const data = await api('/update/apply', {
method: 'POST',
body: JSON.stringify({ version: latestUpdateCheck.latest_version })
});
updateResultDiv.textContent = `
✓ ${data.message}
Das Update läuft jetzt im Hintergrund.
Aktualisiere den Status in wenigen Sekunden, um den Fortschritt zu sehen.
`.trim();
// Auto-refresh nach 5 Sekunden
setTimeout(() => {
refreshUpdateStatus();
applyUpdateBtn.disabled = true;
}, 5000);
} catch (err) {
updateResultDiv.textContent = `❌ Fehler beim Starten des Updates: ${err.message}`;
applyUpdateBtn.disabled = false;
}
});
rollbackBtn.addEventListener('click', async () => {
const confirmed = confirm(
`Rollback zum letzten Backup durchführen?\n\n` +
`⚠️ WICHTIG:\n` +
`- Dies stellt die vorherige Version wieder her\n` +
`- Der Service wird neu gestartet\n` +
`- Ein Backup muss vorhanden sein\n\n` +
`Fortfahren?`
);
if (!confirmed) return;
updateResultDiv.textContent = 'Rollback wird gestartet... (läuft im Hintergrund)';
rollbackBtn.disabled = true;
try {
const data = await api('/update/rollback', { method: 'POST' });
updateResultDiv.textContent = `
✓ ${data.message}
Der Rollback läuft jetzt im Hintergrund.
Aktualisiere den Status in wenigen Sekunden.
`.trim();
// Auto-refresh nach 5 Sekunden
setTimeout(() => {
refreshUpdateStatus();
}, 5000);
} catch (err) {
updateResultDiv.textContent = `❌ Fehler beim Rollback: ${err.message}`;
} finally {
rollbackBtn.disabled = false;
}
});
async function refreshUpdateLogs() {
updateLogsDiv.textContent = 'Lade Logs...';
try {
const logs = await api('/update/logs');
if (!logs || logs.length === 0) {
updateLogsDiv.textContent = 'Keine Logs vorhanden.';
return;
}
// Reverse chronological (newest first)
const logEntries = logs.reverse().map(entry => {
const timestamp = entry.timestamp ? new Date(entry.timestamp).toLocaleString('de-DE') : 'unbekannt';
const status = entry.status || 'unknown';
const version = entry.version || '-';
const error = entry.error ? `\n Fehler: ${entry.error}` : '';
return `[${timestamp}] ${status} - Version: ${version}${error}`;
});
updateLogsDiv.textContent = logEntries.join('\n\n');
} catch (err) {
updateLogsDiv.textContent = `Fehler beim Laden der Logs: ${err.message}`;
}
}
refreshLogsBtn.addEventListener('click', refreshUpdateLogs);
// Initial load
refreshUpdateStatus();
</script>
</body>
</html>