Files
deskhub/DeskHub-Webseite/includes/status_page.php
T
2026-10-24 20:14:54 +00:00

176 lines
9.3 KiB
PHP

<?php
/**
* includes/status_page.php
*
* Statusseite für Kunden (status.php): Zustand der Dienste („Komponenten“) und Meldungen zu Störungen und Wartungen.
* Verwaltet wird sie im Mitarbeiterbereich (Adminportal → Statusseite) und über die Apps.
*/
function status_page_enabled(): bool
{
return get_setting('status_page_enabled', '0') === '1';
}
/** Zustände einer Komponente, vom besten zum schlechtesten. Schlüssel => [Beschriftung, Farbe, Rang]. */
function status_component_states(): array
{
return [
'operational' => ['In Betrieb', '#157a45', 0],
'maintenance' => ['Wartung', '#2563eb', 1],
'degraded' => ['Eingeschränkt', '#b45309', 2],
'outage' => ['Ausfall', '#dc2626', 3],
];
}
/** Stufen einer Störungsmeldung. */
function status_incident_states(): array
{
return [
'investigating' => 'Wird untersucht',
'identified' => 'Ursache gefunden',
'monitoring' => 'Wird beobachtet',
'resolved' => 'Behoben',
];
}
function status_components(PDO $pdo): array
{
return $pdo->query('SELECT * FROM status_components ORDER BY sort_order, name')->fetchAll();
}
/** Gesamtzustand: der schlechteste Zustand aller Komponenten und offener Störungen. @return array{key:string,label:string,color:string} */
function status_overall(PDO $pdo): array
{
$states = status_component_states();
$worst = 'operational';
foreach (status_components($pdo) as $c) {
if (($states[$c['status']][2] ?? 0) > $states[$worst][2]) $worst = $c['status'];
}
// Eine offene Störung ohne zugeordnete Komponente zählt mindestens als „eingeschränkt“
$open = (int) $pdo->query("SELECT COUNT(*) FROM status_incidents WHERE status <> 'resolved'")->fetchColumn();
if ($open > 0 && $states[$worst][2] < $states['degraded'][2] && $worst !== 'maintenance') $worst = 'degraded';
return ['key' => $worst, 'label' => $worst === 'operational' ? 'Alle Systeme laufen' : ($worst === 'maintenance' ? 'Wartungsarbeiten' : ($worst === 'outage' ? 'Ausfall' : 'Teilweise eingeschränkt')), 'color' => $states[$worst][1]];
}
/** Offene Störungen und die der letzten Tage (neueste zuerst), jeweils mit ihren Updates. */
function status_incidents(PDO $pdo, int $days = 14, bool $onlyOpen = false): array
{
$since = date('Y-m-d H:i:s', time() - max(1, $days) * 86400);
$sql = "SELECT i.*, c.name AS component_name FROM status_incidents i LEFT JOIN status_components c ON c.id = i.component_id WHERE "
. ($onlyOpen ? "i.status <> 'resolved'" : "(i.status <> 'resolved' OR i.created_at >= ?)") . ' ORDER BY i.created_at DESC, i.id DESC LIMIT 50';
$stmt = $pdo->prepare($sql);
$stmt->execute($onlyOpen ? [] : [$since]);
$rows = $stmt->fetchAll();
$up = $pdo->prepare('SELECT * FROM status_incident_updates WHERE incident_id = ? ORDER BY created_at DESC, id DESC');
foreach ($rows as &$r) {
$up->execute([$r['id']]);
$r['updates'] = $up->fetchAll();
}
return $rows;
}
function status_set_component(PDO $pdo, int $id, string $status): bool
{
if (!isset(status_component_states()[$status])) return false;
$s = $pdo->prepare('UPDATE status_components SET status = ? WHERE id = ?');
$s->execute([$status, $id]);
return $s->rowCount() > 0 || (bool) $pdo->query('SELECT 1 FROM status_components WHERE id = ' . (int) $id)->fetchColumn();
}
function status_save_component(PDO $pdo, ?int $id, string $name, string $description, int $sort): ?int
{
$name = trim(mb_substr($name, 0, 120));
if ($name === '') return null;
$description = trim(mb_substr($description, 0, 255));
if ($id) {
$pdo->prepare('UPDATE status_components SET name = ?, description = ?, sort_order = ? WHERE id = ?')->execute([$name, $description !== '' ? $description : null, $sort, $id]);
return $id;
}
$pdo->prepare('INSERT INTO status_components (name, description, status, sort_order, created_at) VALUES (?,?,?,?,?)')->execute([$name, $description !== '' ? $description : null, 'operational', $sort, db_now()]);
return (int) $pdo->lastInsertId();
}
function status_delete_component(PDO $pdo, int $id): void
{
$pdo->prepare('UPDATE status_incidents SET component_id = NULL WHERE component_id = ?')->execute([$id]);
$pdo->prepare('DELETE FROM status_components WHERE id = ?')->execute([$id]);
}
/**
* Neue Störungs- oder Wartungsmeldung. Mit $componentState wird die zugeordnete Komponente gleich auf diesen Zustand gesetzt.
* @return array{ok:bool,id?:int,error?:string}
*/
function status_create_incident(PDO $pdo, string $title, string $body, string $status, ?int $componentId, ?string $componentState, ?int $staffId): array
{
$title = trim(mb_substr($title, 0, 200));
if ($title === '') return ['ok' => false, 'error' => 'Bitte einen Titel angeben.'];
if (!isset(status_incident_states()[$status])) $status = 'investigating';
if ($componentId !== null) {
$c = $pdo->prepare('SELECT COUNT(*) FROM status_components WHERE id = ?');
$c->execute([$componentId]);
if (!(int) $c->fetchColumn()) $componentId = null;
}
$now = db_now();
$pdo->prepare('INSERT INTO status_incidents (title, body, status, impact, component_id, created_by, created_at, updated_at, resolved_at) VALUES (?,?,?,?,?,?,?,?,?)')
->execute([$title, trim($body) !== '' ? trim($body) : null, $status, 'minor', $componentId, $staffId, $now, $now, $status === 'resolved' ? $now : null]);
$id = (int) $pdo->lastInsertId();
if (trim($body) !== '') {
$pdo->prepare('INSERT INTO status_incident_updates (incident_id, status, body, created_by, created_at) VALUES (?,?,?,?,?)')->execute([$id, $status, trim($body), $staffId, $now]);
}
if ($componentId !== null && $componentState !== null && isset(status_component_states()[$componentState])) status_set_component($pdo, $componentId, $componentState);
return ['ok' => true, 'id' => $id];
}
/**
* Update zu einer Meldung. Wird sie als „Behoben“ markiert, setzt die zugeordnete Komponente zurück auf „In Betrieb“, wenn $restore true ist.
* @return array{ok:bool,error?:string}
*/
function status_update_incident(PDO $pdo, int $id, string $status, string $body, ?int $staffId, bool $restore = true): array
{
if (!isset(status_incident_states()[$status])) return ['ok' => false, 'error' => 'Unbekannter Status.'];
$s = $pdo->prepare('SELECT * FROM status_incidents WHERE id = ?');
$s->execute([$id]);
$inc = $s->fetch();
if (!$inc) return ['ok' => false, 'error' => 'Meldung nicht gefunden.'];
$now = db_now();
$pdo->prepare('UPDATE status_incidents SET status = ?, updated_at = ?, resolved_at = ? WHERE id = ?')
->execute([$status, $now, $status === 'resolved' ? ($inc['resolved_at'] ?: $now) : null, $id]);
$pdo->prepare('INSERT INTO status_incident_updates (incident_id, status, body, created_by, created_at) VALUES (?,?,?,?,?)')
->execute([$id, $status, trim(mb_substr($body, 0, 4000)), $staffId, $now]);
if ($status === 'resolved' && $restore && $inc['component_id']) {
// Nur zurücksetzen, wenn keine andere offene Meldung dieselbe Komponente betrifft
$o = $pdo->prepare("SELECT COUNT(*) FROM status_incidents WHERE component_id = ? AND status <> 'resolved' AND id <> ?");
$o->execute([$inc['component_id'], $id]);
if (!(int) $o->fetchColumn()) status_set_component($pdo, (int) $inc['component_id'], 'operational');
}
return ['ok' => true];
}
function status_delete_incident(PDO $pdo, int $id): void
{
$pdo->prepare('DELETE FROM status_incident_updates WHERE incident_id = ?')->execute([$id]);
$pdo->prepare('DELETE FROM status_incidents WHERE id = ?')->execute([$id]);
}
/** Zusammenfassung für die Apps. */
function status_summary(PDO $pdo): array
{
$states = status_component_states();
$overall = status_overall($pdo);
return [
'enabled' => status_page_enabled(),
'title' => (string) get_setting('status_page_title', 'Systemstatus'),
'url' => base_url() . '/status.php',
'overall' => $overall,
'componentStates' => array_map(fn($k, $v) => ['key' => $k, 'label' => $v[0]], array_keys($states), $states),
'incidentStates' => array_map(fn($k, $v) => ['key' => $k, 'label' => $v], array_keys(status_incident_states()), status_incident_states()),
'components' => array_map(fn($c) => ['id' => (int) $c['id'], 'name' => (string) $c['name'], 'description' => (string) ($c['description'] ?? ''), 'status' => (string) $c['status'], 'statusLabel' => $states[$c['status']][0] ?? $c['status']], status_components($pdo)),
'incidents' => array_map(fn($i) => [
'id' => (int) $i['id'], 'title' => (string) $i['title'], 'status' => (string) $i['status'], 'statusLabel' => status_incident_states()[$i['status']] ?? $i['status'],
'component' => $i['component_name'] ?? null, 'componentId' => $i['component_id'] !== null ? (int) $i['component_id'] : null,
'createdAt' => (string) $i['created_at'], 'updatedAt' => (string) $i['updated_at'], 'resolvedAt' => $i['resolved_at'],
'updates' => array_map(fn($u) => ['status' => (string) $u['status'], 'statusLabel' => status_incident_states()[$u['status']] ?? $u['status'], 'body' => (string) ($u['body'] ?? ''), 'createdAt' => (string) $u['created_at']], $i['updates']),
], status_incidents($pdo, 14)),
];
}