337 lines
33 KiB
PHP
337 lines
33 KiB
PHP
<?php
|
||
require_once __DIR__ . '/../includes/bootstrap.php';
|
||
$admin = require_admin_or_permission('manage_status_page');
|
||
$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');
|
||
set_setting('status_notify_admins', !empty($_POST['notify_admins']) ? '1' : '0');
|
||
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_enabled') {
|
||
$enabled = !empty($_POST['enabled']);
|
||
if (!status_set_component_enabled($pdo, (int) ($_POST['id'] ?? 0), $enabled)) $errors[] = 'Dienst konnte nicht gefunden werden.';
|
||
else flash('success', $enabled ? 'Dienst aktiviert und wieder zur Prüfung freigegeben.' : 'Dienst deaktiviert, ausgeblendet und von Prüfungen ausgenommen.');
|
||
} elseif ($action === 'component_internal') {
|
||
$internal = !empty($_POST['internal']);
|
||
if (!status_set_component_internal($pdo, (int) ($_POST['id'] ?? 0), $internal)) $errors[] = 'Nicht möglich: Bitte zuerst das Datenbank-Update einspielen (System-Update).';
|
||
else flash('success', $internal ? 'Dienst ist jetzt intern: nur Mitarbeiter sehen ihn, geprüft wird er weiter.' : 'Dienst ist jetzt öffentlich sichtbar.');
|
||
} 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'] ?? ''), (int) ($_POST['check_interval_minutes'] ?? 1));
|
||
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', $restored > 0
|
||
? 'Fehlende DeskHub-Dienste wurden hinzugefügt. Der Minecraft-Server liegt unter „Apps“ und bleibt bis zur Adresseingabe deaktiviert.'
|
||
: 'Alle DeskHub-Dienste sind bereits vorhanden. Der Minecraft-Server liegt unter „Apps“; Adresse und Port eintragen und danach aktivieren.');
|
||
} 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.');
|
||
} elseif ($action === 'maintenance_create') {
|
||
$componentId = (int) ($_POST['component_id'] ?? 0) ?: null;
|
||
$result = status_create_maintenance_window($pdo, (string) ($_POST['title'] ?? ''), (string) ($_POST['body'] ?? ''), (string) ($_POST['starts_at'] ?? ''), (string) ($_POST['ends_at'] ?? ''), $componentId, (int) $admin['id']);
|
||
if (!$result['ok']) $errors[] = $result['error'];
|
||
else { audit_log($admin['id'], 'Statusseite: Wartungsfenster geplant'); flash('success', 'Wartungsfenster wurde geplant.'); }
|
||
} elseif ($action === 'maintenance_cancel') {
|
||
if (!status_cancel_maintenance_window($pdo, (int) ($_POST['id'] ?? 0))) $errors[] = 'Wartungsfenster konnte nicht gefunden werden.';
|
||
else { audit_log($admin['id'], 'Statusseite: Wartungsfenster abgesagt'); flash('success', 'Wartungsfenster wurde abgesagt.'); }
|
||
}
|
||
if (!$errors) redirect('/staff/status_page.php');
|
||
}
|
||
|
||
$components = status_components($pdo, true);
|
||
$categories = status_categories($pdo);
|
||
$automaticChecks = 0;
|
||
$pendingChecks = 0;
|
||
foreach ($components as $component) {
|
||
if (empty($component['is_enabled'])) continue;
|
||
if (status_component_monitor_target($pdo, $component) !== null) $automaticChecks++;
|
||
elseif (($component['monitor_type'] ?? 'auto') === 'auto') $pendingChecks++;
|
||
}
|
||
$incidents = status_incidents($pdo, 60, false, true);
|
||
$maintenanceWindows = status_maintenance_windows($pdo, true);
|
||
$states = status_component_states();
|
||
$monitorTypes = status_monitor_types();
|
||
$checkIntervals = [1 => 'Jede Minute', 2 => 'Alle 2 Minuten', 5 => 'Alle 5 Minuten', 10 => 'Alle 10 Minuten', 15 => 'Alle 15 Minuten', 30 => 'Alle 30 Minuten', 60 => 'Jede Stunde'];
|
||
$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>
|
||
<style>
|
||
.status-tabs{display:flex;gap:8px;margin:18px 0 12px;border-bottom:1px solid var(--border,#d8e0e8)}
|
||
.status-tab-button{border:0;border-bottom:3px solid transparent;border-radius:7px 7px 0 0;background:transparent;color:var(--muted,#64748b);padding:10px 16px;font-weight:700;cursor:pointer}
|
||
.status-tab-button[aria-selected="true"]{color:var(--text,#0f172a);border-bottom-color:var(--primary,#0f766e);background:var(--surface,#fff)}
|
||
.status-tab-panel[hidden]{display:none!important}
|
||
</style>
|
||
<div class="status-tabs" role="tablist" aria-label="Statusseiten-Verwaltung">
|
||
<button type="button" class="status-tab-button" id="status-tab-services" role="tab" aria-controls="status-panel-services" aria-selected="true">Dienste</button>
|
||
<button type="button" class="status-tab-button" id="status-tab-maintenance" role="tab" aria-controls="status-panel-maintenance" aria-selected="false">Wartungen & Meldungen</button>
|
||
</div>
|
||
<section class="status-tab-panel" id="status-panel-services" role="tabpanel" aria-labelledby="status-tab-services">
|
||
<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 die Erreichbarkeit der Schnittstelle für Android- und Windows-Apps automatisch. Ob gerade ein Gerät verbunden ist, steht nur als Zusatzinfo daneben und führt nicht zu „Offline“. Der Zeitgeber prüft jede Minute, einzelne Dienste können aber mit einem längeren Intervall geprüft werden.</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 style="display:flex;align-items:center;gap:8px;font-weight:400;margin-top:8px"><input type="checkbox" name="notify_admins" value="1" style="width:auto" <?= status_notify_admins_enabled() ? 'checked' : '' ?>> Administratoren per E-Mail und Push benachrichtigen, wenn ein Dienst ausfällt oder wieder läuft (auch bei internen Diensten)</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}
|
||
.status-monitor-form{display:grid;grid-template-columns:minmax(160px,210px) minmax(220px,1fr) minmax(145px,175px) auto;gap:8px;align-items:end;padding:10px 0}
|
||
.status-monitor-form>div{min-width:0}
|
||
.status-monitor-form label{display:block;margin:0 0 6px}
|
||
.status-monitor-form input,.status-monitor-form select{box-sizing:border-box;width:100%;min-height:40px}
|
||
.status-monitor-form>button{grid-column:4;grid-row:1;min-height:40px;white-space:nowrap}
|
||
.status-monitor-help{grid-column:2;margin-top:-4px}
|
||
.status-minecraft-row{flex-wrap:nowrap!important;gap:10px!important}
|
||
.status-minecraft-row>form:last-child{margin-left:auto}
|
||
@media(max-width:650px){.status-monitor-form{grid-template-columns:1fr}.status-monitor-form>button{grid-column:1;grid-row:auto}.status-monitor-help{grid-column:1}}
|
||
@media(max-width:900px){.status-minecraft-row{flex-wrap:wrap!important}}
|
||
@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. Jeder Dienst lässt sich vollständig deaktivieren; er wird dann auf der öffentlichen Seite ausgeblendet und nicht mehr geprüft.</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ü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 class="<?= str_contains(mb_strtolower((string) $c['name']), 'minecraft') ? 'status-minecraft-row' : '' ?>" style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;padding:8px 0;border-top:1px solid var(--border,#e3e9ee);<?= empty($c['is_enabled']) ? 'opacity:.58' : '' ?>">
|
||
<?php
|
||
$checkInfo = status_component_monitor_target($pdo, $c);
|
||
$selectedMonitorType = ($c['monitor_type'] ?? 'auto') === 'auto' && $checkInfo ? ($checkInfo['type'] === 'minecraft' ? 'tcp' : $checkInfo['type']) : (string) ($c['monitor_type'] ?? 'auto');
|
||
$isMinecraftComponent = str_contains(mb_strtolower((string) $c['name']), 'minecraft');
|
||
$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', 'plugin_site'], true);
|
||
$detectedTargets = $showsDetectedTargets ? status_component_detected_targets($pdo, $checkInfo) : [];
|
||
?>
|
||
<div style="flex:<?= str_contains(mb_strtolower((string) $c['name']), 'minecraft') ? '0 0 249px' : '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; ?><?php if (empty($c['is_enabled'])): ?><div class="muted" style="font-size:.75rem">Deaktiviert – nicht öffentlich sichtbar und wird nicht geprüft</div><?php elseif (!empty($c['is_internal'])): ?><div class="muted" style="font-size:.75rem">Intern – nur für Mitarbeiter sichtbar, wird geprüft</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' ?> · alle <?= max(1, (int) ($c['check_interval_minutes'] ?? 1)) ?> Min.<?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>
|
||
<form method="post"><?= csrf_field() ?><input type="hidden" name="action" value="component_enabled"><input type="hidden" name="id" value="<?= (int) $c['id'] ?>"><input type="hidden" name="enabled" value="<?= empty($c['is_enabled']) ? '1' : '0' ?>"><button class="btn secondary" type="submit" style="padding:4px 8px;font-size:.78rem"><?= empty($c['is_enabled']) ? 'Aktivieren' : 'Deaktivieren' ?></button></form>
|
||
<form method="post"><?= csrf_field() ?><input type="hidden" name="action" value="component_internal"><input type="hidden" name="id" value="<?= (int) $c['id'] ?>"><input type="hidden" name="internal" value="<?= empty($c['is_internal']) ? '1' : '0' ?>"><button class="btn secondary" type="submit" style="padding:4px 8px;font-size:.78rem" title="Intern: wird geprüft, steht aber nicht auf der öffentlichen Statusseite"><?= empty($c['is_internal']) ? 'Intern machen' : 'Öffentlich machen' ?></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" class="status-monitor-form"><?= 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ü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><label><?= $selectedMonitorType === 'tcp' ? ($isMinecraftComponent ? 'IP-Adresse oder Domain (Port optional)' : 'IP-Adresse oder Domain mit Port') : 'Prüf-URL' ?></label><input type="text" name="monitor_target" value="<?= e($monitorTargetValue) ?>" placeholder="<?= $selectedMonitorType === 'tcp' ? ($isMinecraftComponent ? 'mc.viper.ipv64.net' : 'play.example.de:25565 oder 203.0.113.10:25565') : 'https://example.com' ?>"></div>
|
||
<?php endif; ?>
|
||
<div><label>Prüfintervall</label><select name="check_interval_minutes"><?php foreach ($checkIntervals as $minutes => $intervalLabel): ?><option value="<?= (int) $minutes ?>" <?= (int) ($c['check_interval_minutes'] ?? 1) === $minutes ? 'selected' : '' ?>><?= e($intervalLabel) ?></option><?php endforeach; ?></select></div>
|
||
<?php if ($selectedMonitorType === 'tcp'): ?><span class="status-monitor-help muted" style="font-size:.75rem"><?= $isMinecraftComponent ? 'Ohne Port: Minecraft-SRV, sonst 25565. IPv6: [2001:db8::1] oder [2001:db8::1]:25565.' : 'IPv6: [2001:db8::1]:25565' ?></span><?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">↕</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">×</button></form>
|
||
</div><?php endforeach; ?>
|
||
</div><?php else: ?><p class="muted">Noch keine Kategorien angelegt.</p><?php endif; ?>
|
||
</section>
|
||
</div>
|
||
|
||
</section>
|
||
<section class="status-tab-panel" id="status-panel-maintenance" role="tabpanel" aria-labelledby="status-tab-maintenance" hidden>
|
||
|
||
<details class="card" style="max-width:820px;margin-bottom:16px">
|
||
<summary style="cursor:pointer;font-weight:700">Wartungsfenster planen</summary>
|
||
<p class="muted" style="margin:8px 0">Kündigt eine geplante Wartung mit Start- und Endzeit auf der öffentlichen Statusseite an. Während des Zeitraums wird der betroffene Dienst als „Wartung“ angezeigt.</p>
|
||
<form method="post" style="margin-top:12px"><?= csrf_field() ?><input type="hidden" name="action" value="maintenance_create">
|
||
<label for="maintenance_title">Titel</label><input type="text" id="maintenance_title" name="title" maxlength="200" required placeholder="z. B. Server- und Datenbankwartung">
|
||
<label for="maintenance_body" style="margin-top:8px">Beschreibung (optional)</label><textarea id="maintenance_body" name="body" rows="3" maxlength="4000" placeholder="Welche Arbeiten sind geplant und welche Auswirkungen gibt es?"></textarea>
|
||
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:12px;margin-top:8px">
|
||
<div><label for="maintenance_starts_at">Startzeit</label><input type="datetime-local" id="maintenance_starts_at" name="starts_at" required></div>
|
||
<div><label for="maintenance_ends_at">Endzeit</label><input type="datetime-local" id="maintenance_ends_at" name="ends_at" required></div>
|
||
<div><label for="maintenance_component">Betrifft</label><select id="maintenance_component" name="component_id"><option value="">Gesamte Plattform</option><?php foreach ($components as $c): if (empty($c['is_enabled'])) continue; ?><option value="<?= (int) $c['id'] ?>"><?= e($c['name']) ?></option><?php endforeach; ?></select></div>
|
||
</div>
|
||
<div class="btn-row"><button type="submit" class="btn">Wartung planen</button></div>
|
||
</form>
|
||
<?php if ($maintenanceWindows): ?><h3 style="margin-top:18px">Geplante und kürzliche Wartungen</h3>
|
||
<?php foreach ($maintenanceWindows as $window):
|
||
$windowActive = empty($window['cancelled_at']) && strtotime((string) $window['starts_at']) <= time() && strtotime((string) $window['ends_at']) >= time();
|
||
$windowFuture = empty($window['cancelled_at']) && strtotime((string) $window['starts_at']) > time();
|
||
?>
|
||
<div style="padding:10px 0;border-top:1px solid var(--border,#e3e9ee)">
|
||
<strong><?= e($window['title']) ?></strong> <span class="muted" style="font-size:.8rem">· <?= e($window['cancelled_at'] ? 'Abgesagt' : ($windowActive ? 'Läuft jetzt' : ($windowFuture ? 'Geplant' : 'Beendet'))) ?> · <?= e(format_dt((string) $window['starts_at'])) ?>–<?= e(format_dt((string) $window['ends_at'])) ?><?= $window['component_name'] ? ' · ' . e($window['component_name']) : ' · Gesamte Plattform' ?></span>
|
||
<?php if (!empty($window['body'])): ?><div class="muted" style="font-size:.85rem;margin-top:3px"><?= nl2br(e((string) $window['body'])) ?></div><?php endif; ?>
|
||
<?php if ($windowFuture || $windowActive): ?><form method="post" onsubmit="return confirm('Wartungsfenster absagen?')" style="margin-top:5px"><?= csrf_field() ?><input type="hidden" name="action" value="maintenance_cancel"><input type="hidden" name="id" value="<?= (int) $window['id'] ?>"><button type="submit" class="btn secondary" style="padding:2px 10px;font-size:.76rem">Absagen</button></form><?php endif; ?>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
<?php endif; ?>
|
||
</details>
|
||
|
||
<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>
|
||
</section>
|
||
<script>
|
||
(() => {
|
||
const tabs = Array.from(document.querySelectorAll('.status-tab-button'));
|
||
const panels = Array.from(document.querySelectorAll('.status-tab-panel'));
|
||
const storageKey = 'deskhub-status-admin-tab';
|
||
function selectTab(name, save = true) {
|
||
for (const tab of tabs) {
|
||
const active = tab.id === 'status-tab-' + name;
|
||
tab.setAttribute('aria-selected', active ? 'true' : 'false');
|
||
tab.tabIndex = active ? 0 : -1;
|
||
}
|
||
for (const panel of panels) panel.hidden = panel.id !== 'status-panel-' + name;
|
||
if (save) { try { localStorage.setItem(storageKey, name); } catch (_) {} }
|
||
}
|
||
let saved = 'services';
|
||
try { saved = localStorage.getItem(storageKey) || saved; } catch (_) {}
|
||
if (!['services', 'maintenance'].includes(saved)) saved = 'services';
|
||
selectTab(saved, false);
|
||
for (const tab of tabs) tab.addEventListener('click', () => selectTab(tab.id === 'status-tab-maintenance' ? 'maintenance' : 'services'));
|
||
for (const tab of tabs) tab.addEventListener('keydown', event => {
|
||
if (!['ArrowLeft', 'ArrowRight'].includes(event.key)) return;
|
||
event.preventDefault();
|
||
const next = tabs[(tabs.indexOf(tab) + (event.key === 'ArrowRight' ? 1 : tabs.length - 1)) % tabs.length];
|
||
next.focus();
|
||
next.click();
|
||
});
|
||
})();
|
||
</script>
|
||
<?php require __DIR__ . '/_footer.php'; ?>
|