Quelltext des aktuellen Standes
<?php
$this->layout('layouts/main', [
'page_title' => $page_title,
'current_route' => $current_route,
'full_width' => true,
]);
use OpenNIT\Status\StatusManager;
$services = $services ?? [];
$activeIncidents = $active_incidents ?? [];
$recentIncidents = $recent_incidents ?? [];
$maintenances = $maintenances ?? [];
$groups = $groups ?? [];
$overrides = $service_overrides ?? [];
$view = $this;
$csrf = $this->csrf();
$svcPill = [
'operational' => 'status-pill--success',
'degraded' => 'status-pill--warning',
'partial_outage' => 'status-pill--orange',
'major_outage' => 'status-pill--danger',
];
$svcColor = [
'operational' => '#16a34a',
'degraded' => '#d97706',
'partial_outage' => '#ea580c',
'major_outage' => '#dc2626',
];
$suggestions = $monitoring_suggestions ?? [];
$resolvedAlarms = $monitoring_resolved ?? [];
$overall = $overall ?? ['key' => 'operational', 'label' => 'Alle Systeme betriebsbereit', 'all_ok' => true];
$kpi = $kpi ?? ['services_total' => 0, 'services_down' => 0, 'incidents' => 0, 'maintenances' => 0];
$overallShort = ['operational' => 'Alle OK', 'degraded' => 'Beeinträchtigt', 'partial_outage' => 'Teilausfall', 'major_outage' => 'Störung'];
$overallBg = ['operational' => 'bg-success', 'degraded' => 'bg-warning', 'partial_outage' => 'bg-orange', 'major_outage' => 'bg-danger'];
?>
<style>
/* Statusseiten-Verwaltung – Tab-Layout (modul-spezifisch, sp-Prefix) */
.sp-tab-panel { display:none; }
.sp-tab-panel.active { display:block; }
.sp-panel-inner .data-table td { padding-top:.4rem; padding-bottom:.4rem; }
/* Tabs + Suche/Aktion in der Filter-Card (wie /orders) */
.sp-subnav { margin-bottom:0; flex:1 1 auto; }
.sp-search-wrap { flex:0 1 260px; margin-left:auto; }
/* Aktionszelle: Select + Buttons auf gleiche Höhe ziehen */
.sp-row-actions { display:inline-flex; align-items:stretch; gap:.35rem; flex-wrap:nowrap; }
.sp-row-actions > form { display:inline-flex; margin:0; }
.sp-row-actions .form-control-sm { width:auto; }
.sp-row-actions .btn { display:inline-flex; align-items:center; }
</style>
<div class="content-header">
<div>
<h1><?= $this->te('Status-Seite verwalten') ?></h1>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/"><?= $this->te('Home') ?></a></li>
<li class="breadcrumb-item active"><?= $this->te('Statusseite') ?></li>
</ol>
</div>
<div class="content-header-actions">
<button type="button" class="btn btn-default btn-sm" onclick="document.getElementById('spEmbedModal').style.display='flex'"><i class="fas fa-code"></i> <?= $this->te('Einbetten & teilen') ?></button>
<a class="btn btn-default btn-sm" href="/status<?= ($status_token ?? '') !== '' ? '?token=' . $this->e($status_token) : '' ?>" target="_blank" rel="noopener"><i class="fas fa-external-link-alt"></i> <?= $this->te('Öffentliche Seite') ?></a>
</div>
</div>
<div class="page-body">
<!-- KPI-Übersicht -->
<div class="kpi-row">
<div class="kpi-col">
<div class="small-box <?= $overallBg[$overall['key']] ?? 'bg-success' ?>">
<div class="inner">
<h3 style="font-size:1.5rem;"><?= $view->e($overallShort[$overall['key']] ?? $overall['label']) ?></h3>
<p><?= $this->te('Gesamtstatus (öffentlich)') ?></p>
</div>
<div class="icon"><i class="fas fa-traffic-light"></i></div>
</div>
</div>
<div class="kpi-col">
<div class="small-box <?= $kpi['services_down'] > 0 ? 'bg-warning' : 'bg-success' ?>">
<div class="inner">
<h3><?= (int)$kpi['services_down'] ?></h3>
<p><?= $this->t('Dienste beeinträchtigt (von') ?> <?= (int)$kpi['services_total'] ?>)</p>
</div>
<div class="icon"><i class="fas fa-server"></i></div>
</div>
</div>
<div class="kpi-col">
<div class="small-box <?= $kpi['incidents'] > 0 ? 'bg-danger' : 'bg-success' ?>">
<div class="inner">
<h3><?= (int)$kpi['incidents'] ?></h3>
<p><?= $this->te('Aktive Störungen') ?></p>
</div>
<div class="icon"><i class="fas fa-exclamation-triangle"></i></div>
</div>
</div>
<div class="kpi-col">
<div class="small-box bg-info">
<div class="inner">
<h3><?= (int)$kpi['maintenances'] ?></h3>
<p><?= $this->te('Geplante Wartungen') ?></p>
</div>
<div class="icon"><i class="fas fa-calendar-check"></i></div>
</div>
</div>
</div>
<!-- Filter-Card: Tabs + Suche/Aktion (wie /orders) -->
<div class="card card-outline card-primary mb-3">
<div class="filter-card-body">
<div class="subnav sp-subnav" id="spTabs">
<button type="button" class="subnav-tab active" data-sp-tab="dienste"><i class="fas fa-server"></i> <?= $this->te('Dienste') ?></button>
<button type="button" class="subnav-tab" data-sp-tab="stoerungen"><i class="fas fa-exclamation-triangle"></i> <?= $this->te('Störungen') ?></button>
<button type="button" class="subnav-tab" data-sp-tab="wartungen"><i class="fas fa-calendar-check"></i> <?= $this->te('Wartungen') ?></button>
</div>
<div class="filter-input-wrap sp-search-wrap" data-sp-show="dienste">
<svg class="filter-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="15" height="15" aria-hidden="true" focusable="false"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<input type="search" class="form-control form-control-sm" placeholder="<?= $this->te('Dienste filtern …') ?>" oninput="spFilterServices(this.value)">
</div>
<button type="button" class="btn btn-primary btn-sm sp-action" data-sp-show="dienste" onclick="spNewService()"><i class="fas fa-plus"></i> <?= $this->te('Dienst') ?></button>
<button type="button" class="btn btn-primary btn-sm sp-action" data-sp-show="stoerungen" style="display:none;" onclick="spNewIncident()"><i class="fas fa-plus"></i> <?= $this->te('Störung melden') ?></button>
<button type="button" class="btn btn-primary btn-sm sp-action" data-sp-show="wartungen" style="display:none;" onclick="document.getElementById('spMaintModal').style.display='flex'"><i class="fas fa-plus"></i> <?= $this->te('Wartung planen') ?></button>
</div>
</div>
<!-- Tab: Dienste -->
<div class="sp-tab-panel active" id="sp-panel-dienste">
<div class="sp-panel-inner">
<?php if (empty($services)): ?>
<div class="empty-state"><p><?= $this->te('Noch keine Dienste. Lege z. B. „Drucksystem", „Vivendi" oder „Sage" an.') ?></p></div>
<?php else: ?>
<div class="table-wrapper">
<table class="data-table" id="spServiceTable">
<thead><tr><th scope="col" style="width:30px;text-align:center;" title="<?= $this->te('Zeilen am Griff ziehen, um die Reihenfolge zu ändern') ?>"><i class="fas fa-grip-vertical" style="color:var(--gray-300);"></i></th><th scope="col"><?= $this->te('Dienst') ?></th><th scope="col"><?= $this->te('Status') ?></th><th scope="col" style="width:240px;"></th></tr></thead>
<tbody>
<?php foreach ($services as $s): ?>
<tr data-id="<?= (int)$s['id'] ?>">
<td class="sp-drag-cell" style="text-align:center;color:var(--gray-400);cursor:grab;" draggable="true" title="<?= $this->te('Ziehen zum Sortieren') ?>"><i class="fas fa-grip-vertical"></i></td>
<td>
<strong><?= $view->e($s['name']) ?></strong>
<?php if (!empty($s['group_name'])): ?> <span class="badge"><?= $view->e($s['group_name']) ?></span><?php endif; ?>
<?php if (!empty($s['description'])): ?><div style="font-size:.8rem;color:var(--gray-500);"><?= $view->e($s['description']) ?></div><?php endif; ?>
</td>
<td>
<span class="status-pill <?= $svcPill[$s['status']] ?? 'status-pill--neutral' ?>"><?= $view->e(StatusManager::SERVICE_STATUS[$s['status']] ?? $s['status']) ?></span>
<?php if (isset($overrides[(int)$s['id']])): $ov = $overrides[(int)$s['id']]; ?>
<div style="font-size:.75rem;color:var(--orange-800);margin-top:.25rem;">
<i class="fas fa-clock"></i> <?= $this->t('öffentlich') ?>: <?= $view->e(StatusManager::SERVICE_STATUS[$ov['status']] ?? $ov['status']) ?>
<a href="/configlog/<?= (int)$ov['id'] ?>" class="table-link" title="<?= $view->e($ov['title']) ?>"><?= $this->te('(geplante Änderung)') ?></a>
</div>
<?php endif; ?>
</td>
<td>
<div class="sp-row-actions">
<form method="post" action="/status/services/<?= (int)$s['id'] ?>/status">
<input type="hidden" name="_csrf" value="<?= $view->e($csrf) ?>">
<select name="status" class="form-control form-control-sm" style="font-weight:600;color:<?= $svcColor[$s['status']] ?? 'var(--gray-700)' ?>;" onchange="this.form.submit()">
<?php foreach (StatusManager::SERVICE_STATUS as $k => $lbl): ?>
<option value="<?= $view->e($k) ?>" <?= $s['status'] === $k ? 'selected' : '' ?>><?= $view->e($lbl) ?></option>
<?php endforeach; ?>
</select>
</form>
<button type="button" class="btn btn-default btn-sm"
onclick='spEditService(<?= json_encode(["id"=>(int)$s["id"],"name"=>$s["name"],"description"=>$s["description"]??"","group_name"=>$s["group_name"]??"","status"=>$s["status"],"sort_order"=>(int)$s["sort_order"]], JSON_HEX_APOS|JSON_HEX_QUOT) ?>)'><?= $this->te('Bearbeiten') ?></button>
<form method="post" action="/status/services/<?= (int)$s['id'] ?>/delete" onsubmit="return confirm('Dienst löschen?');">
<input type="hidden" name="_csrf" value="<?= $view->e($csrf) ?>">
<button aria-label="<?= $this->te('Löschen') ?>" type="submit" class="btn btn-danger btn-sm">×</button>
</form>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
<!-- Tab: Störungen -->
<div class="sp-tab-panel" id="sp-panel-stoerungen">
<div class="sp-panel-inner">
<?php if (!empty($suggestions)): ?>
<!-- Offene Monitoring-Alarme, über die noch nicht entschieden ist.
Die Übernahme füllt den Melden-Dialog vor; abgeschickt wird er
wie jede andere Störung auch. -->
<div class="detail-card" style="margin-bottom:1rem;">
<div class="detail-card-header">
<h2 class="detail-card-title">
<i class="fas fa-satellite-dish"></i> <?= $this->te('Aus dem Monitoring') ?>
<span class="badge"><?= count($suggestions) ?></span>
</h2>
</div>
<div class="detail-card-body">
<p style="font-size:.85rem;color:var(--gray-500);margin-top:0;">
<?= $this->te('Offene Alarme, die noch keine Störungsmeldung sind. Übernehmen füllt den Melden-Dialog vor – betroffene Dienste und Text bleiben Ihre Entscheidung.') ?>
</p>
<div class="table-wrapper">
<table class="data-table">
<thead><tr><th scope="col"><?= $this->te('Alarm') ?></th><th scope="col"><?= $this->te('Schweregrad') ?></th><th scope="col"><?= $this->te('Seit') ?></th><th scope="col"></th></tr></thead>
<tbody>
<?php foreach ($suggestions as $sug): ?>
<tr>
<td>
<strong><?= $view->e((string)$sug['trigger_name']) ?></strong>
<?php $wer = trim((string)($sug['hostname'] ?? '')) ?: trim((string)($sug['host'] ?? '')); ?>
<?php if ($wer !== ''): ?>
<div style="font-size:.8rem;color:var(--gray-500);"><?= $view->e($wer) ?></div>
<?php endif; ?>
</td>
<td><span class="badge"><?= $view->e(StatusManager::MONITORING_SEVERITY[$sug['severity']] ?? (string)$sug['severity']) ?></span></td>
<td style="white-space:nowrap;"><?= $this->te('{zeitpunkt} Uhr', ['zeitpunkt' => date('d.m.Y H:i', strtotime((string)$sug['created_at']))]) ?></td>
<td style="white-space:nowrap;text-align:right;">
<button type="button" class="btn btn-primary btn-xs"
onclick='spFromMonitoring(<?= json_encode([
"id" => (int)$sug["id"],
"title" => (string)$sug["vorschlag_titel"],
"impact" => (string)$sug["vorschlag_impact"],
"body" => trim((string)($sug["message"] ?? "")) !== ""
? (string)$sug["message"]
: (string)$sug["trigger_name"],
], JSON_HEX_APOS|JSON_HEX_QUOT) ?>)'><?= $this->te('Übernehmen') ?></button>
<form method="post" action="/status/suggestions/<?= (int)$sug['id'] ?>/dismiss" style="display:inline;">
<input type="hidden" name="_csrf" value="<?= $view->e($csrf) ?>">
<button type="submit" class="btn btn-default btn-xs" title="<?= $this->te('Diesen Alarm nicht auf der Statusseite melden') ?>"><?= $this->te('Verwerfen') ?></button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<?php endif; ?>
<?php if (empty($activeIncidents) && empty($recentIncidents)): ?>
<div class="empty-state"><p><?= $this->te('Keine Störungen erfasst.') ?></p></div>
<?php endif; ?>
<?php foreach ($activeIncidents as $inc): ?>
<div class="detail-card" style="margin-bottom:1rem;">
<div class="detail-card-header">
<h2 class="detail-card-title"><?= $view->e($inc['title']) ?>
<span class="badge"><?= $view->e(StatusManager::INCIDENT_STATUS[$inc['status']] ?? $inc['status']) ?></span></h2>
<form method="post" action="/status/incidents/<?= (int)$inc['id'] ?>/delete" style="display:inline;" onsubmit="return confirm('Störung löschen?');">
<input type="hidden" name="_csrf" value="<?= $view->e($csrf) ?>">
<button aria-label="<?= $this->te('Löschen') ?>" type="submit" class="btn btn-danger btn-xs">×</button>
</form>
</div>
<div class="detail-card-body">
<?php if (isset($resolvedAlarms[(int)$inc['id']])): ?>
<p class="text-muted" style="font-size:.85rem;margin-top:0;">
<i class="fas fa-satellite-dish"></i>
<?= $this->t('Das Monitoring führt den zugehörigen Alarm seit {zeit} Uhr als behoben.', ['zeit' => date('d.m.Y H:i', strtotime((string)$resolvedAlarms[(int)$inc['id']]['resolved_at']))]) ?>
</p>
<?php endif; ?>
<?php if (!empty($inc['services'])): ?><p style="font-size:.85rem;color:var(--gray-500);"><?= $this->t('Betroffen') ?>: <?= $view->e(implode(', ', array_column($inc['services'], 'name'))) ?></p><?php endif; ?>
<ul style="list-style:none;padding-left:0;margin:0 0 1rem;">
<?php foreach ($inc['updates'] as $u): ?>
<li style="padding:.4rem 0;border-top:1px solid var(--gray-100);">
<span style="float:right;display:inline-flex;gap:.3rem;">
<button type="button" class="btn btn-default btn-xs" onclick="spToggle('spUpd<?= (int)$u['id'] ?>')"><?= $this->te('Bearbeiten') ?></button>
<form method="post" action="/status/incidents/<?= (int)$inc['id'] ?>/updates/<?= (int)$u['id'] ?>/delete" style="display:inline;" onsubmit="return confirm('Update löschen?');">
<input type="hidden" name="_csrf" value="<?= $view->e($csrf) ?>">
<button type="submit" class="btn btn-danger btn-xs" title="<?= $this->te('Update löschen') ?>">×</button>
</form>
</span>
<strong><?= $view->e(StatusManager::INCIDENT_STATUS[$u['status']] ?? $u['status']) ?></strong>
<span style="font-size:.78rem;color:var(--gray-400);"> · <?= $this->te('{zeitpunkt} Uhr', ['zeitpunkt' => date('d.m.Y H:i', strtotime($u['created_at']))]) ?><?= !empty($u['author_name']) ? ' · ' . $view->e($u['author_name']) : '' ?></span>
<div style="white-space:pre-wrap;"><?= $view->e($u['body']) ?></div>
<form id="spUpd<?= (int)$u['id'] ?>" method="post" action="/status/incidents/<?= (int)$inc['id'] ?>/updates/<?= (int)$u['id'] ?>/edit" style="display:none;margin-top:.4rem;">
<input type="hidden" name="_csrf" value="<?= $view->e($csrf) ?>">
<div style="display:flex;gap:.4rem;">
<input type="text" name="body" class="form-control" value="<?= $view->e($u['body']) ?>" required>
<button type="submit" class="btn btn-secondary btn-xs"><?= $this->te('Speichern') ?></button>
</div>
</form>
</li>
<?php endforeach; ?>
</ul>
<form method="post" action="/status/incidents/<?= (int)$inc['id'] ?>/update">
<input type="hidden" name="_csrf" value="<?= $view->e($csrf) ?>">
<div style="display:flex;gap:.5rem;">
<div class="form-group" style="flex:0 0 200px;">
<label><?= $this->te('Neuer Status') ?></label>
<select name="status" class="form-control">
<?php foreach (StatusManager::INCIDENT_STATUS as $k => $lbl): ?>
<option value="<?= $view->e($k) ?>" <?= $inc['status'] === $k ? 'selected' : '' ?>><?= $view->e($lbl) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group" style="flex:1;">
<label><?= $this->te('Update-Text') ?></label>
<input type="text" name="body" class="form-control" placeholder="<?= $this->te('Was ist der aktuelle Stand?') ?>" required>
</div>
</div>
<button type="submit" class="btn btn-secondary btn-sm"><?= $this->te('Update veröffentlichen') ?></button>
</form>
</div>
</div>
<?php endforeach; ?>
<?php if (!empty($recentIncidents)): ?>
<h3 style="font-size:.9rem;color:var(--gray-500);margin:1rem 0 .5rem;"><?= $this->te('Behoben (letzte 30 Tage)') ?></h3>
<div class="table-wrapper">
<table class="data-table">
<thead><tr><th scope="col"><?= $this->te('Störung') ?></th><th scope="col"><?= $this->te('Behoben') ?></th><th scope="col"></th></tr></thead>
<tbody>
<?php foreach ($recentIncidents as $inc): ?>
<tr>
<td><?= $view->e($inc['title']) ?></td>
<td style="white-space:nowrap;"><?= $this->te('{zeitpunkt} Uhr', ['zeitpunkt' => date('d.m.Y H:i', strtotime($inc['resolved_at']))]) ?></td>
<td>
<form method="post" action="/status/incidents/<?= (int)$inc['id'] ?>/delete" style="display:inline;" onsubmit="return confirm('Störung löschen?');">
<input type="hidden" name="_csrf" value="<?= $view->e($csrf) ?>">
<button aria-label="<?= $this->te('Löschen') ?>" type="submit" class="btn btn-danger btn-xs">×</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
<!-- Tab: Wartungen -->
<div class="sp-tab-panel" id="sp-panel-wartungen">
<div class="sp-panel-inner">
<?php if (empty($maintenances)): ?>
<div class="empty-state"><p><?= $this->te('Keine Wartungen geplant.') ?></p></div>
<?php else: ?>
<div class="table-wrapper">
<table class="data-table">
<thead><tr><th scope="col"><?= $this->te('Titel') ?></th><th scope="col"><?= $this->te('Zeitraum') ?></th><th scope="col"><?= $this->te('Status') ?></th><th scope="col" style="width:160px;"></th></tr></thead>
<tbody>
<?php foreach ($maintenances as $m): ?>
<tr>
<td>
<strong><?= $view->e($m['title']) ?></strong>
<?php if (!empty($m['services'])): ?><div style="font-size:.8rem;color:var(--gray-500);"><?= $view->e(implode(', ', array_column($m['services'], 'name'))) ?></div><?php endif; ?>
</td>
<td style="white-space:nowrap;"><?= date('d.m.Y H:i', strtotime($m['scheduled_start'])) ?><?= !empty($m['scheduled_end']) ? ' – ' . date('d.m.Y H:i', strtotime($m['scheduled_end'])) : '' ?></td>
<td>
<form method="post" action="/status/maintenances/<?= (int)$m['id'] ?>/update" style="display:inline;">
<input type="hidden" name="_csrf" value="<?= $view->e($csrf) ?>">
<select name="status" class="form-control form-control-sm" style="width:auto;" onchange="this.form.submit()">
<?php foreach (StatusManager::MAINTENANCE_STATUS as $k => $lbl): ?>
<option value="<?= $view->e($k) ?>" <?= $m['status'] === $k ? 'selected' : '' ?>><?= $view->e($lbl) ?></option>
<?php endforeach; ?>
</select>
</form>
</td>
<td>
<form method="post" action="/status/maintenances/<?= (int)$m['id'] ?>/delete" style="display:inline;" onsubmit="return confirm('Wartung löschen?');">
<input type="hidden" name="_csrf" value="<?= $view->e($csrf) ?>">
<button aria-label="<?= $this->te('Löschen') ?>" type="submit" class="btn btn-danger btn-sm">×</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
</div>
<?php
// Wiederverwendbarer Service-Checkbox-Block für Modals
$serviceChecks = function (string $idPrefix) use ($services, $view): string {
if (empty($services)) { return '<span style="color:var(--gray-400);">Erst Dienste anlegen.</span>'; }
$html = '<div style="display:flex;flex-wrap:wrap;gap:.5rem;">';
foreach ($services as $s) {
$html .= '<label style="font-size:.85rem;display:inline-flex;align-items:center;gap:.3rem;">'
. '<input type="checkbox" name="services[]" value="' . (int)$s['id'] . '"> ' . $view->e($s['name']) . '</label>';
}
return $html . '</div>';
};
?>
<!-- Modal: Dienst -->
<div id="spServiceModal" class="modal-overlay" role="dialog" aria-modal="true" style="display:none;" data-modal>
<div class="modal-card">
<div class="modal-header">
<h2 id="spServiceTitle"><?= $this->te('Dienst anlegen') ?></h2>
<button aria-label="<?= $this->te('Schließen') ?>" type="button" class="modal-close" onclick="document.getElementById('spServiceModal').style.display='none'">×</button>
</div>
<form method="post" id="spServiceForm" action="/status/services">
<input type="hidden" name="_csrf" value="<?= $view->e($csrf) ?>">
<div class="modal-body">
<div class="form-group">
<label for="sp-svc-name"><?= $this->te('Name') ?> <span class="text-danger">*</span></label>
<input type="text" id="sp-svc-name" name="name" class="form-control" required placeholder="<?= $this->te('z. B. Drucksystem') ?>">
</div>
<div class="form-group">
<label for="sp-svc-desc"><?= $this->te('Beschreibung') ?></label>
<input type="text" id="sp-svc-desc" name="description" class="form-control">
</div>
<div class="form-group">
<label for="sp-svc-group"><?= $this->te('Gruppe') ?></label>
<input type="text" id="sp-svc-group" name="group_name" class="form-control" list="sp-groups" placeholder="<?= $this->te('z. B. Fachverfahren (optional)') ?>">
<datalist id="sp-groups"><?php foreach ($groups as $g): ?><option value="<?= $view->e($g) ?>"></option><?php endforeach; ?></datalist>
</div>
<div class="form-group">
<label for="sp-svc-status"><?= $this->te('Status') ?></label>
<select id="sp-svc-status" name="status" class="form-control">
<?php foreach (StatusManager::SERVICE_STATUS as $k => $lbl): ?>
<option value="<?= $view->e($k) ?>"><?= $view->e($lbl) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label for="sp-svc-sort"><?= $this->te('Sortierung') ?></label>
<input type="number" id="sp-svc-sort" name="sort_order" class="form-control" value="0">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="document.getElementById('spServiceModal').style.display='none'"><?= $this->te('Abbrechen') ?></button>
<button type="submit" class="btn btn-primary"><?= $this->te('Speichern') ?></button>
</div>
</form>
</div>
</div>
<!-- Modal: Störung -->
<div id="spIncidentModal" class="modal-overlay" role="dialog" aria-modal="true" style="display:none;" data-modal>
<div class="modal-card">
<div class="modal-header">
<h2><?= $this->te('Störung melden') ?></h2>
<button aria-label="<?= $this->te('Schließen') ?>" type="button" class="modal-close" onclick="document.getElementById('spIncidentModal').style.display='none'">×</button>
</div>
<form method="post" action="/status/incidents">
<input type="hidden" name="_csrf" value="<?= $view->e($csrf) ?>">
<!-- Herkunft aus dem Monitoring; nur gesetzt, wenn der Dialog aus
einem Vorschlag heraus geöffnet wurde. -->
<input type="hidden" name="monitoring_event_id" id="sp-inc-monitoring" value="">
<div class="modal-body">
<div class="form-group">
<label for="sp-inc-title"><?= $this->te('Titel') ?> <span class="text-danger">*</span></label>
<input type="text" id="sp-inc-title" name="title" class="form-control" required placeholder="<?= $this->te('z. B. Vivendi nicht erreichbar') ?>">
</div>
<div style="display:flex;gap:.5rem;">
<div class="form-group" style="flex:1;">
<label for="sp-inc-impact"><?= $this->te('Auswirkung') ?></label>
<select id="sp-inc-impact" name="impact" class="form-control">
<?php foreach (StatusManager::INCIDENT_IMPACT as $k => $lbl): ?><option value="<?= $view->e($k) ?>"><?= $view->e($lbl) ?></option><?php endforeach; ?>
</select>
</div>
<div class="form-group" style="flex:1;">
<label for="sp-inc-status"><?= $this->te('Status') ?></label>
<select id="sp-inc-status" name="status" class="form-control">
<?php foreach (StatusManager::INCIDENT_STATUS as $k => $lbl): ?><option value="<?= $view->e($k) ?>"><?= $view->e($lbl) ?></option><?php endforeach; ?>
</select>
</div>
</div>
<div class="form-group">
<label for="sp-inc-started"><?= $this->te('Beginn (optional)') ?></label>
<input type="datetime-local" id="sp-inc-started" name="started_at" class="form-control">
<small class="form-hint"><?= $this->te('Leer = jetzt. Für nachträglich dokumentierte Störungen den tatsächlichen Beginn eintragen.') ?></small>
</div>
<div class="form-group">
<label for="sp-inc-body"><?= $this->te('Erste Meldung') ?> <span class="text-danger">*</span></label>
<textarea id="sp-inc-body" name="body" class="form-control" rows="3" required placeholder="<?= $this->te('Was ist passiert / Stand der Dinge?') ?>"></textarea>
</div>
<div class="form-group">
<label><?= $this->te('Betroffene Dienste') ?></label>
<?= $serviceChecks('inc') ?>
</div>
<div class="form-group">
<label style="display:inline-flex;align-items:center;gap:.5rem;font-weight:normal;">
<input type="checkbox" name="apply_impact" value="1">
<?= $this->te('Betroffene Dienste automatisch entsprechend der Auswirkung herabstufen') ?>
</label>
</div>
<?php if (!empty($portal_enabled)): ?>
<div class="form-group">
<label style="display:inline-flex;align-items:center;gap:.5rem;font-weight:normal;">
<input type="checkbox" name="portal_visible" value="1">
<?= $this->te('Im Self-Service-Portal als Hinweis anzeigen') ?>
</label>
</div>
<div class="form-group">
<label for="sp-inc-location"><?= $this->te('Standort (Portal-Anzeige)') ?></label>
<select id="sp-inc-location" name="location_id" class="form-control">
<option value=""><?= $this->te('Alle Standorte') ?></option>
<?php foreach (($locations ?? []) as $loc): ?>
<option value="<?= (int)$loc['id'] ?>"><?= str_repeat(' ', (int)($loc['_depth'] ?? 0)) . $this->e($loc['name']) ?></option>
<?php endforeach; ?>
</select>
<small class="form-hint"><?= $this->te('Leer = allen Portal-Nutzern anzeigen. Ein Oberstandort gilt auch für dessen Unterstandorte.') ?></small>
</div>
<div class="form-group">
<label for="sp-inc-ticket"><?= $this->te('Sammelticket (optional)') ?></label>
<input type="number" id="sp-inc-ticket" name="ticket_id" class="form-control" min="1"
placeholder="<?= $this->te('Ticket-Nummer, z. B. 1234') ?>">
<small class="text-muted"><?= $this->te('Betroffene Portal-Nutzer können sich per „Ich bin auch betroffen“ als Beobachter an dieses Ticket hängen, statt Duplikate zu melden.') ?></small>
</div>
<?php endif; ?>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="document.getElementById('spIncidentModal').style.display='none'"><?= $this->te('Abbrechen') ?></button>
<button type="submit" class="btn btn-primary"><?= $this->te('Melden') ?></button>
</div>
</form>
</div>
</div>
<!-- Modal: Wartung -->
<div id="spMaintModal" class="modal-overlay" role="dialog" aria-modal="true" style="display:none;" data-modal>
<div class="modal-card">
<div class="modal-header">
<h2><?= $this->te('Wartung planen') ?></h2>
<button aria-label="<?= $this->te('Schließen') ?>" type="button" class="modal-close" onclick="document.getElementById('spMaintModal').style.display='none'">×</button>
</div>
<form method="post" action="/status/maintenances">
<input type="hidden" name="_csrf" value="<?= $view->e($csrf) ?>">
<div class="modal-body">
<div class="form-group">
<label for="sp-mnt-title"><?= $this->te('Titel') ?> <span class="text-danger">*</span></label>
<input type="text" id="sp-mnt-title" name="title" class="form-control" required placeholder="<?= $this->te('z. B. Wartung Sage-Server') ?>">
</div>
<div style="display:flex;gap:.5rem;">
<div class="form-group" style="flex:1;">
<label for="sp-mnt-start"><?= $this->te('Beginn') ?> <span class="text-danger">*</span></label>
<input type="datetime-local" id="sp-mnt-start" name="scheduled_start" class="form-control" required>
</div>
<div class="form-group" style="flex:1;">
<label for="sp-mnt-end"><?= $this->te('Ende') ?></label>
<input type="datetime-local" id="sp-mnt-end" name="scheduled_end" class="form-control">
</div>
</div>
<div class="form-group">
<label for="sp-mnt-body"><?= $this->te('Beschreibung') ?></label>
<textarea id="sp-mnt-body" name="body" class="form-control" rows="2"></textarea>
</div>
<div class="form-group">
<label><?= $this->te('Betroffene Dienste') ?></label>
<?= $serviceChecks('mnt') ?>
</div>
<?php if (!empty($portal_enabled)): ?>
<div class="form-group">
<label style="display:inline-flex;align-items:center;gap:.5rem;font-weight:normal;">
<input type="checkbox" name="portal_visible" value="1">
<?= $this->te('Im Self-Service-Portal als Hinweis anzeigen') ?>
</label>
</div>
<div class="form-group">
<label for="sp-mnt-location"><?= $this->te('Standort (Portal-Anzeige)') ?></label>
<select id="sp-mnt-location" name="location_id" class="form-control">
<option value=""><?= $this->te('Alle Standorte') ?></option>
<?php foreach (($locations ?? []) as $loc): ?>
<option value="<?= (int)$loc['id'] ?>"><?= str_repeat(' ', (int)($loc['_depth'] ?? 0)) . $this->e($loc['name']) ?></option>
<?php endforeach; ?>
</select>
<small class="form-hint"><?= $this->te('Leer = allen Portal-Nutzern anzeigen. Ein Oberstandort gilt auch für dessen Unterstandorte.') ?></small>
</div>
<?php endif; ?>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="document.getElementById('spMaintModal').style.display='none'"><?= $this->te('Abbrechen') ?></button>
<button type="submit" class="btn btn-primary"><?= $this->te('Planen') ?></button>
</div>
</form>
</div>
</div>
<!-- Modal: Einbetten & teilen -->
<div id="spEmbedModal" class="modal-overlay" role="dialog" aria-modal="true" style="display:none;" data-modal>
<div class="modal-card">
<div class="modal-header">
<h2><?= $this->te('Einbetten & teilen') ?></h2>
<button aria-label="<?= $this->te('Schließen') ?>" type="button" class="modal-close" onclick="document.getElementById('spEmbedModal').style.display='none'">×</button>
</div>
<div class="modal-body">
<p><?= $this->te('Öffentliche Statusseite:') ?> <a href="/status" target="_blank" rel="noopener" class="table-link"><span id="sp-url-public"><?= $this->te('/status') ?></span></a>
<?= $this->te(' · iframe-Variante:') ?> <a href="/status/embed<?= ($status_token ?? '') !== '' ? '?token=' . $this->e($status_token) : '' ?>" target="_blank" rel="noopener" class="table-link"><?= $this->te('/status/embed') ?></a></p>
<?php if (($status_token ?? '') !== ''): ?>
<p class="form-hint" style="margin-top:.25rem;"><?= $this->te('Der Zugriff ist an einen Token gebunden – die Adressen oben und der Einbettungscode enthalten ihn bereits.') ?></p>
<?php endif; ?>
<div class="form-group" style="margin-top:.5rem;">
<label for="sp-embed-code"><?= $this->te('iframe-Einbettungscode (passt die Höhe automatisch an)') ?></label>
<textarea class="form-control" rows="11" readonly onclick="this.select()" id="sp-embed-code" style="font-family:monospace;font-size:.8rem;"></textarea>
<small class="form-hint"><?= $this->te('Kompletten Code (iframe + Script) anklicken (markiert alles) und mit Strg+C übernehmen. Das Script passt die Höhe an den Inhalt an – kein Scrollen im iframe.') ?></small>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="document.getElementById('spEmbedModal').style.display='none'"><?= $this->te('Schließen') ?></button>
</div>
</div>
</div>
<script>
// Modals: Backdrop-Klick + Escape
document.querySelectorAll('.modal-overlay[data-modal]').forEach(function (o) {
o.addEventListener('click', function (e) { if (e.target === o) o.style.display = 'none'; });
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') document.querySelectorAll('.modal-overlay[data-modal]').forEach(function (o) { o.style.display = 'none'; });
});
// Inline-Element ein-/ausblenden (z. B. Update-Bearbeiten-Formular)
function spToggle(id) {
var el = document.getElementById(id);
if (el) { el.style.display = (el.style.display === 'none' || !el.style.display) ? 'block' : 'none'; }
}
// Dienste-Tabelle filtern
function spFilterServices(q) {
q = (q || '').toLowerCase();
document.querySelectorAll('#spServiceTable tbody tr').forEach(function (tr) {
tr.style.display = tr.textContent.toLowerCase().indexOf(q) !== -1 ? '' : 'none';
});
}
// Dienst anlegen (Formular zuruecksetzen)
function spNewService() {
var f = document.getElementById('spServiceForm');
f.action = '/status/services';
f.reset();
document.getElementById('spServiceTitle').textContent = 'Dienst anlegen';
document.getElementById('spServiceModal').style.display = 'flex';
}
// Dienst bearbeiten (Service-Modal wiederverwenden)
function spEditService(s) {
var f = document.getElementById('spServiceForm');
f.action = '/status/services/' + s.id + '/update';
document.getElementById('spServiceTitle').textContent = 'Dienst bearbeiten';
document.getElementById('sp-svc-name').value = s.name || '';
document.getElementById('sp-svc-desc').value = s.description || '';
document.getElementById('sp-svc-group').value = s.group_name || '';
document.getElementById('sp-svc-status').value = s.status;
document.getElementById('sp-svc-sort').value = s.sort_order || 0;
document.getElementById('spServiceModal').style.display = 'flex';
}
// Störung melden (Dialog ohne Vorbelegung)
function spNewIncident() {
spResetIncidentSource();
document.getElementById('spIncidentModal').style.display = 'flex';
}
// Störung aus einem Monitoring-Vorschlag melden: Titel, Auswirkung und erste
// Meldung sind vorbelegt, betroffene Dienste bleiben offen.
function spFromMonitoring(a) {
document.getElementById('sp-inc-monitoring').value = a.id;
document.getElementById('sp-inc-title').value = a.title || '';
document.getElementById('sp-inc-body').value = a.body || '';
document.getElementById('sp-inc-impact').value = a.impact || 'minor';
document.getElementById('sp-inc-status').value = 'investigating';
document.getElementById('spIncidentModal').style.display = 'flex';
}
// Die Herkunft gilt nur für den Vorgang, aus dem heraus der Dialog geöffnet
// wurde – ein danach von Hand gemeldeter Vorfall trägt sie nicht mit.
function spResetIncidentSource() {
var f = document.getElementById('sp-inc-monitoring');
if (f) { f.value = ''; }
}
// Tabs umschalten (Dienste / Störungen / Wartungen), Zustand wird gemerkt
function spShowTab(name) {
document.querySelectorAll('#spTabs .subnav-tab').forEach(function (b) {
b.classList.toggle('active', b.getAttribute('data-sp-tab') === name);
});
document.querySelectorAll('.sp-tab-panel').forEach(function (p) {
p.classList.toggle('active', p.id === 'sp-panel-' + name);
});
// Kontextabhängige Suche/Aktion in der Filter-Card ein-/ausblenden
document.querySelectorAll('[data-sp-show]').forEach(function (el) {
el.style.display = el.getAttribute('data-sp-show') === name ? '' : 'none';
});
try { localStorage.setItem('sp-active-tab', name); } catch (e) {}
}
(function () {
document.querySelectorAll('#spTabs .subnav-tab').forEach(function (b) {
b.addEventListener('click', function () { spShowTab(b.getAttribute('data-sp-tab')); });
});
var saved = 'dienste';
try { saved = localStorage.getItem('sp-active-tab') || 'dienste'; } catch (e) {}
if (document.getElementById('sp-panel-' + saved)) { spShowTab(saved); }
})();
// iframe-Einbettungscode bauen: iframe + Listener, der die Höhe automatisch an
// den Inhalt anpasst (die Embed-Seite sendet ihre Höhe per postMessage).
(function () {
var origin = window.location.origin;
// Ist der Zugriff an einen Token gebunden, gehoert er in jede Adresse –
// sonst bliebe der eingebettete Rahmen leer.
var query = <?= json_encode(($status_token ?? '') !== '' ? '?token=' . $status_token : '', JSON_UNESCAPED_SLASHES) ?>;
var code = document.getElementById('sp-embed-code');
if (code) {
code.value =
'<iframe id="opennit-status" src="' + origin + '/status/embed' + query + '" style="width:100%;border:0;" scrolling="no"></iframe>\n' +
'<script>\n' +
'window.addEventListener("message", function (e) {\n' +
' if (e.origin !== "' + origin + '") return;\n' +
' if (e.data && e.data.type === "opennit-status-height") {\n' +
' var f = document.getElementById("opennit-status");\n' +
' if (f) f.style.height = e.data.height + "px";\n' +
' }\n' +
'});\n' +
'<\/script>';
}
var pub = document.getElementById('sp-url-public');
if (pub) {
pub.textContent = origin + '/status' + query;
var link = pub.closest('a');
if (link) { link.href = '/status' + query; }
}
})();
// Dienste per Drag&Drop sortieren (nativ) und Reihenfolge speichern
(function () {
var table = document.getElementById('spServiceTable');
if (!table) return;
var tbody = table.querySelector('tbody');
if (!tbody) return;
var dragRow = null;
tbody.addEventListener('dragstart', function (e) {
var cell = e.target.closest('.sp-drag-cell');
if (!cell) { e.preventDefault(); return; }
dragRow = cell.closest('tr');
e.dataTransfer.effectAllowed = 'move';
try { e.dataTransfer.setData('text/plain', dragRow.getAttribute('data-id') || ''); } catch (err) {}
dragRow.style.opacity = '0.4';
});
tbody.addEventListener('dragover', function (e) {
if (!dragRow) return;
e.preventDefault();
var over = e.target.closest('tr');
if (!over || over === dragRow || over.parentNode !== tbody) return;
var rect = over.getBoundingClientRect();
var after = (e.clientY - rect.top) > rect.height / 2;
tbody.insertBefore(dragRow, after ? over.nextSibling : over);
});
tbody.addEventListener('dragend', function () {
if (dragRow) dragRow.style.opacity = '';
dragRow = null;
var token = document.querySelector('input[name="_csrf"]')?.value || '';
var fd = new FormData();
Array.prototype.forEach.call(tbody.querySelectorAll('tr[data-id]'), function (tr) {
fd.append('ids[]', tr.getAttribute('data-id'));
});
fd.append('_csrf', token);
fetch('/status/services/reorder', {
method: 'POST',
credentials: 'same-origin',
headers: { 'X-CSRF-Token': token },
body: fd
}).catch(function () {});
});
}());
</script>