OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

index.php

15 KB
<?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 &ndash; keine eigentlichen Secrets. Damit kannst du Fehlkonfigurationen (z.&nbsp;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&uuml;ssel, tr&auml;gt ihn in die <code>.env</code> ein und verschl&uuml;sselt alle aktuell im Klartext gespeicherten Zugangsdaten (runtime.php + Datenbank) &ndash; ohne manuelle Eingabe.') ?>
        <div class="sh-fix-list" style="margin-top:0.4rem;">
            <?= $this->t('Voraussetzung: die <code>.env</code> ist f&uuml;r den Webserver-User beschreibbar. Bereits verschl&uuml;sselte Werte bleiben unangetastet; ein vorhandener Schl&uuml;ssel wird nie &uuml;berschrieben.') ?>
        </div>
    </div>
    <button type="submit" class="btn btn-warning"><?= $this->te('APP_KEY erzeugen &amp; verschl&uuml;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&ouml;nnen jetzt angelegt werden') ?>:
        <div class="sh-fix-list">
            <?php foreach ($missing_dirs as $d): ?>
                &middot; <?= $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 &ndash; 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 &amp; Migrationen &rarr; „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&auml;dt eine Seite tr&auml;ge, ist von au&szlig;en nicht zu erkennen, woran es liegt &ndash; die Zeit im Browser enth&auml;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 &ouml;ffnen (<kbd>F12</kbd>), Reiter <strong>Netzwerk</strong>, Seite neu laden, den obersten Eintrag anklicken &ndash; 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> &ndash; Millisekunden in der Anwendung, vom Beginn der Anfrage bis zur fertigen Antwort.') ?></li>
            <li><?= $this->t('<code>db</code> &ndash; 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&szlig;, liegt die Zeit auf dem Weg oder im Aufbau der Seite &ndash; nicht in OpenNIT. Ist <code>db</code> fast so gro&szlig; 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 &ndash; 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>