[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)), ]; }