Quelltext des aktuellen Standes
<?php $this->layout('layouts/admin', [
'page_title' => 'System-Health',
'current_admin_page' => 'system-health',
]); ?>
<style>
/* Modul-spezifisch: Fix-Boxen (Aktionshinweise) + Server-Befehls-Code-Block.
Für diese gibt es keine zentrale Komponente. */
.sh-fix-box { background: var(--amber-100); border: 1px solid var(--amber-400); padding: 0.85rem 1rem; border-radius: 6px; color: var(--amber-900); font-size: 0.9rem; line-height: 1.5; margin-bottom: 1rem; display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem; justify-content: space-between; }
.sh-fix-box .sh-fix-text { flex: 1; min-width: 280px; }
.sh-fix-box .sh-fix-list { margin-top: 0.4rem; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.8rem; color: var(--amber-800); }
/* Fix-Befehl: Dark-Style direkt aufs <code>-Element, sonst überschreibt
eine globale `code`-Regel des Admin-Layouts das Farbschema. */
.sh-cmd-wrap { margin-top: 0.5rem; position: relative; }
.sh-cmd-label { display: block; font-size: 0.7rem; font-weight: 600; color: var(--slate-600); margin-bottom: 0.25rem; text-transform: uppercase; letter-spacing: 0.04em; }
.sh-cmd-code {
display: block;
background: var(--slate-900) !important;
color: var(--slate-200) !important;
padding: 0.6rem 2.6rem 0.6rem 0.75rem;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.78rem;
line-height: 1.45;
white-space: pre-wrap;
word-break: break-all;
user-select: all;
border: 1px solid var(--slate-800);
}
.sh-cmd-copy {
position: absolute; right: 0.4rem; top: 1.55rem;
background: rgba(255,255,255,0.12); border: 0; color: var(--slate-300);
font-size: 0.85rem; padding: 0.2rem 0.45rem; border-radius: 3px;
cursor: pointer;
}
.sh-cmd-copy:hover { background: rgba(255,255,255,0.22); color: var(--slate-50); }
</style>
<p class="text-muted" style="margin-bottom:1rem;">
<?= $this->t('Diese Seite prueft die wichtigsten Konfigurationswerte und zeigt nur ihren <strong>Status</strong> an – keine eigentlichen Secrets. Damit kannst du Fehlkonfigurationen (z. B. fehlender <code>APP_KEY</code> in der <code>.env</code>) erkennen, ohne dich auf den Server einloggen zu müssen.') ?>
</p>
<?php
$counts = ['ok' => 0, 'warning' => 0, 'error' => 0, 'info' => 0];
foreach ($checks as $c) {
$counts[$c['status']] = ($counts[$c['status']] ?? 0) + 1;
}
// Schema-Abgleich fließt wie die übrigen Prüfungen in die Kennzahlen ein.
$drift = $schema_drift ?? ['ok' => true, 'missing_tables' => [], 'shrunk_columns' => [], 'charset_tables' => []];
$driftClean = !empty($drift['ok']);
if ($driftClean) {
$counts['ok']++;
} else {
$counts['error'] += count($drift['shrunk_columns'] ?? []) + count($drift['missing_tables'] ?? []);
$counts['warning'] += count($drift['charset_tables'] ?? []);
}
?>
<div class="kpi-row">
<div class="kpi-col"><div class="small-box bg-danger"><div class="inner"><h3><?= $counts['error'] ?></h3><p><?= $this->te('Fehler') ?></p></div><div class="icon"><i class="fas fa-times-circle"></i></div></div></div>
<div class="kpi-col"><div class="small-box bg-warning"><div class="inner"><h3><?= $counts['warning'] ?></h3><p><?= $this->te('Warnungen') ?></p></div><div class="icon"><i class="fas fa-exclamation-triangle"></i></div></div></div>
<div class="kpi-col"><div class="small-box bg-success"><div class="inner"><h3><?= $counts['ok'] ?></h3><p>OK</p></div><div class="icon"><i class="fas fa-check-circle"></i></div></div></div>
<div class="kpi-col"><div class="small-box bg-info"><div class="inner"><h3><?= $counts['info'] ?></h3><p><?= $this->te('Hinweise') ?></p></div><div class="icon"><i class="fas fa-info-circle"></i></div></div></div>
</div>
<?php if (!empty($app_key_broken)): ?>
<form method="POST" action="/admin/system-health/setup-encryption" class="sh-fix-box"
onsubmit="return confirm('Einen neuen APP_KEY erzeugen, in die .env schreiben und alle im Klartext gespeicherten Zugangsdaten verschluesseln?\n\nNur ausfuehren, wenn noch KEIN gueltiger APP_KEY gesetzt ist.');">
<input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
<div class="sh-fix-text">
<?= $this->t('<strong>Kein APP_KEY gesetzt.</strong> Erzeugt automatisch einen sicheren Schlüssel, trägt ihn in die <code>.env</code> ein und verschlüsselt alle aktuell im Klartext gespeicherten Zugangsdaten (runtime.php + Datenbank) – ohne manuelle Eingabe.') ?>
<div class="sh-fix-list" style="margin-top:0.4rem;">
<?= $this->t('Voraussetzung: die <code>.env</code> ist für den Webserver-User beschreibbar. Bereits verschlüsselte Werte bleiben unangetastet; ein vorhandener Schlüssel wird nie überschrieben.') ?>
</div>
</div>
<button type="submit" class="btn btn-warning"><?= $this->te('APP_KEY erzeugen & verschlüsseln') ?></button>
</form>
<?php endif; ?>
<?php if (!empty($missing_dirs)): ?>
<form method="POST" action="/admin/system-health/fix-storage" class="sh-fix-box">
<input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
<?php foreach ($missing_dirs as $d): ?>
<input type="hidden" name="dirs[]" value="<?= $this->e($d) ?>">
<?php endforeach; ?>
<div class="sh-fix-text">
<?= $this->t('<strong>Fehlende Verzeichnisse erkannt.</strong> Folgende Verzeichnisse können jetzt angelegt werden') ?>:
<div class="sh-fix-list">
<?php foreach ($missing_dirs as $d): ?>
· <?= $this->e($d) ?><br>
<?php endforeach; ?>
</div>
</div>
<button type="submit" class="btn btn-warning"><?= $this->te('Verzeichnisse anlegen') ?></button>
</form>
<?php endif; ?>
<div class="table-wrapper">
<table class="data-table">
<thead>
<tr>
<th scope="col" style="width: 110px;"><?= $this->te('Status') ?></th>
<th scope="col" style="width: 240px;"><?= $this->te('Pruefung') ?></th>
<th scope="col"><?= $this->te('Details') ?></th>
</tr>
</thead>
<tbody>
<?php
$grouped = [];
foreach ($checks as $c) {
$grouped[$c['group']][] = $c;
}
foreach ($grouped as $group => $rows): ?>
<tr>
<td colspan="3" style="background:var(--gray-50);font-weight:600;"><?= $this->e($group) ?></td>
</tr>
<?php foreach ($rows as $row):
$pill = match ($row['status']) {
'ok' => 'status-pill--success',
'warning' => 'status-pill--warning',
'error' => 'status-pill--danger',
default => 'status-pill--info',
};
$label = match ($row['status']) {
'ok' => 'OK',
'warning' => 'Warnung',
'error' => 'Fehler',
default => 'Hinweis',
};
?>
<tr>
<td><span class="status-pill <?= $pill ?>"><?= $label ?></span></td>
<td><strong><?= $this->e($row['name']) ?></strong></td>
<td>
<?= $this->e($row['detail']) ?>
<?php if (!empty($row['fix_command'])): ?>
<div class="sh-cmd-wrap">
<span class="sh-cmd-label"><?= $this->te('Auf dem Server ausführen (als Root oder via sudo):') ?></span>
<code class="sh-cmd-code"><?= $this->e($row['fix_command']) ?></code>
<button type="button" class="sh-cmd-copy" title="<?= $this->te('Kopieren') ?>" onclick="shCopyCmd(this)">📋</button>
</div>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php endforeach; ?>
<?php // ── Datenbank-Schema-Abgleich als reguläre Tabellenzeilen (wie die übrigen Prüfungen) ── ?>
<tr>
<td colspan="3" style="background:var(--gray-50);font-weight:600;"><?= $this->te('Datenbank-Schema (Abgleich mit Migrationen)') ?></td>
</tr>
<?php if ($driftClean): ?>
<tr>
<td><span class="status-pill status-pill--success">OK</span></td>
<td><strong><?= $this->te('Schema-Abgleich') ?></strong></td>
<td><?= $this->te('Kein Drift – das laufende DB-Schema entspricht dem erwarteten Stand aus den Migrationen.') ?></td>
</tr>
<?php else: ?>
<?php foreach (($drift['shrunk_columns'] ?? []) as $c): ?>
<tr data-table="<?= $this->e($c['table']) ?>" data-column="<?= $this->e($c['column']) ?>">
<td><span class="status-pill status-pill--danger"><?= $this->te('Fehler') ?></span></td>
<td><strong><?= $this->te('Verkleinerte Spalte') ?></strong><br><code><?= $this->e($c['table']) ?>.<?= $this->e($c['column']) ?></code></td>
<td>
<?= $this->t('Erwartet <span class="status-pill status-pill--info">{erwartet}</span>, ist <span class="status-pill status-pill--danger">{ist}</span>.', ['erwartet' => $this->e($c['expected']), 'ist' => $this->e($c['actual'])]) ?>
<?= $this->te('Häufige Ursache von „Data too long"-Migrationsfehlern.') ?>
<div style="margin-top:0.5rem;">
<button type="button" class="btn btn-xs btn-primary" onclick="shRepairColumn(this)"><?= $this->te('Auf {ziel} vergrößern', ['ziel' => $c['expected']]) ?></button>
<span class="text-muted" style="font-size:0.75rem;margin-left:0.4rem;"><code>ALTER TABLE … MODIFY</code> <?= $this->te('(nur Vergrößern, nie Verkleinern)') ?></span>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php if (!empty($drift['missing_tables'])): ?>
<tr>
<td><span class="status-pill status-pill--danger"><?= $this->te('Fehler') ?></span></td>
<td><strong><?= $this->te('Fehlende Tabellen') ?></strong> (<?= count($drift['missing_tables']) ?>)</td>
<td>
<?php foreach ($drift['missing_tables'] as $t): ?><code style="margin-right:0.4rem;"><?= $this->e($t) ?></code><?php endforeach; ?>
<div class="text-muted" style="font-size:0.78rem;margin-top:0.3rem;">
<?= $this->t('Über <a href="/admin/update">Update & Migrationen → „Basis-Schema reparieren"</a> lassen sich die CREATE-Statements erneut ausführen.') ?>
</div>
</td>
</tr>
<?php endif; ?>
<?php foreach (($drift['charset_tables'] ?? []) as $t): ?>
<tr>
<td><span class="status-pill status-pill--warning"><?= $this->te('Warnung') ?></span></td>
<td><strong><?= $this->te('Zeichensatz-Abweichung') ?></strong><br><code><?= $this->e($t['table']) ?></code></td>
<td><?= $this->t('Erwartet durchgängig <code>utf8mb4</code>, ist <code>{ist}</code>.', ['ist' => $this->e($t['collation'])]) ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<div class="detail-card" style="margin-top: 1.5rem;">
<div class="detail-card-header">
<h3 class="detail-card-title"><i class="fas fa-stopwatch"></i> <?= $this->te('Wo die Zeit einer Seite hingeht') ?></h3>
</div>
<div class="detail-card-body">
<p class="text-muted" style="margin-bottom: 0.75rem;">
<?= $this->te('Lädt eine Seite träge, ist von außen nicht zu erkennen, woran es liegt – die Zeit im Browser enthält Leitung, Webserver, Anwendung und Seitenaufbau in einer Zahl. OpenNIT nennt deshalb bei jeder Antwort an eine angemeldete Person seinen eigenen Anteil.') ?>
</p>
<p style="margin-bottom: 0.75rem;">
<?= $this->t('So kommen Sie an die Zahlen: Entwicklerwerkzeuge des Browsers öffnen (<kbd>F12</kbd>), Reiter <strong>Netzwerk</strong>, Seite neu laden, den obersten Eintrag anklicken – unter <strong>Timing</strong> bzw. in der Kopfzeile <code>Server-Timing</code> stehen:') ?>
</p>
<ul style="margin: 0 0 0.75rem 1.25rem; line-height: 1.7;">
<li><?= $this->t('<code>app</code> – Millisekunden in der Anwendung, vom Beginn der Anfrage bis zur fertigen Antwort.') ?></li>
<li><?= $this->t('<code>db</code> – der darin enthaltene Anteil der Datenbank, mit der Zahl der Abfragen.') ?></li>
</ul>
<p class="text-muted" style="margin-bottom: 0;">
<?= $this->t('Ist <code>app</code> klein und die Gesamtdauer groß, liegt die Zeit auf dem Weg oder im Aufbau der Seite – nicht in OpenNIT. Ist <code>db</code> fast so groß wie <code>app</code>, liegt sie in der Datenbank. Eine dreistellige Zahl von Abfragen deutet auf eine Seite hin, die pro Zeile nachfragt. Ohne Anmeldung wird die Angabe nicht mitgesendet.') ?>
</p>
</div>
</div>
<p class="text-muted" style="margin-top: 1rem; font-size: 0.8rem;">
<?= $this->t('Hinweis: Aktuelle Werte aus der <code>.env</code> werden hier nicht angezeigt – nur, ob sie gesetzt und plausibel sind. Aenderungen an der <code>.env</code> erfordern einen Reload von PHP-FPM bzw. Apache.') ?>
</p>
<script>
function shCopyCmd(btn) {
var code = btn.parentElement.querySelector('.sh-cmd-code');
if (!code) return;
navigator.clipboard.writeText(code.textContent).then(function() {
var orig = btn.textContent;
btn.textContent = '✓';
setTimeout(function() { btn.textContent = orig; }, 1500);
}).catch(function() {
var range = document.createRange();
range.selectNodeContents(code);
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
});
}
function shRepairColumn(btn) {
var row = btn.closest('tr');
if (!row) return;
if (!confirm('Spalte ' + row.getAttribute('data-table') + '.' + row.getAttribute('data-column') + ' auf den erwarteten Typ vergrößern?')) return;
btn.disabled = true;
btn.textContent = 'läuft…';
var body = '_csrf=' + encodeURIComponent(<?= json_encode($this->csrf(), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP) ?>)
+ '&table=' + encodeURIComponent(row.getAttribute('data-table'))
+ '&column=' + encodeURIComponent(row.getAttribute('data-column'));
fetch('/admin/system-health/repair-schema-column', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' },
body: body
})
.then(function (r) { return r.json(); })
.then(function (d) {
alert(d.message || (d.ok ? 'Erledigt.' : 'Fehler.'));
if (d.ok) location.reload();
else { btn.disabled = false; btn.textContent = 'Erneut versuchen'; }
})
.catch(function (e) { alert('Verbindungsfehler: ' + e.message); btn.disabled = false; btn.textContent = 'Erneut versuchen'; });
}
</script>