Upload via GUI (338 Dateien)

This commit is contained in:
2026-10-25 17:47:04 +00:00
parent 63561d259f
commit 7ca9dd3f12
4 changed files with 244 additions and 111 deletions
+1
View File
@@ -2,6 +2,7 @@
/** @var array $staff (current_staff(), wird von der aufrufenden Seite gesetzt) */
$staff = current_staff();
$activePage = $activePage ?? '';
header('Content-Type: text/html; charset=UTF-8');
?>
<!DOCTYPE html>
<html lang="de">
+9
View File
@@ -21,6 +21,7 @@ $cronJobs = [
'process_snoozes' => ['label' => 'Wiedervorlagen', 'file' => 'process_snoozes.php', 'fn' => 'run_process_snoozes'],
'remind_customers' => ['label' => 'Kundenerinnerung', 'file' => 'remind_customers.php', 'fn' => 'run_remind_customers'],
'send_csat' => ['label' => 'Zufriedenheitsabfrage', 'file' => 'send_csat.php', 'fn' => 'run_send_csat'],
'status_checks' => ['label' => 'Statusprüfungen', 'file' => 'status_checks.php', 'fn' => 'run_status_checks'],
'run_plugins' => ['label' => 'Plugins (Hintergrundaufgaben)', 'file' => 'run_plugins.php', 'fn' => 'run_plugins_cron'],
];
@@ -40,6 +41,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
set_setting('offline_message', $offlineMsg ?: 'Das Supportcenter ist derzeit nicht erreichbar.');
set_setting('default_department_id', (string)$defaultDept);
set_setting('kb_enabled', $kbEnabled);
set_setting('minecraft_enabled', !empty($_POST['minecraft_enabled']) ? '1' : '0');
set_setting('ticket_ratings_enabled', !empty($_POST['ticket_ratings_enabled']) ? '1' : '0');
set_setting('auto_time_tracking_enabled', !empty($_POST['auto_time_tracking_enabled']) ? '1' : '0');
set_setting('auto_assign_on_open', !empty($_POST['auto_assign_on_open']) ? '1' : '0');
@@ -181,6 +183,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
elseif ($file['error'] !== UPLOAD_ERR_OK) { $errors[] = 'Upload fehlgeschlagen.'; }
elseif ($file['size'] > 2*1024*1024) { $errors[] = 'Max. 2 MB.'; }
elseif (!in_array($ext, $allowed, true)) { $errors[] = 'Erlaubt: PNG, JPG, GIF, SVG.'; }
elseif (($imgErr = favicon_validate_file($file['tmp_name'], $ext)) !== null) { $errors[] = $imgErr; }
else {
foreach ($allowed as $e2) @unlink($branding.'/logo.'.$e2);
$dest = $branding.'/logo.'.$ext;
@@ -227,6 +230,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
elseif ($file['error'] !== UPLOAD_ERR_OK) { $errors[] = 'Upload fehlgeschlagen.'; }
elseif ($file['size'] > 5*1024*1024) { $errors[] = 'Max. 5 MB.'; }
elseif (!in_array($ext, $allowed, true)) { $errors[] = 'Erlaubt: PNG, JPG, GIF.'; }
elseif (($imgErr = favicon_validate_file($file['tmp_name'], $ext)) !== null) { $errors[] = $imgErr; }
else {
foreach ($allowed as $e2) @unlink($branding.'/banner.'.$e2);
$dest = $branding.'/banner.'.$ext;
@@ -542,6 +546,10 @@ require __DIR__ . '/_header.php';
<input type="checkbox" name="kb_enabled" value="1" style="width:auto" <?= $kbEnabled?'checked':'' ?>>
Wissensdatenbank im Kundenportal anzeigen
</label>
<label style="display:flex;align-items:center;gap:8px;margin-top:10px;font-weight:400;cursor:pointer">
<input type="checkbox" name="minecraft_enabled" value="1" style="width:auto" <?= get_setting('minecraft_enabled','0')==='1'?'checked':'' ?>>
Minecraft-Plugin auf der Apps-Seite anzeigen (Download und Einrichtung, Ausfall-Tickets für Minecraft-Server)
</label>
<label style="display:flex;align-items:center;gap:8px;margin-top:10px;font-weight:400;cursor:pointer">
<input type="checkbox" name="ticket_ratings_enabled" value="1" style="width:auto" <?= get_setting('ticket_ratings_enabled','1')==='1'?'checked':'' ?>>
Ticket-Bewertung aktivieren (Kunden können nach Schließung 1–5 Sterne vergeben)
@@ -1167,6 +1175,7 @@ require __DIR__ . '/_header.php';
'remind_customers' => $cronRemind,
'run_plugins' => $cronPlugins,
'send_csat' => $cronCsat,
'status_checks' => cron_last_run_label(get_setting('cron_status_checks_last_run')),
];
?>
<table style="width:100%">
+139 -21
View File
@@ -15,14 +15,44 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
flash('success', 'Einstellungen gespeichert.');
} elseif ($action === 'component_save') {
$id = (int) ($_POST['id'] ?? 0) ?: null;
$r = status_save_component($pdo, $id, (string) ($_POST['name'] ?? ''), (string) ($_POST['description'] ?? ''), (int) ($_POST['sort_order'] ?? 0));
$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_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'] ?? ''));
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', 'Fehlende DeskHub-Dienste und passende Kategorien wurden hinzugefuegt. Vorhandene Eintraege blieben erhalten.');
} 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']);
@@ -38,8 +68,16 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}
$components = status_components($pdo);
$categories = status_categories($pdo);
$automaticChecks = 0;
$pendingChecks = 0;
foreach ($components as $component) {
if (status_component_monitor_target($pdo, $component) !== null) $automaticChecks++;
elseif (($component['monitor_type'] ?? 'auto') === 'auto') $pendingChecks++;
}
$incidents = status_incidents($pdo, 60);
$states = status_component_states();
$monitorTypes = status_monitor_types();
$incStates = status_incident_states();
$pageTitle = 'Statusseite';
$activePage = 'status_page';
@@ -47,37 +85,116 @@ require __DIR__ . '/_header.php';
?>
<h1>Statusseite</h1>
<?php foreach ($errors as $err): ?><div class="flash error"><?= e($err) ?></div><?php endforeach; ?>
<p class="muted" style="max-width:820px">Eine öffentliche Seite, auf der Ihre Kunden sehen, ob alles läuft, und über Störungen und Wartungen informiert werden.
<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>
<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>
<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>
<form method="post" class="card" style="max-width:820px;margin-bottom:16px">
<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 for="title" style="margin-top:8px">Überschrift</label>
<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 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>
<div class="card" style="max-width:820px;margin-bottom:16px">
<h3>Dienste</h3>
<?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 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>
<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>
<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>
<?php endforeach; ?>
<form method="post" style="display:flex;gap:8px;flex-wrap:wrap;margin-top:12px;border-top:1px solid var(--border,#e3e9ee);padding-top:12px"><?= csrf_field() ?><input type="hidden" name="action" value="component_save">
<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}
@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>
<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">
<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 style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;padding:8px 0;border-top:1px solid var(--border,#e3e9ee)">
<?php
$checkInfo = status_component_monitor_target($pdo, $c);
$selectedMonitorType = ($c['monitor_type'] ?? 'auto') === 'auto' && $checkInfo ? $checkInfo['type'] : (string) ($c['monitor_type'] ?? 'auto');
$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>
<?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>
<?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" 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'] ?>">
<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>
<?php endif; ?>
<button class="btn secondary" type="submit">Prüfung speichern</button>
</form>
</details>
<?php endforeach; ?>
<form method="post" class="card" style="max-width:820px;margin-bottom:16px"><?= csrf_field() ?><input type="hidden" name="action" value="incident_create">
</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">&#8597;</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">&times;</button></form>
</div><?php endforeach; ?>
</div><?php else: ?><p class="muted">Noch keine Kategorien angelegt.</p><?php endif; ?>
</section>
</div>
<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>
@@ -88,9 +205,10 @@ require __DIR__ . '/_header.php';
</div>
<div class="btn-row"><button type="submit" class="btn">Veröffentlichen</button></div>
</form>
</details>
<div class="card" style="max-width:820px">
<h3>Meldungen</h3>
<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)">
@@ -106,5 +224,5 @@ require __DIR__ . '/_header.php';
<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; ?>
</div>
</details>
<?php require __DIR__ . '/_footer.php'; ?>
+6 -1
View File
@@ -108,7 +108,9 @@ try {
// 2) Einrichten über das Formular
[$s, $b] = $req('POST', $pluginUrl, ['csrf_token' => $csrf($page), 'do' => 'save', 'enabled' => '1', 'host' => '127.0.0.1', 'port' => (string) $davPort, 'https' => '', 'user' => 'nas', 'password' => $davPass,
'target' => '/dav/DeskHub-Backups/', 'mirror' => '1', 'db' => '1', 'files' => '1', 'scope' => 'important', 'interval' => 'daily', 'time' => '02:00', 'keep_days' => '0', 'keep' => '10', 'verify_tls' => '1'], $jar);
$check($s === 200 && str_contains($b, 'Einstellungen gespeichert'), 'Einstellungen speichern meldet Erfolg (HTTP ' . $s . ')');
$check($s === 200 && str_contains($b, 'location.replace') && !str_contains($b, 'Fatal error'), 'Nach dem Speichern ersetzt sich die Seite durch die normale Adresse (kein erneutes Absenden beim Neuladen, HTTP ' . $s . ')');
[, $b] = $req('GET', $pluginUrl, [], $jar);
$check(str_contains($b, 'Einstellungen gespeichert'), 'Einstellungen speichern meldet Erfolg');
$check(str_contains($b, 'NAS erreichbar'), 'Die Seite zeigt „NAS erreichbar“ nach dem Einrichten');
$check(!str_contains($b, 'Fatal error') && !str_contains($b, 'Warning:'), 'Die Seite erzeugt keine PHP-Fehler');
@@ -124,9 +126,12 @@ try {
$check(str_contains($bp, '☁ auf NAS') || (str_contains($bp, 'nur hier') && str_contains($bp, '☁ Auf NAS')), 'Die Backup-Liste zeigt bei dem Backup die NAS-Marke (schon kopiert oder „nur hier“ mit Knopf „Auf NAS“; der Plugin-Takt kann bereits gelaufen sein)');
[, $pp] = $req('GET', $pluginUrl, [], $jar);
[$s, $b] = $req('POST', $pluginUrl, ['csrf_token' => $csrf($pp), 'do' => 'sync'], $jar);
$check(str_contains($b, 'location.replace'), 'Auch nach „Fehlende kopieren“ lädt die Seite die normale Adresse: Neuladen (alle 10 s) oder F5 sendet den Klick nicht noch einmal ab');
$check($name !== '' && in_array($name, $remoteFiles(), true), 'Das Backup der Backup-Seite wird auf die NAS kopiert (Aktion „Fehlende kopieren“)');
[, $bp] = $req('GET', '/staff/backup.php', [], $jar);
$check(str_contains($bp, '☁ auf NAS'), 'Danach trägt es in der Backup-Liste die Marke „auf NAS“');
// Das Kopieren läuft seit Plugin 2.3 im Hintergrund (nach der Antwort): die Seite nach dem Klick neu laden
[, $b] = $req('GET', $pluginUrl, [], $jar);
$check(str_contains($b, '☁ auf der NAS') && str_contains($b, $name), 'Die Plugin-Seite zeigt es als „auf der NAS“');
// 4) Lokal löschen: bleibt als „nur auf der NAS“ sichtbar und lässt sich zurückholen