Unifi-Voucher-Tool/updater/templates/update.php
Friederich Loheide 7fa423a74c
Some checks are pending
CI / PHP Lint (push) Waiting to run
CI / PHP Lint-1 (push) Waiting to run
CI / Unit Tests & Static Analysis (push) Waiting to run
CI / PHP Lint (pull_request) Waiting to run
CI / PHP Lint-1 (pull_request) Waiting to run
CI / Unit Tests & Static Analysis (pull_request) Waiting to run
Entwicklerhinweis "Entwickelt von Loheide.eu" ergänzen
Zentral in Ui::credit() gepflegt und dezent im Seitenfuß eingebunden:

- Admin-Bereich: unter dem Benutzerbereich in der Sidebar
- öffentliche Voucher-Seite: unter der Karte
- Anmeldung, Installer und Updater: am Ende der jeweiligen Karte
- Wartungsseite: direkt im Markup, da sie bewusst eigenständig bleibt
- README: im Kopf und in der Fußzeile

Der Hinweis ist übersetzt ("Entwickelt von" / "Developed by") und stört
das Branding der Betreiber nicht – die konfigurierbare Fußzeile der
Login-Seite bleibt unverändert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 11:21:53 +00:00

267 lines
13 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* Admin-UI fuer den Updater. Nutzt das gemeinsame Design-System
* (assets/global.css); nur Updater-spezifische Regeln stehen inline.
*
* Verfuegbare Variablen: $manager, $auth, $currentSha, $channel, $csrfToken
*
* @var \Updater\UpdateManager $manager
* @var \Auth $auth
* @var string $currentSha
* @var string $channel
* @var string $csrfToken
*/
$shortSha = $currentSha !== '' ? substr($currentSha, 0, 7) : 'unbekannt';
$channels = \Updater\UpdateManager::CHANNELS;
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>System-Update</title>
<?php require_once dirname(__DIR__, 2) . '/includes/Ui.php'; ?>
<?= Ui::head(class_exists('Database') ? Database::getInstance() : null, '../') ?>
<style>
/* Updater-spezifisch der Rest kommt aus dem gemeinsamen Design-System. */
.wrap { max-width: 780px; margin: 0 auto; padding: 36px 0 60px; }
.update-head { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 24px; }
.update-head .title { display: flex; align-items: center; gap: 13px; }
.kv-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 11px 0; border-bottom: 1px solid var(--border-color); }
.kv-row:last-of-type { border-bottom: none; }
.kv-row .k { color: var(--text-secondary); font-size: 13.5px; }
.kv-row .v { color: var(--text-primary); font-size: 13.5px; font-weight: 600; font-family: var(--font-mono); }
.kv-row .v select { font-family: var(--font-sans); font-weight: 500; width: auto; min-width: 150px; }
.progress-wrap { display: none; margin-top: 18px; }
.progress-bar { height: 8px; border-radius: var(--r-pill); background: var(--bg-hover); overflow: hidden; }
.progress-fill { height: 100%; width: 0; border-radius: var(--r-pill); background: var(--accent); transition: width .3s; }
.progress-msg { margin-top: 8px; font-size: 12.5px; color: var(--text-muted); }
.alert { display: none; margin: 16px 0 0; }
.alert.show { display: flex; }
.changelog {
display: none;
margin-top: 16px; padding: 14px;
max-height: 260px; overflow: auto;
background: var(--bg-subtle); border: 1px solid var(--border-color); border-radius: var(--r-md);
font-size: 13px; color: var(--text-secondary); white-space: pre-wrap;
}
.mig-item { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 9px 0; border-bottom: 1px solid var(--border-color); font-size: 13.5px; }
.mig-item:last-child { border-bottom: none; }
.mig-item span:first-child { font-family: var(--font-mono); font-size: 12.5px; }
.badge-on { background: var(--success-soft); color: var(--success); border: 1px solid var(--success-border); }
.badge-off { background: var(--warning-soft); color: var(--warning); border: 1px solid var(--warning-border); }
.tabs { display: inline-flex; gap: 3px; padding: 3px; margin-bottom: 18px; background: var(--bg-hover); border: 1px solid var(--border-color); border-radius: var(--r-md); }
.tab { padding: 7px 16px; border-radius: var(--r-sm); font-size: 13px; font-weight: 550; color: var(--text-secondary); cursor: pointer; }
.tab.active { background: var(--bg-card); color: var(--text-primary); box-shadow: var(--shadow-xs); font-weight: 600; }
.pane { display: none; }
.pane.active { display: block; }
</style>
</head>
<body class="app-body">
<div class="wrap">
<div class="update-head">
<div class="title">
<span class="focus-icon"><i class="fas fa-rotate" aria-hidden="true"></i></span>
<div>
<h1 style="font-size:20px;">System-Update</h1>
<p class="sub">Aktualisierung aus dem gewählten Release-Channel</p>
</div>
</div>
<a href="/admin/" class="btn btn-secondary"><i class="fas fa-arrow-left" aria-hidden="true"></i> Administration</a>
</div>
<div class="tabs">
<div class="tab active" data-pane="update">Update</div>
<div class="tab" data-pane="migrations">Migrationen</div>
</div>
<div class="pane active" id="pane-update">
<div class="card">
<h2>Aktuelle Version</h2>
<div class="kv-row"><span class="k">Installierte Version (SHA)</span><span class="v" id="curSha"><?= htmlspecialchars($shortSha) ?></span></div>
<div class="kv-row"><span class="k">Update-Channel</span>
<span class="v">
<select id="channel">
<?php foreach ($channels as $key => $url): ?>
<option value="<?= htmlspecialchars($key) ?>" <?= $channel === $key ? 'selected' : '' ?>><?= htmlspecialchars(ucfirst($key)) ?></option>
<?php endforeach; ?>
</select>
</span>
</div>
<p class="muted">Channel wird in <code>updater/storage/updater-settings.json</code> gespeichert.</p>
</div>
<div class="card">
<h2>Updates</h2>
<button class="btn btn-primary" id="btnCheck"><i class="fas fa-magnifying-glass" aria-hidden="true"></i> Auf Updates prüfen</button>
<button class="btn btn-success" id="btnInstall" style="display:none;margin-left:8px;"><i class="fas fa-download" aria-hidden="true"></i> Update installieren</button>
<div class="changelog" id="changelog"></div>
<div class="progress-wrap" id="progressWrap">
<div class="progress-bar"><div class="progress-fill" id="progressFill"></div></div>
<div class="progress-msg" id="progressMsg"></div>
</div>
<div class="alert alert-error" id="alertError"></div>
<div class="alert alert-ok" id="alertOk"></div>
</div>
</div>
<div class="pane" id="pane-migrations">
<div class="card">
<h2>Migrations-Status</h2>
<div id="migList"><p class="muted">Wird geladen …</p></div>
<button class="btn btn-primary" id="btnRunMig" style="margin-top:16px; display:none;">Ausstehende Migrationen ausführen</button>
<div class="alert alert-ok" id="migAlert"></div>
</div>
</div>
<?= Ui::credit() ?>
</div>
<script>
const CSRF = <?= json_encode($csrfToken) ?>;
const $ = (id) => document.getElementById(id);
function showAlert(type, msg) {
const el = type === 'error' ? $('alertError') : $('alertOk');
el.textContent = msg; el.classList.add('show');
const other = type === 'error' ? $('alertOk') : $('alertError');
other.classList.remove('show');
}
function clearAlerts() { $('alertError').classList.remove('show'); $('alertOk').classList.remove('show'); }
// Tabs
document.querySelectorAll('.tab').forEach(t => t.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach(x => x.classList.remove('active'));
document.querySelectorAll('.pane').forEach(x => x.classList.remove('active'));
t.classList.add('active');
$('pane-' + t.dataset.pane).classList.add('active');
if (t.dataset.pane === 'migrations') loadMigrations();
}));
// Channel speichern
$('channel').addEventListener('change', async (e) => {
const body = new URLSearchParams({ action: 'set_channel', channel: e.target.value, csrf_token: CSRF });
const r = await fetch('update.php', { method: 'POST', body });
if (r.ok) showAlert('ok', 'Channel auf "' + e.target.value + '" gesetzt.');
else showAlert('error', 'Channel konnte nicht gespeichert werden.');
});
// Auf Updates pruefen
$('btnCheck').addEventListener('click', async () => {
clearAlerts();
$('btnCheck').disabled = true; $('btnCheck').innerHTML = '<i class="fas fa-circle-notch fa-spin" aria-hidden="true"></i> Prüfe …';
try {
const r = await fetch('update.php?action=check');
const d = await r.json();
if (d.error) { showAlert('error', d.error); return; }
if (d.has_update) {
showAlert('ok', 'Update verfügbar: ' + (d.latest_sha ? d.latest_sha.substring(0,7) : '') +
(d.versions_behind ? ' (' + d.versions_behind + ' Commits zurück)' : ''));
$('btnInstall').style.display = 'inline-block';
if (d.changelog || d.title) {
const cl = $('changelog');
cl.textContent = (d.title ? d.title + '\n\n' : '') + (d.changelog || '');
cl.style.display = 'block';
}
} else {
showAlert('ok', 'System ist aktuell.');
$('btnInstall').style.display = 'none';
$('changelog').style.display = 'none';
}
} catch (e) {
showAlert('error', 'Prüfung fehlgeschlagen: ' + e.message);
} finally {
$('btnCheck').disabled = false; $('btnCheck').innerHTML = '<i class="fas fa-magnifying-glass" aria-hidden="true"></i> Auf Updates prüfen';
}
});
// Update installieren
$('btnInstall').addEventListener('click', async () => {
if (!confirm('Update jetzt installieren? Die Seite ist während des Updates kurz im Wartungsmodus.')) return;
clearAlerts();
$('btnInstall').disabled = true; $('btnCheck').disabled = true;
$('progressWrap').style.display = 'block';
let polling = setInterval(pollProgress, 1500);
try {
const body = new URLSearchParams({ action: 'install', csrf_token: CSRF });
const r = await fetch('update.php', { method: 'POST', body });
const d = await r.json();
clearInterval(polling); pollProgress();
if (d.success) {
setProgress(100, 'Update abgeschlossen.');
showAlert('ok', 'Update erfolgreich installiert (' + (d.sha ? d.sha.substring(0,7) : '') + '). Seite wird neu geladen …');
setTimeout(() => location.reload(), 2500);
} else {
showAlert('error', 'Update fehlgeschlagen: ' + (d.error || 'Unbekannter Fehler'));
$('btnInstall').disabled = false; $('btnCheck').disabled = false;
}
} catch (e) {
clearInterval(polling);
showAlert('error', 'Update-Request fehlgeschlagen: ' + e.message);
$('btnInstall').disabled = false; $('btnCheck').disabled = false;
}
});
function setProgress(pct, msg) {
$('progressFill').style.width = pct + '%';
$('progressMsg').textContent = msg || '';
}
async function pollProgress() {
try {
const r = await fetch('update.php?action=progress');
const d = await r.json();
setProgress(d.percent || 0, d.message || '');
} catch (e) { /* ignore */ }
}
async function loadMigrations() {
const el = $('migList');
el.innerHTML = '<p class="muted">Wird geladen …</p>';
try {
const r = await fetch('update.php?action=migrations');
const d = await r.json();
if (d.error) { el.innerHTML = '<p class="muted">' + d.error + '</p>'; return; }
if (!d.migrations || !d.migrations.length) {
el.innerHTML = '<p class="muted">Keine Updater-Migrationen vorhanden.</p>';
return;
}
el.innerHTML = d.migrations.map(m =>
'<div class="mig-item"><span>' + m.filename + '</span>' +
'<span class="badge ' + (m.applied ? 'badge-on">angewandt' : 'badge-off">offen') + '</span></div>'
).join('');
const pending = d.migrations.some(m => !m.applied);
$('btnRunMig').style.display = pending ? 'inline-block' : 'none';
} catch (e) {
el.innerHTML = '<p class="muted">Fehler: ' + e.message + '</p>';
}
}
$('btnRunMig').addEventListener('click', async () => {
$('btnRunMig').disabled = true; $('btnRunMig').textContent = 'Führe aus …';
$('migAlert').classList.remove('show');
try {
const body = new URLSearchParams({ action: 'run_migrations', csrf_token: CSRF });
const r = await fetch('update.php', { method: 'POST', body });
const d = await r.json();
if (d.error) {
$('migAlert').textContent = 'Fehler: ' + d.error;
$('migAlert').className = 'alert alert-error show';
} else {
const n = (d.applied || []).length;
$('migAlert').textContent = n > 0 ? (n + ' Migration(en) ausgeführt.') : 'Keine ausstehenden Migrationen.';
$('migAlert').className = 'alert alert-ok show';
loadMigrations();
}
} catch (e) {
$('migAlert').textContent = 'Fehler: ' + e.message;
$('migAlert').className = 'alert alert-error show';
} finally {
$('btnRunMig').disabled = false; $('btnRunMig').textContent = 'Ausstehende Migrationen ausführen';
}
});
</script>
</body>
</html>