Files
deskhub/DeskHub-Webseite/staff/status_page.php
T
2026-10-25 17:47:04 +00:00

229 lines
22 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
require_once __DIR__ . '/../includes/bootstrap.php';
$admin = require_admin();
$pdo = get_db();
$errors = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
verify_csrf();
$action = $_POST['action'] ?? '';
if ($action === 'settings') {
set_setting('status_page_enabled', !empty($_POST['enabled']) ? '1' : '0');
$title = trim(mb_substr((string) ($_POST['title'] ?? ''), 0, 80));
set_setting('status_page_title', $title !== '' ? $title : 'Systemstatus');
audit_log($admin['id'], 'Statusseite: Einstellungen geändert');
flash('success', 'Einstellungen gespeichert.');
} elseif ($action === 'component_save') {
$id = (int) ($_POST['id'] ?? 0) ?: null;
$categoryId = (int) ($_POST['category_id'] ?? 0) ?: null;
$r = status_save_component($pdo, $id, (string) ($_POST['name'] ?? ''), (string) ($_POST['description'] ?? ''), (int) ($_POST['sort_order'] ?? 0), $categoryId);
if ($r === null) $errors[] = 'Bitte einen Namen angeben.'; else flash('success', 'Dienst gespeichert.');
} elseif ($action === 'component_state') {
status_set_component($pdo, (int) ($_POST['id'] ?? 0), (string) ($_POST['status'] ?? ''));
flash('success', 'Zustand geändert.');
} elseif ($action === 'component_maintenance') {
if (!status_toggle_component_maintenance($pdo, (int) ($_POST['id'] ?? 0))) $errors[] = 'Dienst konnte nicht gefunden werden.';
else flash('success', 'Wartungsstatus geändert.');
} elseif ($action === 'component_monitor') {
$ok = status_set_component_monitor($pdo, (int) ($_POST['id'] ?? 0), (string) ($_POST['monitor_type'] ?? 'auto'), (string) ($_POST['monitor_target'] ?? ''));
if (!$ok) $errors[] = 'Bitte Prüfart und eine gültige Zieladresse prüfen.';
else flash('success', 'Automatische Prüfung gespeichert.');
} elseif ($action === 'auto_discover') {
$result = status_discover_and_check($pdo);
audit_log($admin['id'], 'Statusseite: Dienste automatisch erkannt und geprüft');
$message = count($result['recognized']) . ' Dienste erkannt, ' . $result['checked'] . ' geprüft.';
if ($result['pending']) $message .= ' Bei ' . count($result['pending']) . ' externen Dienst(en) fehlt noch eine Prüf-URL.';
flash('success', $message);
} elseif ($action === 'component_category') {
status_set_component_category($pdo, (int) ($_POST['id'] ?? 0), (int) ($_POST['category_id'] ?? 0) ?: null, (int) ($_POST['sort_order'] ?? 0));
flash('success', 'Kategorie und Reihenfolge gespeichert.');
} elseif ($action === 'category_order') {
status_set_category_order($pdo, (int) ($_POST['id'] ?? 0), (int) ($_POST['sort_order'] ?? 0));
flash('success', 'Reihenfolge gespeichert.');
} elseif ($action === 'category_save') {
if (!status_save_category($pdo, (string) ($_POST['name'] ?? ''))) $errors[] = 'Bitte einen Namen angeben. Kategorienamen müssen eindeutig sein.';
else flash('success', 'Kategorie angelegt.');
} elseif ($action === 'category_delete') {
status_delete_category($pdo, (int) ($_POST['id'] ?? 0));
flash('success', 'Kategorie entfernt. Zugeordnete Dienste bleiben erhalten.');
} elseif ($action === 'component_delete') {
status_delete_component($pdo, (int) ($_POST['id'] ?? 0));
flash('success', 'Dienst entfernt.');
} elseif ($action === 'component_restore_core') {
$restored = status_restore_core_components($pdo);
audit_log($admin['id'], 'Statusseite: fehlende DeskHub-Standarddienste wiederhergestellt');
flash('success', 'Fehlende DeskHub-Dienste und passende Kategorien wurden hinzugefuegt. Vorhandene Eintraege blieben erhalten.');
} elseif ($action === 'incident_create') {
$cid = (int) ($_POST['component_id'] ?? 0) ?: null;
$r = status_create_incident($pdo, (string) ($_POST['title'] ?? ''), (string) ($_POST['body'] ?? ''), (string) ($_POST['status'] ?? 'investigating'), $cid, ($_POST['component_state'] ?? '') !== '' ? (string) $_POST['component_state'] : null, (int) $admin['id']);
if (!$r['ok']) $errors[] = $r['error']; else { audit_log($admin['id'], 'Statusseite: Meldung veröffentlicht'); flash('success', 'Meldung veröffentlicht.'); }
} elseif ($action === 'incident_update') {
$r = status_update_incident($pdo, (int) ($_POST['id'] ?? 0), (string) ($_POST['status'] ?? ''), (string) ($_POST['body'] ?? ''), (int) $admin['id'], true);
if (!$r['ok']) $errors[] = $r['error']; else flash('success', 'Update veröffentlicht.');
} elseif ($action === 'incident_delete') {
status_delete_incident($pdo, (int) ($_POST['id'] ?? 0));
flash('success', 'Meldung gelöscht.');
}
if (!$errors) redirect('/staff/status_page.php');
}
$components = status_components($pdo);
$categories = status_categories($pdo);
$automaticChecks = 0;
$pendingChecks = 0;
foreach ($components as $component) {
if (status_component_monitor_target($pdo, $component) !== null) $automaticChecks++;
elseif (($component['monitor_type'] ?? 'auto') === 'auto') $pendingChecks++;
}
$incidents = status_incidents($pdo, 60);
$states = status_component_states();
$monitorTypes = status_monitor_types();
$incStates = status_incident_states();
$pageTitle = 'Statusseite';
$activePage = 'status_page';
require __DIR__ . '/_header.php';
?>
<h1>Statusseite</h1>
<?php foreach ($errors as $err): ?><div class="flash error"><?= e($err) ?></div><?php endforeach; ?>
<p class="muted">Eine öffentliche Seite, auf der Ihre Kunden sehen, ob alles läuft, und über Störungen und Wartungen informiert werden.
<?php if (status_page_enabled()): ?>Adresse: <a href="/status.php" target="_blank"><?= e(base_url()) ?>/status.php</a><?php endif; ?></p>
<div class="card" style="margin-bottom:16px;padding:12px 16px">
<strong><?= $automaticChecks ?> Dienst<?= $automaticChecks === 1 ? '' : 'e' ?> werden automatisch geprüft</strong>
<?php if ($pendingChecks): ?><span class="muted"> · <?= $pendingChecks ?> <?= $pendingChecks === 1 ? 'Dienst wartet' : 'Dienste warten' ?> auf ein Prüfziel</span><?php endif; ?>
<div style="display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;margin-top:6px">
<span class="muted" style="font-size:.8rem">DeskHub erkennt Datenbank, API, konfigurierte Mailserver, Push-Zugang sowie Android- und Windows-Verbindungen automatisch. Android-Apps gelten bei einem API-Kontakt innerhalb der letzten 60 Minuten, Windows-Apps innerhalb der letzten 10 Minuten als verbunden. Statuspr&uuml;fungen laufen jede Minute; externe Webseiten und weitere Apps ben&ouml;tigen eine Pr&uuml;f-URL.</span>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<form method="post"><?= csrf_field() ?><input type="hidden" name="action" value="auto_discover"><button class="btn secondary" type="submit">Jetzt erkennen und prüfen</button></form>
<form method="post"><?= csrf_field() ?><input type="hidden" name="action" value="component_restore_core"><button class="btn" type="submit">DeskHub-Dienste automatisch hinzufügen</button></form>
</div>
</div>
</div>
<details class="card" style="margin-bottom:16px">
<summary style="cursor:pointer;font-weight:700">Seiteneinstellungen</summary>
<form method="post" style="margin-top:12px">
<?= csrf_field() ?><input type="hidden" name="action" value="settings">
<h3>Einstellungen</h3>
<label style="display:flex;align-items:center;gap:8px;font-weight:400"><input type="checkbox" name="enabled" value="1" style="width:auto" <?= status_page_enabled() ? 'checked' : '' ?>> Statusseite für Kunden anzeigen (Link „Status“ im Kundenbereich)</label>
<label for="title" style="margin-top:8px">Überschrift</label>
<input type="text" id="title" name="title" maxlength="80" value="<?= e((string) get_setting('status_page_title', 'Systemstatus')) ?>" style="max-width:340px">
<div class="btn-row"><button type="submit" class="btn">Speichern</button></div>
</form>
</details>
<style>
.status-admin-layout{display:grid;grid-template-columns:minmax(0,1.8fr) minmax(320px,1fr);gap:16px;align-items:start;margin-bottom:16px}
@media(max-width:1100px){.status-admin-layout{grid-template-columns:1fr}}
.status-category-row{display:grid;grid-template-columns:minmax(0,1fr) 104px 32px;gap:8px;align-items:center;padding:9px 4px;border-bottom:1px solid var(--border,#e3e9ee)}
.status-category-head{font-size:.75rem;font-weight:700;color:var(--muted);padding:0 4px 6px}
.status-category-row form{margin:0}
.status-component-add{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin:0!important;padding:0!important;border:0!important}
.status-component-add input,.status-component-add select{width:100%;min-width:0;box-sizing:border-box}
.status-component-add button{grid-column:1/-1;justify-self:start}
@media(max-width:420px){.status-category-row{grid-template-columns:minmax(0,1fr) 94px 30px;gap:5px}}
</style>
<div class="status-admin-layout">
<section class="card">
<h3>Dienste & automatische Prüfungen</h3>
<p class="muted">Automatisch geprüfte Dienste erhalten ihren Status selbst. Während geplanter Arbeiten kannst du sie auf Wartung setzen. Für nicht erkannte externe Dienste ist eine Prüf-URL nötig.</p>
<div style="margin:14px 0;padding:12px;border:1px solid var(--border,#e3e9ee);border-radius:8px;background:var(--bg-soft,#f8fafc)">
<strong style="display:block;margin-bottom:8px">Eigenen Dienst hinzuf&uuml;gen</strong>
<form method="post" class="status-component-add"><?= csrf_field() ?><input type="hidden" name="action" value="component_save">
<input type="text" name="name" placeholder="Neuer Dienst, z. B. Webseite" maxlength="120" required style="flex:1;min-width:180px">
<input type="text" name="description" placeholder="Beschreibung (optional)" maxlength="255" style="flex:1;min-width:180px">
<select name="category_id" style="flex:1;min-width:150px"><option value="">Ohne Kategorie</option><?php foreach ($categories as $category): ?><option value="<?= (int) $category['id'] ?>"><?= e($category['name']) ?></option><?php endforeach; ?></select>
<input type="number" name="sort_order" value="<?= count($components) * 10 + 10 ?>" title="Reihenfolge" style="width:80px">
<button class="btn" type="submit">Hinzufügen</button></form>
</div>
<?php if (!$components): ?><p class="muted">Noch keine Dienste. Legen Sie unten zum Beispiel Webseite, E-Mail oder Telefon an.</p><?php endif; ?>
<?php foreach ($components as $c): $st = $states[$c['status']] ?? $states['operational']; ?>
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;padding:8px 0;border-top:1px solid var(--border,#e3e9ee)">
<?php
$checkInfo = status_component_monitor_target($pdo, $c);
$selectedMonitorType = ($c['monitor_type'] ?? 'auto') === 'auto' && $checkInfo ? $checkInfo['type'] : (string) ($c['monitor_type'] ?? 'auto');
$monitorTargetValue = trim((string) ($c['monitor_target'] ?? ''));
if ($monitorTargetValue === '' && $checkInfo && $checkInfo['type'] === 'http') $monitorTargetValue = (string) $checkInfo['target'];
$showsDetectedTargets = $checkInfo && in_array($checkInfo['type'], ['database', 'mail', 'fcm', 'android_app', 'windows_app'], true);
$detectedTargets = $showsDetectedTargets ? status_component_detected_targets($pdo, $checkInfo) : [];
?>
<div style="flex:1;min-width:180px"><strong><?= e($c['name']) ?></strong><?php if ($c['description']): ?><div class="muted" style="font-size:.8rem"><?= e($c['description']) ?></div><?php endif; ?>
<div class="muted" style="font-size:.76rem;margin-top:3px"><?php if ($checkInfo): ?>Automatische Prüfung: <?= e($checkInfo['label']) ?> · <?= $c['last_checked_at'] ? 'zuletzt ' . e(format_dt((string) $c['last_checked_at'])) : 'wartet auf ersten Lauf' ?><?php elseif (($c['monitor_type'] ?? 'auto') === 'manual'): ?>Manuell verwaltet<?php else: ?>Prüfziel fehlt – wartet auf Zuordnung<?php endif; ?><?php if (!empty($c['last_check_ms'])): ?> · <?= (int) $c['last_check_ms'] ?> ms<?php endif; ?></div>
<?php if (!empty($c['last_check_error'])): ?><div style="color:#b91c1c;font-size:.76rem"><?= e((string) $c['last_check_error']) ?></div><?php endif; ?>
</div>
<form method="post" style="display:flex;gap:5px;align-items:center"><?= csrf_field() ?><input type="hidden" name="action" value="component_category"><input type="hidden" name="id" value="<?= (int) $c['id'] ?>"><select name="category_id" aria-label="Kategorie für <?= e($c['name']) ?>" style="max-width:170px;padding:5px"><option value="">Ohne Kategorie</option><?php foreach ($categories as $category): ?><option value="<?= (int) $category['id'] ?>" <?= (int) ($c['category_id'] ?? 0) === (int) $category['id'] ? 'selected' : '' ?>><?= e($category['name']) ?></option><?php endforeach; ?></select><input type="number" name="sort_order" value="<?= (int) $c['sort_order'] ?>" title="Reihenfolge" aria-label="Reihenfolge für <?= e($c['name']) ?>" style="width:58px;padding:4px"><button class="btn secondary" type="submit" style="padding:4px 8px;font-size:.78rem">Speichern</button></form>
<?php if ($checkInfo): ?>
<div style="display:flex;gap:8px;align-items:center;white-space:nowrap">
<span style="color:<?= e($st[1]) ?>;font-weight:600"><?= e($st[0]) ?></span>
<form method="post"><?= csrf_field() ?><input type="hidden" name="action" value="component_maintenance"><input type="hidden" name="id" value="<?= (int) $c['id'] ?>"><button class="btn secondary" type="submit" style="padding:4px 8px;font-size:.78rem"><?= $c['status'] === 'maintenance' ? 'Wartung beenden' : 'Wartung aktivieren' ?></button></form>
</div>
<?php else: ?>
<form method="post" style="display:flex;gap:6px;align-items:center"><?= csrf_field() ?><input type="hidden" name="action" value="component_state"><input type="hidden" name="id" value="<?= (int) $c['id'] ?>">
<select name="status" onchange="this.form.submit()" style="color:<?= e($st[1]) ?>;font-weight:600"><?php foreach ($states as $k => $s): ?><option value="<?= e($k) ?>" <?= $c['status'] === $k ? 'selected' : '' ?>><?= e($s[0]) ?></option><?php endforeach; ?></select></form>
<?php endif; ?>
<form method="post" onsubmit="return confirm('Dienst entfernen?')"><?= csrf_field() ?><input type="hidden" name="action" value="component_delete"><input type="hidden" name="id" value="<?= (int) $c['id'] ?>"><button class="btn secondary" type="submit" style="padding:2px 10px;font-size:.78rem">Entfernen</button></form>
</div>
<details style="padding:0 0 10px 0;border-bottom:1px solid var(--border,#e3e9ee)">
<summary style="cursor:pointer;color:var(--muted,#64748b);font-size:.82rem">Prüfung konfigurieren</summary>
<form method="post" style="display:flex;gap:8px;flex-wrap:wrap;align-items:end;padding:10px 0"><?= csrf_field() ?><input type="hidden" name="action" value="component_monitor"><input type="hidden" name="id" value="<?= (int) $c['id'] ?>">
<div><label>Prüfart</label><select name="monitor_type"><?php foreach ($monitorTypes as $type => $label): ?><option value="<?= e($type) ?>" <?= $selectedMonitorType === $type ? 'selected' : '' ?>><?= e($label) ?></option><?php endforeach; ?></select></div>
<?php if ($showsDetectedTargets): ?>
<div style="flex:1;min-width:220px"><label>Automatisch erkannte Pr&uuml;fziele</label><div style="min-height:38px;box-sizing:border-box;padding:9px 12px;border:1px solid var(--border,#d8e0e8);border-radius:8px;background:var(--surface,#f5f7f9)"><?php if ($detectedTargets): ?><?= e(implode(' · ', $detectedTargets)) ?><?php else: ?>Keine passenden Ziele in der Dienst-Konfiguration gefunden<?php endif; ?></div></div>
<?php else: ?>
<div style="flex:1;min-width:220px"><label>Pr&uuml;f-URL oder Host:Port</label><input type="text" name="monitor_target" value="<?= e($monitorTargetValue) ?>" placeholder="https://example.com"></div>
<?php endif; ?>
<button class="btn secondary" type="submit">Prüfung speichern</button>
</form>
</details>
<?php endforeach; ?>
</section>
<section class="card">
<h3 style="margin-top:0">Kategorien verwalten (<?= count($categories) ?>)</h3>
<form method="post" style="display:flex;gap:8px;flex-wrap:wrap;margin:0 0 12px"><?= csrf_field() ?><input type="hidden" name="action" value="category_save"><input type="text" name="name" placeholder="Neue Kategorie, z. B. API-Schnittstellen" maxlength="80" required style="flex:1;min-width:200px"><button class="btn" type="submit">Kategorie anlegen</button></form>
<?php if ($categories): ?><div style="border-top:1px solid var(--border,#e3e9ee)">
<div class="status-category-row status-category-head"><span>Kategorie</span><span>Reihenfolge</span><span></span></div>
<?php foreach ($categories as $category): ?><div class="status-category-row">
<span><?= e($category['name']) ?></span>
<form method="post" style="display:flex;gap:4px;align-items:center"><?= csrf_field() ?><input type="hidden" name="action" value="category_order"><input type="hidden" name="id" value="<?= (int) $category['id'] ?>"><input type="number" name="sort_order" value="<?= (int) $category['sort_order'] ?>" title="Reihenfolge" aria-label="Reihenfolge für <?= e($category['name']) ?>" style="width:58px;padding:3px"><button class="btn secondary" type="submit" style="padding:2px 6px;font-size:.76rem" aria-label="Reihenfolge speichern">&#8597;</button></form>
<form method="post" onsubmit="return confirm('Kategorie entfernen? Zugeordnete Dienste bleiben ohne Kategorie erhalten.')"><?= csrf_field() ?><input type="hidden" name="action" value="category_delete"><input type="hidden" name="id" value="<?= (int) $category['id'] ?>"><button class="btn secondary" type="submit" aria-label="Kategorie <?= e($category['name']) ?> entfernen" style="padding:1px 7px">&times;</button></form>
</div><?php endforeach; ?>
</div><?php else: ?><p class="muted">Noch keine Kategorien angelegt.</p><?php endif; ?>
</section>
</div>
<details class="card" style="max-width:820px;margin-bottom:16px">
<summary style="cursor:pointer;font-weight:700">Störung oder Wartung melden</summary>
<form method="post" style="margin-top:12px"><?= csrf_field() ?><input type="hidden" name="action" value="incident_create">
<h3>Neue Meldung (Störung oder Wartung)</h3>
<label for="inc_title">Titel</label><input type="text" id="inc_title" name="title" maxlength="200" required placeholder="z. B. E-Mail-Versand verzögert">
<label for="inc_body" style="margin-top:8px">Beschreibung</label><textarea id="inc_body" name="body" rows="3" maxlength="4000"></textarea>
<div style="display:flex;gap:12px;flex-wrap:wrap;margin-top:8px">
<div><label>Stufe</label><select name="status"><?php foreach ($incStates as $k => $l): ?><option value="<?= e($k) ?>"><?= e($l) ?></option><?php endforeach; ?></select></div>
<div><label>Betrifft</label><select name="component_id"><option value="">– kein bestimmter Dienst –</option><?php foreach ($components as $c): ?><option value="<?= (int) $c['id'] ?>"><?= e($c['name']) ?></option><?php endforeach; ?></select></div>
<div><label>Dienst setzen auf</label><select name="component_state"><option value="">– nicht ändern –</option><?php foreach ($states as $k => $s): ?><option value="<?= e($k) ?>"><?= e($s[0]) ?></option><?php endforeach; ?></select></div>
</div>
<div class="btn-row"><button type="submit" class="btn">Veröffentlichen</button></div>
</form>
</details>
<details class="card" style="max-width:820px">
<summary style="cursor:pointer;font-weight:700">Meldungen (<?= count($incidents) ?>)</summary>
<?php if (!$incidents): ?><p class="muted">Keine Meldungen in den letzten 60 Tagen.</p><?php endif; ?>
<?php foreach ($incidents as $i): ?>
<div style="padding:10px 0;border-top:1px solid var(--border,#e3e9ee)">
<strong><?= e($i['title']) ?></strong>
<span class="muted" style="font-size:.8rem">· <?= e(format_dt((string) $i['created_at'])) ?><?= $i['component_name'] ? ' · ' . e($i['component_name']) : '' ?> · <?= e($incStates[$i['status']] ?? $i['status']) ?></span>
<?php foreach ($i['updates'] as $u): ?><div style="font-size:.86rem;margin-top:3px"><span class="muted"><?= e($incStates[$u['status']] ?? $u['status']) ?>:</span> <?= nl2br(e((string) $u['body'])) ?></div><?php endforeach; ?>
<?php if ($i['status'] !== 'resolved'): ?>
<form method="post" style="display:flex;gap:6px;flex-wrap:wrap;margin-top:6px"><?= csrf_field() ?><input type="hidden" name="action" value="incident_update"><input type="hidden" name="id" value="<?= (int) $i['id'] ?>">
<select name="status"><?php foreach ($incStates as $k => $l): ?><option value="<?= e($k) ?>" <?= $k === $i['status'] ? 'selected' : '' ?>><?= e($l) ?></option><?php endforeach; ?></select>
<input type="text" name="body" placeholder="Update für die Kunden" maxlength="4000" style="flex:1;min-width:200px">
<button class="btn secondary" type="submit">Update senden</button></form>
<?php endif; ?>
<form method="post" onsubmit="return confirm('Meldung löschen?')" style="margin-top:4px"><?= csrf_field() ?><input type="hidden" name="action" value="incident_delete"><input type="hidden" name="id" value="<?= (int) $i['id'] ?>"><button class="btn secondary" type="submit" style="padding:2px 10px;font-size:.76rem">Löschen</button></form>
</div>
<?php endforeach; ?>
</details>
<?php require __DIR__ . '/_footer.php'; ?>