Upload via GUI (335 Dateien)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
</main>
|
||||
</div>
|
||||
<script src="/assets/js/richtext.js?v=<?= @filemtime(__DIR__ . '/../assets/js/richtext.js') ?: time() ?>"></script>
|
||||
<script src="/assets/js/live-tickets.js?v=<?= @filemtime(__DIR__ . '/../assets/js/live-tickets.js') ?: time() ?>"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -36,8 +36,11 @@ if ('serviceWorker' in navigator) {
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script>try { if (localStorage.getItem('deskhub_sidebar') === 'closed') document.documentElement.classList.add('sidebar-closed'); } catch (e) {}</script>
|
||||
<button type="button" id="sidebar-open" class="sidebar-toggle sidebar-toggle-open" aria-label="Seitenleiste einblenden" title="Seitenleiste einblenden" onclick="document.getElementById('sidebar-toggle').click()">☰</button>
|
||||
<div class="app-shell">
|
||||
<aside class="sidebar">
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<button type="button" id="sidebar-toggle" class="sidebar-toggle sidebar-toggle-close" aria-label="Seitenleiste einklappen" title="Seitenleiste einklappen">«</button>
|
||||
<div class="brand">
|
||||
<?php $logo = get_setting('logo_path'); ?>
|
||||
<?php if ($logo): ?>
|
||||
@@ -102,12 +105,17 @@ if ('serviceWorker' in navigator) {
|
||||
<?php if ($staff['role'] === 'admin'): ?><a href="/staff/custom_lists.php" class="<?= $activePage==='custom_lists'?'active':'' ?>">Benutzerdefinierte Listen</a><?php endif; ?>
|
||||
<?php if ($staff['role'] === 'admin'): ?><a href="/staff/canned_responses.php" class="<?= $activePage==='canned'?'active':'' ?>">Vorlagen</a><?php endif; ?>
|
||||
<?php if ($staff['role'] === 'admin'): ?><a href="/staff/site_pages.php" class="<?= $activePage==='site_pages'?'active':'' ?>">Seiten</a><?php endif; ?>
|
||||
<?php if ($staff['role'] === 'admin'): ?><a href="/staff/languages.php" class="<?= $activePage==='languages'?'active':'' ?>">Sprachen</a><?php endif; ?>
|
||||
<?php if ($staff['role'] === 'admin'): ?><a href="/staff/ticket_templates.php" class="<?= $activePage==='ticket_templates'?'active':'' ?>">Ticket-Vorlagen</a><a href="/staff/status_page.php" class="<?= $activePage==='status_page'?'active':'' ?>">Statusseite</a><?php endif; ?>
|
||||
<?php if (has_permission($staff, 'manage_email_addresses')): ?><a href="/staff/email_addresses.php" class="<?= $activePage==='email_addresses'?'active':'' ?>">E-Mail-Adressen</a><?php endif; ?>
|
||||
<?php if (has_permission($staff, 'manage_email_templates')): ?><a href="/staff/email_templates.php" class="<?= $activePage==='email_templates'?'active':'' ?>">E-Mail-Vorlagen</a><?php endif; ?>
|
||||
<?php if (has_permission($staff, 'manage_kb')): ?><a href="/staff/kb_manage.php" class="<?= $activePage==='kb'?'active':'' ?>">Wissensdatenbank</a><?php endif; ?>
|
||||
<?php if (has_permission($staff, 'manage_assistant')): ?><a href="/staff/assistant.php" class="<?= $activePage==='assistant'?'active':'' ?>">Digitaler Assistent</a><?php endif; ?>
|
||||
|
||||
<?php if ($staff['role'] === 'admin'): ?><a href="/staff/plugins.php" class="<?= $activePage==='plugins'?'active':'' ?>">Plugins</a><?php endif; ?>
|
||||
<?php foreach (plugin_admin_menu_items($staff) as $__pm): ?>
|
||||
<a href="<?= e($__pm['url']) ?>" class="<?= ($activePage ?? '') === 'plugin_' . $__pm['plugin'] ? 'active' : '' ?>"><?= e($__pm['label']) ?></a>
|
||||
<?php endforeach; ?>
|
||||
<span class="nav-group-label">Kunden</span>
|
||||
<?php if ($staff['role'] === 'admin'): ?><a href="/staff/customers.php" class="<?= $activePage==='customers'?'active':'' ?>">Kundenverzeichnis</a><?php endif; ?>
|
||||
<?php if (has_permission($staff, 'manage_organizations')): ?><a href="/staff/organizations.php" class="<?= $activePage==='organizations'?'active':'' ?>">Organisationen</a><?php endif; ?>
|
||||
@@ -173,6 +181,7 @@ if ('serviceWorker' in navigator) {
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lang-row" style="margin-bottom:10px"><?= i18n_switcher_html('dark') ?></div>
|
||||
<div style="display:flex; gap:14px; align-items:center">
|
||||
|
||||
<?php
|
||||
@@ -354,12 +363,14 @@ window.sessionTimerTimeout = <?= (int)$_jsTimeout ?>;
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
<script src="/assets/js/sidebar.js?v=<?= @filemtime(__DIR__ . "/../assets/js/sidebar.js") ?: time() ?>"></script>
|
||||
<main class="main">
|
||||
<!-- Session-Timer: kompakte Pille oben rechts -->
|
||||
<div id="session-timer-bar" style="position:fixed;top:12px;right:16px;z-index:9999;display:flex;align-items:center;gap:6px;background:rgba(15,23,42,.75);backdrop-filter:blur(6px);border:1px solid rgba(255,255,255,.1);border-radius:20px;padding:4px 12px 4px 8px;pointer-events:none;opacity:0;transition:opacity .3s">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#94a3b8" stroke-width="2.5"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||||
<span id="session-timer" style="font-size:.72rem;font-weight:700;color:#cbd5e1;font-variant-numeric:tabular-nums;letter-spacing:.02em"></span>
|
||||
</div>
|
||||
<?= plugin_filter('staff.page.top', '', ['staff' => $staff ?? null, 'activePage' => $activePage ?? '']) /* Hinweise von Plugins oben auf jeder Seite (fertiges HTML aus vertrauenswürdigem Plugin-Code) */ ?>
|
||||
<?php foreach (get_flashes() as $f): ?>
|
||||
<div class="flash <?= e($f['type']) ?>"><?= e($f['message']) ?></div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
@@ -42,6 +42,7 @@ $pageTitle = 'Passwort zurücksetzen';
|
||||
<?= site_icon_links() ?>
|
||||
</head>
|
||||
<body>
|
||||
<?= i18n_switcher_html('floating') ?>
|
||||
<div class="auth-wrap">
|
||||
<div class="auth-card">
|
||||
<?php require __DIR__ . '/../includes/brand_block.php'; ?>
|
||||
|
||||
+1241
-1212
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/bootstrap.php';
|
||||
$admin = require_admin();
|
||||
$pdo = get_db();
|
||||
$errors = [];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
verify_csrf();
|
||||
$action = $_POST['action'] ?? '';
|
||||
if ($action === 'settings') {
|
||||
set_setting('status_page_enabled', !empty($_POST['enabled']) ? '1' : '0');
|
||||
$title = trim(mb_substr((string) ($_POST['title'] ?? ''), 0, 80));
|
||||
set_setting('status_page_title', $title !== '' ? $title : 'Systemstatus');
|
||||
audit_log($admin['id'], 'Statusseite: Einstellungen geändert');
|
||||
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));
|
||||
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_delete') {
|
||||
status_delete_component($pdo, (int) ($_POST['id'] ?? 0));
|
||||
flash('success', 'Dienst entfernt.');
|
||||
} 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']);
|
||||
if (!$r['ok']) $errors[] = $r['error']; else { audit_log($admin['id'], 'Statusseite: Meldung veröffentlicht'); flash('success', 'Meldung veröffentlicht.'); }
|
||||
} elseif ($action === 'incident_update') {
|
||||
$r = status_update_incident($pdo, (int) ($_POST['id'] ?? 0), (string) ($_POST['status'] ?? ''), (string) ($_POST['body'] ?? ''), (int) $admin['id'], true);
|
||||
if (!$r['ok']) $errors[] = $r['error']; else flash('success', 'Update veröffentlicht.');
|
||||
} elseif ($action === 'incident_delete') {
|
||||
status_delete_incident($pdo, (int) ($_POST['id'] ?? 0));
|
||||
flash('success', 'Meldung gelöscht.');
|
||||
}
|
||||
if (!$errors) redirect('/staff/status_page.php');
|
||||
}
|
||||
|
||||
$components = status_components($pdo);
|
||||
$incidents = status_incidents($pdo, 60);
|
||||
$states = status_component_states();
|
||||
$incStates = status_incident_states();
|
||||
$pageTitle = 'Statusseite';
|
||||
$activePage = 'status_page';
|
||||
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.
|
||||
<?php if (status_page_enabled()): ?>Adresse: <a href="/status.php" target="_blank"><?= e(base_url()) ?>/status.php</a><?php endif; ?></p>
|
||||
|
||||
<form method="post" class="card" style="max-width:820px;margin-bottom:16px">
|
||||
<?= 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>
|
||||
<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>
|
||||
|
||||
<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">
|
||||
<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">
|
||||
<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>
|
||||
|
||||
<form method="post" class="card" style="max-width:820px;margin-bottom:16px"><?= 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>
|
||||
<div style="display:flex;gap:12px;flex-wrap:wrap;margin-top:8px">
|
||||
<div><label>Stufe</label><select name="status"><?php foreach ($incStates as $k => $l): ?><option value="<?= e($k) ?>"><?= e($l) ?></option><?php endforeach; ?></select></div>
|
||||
<div><label>Betrifft</label><select name="component_id"><option value="">– kein bestimmter Dienst –</option><?php foreach ($components as $c): ?><option value="<?= (int) $c['id'] ?>"><?= e($c['name']) ?></option><?php endforeach; ?></select></div>
|
||||
<div><label>Dienst setzen auf</label><select name="component_state"><option value="">– nicht ändern –</option><?php foreach ($states as $k => $s): ?><option value="<?= e($k) ?>"><?= e($s[0]) ?></option><?php endforeach; ?></select></div>
|
||||
</div>
|
||||
<div class="btn-row"><button type="submit" class="btn">Veröffentlichen</button></div>
|
||||
</form>
|
||||
|
||||
<div class="card" style="max-width:820px">
|
||||
<h3>Meldungen</h3>
|
||||
<?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)">
|
||||
<strong><?= e($i['title']) ?></strong>
|
||||
<span class="muted" style="font-size:.8rem">· <?= e(format_dt((string) $i['created_at'])) ?><?= $i['component_name'] ? ' · ' . e($i['component_name']) : '' ?> · <?= e($incStates[$i['status']] ?? $i['status']) ?></span>
|
||||
<?php foreach ($i['updates'] as $u): ?><div style="font-size:.86rem;margin-top:3px"><span class="muted"><?= e($incStates[$u['status']] ?? $u['status']) ?>:</span> <?= nl2br(e((string) $u['body'])) ?></div><?php endforeach; ?>
|
||||
<?php if ($i['status'] !== 'resolved'): ?>
|
||||
<form method="post" style="display:flex;gap:6px;flex-wrap:wrap;margin-top:6px"><?= csrf_field() ?><input type="hidden" name="action" value="incident_update"><input type="hidden" name="id" value="<?= (int) $i['id'] ?>">
|
||||
<select name="status"><?php foreach ($incStates as $k => $l): ?><option value="<?= e($k) ?>" <?= $k === $i['status'] ? 'selected' : '' ?>><?= e($l) ?></option><?php endforeach; ?></select>
|
||||
<input type="text" name="body" placeholder="Update für die Kunden" maxlength="4000" style="flex:1;min-width:200px">
|
||||
<button class="btn secondary" type="submit">Update senden</button></form>
|
||||
<?php endif; ?>
|
||||
<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>
|
||||
<?php require __DIR__ . '/_footer.php'; ?>
|
||||
@@ -149,6 +149,25 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
}
|
||||
}
|
||||
|
||||
// Ticket-Vorlagen: ?template=ID füllt das Formular vor
|
||||
$ticketTemplates = [];
|
||||
$activeTemplateId = 0;
|
||||
try {
|
||||
$ticketTemplates = ticket_templates_list($pdo);
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST' && (int) ($_GET['template'] ?? 0) > 0) {
|
||||
$tpl = ticket_template_get($pdo, (int) $_GET['template']);
|
||||
if ($tpl) {
|
||||
$activeTemplateId = (int) $tpl['id'];
|
||||
$_POST['subject'] = (string) $tpl['subject'];
|
||||
$_POST['message'] = nl2br(e((string) ($tpl['body'] ?? ''))); // der Editor erwartet HTML
|
||||
if ($tpl['department_id']) $_POST['department_id'] = (string) $tpl['department_id'];
|
||||
if ($tpl['help_topic_id']) $_POST['help_topic_id'] = (string) $tpl['help_topic_id'];
|
||||
if ($tpl['priority_id']) $_POST['priority_id'] = (string) $tpl['priority_id'];
|
||||
}
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$ticketTemplates = [];
|
||||
}
|
||||
$pageTitle = 'Ticket erstellen';
|
||||
$activePage = 'tickets';
|
||||
require __DIR__ . '/_header.php';
|
||||
@@ -161,6 +180,15 @@ require __DIR__ . '/_header.php';
|
||||
<a href="/staff/tickets.php" class="btn secondary">← Zur Ticket-Liste</a>
|
||||
</div>
|
||||
<p class="muted">Ticket im Namen eines Kunden anlegen – z.B. bei Telefonanrufen oder Vor-Ort-Anfragen.</p>
|
||||
<?php if ($ticketTemplates): ?>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin:0 0 14px">
|
||||
<span class="muted" style="font-size:.82rem">Vorlage:</span>
|
||||
<?php foreach ($ticketTemplates as $__tp): ?>
|
||||
<a href="/staff/ticket_create.php?template=<?= (int) $__tp['id'] ?>" class="btn <?= $activeTemplateId === (int) $__tp['id'] ? '' : 'secondary' ?>" style="font-size:.82rem;padding:4px 12px"><?= e($__tp['name']) ?></a>
|
||||
<?php endforeach; ?>
|
||||
<?php if ($activeTemplateId): ?><a href="/staff/ticket_create.php" class="muted" style="font-size:.8rem">Ohne Vorlage</a><?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php foreach ($errors as $err): ?>
|
||||
<div class="flash error"><?= e($err) ?></div>
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/bootstrap.php';
|
||||
$admin = require_admin();
|
||||
$pdo = get_db();
|
||||
$errors = [];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
verify_csrf();
|
||||
$action = $_POST['action'] ?? '';
|
||||
if ($action === 'save') {
|
||||
$id = (int) ($_POST['id'] ?? 0) ?: null;
|
||||
$r = ticket_template_save($pdo, $id, $_POST);
|
||||
if (!$r['ok']) {
|
||||
$errors[] = $r['error'];
|
||||
} else {
|
||||
audit_log($admin['id'], 'Ticket-Vorlage gespeichert: ' . trim((string) ($_POST['name'] ?? '')));
|
||||
flash('success', 'Vorlage gespeichert.');
|
||||
redirect('/staff/ticket_templates.php');
|
||||
}
|
||||
} elseif ($action === 'delete') {
|
||||
ticket_template_delete($pdo, (int) ($_POST['id'] ?? 0));
|
||||
audit_log($admin['id'], 'Ticket-Vorlage gelöscht');
|
||||
flash('success', 'Vorlage gelöscht.');
|
||||
redirect('/staff/ticket_templates.php');
|
||||
}
|
||||
}
|
||||
|
||||
$departments = $pdo->query('SELECT id, name FROM departments ORDER BY name')->fetchAll();
|
||||
$priorities = $pdo->query('SELECT id, name FROM priorities ORDER BY id')->fetchAll();
|
||||
$helpTopics = help_topic_options($pdo);
|
||||
$templates = ticket_templates_list($pdo, false, true);
|
||||
$editing = null;
|
||||
if (!empty($_GET['edit'])) {
|
||||
foreach ($templates as $t) if ((int) $t['id'] === (int) $_GET['edit']) $editing = $t;
|
||||
}
|
||||
// Bei einem Fehler die eingegebenen Werte behalten
|
||||
$v = $errors ? $_POST : ($editing ?: ['is_public' => 1, 'is_active' => 1, 'sort_order' => count($templates) * 10 + 10]);
|
||||
$pageTitle = 'Ticket-Vorlagen';
|
||||
$activePage = 'ticket_templates';
|
||||
require __DIR__ . '/_header.php';
|
||||
?>
|
||||
<h1>Ticket-Vorlagen</h1>
|
||||
<?php foreach ($errors as $err): ?><div class="flash error"><?= e($err) ?></div><?php endforeach; ?>
|
||||
<p class="muted" style="max-width:820px">Vorbelegte Formulare für häufige Anliegen, zum Beispiel „Neuer Mitarbeiter“ oder „Passwort zurücksetzen“. Kunden wählen sie auf der Seite „Neues Ticket“ (wenn sie für Kunden sichtbar sind), Mitarbeiter beim Anlegen eines Tickets im Web, im Windows-Programm und in der App.</p>
|
||||
|
||||
<form method="post" class="card" style="max-width:820px;margin-bottom:16px"><?= csrf_field() ?>
|
||||
<input type="hidden" name="action" value="save"><input type="hidden" name="id" value="<?= (int) ($editing['id'] ?? $_POST['id'] ?? 0) ?>">
|
||||
<h3><?= $editing || !empty($_POST['id']) ? 'Vorlage bearbeiten' : 'Neue Vorlage' ?></h3>
|
||||
<div class="field-row">
|
||||
<div><label for="name">Name der Vorlage</label><input type="text" id="name" name="name" maxlength="120" required value="<?= e((string) ($v['name'] ?? '')) ?>" placeholder="z. B. Neuer Mitarbeiter"></div>
|
||||
<div><label for="description">Kurzbeschreibung (optional)</label><input type="text" id="description" name="description" maxlength="255" value="<?= e((string) ($v['description'] ?? '')) ?>"></div>
|
||||
</div>
|
||||
<label for="subject" style="margin-top:8px">Betreff</label>
|
||||
<input type="text" id="subject" name="subject" maxlength="200" value="<?= e((string) ($v['subject'] ?? '')) ?>" placeholder="Vorgabe für den Betreff">
|
||||
<label for="body" style="margin-top:8px">Text</label>
|
||||
<textarea id="body" name="body" rows="7" maxlength="10000" placeholder="Zum Beispiel eine Liste mit den Angaben, die Sie brauchen: Name, Startdatum, benötigte Geräte …"><?= e((string) ($v['body'] ?? '')) ?></textarea>
|
||||
<div class="field-row" style="margin-top:8px">
|
||||
<div><label for="department_id">Abteilung</label><select id="department_id" name="department_id"><option value="">– nicht vorgeben –</option><?php foreach ($departments as $d): ?><option value="<?= (int) $d['id'] ?>" <?= (int) ($v['department_id'] ?? 0) === (int) $d['id'] ? 'selected' : '' ?>><?= e($d['name']) ?></option><?php endforeach; ?></select></div>
|
||||
<div><label for="help_topic_id">Hilfethema</label><select id="help_topic_id" name="help_topic_id"><option value="">– nicht vorgeben –</option><?php foreach ($helpTopics as $h): ?><option value="<?= (int) $h['id'] ?>" <?= (int) ($v['help_topic_id'] ?? 0) === (int) $h['id'] ? 'selected' : '' ?>><?= e($h['path']) ?></option><?php endforeach; ?></select></div>
|
||||
<div><label for="priority_id">Priorität</label><select id="priority_id" name="priority_id"><option value="">– nicht vorgeben –</option><?php foreach ($priorities as $p): ?><option value="<?= (int) $p['id'] ?>" <?= (int) ($v['priority_id'] ?? 0) === (int) $p['id'] ? 'selected' : '' ?>><?= e($p['name']) ?></option><?php endforeach; ?></select></div>
|
||||
</div>
|
||||
<div style="display:flex;gap:18px;flex-wrap:wrap;margin-top:10px;align-items:center">
|
||||
<label style="font-weight:400"><input type="checkbox" name="is_public" value="1" style="width:auto" <?= !empty($v['is_public']) ? 'checked' : '' ?>> Für Kunden sichtbar (auf „Neues Ticket“)</label>
|
||||
<label style="font-weight:400"><input type="checkbox" name="is_active" value="1" style="width:auto" <?= !isset($v['is_active']) || !empty($v['is_active']) ? 'checked' : '' ?>> Aktiv</label>
|
||||
<label style="font-weight:400">Reihenfolge <input type="number" name="sort_order" value="<?= (int) ($v['sort_order'] ?? 0) ?>" style="width:80px"></label>
|
||||
</div>
|
||||
<div class="btn-row"><button type="submit" class="btn">Speichern</button><?php if ($editing): ?> <a class="btn secondary" href="/staff/ticket_templates.php">Abbrechen</a><?php endif; ?></div>
|
||||
</form>
|
||||
|
||||
<div class="card" style="max-width:820px">
|
||||
<h3>Vorhandene Vorlagen</h3>
|
||||
<?php if (!$templates): ?><p class="muted">Noch keine Vorlagen.</p><?php endif; ?>
|
||||
<?php foreach ($templates as $t): ?>
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;padding:10px 0;border-top:1px solid var(--border,#e3e9ee)">
|
||||
<div style="flex:1;min-width:200px"><strong><?= e($t['name']) ?></strong>
|
||||
<?= $t['is_public'] ? '<span class="muted" style="font-size:.76rem"> · für Kunden</span>' : '<span class="muted" style="font-size:.76rem"> · nur Mitarbeiter</span>' ?>
|
||||
<?= $t['is_active'] ? '' : '<span style="color:#b45309;font-size:.76rem"> · inaktiv</span>' ?>
|
||||
<?php if ($t['description']): ?><div class="muted" style="font-size:.82rem"><?= e($t['description']) ?></div><?php endif; ?></div>
|
||||
<a class="btn secondary" href="/staff/ticket_templates.php?edit=<?= (int) $t['id'] ?>" style="padding:3px 12px;font-size:.8rem">Bearbeiten</a>
|
||||
<form method="post" onsubmit="return confirm('Vorlage löschen?')"><?= csrf_field() ?><input type="hidden" name="action" value="delete"><input type="hidden" name="id" value="<?= (int) $t['id'] ?>"><button class="btn secondary" type="submit" style="padding:3px 12px;font-size:.8rem">Löschen</button></form>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php require __DIR__ . '/_footer.php'; ?>
|
||||
@@ -1268,6 +1268,21 @@ $sourceLabel = $sourceLabels[$ticket['source']] ?? $ticket['source'];
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php $csatRow = null; try { $csatRow = csat_for_ticket($pdo, (int) $ticket['id']); } catch (Throwable $e) {} ?>
|
||||
<?php if ($csatRow): ?>
|
||||
<!-- Zufriedenheit des Kunden -->
|
||||
<div class="card" style="padding:14px 20px">
|
||||
<span style="font-size:.71rem; font-weight:700; text-transform:uppercase; letter-spacing:.06em; color:var(--muted)">Kundenzufriedenheit</span>
|
||||
<?php if ($csatRow['rating'] !== null): ?>
|
||||
<div style="font-size:1.25rem;color:#f5b301;margin-top:4px"><?= e(csat_stars((int) $csatRow['rating'])) ?> <span style="font-size:.85rem;color:var(--muted)"><?= (int) $csatRow['rating'] ?> von 5</span></div>
|
||||
<?php if (!empty($csatRow['comment'])): ?><div style="font-size:.85rem;margin-top:4px">„<?= e((string) $csatRow['comment']) ?>“</div><?php endif; ?>
|
||||
<div class="muted" style="font-size:.76rem;margin-top:4px">Bewertet am <?= e(format_dt((string) $csatRow['answered_at'])) ?></div>
|
||||
<?php else: ?>
|
||||
<div class="muted" style="font-size:.84rem;margin-top:4px">Abfrage am <?= e(format_dt((string) $csatRow['sent_at'])) ?> gesendet, noch keine Antwort.</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Wiedervorlage und Beobachter -->
|
||||
<div class="card" style="padding:16px 20px">
|
||||
<span style="font-size:.71rem; font-weight:700; text-transform:uppercase; letter-spacing:.06em; color:var(--muted)">Wiedervorlage</span>
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Prüft die Schnittstellen der Apps zu Zufriedenheitsabfrage, Statusseite, Ticket-Vorlagen und Cc-Empfängern.
|
||||
*/
|
||||
|
||||
date_default_timezone_set('Europe/Berlin'); // wie in der Test-Installation
|
||||
$root = dirname(__DIR__);
|
||||
$work = sys_get_temp_dir() . '/deskhub-apicss-' . bin2hex(random_bytes(4));
|
||||
$tmp = $work . '/inst';
|
||||
mkdir($tmp, 0755, true);
|
||||
$webPort = random_int(36500, 36999);
|
||||
$skip = '#^(config/config\.php|database/[^/]+\.sqlite[^/]*|backups|uploads|tests|plugins/[^/]+|\.git)(/|$)#';
|
||||
foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST) as $f) {
|
||||
$rel = str_replace(chr(92), '/', substr($f->getPathname(), strlen($root) + 1));
|
||||
if (preg_match($skip, $rel) && !preg_match('#^(backups|uploads|plugins)/(\.htaccess|README\.md)$#', $rel)) continue;
|
||||
if ($f->isDir()) { @mkdir($tmp . '/' . $rel, 0755, true); continue; }
|
||||
@mkdir(dirname($tmp . '/' . $rel), 0755, true);
|
||||
copy($f->getPathname(), $tmp . '/' . $rel);
|
||||
}
|
||||
@mkdir($tmp . '/uploads');
|
||||
@mkdir($tmp . '/config');
|
||||
file_put_contents($tmp . '/config/config.php', '<?php return ' . var_export([
|
||||
'app_key' => bin2hex(random_bytes(32)),
|
||||
'db' => ['driver' => 'sqlite', 'sqlite_path' => $tmp . '/database/deskhub.sqlite'],
|
||||
'site' => ['name' => 'Test', 'url' => 'http://127.0.0.1', 'admin_email' => 'a@test.de', 'timezone' => 'Europe/Berlin'],
|
||||
'uploads' => ['max_size_bytes' => 10485760, 'allowed_extensions' => ['png'], 'path' => $tmp . '/uploads'],
|
||||
], true) . ';');
|
||||
|
||||
putenv('DESKHUB_CONFIG_JSON');
|
||||
putenv('DESKHUB_SQLITE_PATH');
|
||||
putenv('DESKHUB_TESTING');
|
||||
putenv('DESKHUB_API_ALLOW_INSECURE=1');
|
||||
require_once $root . '/database/sqlite_schema.php';
|
||||
require_once $root . '/database/migrate.php';
|
||||
$db = new PDO('sqlite:' . $tmp . '/database/deskhub.sqlite');
|
||||
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
$db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
||||
$db->exec('PRAGMA foreign_keys = ON');
|
||||
$db->exec(convert_schema_to_sqlite((string) file_get_contents($root . '/database/schema.sql')));
|
||||
$mig = run_migrations($db, true);
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$dept = (int) $db->query('SELECT id FROM departments ORDER BY id LIMIT 1')->fetchColumn();
|
||||
$db->exec('DELETE FROM staff');
|
||||
$ins = $db->prepare('INSERT INTO staff (name, email, password_hash, role, department_id, is_active, created_at) VALUES (?,?,?,?,?,1,?)');
|
||||
$ids = [];
|
||||
foreach ([['Chef', 'chef@test.de', 'admin'], ['Agent', 'agent@test.de', 'agent']] as [$n, $e, $r]) {
|
||||
$ins->execute([$n, $e, password_hash('Sehr-Sicher-1234', PASSWORD_DEFAULT), $r, $dept, $now]);
|
||||
$ids[$n] = (int) $db->lastInsertId();
|
||||
}
|
||||
$db = null;
|
||||
|
||||
$failures = [];
|
||||
$total = 0;
|
||||
$check = function (bool $ok, string $what) use (&$failures, &$total): void { $total++; if (!$ok) $failures[] = $what; };
|
||||
$webProc = proc_open([PHP_BINARY, '-S', "127.0.0.1:$webPort", '-t', $tmp], [['pipe', 'r'], ['file', $work . '/web.out', 'w'], ['file', $work . '/web.out', 'a']], $wp, $tmp);
|
||||
|
||||
try {
|
||||
for ($i = 0; $i < 50; $i++) { if (@fsockopen('127.0.0.1', $webPort)) break; usleep(100000); }
|
||||
$pdo = function () use ($tmp): PDO {
|
||||
$p = new PDO('sqlite:' . $tmp . '/database/deskhub.sqlite');
|
||||
$p->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
$p->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
||||
return $p;
|
||||
};
|
||||
$api = function (string $method, string $path, ?array $json = null, ?string $bearer = null) use ($webPort): array {
|
||||
$ch = curl_init("http://127.0.0.1:$webPort$path");
|
||||
$h = ['Content-Type: application/json'];
|
||||
if ($bearer) $h[] = 'Authorization: Bearer ' . $bearer;
|
||||
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 60, CURLOPT_HTTPHEADER => $h, CURLOPT_CUSTOMREQUEST => $method]);
|
||||
if ($json !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($json));
|
||||
$b = (string) curl_exec($ch);
|
||||
$s = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
|
||||
curl_close($ch);
|
||||
return [$s, json_decode($b, true) ?? $b];
|
||||
};
|
||||
$p = $pdo();
|
||||
$nowS = date('Y-m-d H:i:s');
|
||||
$p->prepare('INSERT INTO tickets (ticket_number, subject, requester_name, requester_email, department_id, priority_id, status, source, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?)')
|
||||
->execute(['T9001', 'API-Test', 'Kunde', 'kunde@test.de', $dept, 2, 'open', 'web', $nowS, $nowS]);
|
||||
$tid = (int) $p->lastInsertId();
|
||||
$login = function (string $email) use ($api): string {
|
||||
[, $lg] = $api('POST', '/api/staff/v1/login.php', ['email' => $email, 'password' => 'Sehr-Sicher-1234', 'deviceName' => 'Test']);
|
||||
return (string) ($lg['token'] ?? '');
|
||||
};
|
||||
$admin = $login('chef@test.de');
|
||||
$agent = $login('agent@test.de');
|
||||
$check($admin !== '' && $agent !== '', 'Anmeldung von Administrator und Mitarbeiter');
|
||||
|
||||
// Cc-Empfänger
|
||||
[$s, $r] = $api('POST', '/api/staff/v1/ticket_collaborators.php', ['number' => 'T9001', 'action' => 'add', 'email' => 'cc@test.de', 'name' => 'Cc Person'], $agent);
|
||||
$check($s === 200 && count($r['collaborators']) === 1 && $r['collaborators'][0]['email'] === 'cc@test.de' && $r['collaborators'][0]['name'] === 'Cc Person', 'Cc hinzufügen');
|
||||
[$s, $r] = $api('POST', '/api/staff/v1/ticket_collaborators.php', ['number' => 'T9001', 'action' => 'add', 'email' => 'CC@test.de'], $agent);
|
||||
$check(count($r['collaborators']) === 1, 'dieselbe Adresse nicht doppelt');
|
||||
[$s, ] = $api('POST', '/api/staff/v1/ticket_collaborators.php', ['number' => 'T9001', 'action' => 'add', 'email' => 'kaputt'], $agent);
|
||||
$check($s === 422, 'ungültige Adresse wird abgelehnt');
|
||||
[$s, $r] = $api('GET', '/api/staff/v1/ticket_collaborators.php?number=T9001', null, $agent);
|
||||
$cid = (int) ($r['collaborators'][0]['id'] ?? 0);
|
||||
$check($s === 200 && $cid > 0, 'Cc-Liste abrufen');
|
||||
[$s, $r] = $api('GET', '/api/staff/v1/ticket.php?number=T9001', null, $agent);
|
||||
$check(count($r['ticket']['collaborators']) === 1 && $r['ticket']['followers'] === [] && $r['ticket']['csat'] === null, 'Ticket enthält Cc, Beobachter und (noch keine) Bewertung');
|
||||
$api('PATCH', '/api/staff/v1/ticket_update.php', ['number' => 'T9001', 'following' => true], $agent);
|
||||
[, $r] = $api('GET', '/api/staff/v1/ticket.php?number=T9001', null, $agent);
|
||||
$check(count($r['ticket']['followers']) === 1 && $r['ticket']['followers'][0]['name'] === 'Agent', 'Beobachter erscheinen am Ticket');
|
||||
[, $r] = $api('POST', '/api/staff/v1/ticket_collaborators.php', ['number' => 'T9001', 'action' => 'remove', 'id' => $cid], $agent);
|
||||
$check($r['collaborators'] === [], 'Cc entfernen');
|
||||
[$s, ] = $api('POST', '/api/staff/v1/ticket_collaborators.php', ['number' => 'GIBTS-NICHT', 'action' => 'add', 'email' => 'a@b.de'], $agent);
|
||||
$check($s === 404 || $s === 403, 'unbekanntes Ticket: ' . $s);
|
||||
|
||||
// Bewertung am Ticket und Auswertung
|
||||
$p->prepare('INSERT INTO ticket_csat (ticket_id, token, staff_id, department_id, rating, comment, sent_at, answered_at) VALUES (?,?,?,?,?,?,?,?)')
|
||||
->execute([$tid, bin2hex(random_bytes(20)), $ids['Agent'], $dept, 5, 'Top', $nowS, $nowS]);
|
||||
[, $r] = $api('GET', '/api/staff/v1/ticket.php?number=T9001', null, $agent);
|
||||
$check($r['ticket']['csat']['rating'] === 5 && $r['ticket']['csat']['comment'] === 'Top', 'Bewertung am Ticket');
|
||||
[$s, $r] = $api('GET', '/api/staff/v1/csat.php?days=7', null, $agent);
|
||||
$check($s === 200 && $r['enabled'] === false && $r['stats']['answered'] === 1 && $r['stats']['avg'] == 5 && count($r['stats']['dist']) === 5 && $r['stats']['byStaff'][0]['name'] === 'Agent', 'Auswertung über die Schnittstelle');
|
||||
|
||||
// Vorlagen
|
||||
[$s, $r] = $api('GET', '/api/staff/v1/ticket_templates.php', null, $agent);
|
||||
$check($s === 200 && $r['templates'] === [], 'noch keine Vorlagen');
|
||||
$p->prepare("INSERT INTO ticket_templates (name, subject, body, department_id, is_public, is_active, sort_order, created_at) VALUES ('Neuer Mitarbeiter','Neu: ','Name:',?,1,1,1,?)")->execute([$dept, $nowS]);
|
||||
$p->prepare("INSERT INTO ticket_templates (name, subject, is_public, is_active, sort_order, created_at) VALUES ('Inaktiv','x',1,0,2,?)")->execute([$nowS]);
|
||||
[, $r] = $api('GET', '/api/staff/v1/ticket_templates.php', null, $agent);
|
||||
$check(count($r['templates']) === 1 && $r['templates'][0]['name'] === 'Neuer Mitarbeiter' && $r['templates'][0]['departmentId'] === $dept && $r['templates'][0]['body'] === 'Name:', 'Vorlagen für die Apps (nur aktive)');
|
||||
[$s, ] = $api('GET', '/api/staff/v1/ticket_templates.php');
|
||||
$check($s === 401, 'Vorlagen nur mit Anmeldung');
|
||||
|
||||
// Statusseite
|
||||
[$s, $r] = $api('GET', '/api/staff/v1/status.php', null, $agent);
|
||||
$check($s === 200 && $r['canManage'] === false && $r['status']['enabled'] === false && $r['status']['components'] === [], 'Statusseite lesen (Mitarbeiter)');
|
||||
[$s, ] = $api('POST', '/api/staff/v1/status.php', ['action' => 'set_enabled', 'enabled' => true], $agent);
|
||||
$check($s === 403, 'Mitarbeiter dürfen die Statusseite nicht ändern');
|
||||
$p->prepare("INSERT INTO status_components (name, status, sort_order, created_at) VALUES ('Webseite','operational',1,?)")->execute([$nowS]);
|
||||
$comp = (int) $p->lastInsertId();
|
||||
[$s, $r] = $api('POST', '/api/staff/v1/status.php', ['action' => 'set_enabled', 'enabled' => true], $admin);
|
||||
$check($s === 200 && $r['status']['enabled'] === true && $r['canManage'] === true, 'Administrator schaltet die Statusseite ein');
|
||||
[$s, $r] = $api('POST', '/api/staff/v1/status.php', ['action' => 'create_incident', 'title' => 'Portal langsam', 'body' => 'Wir prüfen das.', 'status' => 'investigating', 'componentId' => $comp, 'componentState' => 'degraded'], $admin);
|
||||
$check($s === 200 && $r['status']['overall']['key'] === 'degraded' && count($r['status']['incidents']) === 1 && $r['status']['components'][0]['status'] === 'degraded', 'Meldung veröffentlichen');
|
||||
$inc = (int) ($r['status']['incidents'][0]['id'] ?? 0);
|
||||
[$s, ] = $api('POST', '/api/staff/v1/status.php', ['action' => 'create_incident', 'title' => ''], $admin);
|
||||
$check($s === 422, 'Meldung ohne Titel');
|
||||
[$s, $r] = $api('POST', '/api/staff/v1/status.php', ['action' => 'set_component', 'id' => $comp, 'status' => 'outage'], $admin);
|
||||
$check($r['status']['overall']['key'] === 'outage', 'Dienst-Zustand ändern');
|
||||
[$s, ] = $api('POST', '/api/staff/v1/status.php', ['action' => 'set_component', 'id' => $comp, 'status' => 'kaputt'], $admin);
|
||||
$check($s === 422, 'ungültiger Zustand');
|
||||
[$s, $r] = $api('POST', '/api/staff/v1/status.php', ['action' => 'update_incident', 'id' => $inc, 'status' => 'resolved', 'body' => 'Behoben.'], $admin);
|
||||
$check($r['status']['overall']['key'] === 'operational' && $r['status']['incidents'][0]['status'] === 'resolved' && count($r['status']['incidents'][0]['updates']) === 2, 'Meldung beheben setzt den Dienst zurück');
|
||||
[, $r] = $api('GET', '/api/staff/v1/status.php', null, $agent);
|
||||
$check($r['status']['enabled'] === true && $r['status']['title'] === 'Systemstatus', 'Mitarbeiter sehen den Stand');
|
||||
} finally {
|
||||
if (isset($webProc) && is_resource($webProc)) { proc_terminate($webProc); proc_close($webProc); }
|
||||
}
|
||||
|
||||
usleep(300000);
|
||||
$rm = function (string $d) use (&$rm) {
|
||||
foreach (scandir($d) ?: [] as $e) { if ($e === '.' || $e === '..') continue; is_dir("$d/$e") && !is_link("$d/$e") ? $rm("$d/$e") : @unlink("$d/$e"); }
|
||||
@rmdir($d);
|
||||
};
|
||||
if (str_starts_with($work, sys_get_temp_dir() . '/deskhub-apicss-')) $rm($work);
|
||||
|
||||
if ($failures) {
|
||||
fwrite(STDERR, "Fehlgeschlagen:\n - " . implode("\n - ", $failures) . "\n");
|
||||
exit(1);
|
||||
}
|
||||
echo "API CSAT, Status und Vorlagen checks passed ({$total} Prüfungen).\n";
|
||||
@@ -136,6 +136,21 @@ $t1 = theme_catalog_cached();
|
||||
$n1 = count($requests);
|
||||
$t2 = theme_catalog_cached();
|
||||
$check($t1['ok'] && $t2['ok'] && count($requests) === $n1 && $n1 === $before + 1, 'Theme-Katalog wird beim ersten Aufruf geladen und danach aus dem Zwischenspeicher gezeigt');
|
||||
$tc = json_decode((string) get_setting('theme_catalog_cache', ''), true);
|
||||
$tc['at'] = time() - 400;
|
||||
set_setting('theme_catalog_cache', json_encode($tc));
|
||||
$n3 = count($requests);
|
||||
theme_catalog_cached();
|
||||
$check(count($requests) === $n3 + 1, 'Theme-Katalog wird nach über 5 Minuten von selbst neu geladen');
|
||||
$tc = json_decode((string) get_setting('theme_catalog_cache', ''), true);
|
||||
$tc['at'] = time() - 400;
|
||||
set_setting('theme_catalog_cache', json_encode($tc));
|
||||
$saved = $GLOBALS['deskhub_http_get'];
|
||||
$GLOBALS['deskhub_http_get'] = fn(string $url, int $max): array => [0, null, 'Verbindung fehlgeschlagen'];
|
||||
$tStale = theme_catalog_cached();
|
||||
$check($tStale['ok'] && !empty($tStale['stale']) && is_array($tStale['themes']), 'Theme-Katalog: bei Serverausfall bleibt der letzte Stand sichtbar');
|
||||
$GLOBALS['deskhub_http_get'] = $saved;
|
||||
set_setting('theme_catalog_fail_at', '0');
|
||||
$check(plugin_updates_available($pdo = get_db()) === [], 'ohne installierte Plugins gibt es keine Update-Hinweise');
|
||||
|
||||
catalog_refresh_background();
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Prüft Zufriedenheitsabfrage (CSAT), Statusseite und Ticket-Vorlagen: Datenbank, Versand, Bewertung, Auswertung,
|
||||
* Störungsmeldungen, Vorlagen für Kunden und Mitarbeiter sowie die Seiten und Schnittstellen dazu.
|
||||
*/
|
||||
|
||||
putenv('DESKHUB_TESTING=1');
|
||||
putenv('DESKHUB_CATALOG_DEFAULTS=' . __DIR__ . '/fixtures/no-catalog.php');
|
||||
putenv('DESKHUB_SQLITE_PATH=:memory:');
|
||||
require_once dirname(__DIR__) . '/includes/bootstrap.php';
|
||||
|
||||
$failures = [];
|
||||
$check = function (bool $ok, string $what) use (&$failures): void {
|
||||
if (!$ok) $failures[] = $what;
|
||||
};
|
||||
$pdo = get_db();
|
||||
$now = db_now();
|
||||
|
||||
// ── Zufriedenheitsabfrage ───────────────────────────────────────────────────
|
||||
$check(!csat_enabled() && csat_delay_minutes() === 30, 'Standard: ausgeschaltet, 30 Minuten');
|
||||
$mk = function (string $status, string $email, ?string $closedAt, string $source = 'web', int $spam = 0) use ($pdo, $now): int {
|
||||
static $n = 5000;
|
||||
$n++;
|
||||
$pdo->prepare('INSERT INTO tickets (ticket_number, subject, requester_name, requester_email, department_id, priority_id, status, source, is_spam, closed_at, resolved_at, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)')
|
||||
->execute(["CS-{$n}", "Betreff {$n}", 'Erika Muster', $email, 1, 2, $status, $source, $spam, $status === 'closed' ? $closedAt : null, $status === 'resolved' ? $closedAt : null, $now, $now]);
|
||||
return (int) $pdo->lastInsertId();
|
||||
};
|
||||
$old = date('Y-m-d H:i:s', time() - 3600);
|
||||
$t1 = $mk('closed', 'erika@kunde.example', $old);
|
||||
$check(csat_send_due($pdo) === 0, 'ausgeschaltet: nichts wird gesendet');
|
||||
csat_set_enabled(true, 30);
|
||||
$check(csat_enabled() && get_setting('csat_since', '') !== '', 'Einschalten merkt sich den Zeitpunkt');
|
||||
$check(csat_send_due($pdo) === 0, 'Ticket aus der Zeit vor dem Einschalten bekommt keine Abfrage');
|
||||
set_setting('csat_since', date('Y-m-d H:i:s', time() - 86400));
|
||||
$tNew = $mk('closed', 'neu@kunde.example', date('Y-m-d H:i:s', time() - 60));
|
||||
$tSpam = $mk('closed', 'spam@kunde.example', $old, 'web', 1);
|
||||
$tChat = $mk('closed', 'chat@kunde.example', $old, 'chat');
|
||||
$tOpen = $mk('open', 'offen@kunde.example', null);
|
||||
$tNoMail = $mk('closed', '', $old);
|
||||
$tBad = $mk('closed', 'kein-email', $old);
|
||||
$tRes = $mk('resolved', 'geloest@kunde.example', $old);
|
||||
$sent = csat_send_due($pdo);
|
||||
$rows = $pdo->query('SELECT ticket_id FROM ticket_csat ORDER BY ticket_id')->fetchAll(PDO::FETCH_COLUMN);
|
||||
$check($sent === 2 && array_map('intval', $rows) === [$t1, $tRes], 'gesendet nur für geschlossene/gelöste Tickets mit Adresse, nach der Wartezeit, ohne Spam und Chat: ' . json_encode($rows));
|
||||
$check(csat_send_due($pdo) === 0, 'pro Ticket nur eine Abfrage');
|
||||
$pdo->prepare('UPDATE tickets SET updated_at = ?, closed_at = ? WHERE id = ?')->execute([$old, $old, $tNew]);
|
||||
$check(csat_send_due($pdo) === 1, 'nach Ablauf der Wartezeit kommt auch das neuere Ticket dran');
|
||||
|
||||
$row = csat_for_ticket($pdo, $t1);
|
||||
$token = (string) $row['token'];
|
||||
$check(preg_match('/^[a-f0-9]{40}$/', $token) === 1 && csat_find_by_token($pdo, $token)['ticket_number'] === 'CS-5001', 'Token und Suche');
|
||||
$check(csat_find_by_token($pdo, 'zzz') === null && csat_find_by_token($pdo, str_repeat('a', 40)) === null, 'ungültiger Token');
|
||||
$check(str_contains(csat_mail_html($pdo->query("SELECT * FROM tickets WHERE id = {$t1}")->fetch(), $token), 'csat.php?t=' . $token . '&r=5') || str_contains(csat_mail_html($pdo->query("SELECT * FROM tickets WHERE id = {$t1}")->fetch(), $token), 'r=5'), 'Mail enthält die fünf Sterne-Links');
|
||||
$check(!csat_record($pdo, $token, 0, '')['ok'] && !csat_record($pdo, $token, 6, '')['ok'], 'Bewertung muss 1 bis 5 sein');
|
||||
$check(csat_record($pdo, $token, 4, ' Schnell und freundlich ')['ok'], 'Bewertung wird gespeichert');
|
||||
$check(!csat_record($pdo, $token, 1, '')['ok'], 'zweite Bewertung wird abgelehnt');
|
||||
$row = csat_for_ticket($pdo, $t1);
|
||||
$check((int) $row['rating'] === 4 && $row['comment'] === 'Schnell und freundlich' && $row['answered_at'] !== null, 'Bewertung und Kommentar stehen in der Datenbank');
|
||||
$check(!csat_record($pdo, 'x', 3, '')['ok'], 'unbekannter Link');
|
||||
$tok2 = (string) csat_for_ticket($pdo, $tRes)['token'];
|
||||
csat_record($pdo, $tok2, 2, '');
|
||||
$st = csat_stats($pdo, date('Y-m-d 00:00:00', time() - 86400), date('Y-m-d 23:59:59'), null);
|
||||
$check($st['sent'] === 3 && $st['answered'] === 2 && $st['avg'] === 3.0 && $st['rate'] === 66.7 && $st['dist'][4] === 1 && $st['dist'][2] === 1 && count($st['recent']) === 2, 'Auswertung: ' . json_encode($st));
|
||||
$check(csat_stats($pdo, '2000-01-01 00:00:00', '2000-12-31 23:59:59', null)['sent'] === 0 && csat_stats($pdo, $now, $now, [])['sent'] === 0, 'leere Zeiträume und Abteilungen');
|
||||
$check(csat_stars(4) === '★★★★☆' && csat_stars(9) === '★★★★★', 'Sterne-Text');
|
||||
csat_set_enabled(false, 30);
|
||||
$check(!csat_enabled(), 'Ausschalten');
|
||||
|
||||
// ── Statusseite ─────────────────────────────────────────────────────────────
|
||||
$check(!status_page_enabled() && status_overall($pdo)['key'] === 'operational', 'Statusseite standardmäßig aus, ohne Dienste alles in Ordnung');
|
||||
$web = status_save_component($pdo, null, 'Webseite', 'Kundenportal', 10);
|
||||
$mail = status_save_component($pdo, null, 'E-Mail', '', 20);
|
||||
$check($web !== null && $mail !== null && status_save_component($pdo, null, ' ', '', 0) === null, 'Dienste anlegen (leerer Name abgelehnt)');
|
||||
$check(!status_set_component($pdo, $web, 'kaputt') && status_set_component($pdo, $web, 'degraded') && status_overall($pdo)['key'] === 'degraded', 'Zustand setzen, ungültiger Zustand abgelehnt');
|
||||
status_set_component($pdo, $mail, 'outage');
|
||||
$check(status_overall($pdo)['key'] === 'outage' && status_overall($pdo)['label'] === 'Ausfall', 'schlechtester Zustand zählt');
|
||||
status_set_component($pdo, $mail, 'maintenance');
|
||||
status_set_component($pdo, $web, 'operational');
|
||||
$check(status_overall($pdo)['key'] === 'maintenance', 'Wartung');
|
||||
status_set_component($pdo, $mail, 'operational');
|
||||
$r = status_create_incident($pdo, ' ', 'x', 'investigating', null, null, 1);
|
||||
$check(!$r['ok'], 'Meldung ohne Titel wird abgelehnt');
|
||||
$r = status_create_incident($pdo, 'E-Mail verzögert', 'Wir untersuchen das.', 'investigating', $mail, 'degraded', 1);
|
||||
$check($r['ok'] && status_overall($pdo)['key'] === 'degraded', 'Meldung setzt den Dienst auf „eingeschränkt“');
|
||||
$inc = (int) $r['id'];
|
||||
$check(count(status_incidents($pdo, 14, true)) === 1 && status_incidents($pdo)[0]['updates'][0]['body'] === 'Wir untersuchen das.', 'offene Meldung mit erstem Update');
|
||||
$other = status_create_incident($pdo, 'Zweite Störung', '', 'identified', $mail, 'outage', 1)['id'];
|
||||
$check(status_update_incident($pdo, $inc, 'resolved', 'Behoben.', 1)['ok'] && status_overall($pdo)['key'] === 'outage', 'Dienst bleibt gestört, solange eine andere Meldung offen ist');
|
||||
$check(status_update_incident($pdo, $other, 'resolved', 'Auch behoben.', 1)['ok'] && status_overall($pdo)['key'] === 'operational', 'nach der letzten Meldung wieder „In Betrieb“');
|
||||
$check(!status_update_incident($pdo, 999, 'resolved', '', 1)['ok'] && !status_update_incident($pdo, $inc, 'quatsch', '', 1)['ok'], 'unbekannte Meldung / Stufe');
|
||||
$check(status_incidents($pdo, 14, true) === [] && count(status_incidents($pdo, 14)) === 2 && status_incidents($pdo)[0]['resolved_at'] !== null, 'behobene Meldungen bleiben 14 Tage sichtbar');
|
||||
$pdo->prepare('UPDATE status_incidents SET created_at = ? WHERE id = ?')->execute([date('Y-m-d H:i:s', time() - 30 * 86400), $inc]);
|
||||
$check(count(status_incidents($pdo, 14)) === 1, 'ältere behobene Meldungen verschwinden');
|
||||
$sum = status_summary($pdo);
|
||||
$check($sum['enabled'] === false && count($sum['components']) === 2 && $sum['overall']['key'] === 'operational' && count($sum['componentStates']) === 4 && str_ends_with($sum['url'], '/status.php'), 'Zusammenfassung für die Apps');
|
||||
status_delete_incident($pdo, $inc);
|
||||
status_delete_component($pdo, $mail);
|
||||
$check(count(status_components($pdo)) === 1 && (int) $pdo->query("SELECT COUNT(*) FROM status_incident_updates WHERE incident_id = {$inc}")->fetchColumn() === 0, 'Löschen räumt die Updates mit auf');
|
||||
|
||||
// ── Ticket-Vorlagen ─────────────────────────────────────────────────────────
|
||||
$check(!ticket_template_save($pdo, null, ['name' => ' '])['ok'], 'Vorlage ohne Namen wird abgelehnt');
|
||||
$a = ticket_template_save($pdo, null, ['name' => 'Neuer Mitarbeiter', 'subject' => 'Neuer Mitarbeiter: ', 'body' => "Name:\nStartdatum:", 'department_id' => '1', 'priority_id' => '2', 'is_public' => '1', 'sort_order' => '10']);
|
||||
$b = ticket_template_save($pdo, null, ['name' => 'Interne Vorlage', 'subject' => 'Intern', 'department_id' => '9999', 'is_public' => '', 'sort_order' => '20']);
|
||||
$check($a['ok'] && $b['ok'], 'Vorlagen anlegen');
|
||||
$check(count(ticket_templates_list($pdo)) === 2 && count(ticket_templates_list($pdo, true)) === 1 && ticket_templates_list($pdo, true)[0]['name'] === 'Neuer Mitarbeiter', 'Kunden sehen nur öffentliche Vorlagen');
|
||||
$tb = ticket_template_get($pdo, $b['id']);
|
||||
$check($tb['department_id'] === null && ticket_template_get($pdo, $b['id'], true) === null && ticket_template_get($pdo, 9999) === null, 'unbekannte Abteilung wird verworfen, nicht öffentliche Vorlage bleibt Kunden verborgen');
|
||||
$check(ticket_template_save($pdo, $a['id'], ['name' => 'Neuer Mitarbeiter 2', 'is_public' => '1', 'is_active' => '0'])['ok'] && ticket_template_get($pdo, $a['id']) === null && count(ticket_templates_list($pdo, false, true)) === 2, 'Vorlage deaktivieren');
|
||||
ticket_template_save($pdo, $a['id'], ['name' => 'Neuer Mitarbeiter', 'subject' => 'Neuer Mitarbeiter: ', 'body' => "Name:\nStartdatum:", 'department_id' => '1', 'is_public' => '1', 'is_active' => '1']);
|
||||
$p = ticket_template_payload(ticket_template_get($pdo, $a['id']));
|
||||
$check($p['name'] === 'Neuer Mitarbeiter' && $p['departmentId'] === 1 && $p['priorityId'] === null && $p['isPublic'] === true && $p['body'] === "Name:\nStartdatum:", 'Vorlage als Feldliste');
|
||||
ticket_template_delete($pdo, $b['id']);
|
||||
$check(count(ticket_templates_list($pdo)) === 1, 'Vorlage löschen');
|
||||
|
||||
// ── Seiten ──────────────────────────────────────────────────────────────────
|
||||
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||
$render = function (string $file, array $get = [], array $post = [], string $method = 'GET') {
|
||||
$_GET = $get;
|
||||
$_POST = $post;
|
||||
$_SERVER['REQUEST_METHOD'] = $method;
|
||||
ob_start();
|
||||
try {
|
||||
include dirname(__DIR__) . '/' . $file;
|
||||
} finally {
|
||||
$html = (string) ob_get_clean();
|
||||
}
|
||||
return $html;
|
||||
};
|
||||
// Bewertungsseite
|
||||
csat_set_enabled(true, 30);
|
||||
$pdo->prepare('UPDATE ticket_csat SET rating = NULL, comment = NULL, answered_at = NULL WHERE ticket_id = ?')->execute([$t1]);
|
||||
$html = $render('csat.php', ['t' => $token, 'r' => '5']);
|
||||
$check(str_contains($html, 'Wie zufrieden waren Sie?') && str_contains($html, 'CS-5001') && str_contains($html, 'value="5" style="position:absolute;opacity:0;width:0;height:0" checked'), 'Bewertungsseite zeigt das Ticket mit vorgewähltem Stern');
|
||||
$check((int) csat_for_ticket($pdo, $t1)['answered_at'] === 0 && csat_for_ticket($pdo, $t1)['answered_at'] === null, 'Aufrufen des Links bewertet noch nichts');
|
||||
$html = $render('csat.php', [], ['t' => $token, 'rating' => '3', 'comment' => 'ok'], 'POST');
|
||||
$check(str_contains($html, 'Vielen Dank') && (int) csat_for_ticket($pdo, $t1)['rating'] === 3, 'Absenden speichert die Bewertung');
|
||||
$check(str_contains($render('csat.php', ['t' => $token]), 'Bereits bewertet'), 'zweiter Aufruf: bereits bewertet');
|
||||
$check(str_contains($render('csat.php', ['t' => 'unsinn']), 'Link nicht gültig'), 'ungültiger Link');
|
||||
// Statusseite
|
||||
set_setting('status_page_enabled', '1');
|
||||
status_set_component($pdo, $web, 'degraded');
|
||||
status_create_incident($pdo, 'Portal langsam', 'Wir arbeiten daran.', 'identified', $web, null, 1);
|
||||
$html = $render('status.php');
|
||||
$check(str_contains($html, 'Systemstatus') && str_contains($html, 'Teilweise eingeschränkt') && str_contains($html, 'Webseite') && str_contains($html, 'Portal langsam') && str_contains($html, 'Wir arbeiten daran.') && str_contains($html, 'Ursache gefunden'), 'Statusseite zeigt Dienste und Meldung');
|
||||
$check(str_contains($html, 'href="/status.php"'), 'Link „Status“ im Kundenbereich');
|
||||
// Ticket-Formular mit Vorlage
|
||||
$tpl = (int) $a['id'];
|
||||
$html = $render('ticket_new.php', ['template' => (string) $tpl]);
|
||||
$check(str_contains($html, 'value="Neuer Mitarbeiter:"') && str_contains($html, "Name:\nStartdatum:") && str_contains($html, 'ticket_new.php?template=' . $tpl) && str_contains($html, 'Ohne Vorlage'), 'Kundenformular wird von der Vorlage vorbelegt');
|
||||
$html = $render('ticket_new.php');
|
||||
$check(!str_contains($html, 'value="Neuer Mitarbeiter:"') && str_contains($html, 'Womit können wir helfen'), 'ohne Vorlage bleibt das Formular leer, Vorlagen werden angeboten');
|
||||
$html = $render('ticket_new.php', ['template' => '99999']);
|
||||
$check(!str_contains($html, 'Ohne Vorlage'), 'unbekannte Vorlage wird ignoriert');
|
||||
|
||||
if ($failures) {
|
||||
fwrite(STDERR, "Fehlgeschlagen:\n - " . implode("\n - ", $failures) . "\n");
|
||||
exit(1);
|
||||
}
|
||||
echo 'CSAT, Status und Vorlagen checks passed.' . "\n";
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Prüft die Fehler-Mail bei schweren PHP-Fehlern: Empfänger, Inhalt (ohne Parameter und Serverpfade), Drosselung, Ausschalter
|
||||
* und einmal echt: Eine Seite mit einem schweren Fehler löst in einem eigenen Prozess die Meldung aus.
|
||||
*/
|
||||
|
||||
$db = sys_get_temp_dir() . '/deskhub-ea-' . bin2hex(random_bytes(4)) . '.sqlite';
|
||||
putenv('DESKHUB_TESTING=1');
|
||||
putenv('DESKHUB_SQLITE_PATH=' . $db);
|
||||
require_once dirname(__DIR__) . '/includes/bootstrap.php';
|
||||
|
||||
$failures = [];
|
||||
$check = function (bool $ok, string $what) use (&$failures): void {
|
||||
if (!$ok) $failures[] = $what;
|
||||
};
|
||||
$pdo = get_db();
|
||||
$mails = [];
|
||||
$send = function (string $to, string $subject, string $html) use (&$mails): bool {
|
||||
$mails[] = compact('to', 'subject', 'html');
|
||||
return true;
|
||||
};
|
||||
$root = str_replace('\\', '/', dirname(__DIR__));
|
||||
$err = ['type' => E_ERROR, 'message' => "Uncaught Error: Call to undefined function foo() in {$root}/staff/x.php:12", 'file' => $root . '/staff/x.php', 'line' => 12];
|
||||
|
||||
// Empfänger
|
||||
$pdo->prepare("INSERT INTO staff (name, email, password_hash, role, is_active, created_at) VALUES ('Chef','chef@firma.example','x','admin',1,?)")->execute([db_now()]);
|
||||
set_setting('admin_email', '');
|
||||
$check(in_array('chef@firma.example', error_alert_recipients($pdo), true), 'ohne eigene Adresse gehen die Mails an die Administratoren');
|
||||
set_setting('admin_email', 'it@firma.example');
|
||||
$check(error_alert_recipients($pdo) === ['it@firma.example'], 'die Admin-Adresse der Seite hat Vorrang');
|
||||
set_setting('error_alert_email', 'a@firma.example, b@firma.example; kaputt');
|
||||
$check(error_alert_recipients($pdo) === ['a@firma.example', 'b@firma.example'], 'eigene Empfänger (mehrere, ungültige werden übersprungen)');
|
||||
|
||||
// Inhalt
|
||||
$_SERVER['REQUEST_URI'] = '/staff/tickets.php?token=GEHEIM&x=1';
|
||||
[$subject, $html] = error_alert_compose($err);
|
||||
$check(str_contains($html, 'staff/x.php') && str_contains($html, 'Zeile 12') && str_contains($html, '/staff/tickets.php'), 'Mail nennt Datei, Zeile und Seite');
|
||||
$check(!str_contains($html, 'GEHEIM') && !str_contains($html, $root), 'Mail enthält weder Adress-Parameter noch den Serverpfad');
|
||||
[, $evil] = error_alert_compose(['message' => '<script>alert(1)</script>', 'file' => 'x', 'line' => 1]);
|
||||
$check(!str_contains($evil, '<script>'), 'Fehlertext wird maskiert');
|
||||
|
||||
// Senden und Drosselung
|
||||
$n = error_alert_dispatch($pdo, $err, $send);
|
||||
$check($n === 2 && count($mails) === 2, 'erste Meldung geht an beide Empfänger');
|
||||
$check(error_alert_dispatch($pdo, $err, $send) === 0 && count($mails) === 2, 'dieselbe Meldung wird innerhalb einer Stunde nicht noch einmal gesendet');
|
||||
$check(error_alert_dispatch($pdo, ['line' => 99, 'message' => 'Anderer Fehler'] + $err, $send) === 2, 'eine andere Meldung geht durch');
|
||||
$same = error_alert_dispatch($pdo, ['message' => "Uncaught Error: Call to undefined function foo() in {$root}/staff/x.php:13", 'line' => 13] + $err, $send);
|
||||
$check($same === 0, 'dieselbe Meldung mit anderer Zeilennummer im Text zählt als gleich');
|
||||
for ($i = 0; $i < 20; $i++) error_alert_dispatch($pdo, ['message' => "Fehler Nummer-{$i}x", 'file' => "/f{$i}x.php"] + $err, $send);
|
||||
$check(count($mails) <= 2 * 10 + 0 && count(array_unique(array_map(fn($m) => $m['subject'] . $m['html'], $mails))) <= 10, 'höchstens 10 verschiedene Fehler-Mails pro Stunde');
|
||||
|
||||
// Ausschalter
|
||||
set_setting('error_alert_log', '[]');
|
||||
set_setting('error_alert', '0');
|
||||
$before = count($mails);
|
||||
$check(error_alert_dispatch($pdo, $err, $send) === 0 && count($mails) === $before, 'ausgeschaltet wird nichts gesendet');
|
||||
set_setting('error_alert', '1');
|
||||
|
||||
// Nicht schwere Fehler und Hinweise lösen nichts aus
|
||||
$check(!in_array(E_WARNING, ERROR_ALERT_FATAL, true) && !in_array(E_NOTICE, ERROR_ALERT_FATAL, true) && !in_array(E_DEPRECATED, ERROR_ALERT_FATAL, true), 'Warnungen und Hinweise zählen nicht');
|
||||
|
||||
// Echt: eigener Prozess mit schwerem Fehler
|
||||
set_setting('error_alert_log', '[]');
|
||||
$script = sys_get_temp_dir() . '/ea-page-' . bin2hex(random_bytes(3)) . '.php';
|
||||
file_put_contents($script, "<?php\nputenv('DESKHUB_TESTING=1');\nputenv('DESKHUB_SQLITE_PATH=' . " . var_export($db, true) . ");\nrequire " . var_export(dirname(__DIR__) . '/includes/bootstrap.php', true) . ";\nnoch_nie_definiert_im_system();\n");
|
||||
exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($script) . ' 2>&1', $out, $code);
|
||||
@unlink($script);
|
||||
$log = json_decode((string) $pdo->query("SELECT setting_value FROM settings WHERE setting_key = 'error_alert_log'")->fetchColumn(), true);
|
||||
$check(is_array($log) && count($log) === 1, 'echter schwerer Fehler in einer Seite löst die Meldung aus (Prozess-Ende ' . $code . ')');
|
||||
// Seite ohne Fehler: kein Eintrag
|
||||
set_setting('error_alert_log', '[]');
|
||||
$script = sys_get_temp_dir() . '/ea-ok-' . bin2hex(random_bytes(3)) . '.php';
|
||||
file_put_contents($script, "<?php\nputenv('DESKHUB_TESTING=1');\nputenv('DESKHUB_SQLITE_PATH=' . " . var_export($db, true) . ");\nrequire " . var_export(dirname(__DIR__) . '/includes/bootstrap.php', true) . ";\ntrigger_error('nur eine Warnung', E_USER_WARNING);\n");
|
||||
exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($script) . ' 2>&1', $out2, $code2);
|
||||
@unlink($script);
|
||||
$log = json_decode((string) $pdo->query("SELECT setting_value FROM settings WHERE setting_key = 'error_alert_log'")->fetchColumn(), true);
|
||||
$check(($log === [] || $log === null), 'eine Warnung löst keine Meldung aus');
|
||||
|
||||
@unlink($db);
|
||||
if ($failures) {
|
||||
fwrite(STDERR, "Fehlgeschlagen:\n - " . implode("\n - ", $failures) . "\n");
|
||||
exit(1);
|
||||
}
|
||||
echo 'Error alert checks passed.' . "\n";
|
||||
Reference in New Issue
Block a user