Upload via GUI (340 Dateien)

This commit is contained in:
2026-10-26 21:19:14 +00:00
parent a7aeec1b26
commit 2bc706cb09
6 changed files with 273 additions and 54 deletions
+113 -12
View File
@@ -21,11 +21,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
} 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_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'] ?? ''));
$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') {
@@ -52,7 +56,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
} 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.');
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']);
@@ -63,21 +69,32 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
} 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);
$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);
$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';
@@ -87,11 +104,22 @@ require __DIR__ . '/_header.php';
<?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 &amp; 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 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>
<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. Der Zeitgeber pr&uuml;ft jede Minute, einzelne Dienste k&ouml;nnen aber mit einem l&auml;ngeren Intervall gepr&uuml;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>
@@ -120,12 +148,22 @@ require __DIR__ . '/_header.php';
.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. Während geplanter Arbeiten kannst du sie auf Wartung setzen. Für nicht erkannte externe Dienste ist eine Prüf-URL nötig.</p>
<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&uuml;gen</strong>
<form method="post" class="status-component-add"><?= csrf_field() ?><input type="hidden" name="action" value="component_save">
@@ -137,20 +175,22 @@ require __DIR__ . '/_header.php';
</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)">
<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'] : (string) ($c['monitor_type'] ?? 'auto');
$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'], 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>
<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 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>
<?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>
@@ -164,13 +204,15 @@ require __DIR__ . '/_header.php';
</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'] ?>">
<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&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>
<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>
@@ -192,6 +234,36 @@ require __DIR__ . '/_header.php';
</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">
@@ -225,4 +297,33 @@ require __DIR__ . '/_header.php';
</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'; ?>