Upload via GUI (452 Dateien)
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Gelöste und geschlossene Tickets gelten gleich als erledigt: Die SLA-Uhr läuft nicht weiter, es gibt kein „überfällig“, keine SLA-Meldung,
|
||||
* keine Eskalation, keine Erinnerung und keine Wiedervorlage-Benachrichtigung mehr.
|
||||
*/
|
||||
|
||||
putenv('DESKHUB_TESTING=1');
|
||||
putenv('DESKHUB_SQLITE_PATH=:memory:');
|
||||
require_once dirname(__DIR__) . '/includes/bootstrap.php';
|
||||
if (!defined('DESKHUB_CRON_RUNNER')) define('DESKHUB_CRON_RUNNER', true);
|
||||
require_once dirname(__DIR__) . '/cron/check_sla.php';
|
||||
|
||||
$failures = [];
|
||||
$total = 0;
|
||||
$check = function (bool $ok, string $what) use (&$failures, &$total): void {
|
||||
$total++;
|
||||
if (!$ok) $failures[] = $what;
|
||||
};
|
||||
$pdo = get_db();
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$ago = fn(int $h) => date('Y-m-d H:i:s', time() - $h * 3600);
|
||||
$dept = (int) $pdo->query('SELECT id FROM departments ORDER BY id LIMIT 1')->fetchColumn();
|
||||
$prio = (int) $pdo->query('SELECT id FROM priorities ORDER BY id LIMIT 1')->fetchColumn();
|
||||
$pdo->exec('DELETE FROM staff');
|
||||
$pdo->prepare('INSERT INTO staff (name, email, password_hash, role, department_id, is_active, created_at) VALUES (?,?,?,?,?,1,?)')->execute(['Agent', 'agent@test.de', 'x', 'agent', $dept, $now]);
|
||||
$agent = (int) $pdo->lastInsertId();
|
||||
|
||||
$mk = function (string $n, string $status, array $o = []) use ($pdo, $dept, $prio, $now, $agent): int {
|
||||
$pdo->prepare('INSERT INTO tickets (ticket_number, subject, requester_name, requester_email, department_id, priority_id, status, source, assigned_staff_id, resolution_due_at, first_response_due_at, is_overdue, snoozed_until, snoozed_by, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)')
|
||||
->execute([$n, 'S ' . $n, 'K', 'k@example.org', $dept, $prio, $status, 'web', $agent, $o['due'] ?? null, $o['rdue'] ?? null, $o['overdue'] ?? 0, $o['snooze'] ?? null, $o['snooze'] ?? null ? $agent : null, $o['created'] ?? $now, $o['updated'] ?? $now]);
|
||||
return (int) $pdo->lastInsertId();
|
||||
};
|
||||
$row = fn(int $id) => $pdo->query("SELECT * FROM tickets WHERE id = {$id}")->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
// 1. Erledigen löscht „überfällig“ und die Wiedervorlage, für gelöst und geschlossen gleich
|
||||
foreach (['resolved', 'closed'] as $st) {
|
||||
$t = $mk('D-' . $st, 'open', ['due' => $ago(5), 'overdue' => 1, 'snooze' => $ago(-5), 'created' => $ago(60)]);
|
||||
$pdo->prepare('UPDATE tickets SET status = ? WHERE id = ?')->execute([$st, $t]);
|
||||
ticket_sla_status_transition($pdo, $t, 'open', $st);
|
||||
$r = $row($t);
|
||||
$check((int) $r['is_overdue'] === 0 && $r['snoozed_until'] === null, "offen → {$st}: nicht mehr überfällig, Wiedervorlage entfällt");
|
||||
}
|
||||
// Ein offenes Ticket bleibt unberührt
|
||||
$tOpen = $mk('D-open', 'open', ['due' => $ago(5), 'overdue' => 1, 'created' => $ago(60)]);
|
||||
ticket_sla_status_transition($pdo, $tOpen, 'open', 'answered');
|
||||
$check((int) $row($tOpen)['is_overdue'] === 1, 'offen → beantwortet: bleibt überfällig');
|
||||
// gelöst → geschlossen: nichts Neues
|
||||
$tc = $mk('D-rc', 'closed', ['due' => $ago(5), 'created' => $ago(60)]);
|
||||
ticket_sla_status_transition($pdo, $tc, 'resolved', 'closed');
|
||||
$check((int) $row($tc)['is_overdue'] === 0, 'gelöst → geschlossen: weiter nicht überfällig');
|
||||
|
||||
// 2. SLA-Prüfung fasst erledigte Tickets nicht an und meldet nichts
|
||||
$a = $mk('S-closed', 'closed', ['due' => $ago(9), 'rdue' => $ago(20), 'created' => $ago(80)]);
|
||||
$b = $mk('S-resolved', 'resolved', ['due' => $ago(9), 'rdue' => $ago(20), 'created' => $ago(80)]);
|
||||
$c = $mk('S-flag', 'closed', ['due' => $ago(9), 'overdue' => 1, 'created' => $ago(80)]);
|
||||
$res = run_check_sla($pdo);
|
||||
$check($row($a)['is_overdue'] == 0 && $row($b)['is_overdue'] == 0, 'SLA-Prüfung: geschlossene und gelöste Tickets werden nicht überfällig');
|
||||
$check((int) $row($c)['is_overdue'] === 0, 'SLA-Prüfung: ein übrig gebliebenes „überfällig“ bei einem erledigten Ticket wird entfernt');
|
||||
$check($res['newlyOverdue'] === 0, 'SLA-Prüfung: keine neue Überschreitung gemeldet');
|
||||
$hist = (int) $pdo->query("SELECT COUNT(*) FROM ticket_sla_history WHERE ticket_id IN ({$a},{$b}) AND event_type = 'overdue'")->fetchColumn();
|
||||
$check($hist === 0, 'SLA-Verlauf: für erledigte Tickets entsteht kein „überschritten“');
|
||||
|
||||
// 3. Eskalationsregeln erfassen keine erledigten Tickets
|
||||
require_once dirname(__DIR__) . '/cron/check_escalations.php';
|
||||
$pdo->prepare("INSERT INTO escalation_rules (name,is_active,trigger_type,trigger_hours,action_notify_admin,action_notify_staff,sort_order,created_at) VALUES (?,?,?,?,?,?,?,?)")
|
||||
->execute(['Alt', 1, 'overdue', 1, 1, 1, 1, $now]);
|
||||
$eo = $mk('E-open', 'open', ['due' => $ago(5), 'overdue' => 1, 'created' => $ago(50)]);
|
||||
$ec = $mk('E-closed', 'closed', ['due' => $ago(5), 'overdue' => 1, 'created' => $ago(50)]);
|
||||
$r = run_check_escalations($pdo);
|
||||
$check((int) $pdo->query("SELECT COUNT(*) FROM escalation_log WHERE ticket_id = {$eo}")->fetchColumn() === 1, 'Eskalation: die Regel greift bei einem offenen Ticket (Gegenprobe)');
|
||||
$hit = (int) $pdo->query("SELECT COUNT(*) FROM escalation_log WHERE ticket_id IN ({$a},{$b},{$c},{$ec})")->fetchColumn();
|
||||
$check($hit === 0, 'Eskalation: erledigte Tickets werden nicht eskaliert');
|
||||
|
||||
// 4. Wiedervorlage: bei erledigten Tickets still entfernt, bei offenen mit Benachrichtigung
|
||||
$sc = $mk('W-closed', 'closed', ['snooze' => $ago(1)]);
|
||||
$so = $mk('W-open', 'open', ['snooze' => $ago(1)]);
|
||||
$woken = ticket_snooze_wake_due($pdo);
|
||||
$check($woken === 1, "Wiedervorlage: nur das offene Ticket wird zurückgeholt (war {$woken})");
|
||||
$check($row($sc)['snoozed_until'] === null && $row($so)['snoozed_until'] === null, 'Wiedervorlage: bei beiden aufgeräumt');
|
||||
$act = fn(int $id) => (int) $pdo->query("SELECT COUNT(*) FROM activity_log WHERE ticket_id = {$id} AND action LIKE '%Wiedervorlage fällig%'")->fetchColumn();
|
||||
$check($act($sc) === 0 && $act($so) === 1, 'Wiedervorlage: Hinweis „fällig“ nur beim offenen Ticket');
|
||||
|
||||
// 5. Erinnerung „Ticket wartet auf Antwort“ erfasst nur offene Tickets
|
||||
set_setting('wait_reminder_enabled', '1');
|
||||
set_setting('wait_reminder_hours', '1');
|
||||
$wo = $mk('A-open', 'open', ['due' => $ago(-40), 'created' => $ago(100), 'updated' => $ago(100)]);
|
||||
$wc = $mk('A-closed', 'closed', ['due' => $ago(-40), 'created' => $ago(100), 'updated' => $ago(100)]);
|
||||
$wr = wait_reminders_run($pdo, $now);
|
||||
$check((int) ($wr['sent'] ?? 0) === 1, 'Erinnerung: das offene Ticket wird erinnert (Gegenprobe)');
|
||||
$check($row($wc)['wait_reminded_at'] === null && $row($wo)['wait_reminded_at'] !== null, 'Erinnerung: für das erledigte Ticket wird nichts gesendet');
|
||||
|
||||
echo $failures ? "FEHLGESCHLAGEN:\n - " . implode("\n - ", $failures) . "\n" : '';
|
||||
echo "Ticket-erledigt-Test: " . ($total - count($failures)) . " von {$total} Prüfungen in Ordnung.\n";
|
||||
exit($failures ? 1 : 0);
|
||||
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Prüft über die echte Weboberfläche und die Mitarbeiter-API, dass gelöste und geschlossene Tickets als erledigt gelten:
|
||||
* keine Fälligkeit und kein „überfällig“ in Liste, Ticketansicht und App-Schnittstelle; das Schließen räumt den Merker auf.
|
||||
*/
|
||||
|
||||
if (!extension_loaded('pdo_sqlite') || !function_exists('curl_init')) { echo "Abmelde-Web-Test übersprungen: pdo_sqlite oder curl fehlt.\n"; exit(0); }
|
||||
|
||||
date_default_timezone_set('Europe/Berlin');
|
||||
$root = dirname(__DIR__);
|
||||
$work = sys_get_temp_dir() . '/deskhub-done-' . bin2hex(random_bytes(4));
|
||||
$tmp = $work . '/inst';
|
||||
mkdir($tmp, 0755, true);
|
||||
$webPort = random_int(35500, 35999);
|
||||
$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')));
|
||||
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,?)');
|
||||
foreach ([['Chef', 'chef@test.de', 'admin'], ['Eva', 'eva@test.de', 'agent'], ['Max', 'max@test.de', 'agent']] as [$n, $e, $r]) {
|
||||
$ins->execute([$n, $e, password_hash('Sehr-Sicher-1234', PASSWORD_DEFAULT), $r, $dept, $now]);
|
||||
}
|
||||
$evaId = (int) $db->query("SELECT id FROM staff WHERE email = 'eva@test.de'")->fetchColumn();
|
||||
$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;
|
||||
};
|
||||
$call = function (string $method, string $path, array $form, string $jar, ?string $bearer = null, ?array $json = null) use ($webPort): array {
|
||||
$ch = curl_init("http://127.0.0.1:$webPort$path");
|
||||
$h = ['Sec-Fetch-Mode: navigate'];
|
||||
if ($json !== null) $h[] = 'Content-Type: application/json';
|
||||
if ($bearer) $h[] = 'Authorization: Bearer ' . $bearer;
|
||||
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_TIMEOUT => 60, CURLOPT_HEADER => true, CURLOPT_HTTPHEADER => $h]);
|
||||
if ($jar !== '') { curl_setopt($ch, CURLOPT_COOKIEJAR, $jar); curl_setopt($ch, CURLOPT_COOKIEFILE, $jar); }
|
||||
if ($method === 'POST') { curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $json !== null ? json_encode($json) : http_build_query($form)); }
|
||||
$raw = (string) curl_exec($ch);
|
||||
$s = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
|
||||
$hs = (int) curl_getinfo($ch, CURLINFO_HEADER_SIZE);
|
||||
curl_close($ch);
|
||||
return [$s, substr($raw, $hs), substr($raw, 0, $hs)];
|
||||
};
|
||||
$login = function (string $email) use ($call): string {
|
||||
$jar = tempnam(sys_get_temp_dir(), 'sow');
|
||||
[, $lp] = $call('GET', '/staff/login.php', [], $jar);
|
||||
preg_match('/name="csrf_token" value="([a-f0-9]+)"/', $lp, $m);
|
||||
$call('POST', '/staff/login.php', ['csrf_token' => $m[1] ?? '', 'email' => $email, 'password' => 'Sehr-Sicher-1234'], $jar);
|
||||
return $jar;
|
||||
};
|
||||
$csrf = function (string $path, string $jar) use ($call): string {
|
||||
[, $p] = $call('GET', $path, [], $jar);
|
||||
preg_match('/name="csrf_token" value="([a-f0-9]+)"/', $p, $mm);
|
||||
return $mm[1] ?? '';
|
||||
};
|
||||
$loggedIn = function (string $jar) use ($call): bool {
|
||||
[$s, $b] = $call('GET', '/staff/profile.php?tab=security', [], $jar);
|
||||
return $s === 200 && str_contains($b, 'Von allen Geräten abmelden');
|
||||
};
|
||||
$apiToken = function (string $email) use ($call): string {
|
||||
[, $b] = $call('POST', '/api/staff/v1/login.php', [], '', null, ['email' => $email, 'password' => 'Sehr-Sicher-1234', 'deviceName' => 'Test-Gerät']);
|
||||
return (string) (json_decode($b, true)['token'] ?? '');
|
||||
};
|
||||
$apiOk = fn(string $tok) => $call('GET', '/api/staff/v1/me.php', [], '', $tok)[0];
|
||||
|
||||
$mk = function (string $n, string $status, array $o = []) use ($pdo, $now, $dept): int {
|
||||
$p = $pdo();
|
||||
$prio = (int) $p->query('SELECT id FROM priorities ORDER BY id LIMIT 1')->fetchColumn();
|
||||
$p->prepare('INSERT INTO tickets (ticket_number, subject, requester_name, requester_email, department_id, priority_id, status, source, resolution_due_at, first_response_due_at, is_overdue, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)')
|
||||
->execute([$n, 'Betreff ' . $n, 'K', 'k@example.org', $dept, $prio, $status, 'web', date('Y-m-d H:i:s', time() - 9 * 3600), date('Y-m-d H:i:s', time() - 20 * 3600), $o['overdue'] ?? 0, date('Y-m-d H:i:s', time() - 80 * 3600), $now]);
|
||||
return (int) $p->lastInsertId();
|
||||
};
|
||||
$mk('Z-OPEN', 'open', ['overdue' => 1]);
|
||||
$mk('Z-CLOSED', 'closed', ['overdue' => 1]); // alter Zustand: überfällig-Merker noch gesetzt
|
||||
$mk('Z-RESOLVED', 'resolved');
|
||||
$jar = $login('chef@test.de');
|
||||
$rowOf = function (string $html, string $num): string {
|
||||
return preg_match('#<tr[^>]*>(?:(?!</tr>).)*' . preg_quote($num, '#') . '(?:(?!</tr>).)*</tr>#s', $html, $m) ? $m[0] : '';
|
||||
};
|
||||
[, $list] = $call('GET', '/staff/tickets.php?tab=all', [], $jar);
|
||||
$open = $rowOf($list, 'Z-OPEN'); $closed = $rowOf($list, 'Z-CLOSED'); $resolved = $rowOf($list, 'Z-RESOLVED');
|
||||
$check($open !== '' && $closed !== '' && $resolved !== '', 'Alle drei Tickets stehen in der Liste');
|
||||
$check(str_contains($open, 'überfällig'), 'Liste: das offene Ticket zeigt „überfällig“');
|
||||
$check(!str_contains($closed, 'überfällig') && !str_contains($closed, ' Std') && str_contains($closed, '–'), 'Liste: geschlossenes Ticket zeigt keine Fälligkeit und kein „überfällig“');
|
||||
$check(!str_contains($resolved, 'überfällig') && !str_contains($resolved, ' Std') && str_contains($resolved, '–'), 'Liste: gelöstes Ticket genauso');
|
||||
foreach (['Z-CLOSED' => 'geschlossen', 'Z-RESOLVED' => 'gelöst'] as $num => $label) {
|
||||
[, $view] = $call('GET', '/staff/ticket_view.php?n=' . $num, [], $jar);
|
||||
$check(!str_contains($view, 'überfällig') && str_contains($view, '✓ erledigt'), "Ticketansicht ($label): kein „überfällig“, die Lösung steht als erledigt");
|
||||
}
|
||||
[, $view] = $call('GET', '/staff/ticket_view.php?n=Z-OPEN', [], $jar);
|
||||
$check(str_contains($view, 'überfällig'), 'Ticketansicht (offen): „überfällig“ bleibt');
|
||||
$tok = $apiToken('chef@test.de');
|
||||
[, $b] = $call('GET', '/api/staff/v1/tickets.php?status=open,closed,resolved&perPage=50', [], '', $tok);
|
||||
$items = json_decode($b, true)['tickets'] ?? json_decode($b, true)['items'] ?? [];
|
||||
$by = [];
|
||||
foreach ($items as $it) $by[$it['number'] ?? $it['ticketNumber'] ?? ''] = $it;
|
||||
$check(isset($by['Z-OPEN'], $by['Z-CLOSED'], $by['Z-RESOLVED']), 'App-Schnittstelle liefert alle drei Tickets');
|
||||
$check(!empty($by['Z-OPEN']['isOverdue']) && str_contains((string) $by['Z-OPEN']['dueLabel'], 'überfällig'), 'App-Schnittstelle: offenes Ticket überfällig');
|
||||
$check(empty($by['Z-CLOSED']['isOverdue']) && array_key_exists('dueLabel', $by['Z-CLOSED']) && $by['Z-CLOSED']['dueLabel'] === null, 'App-Schnittstelle: geschlossenes Ticket ohne Fälligkeit und ohne „überfällig“');
|
||||
$check(empty($by['Z-RESOLVED']['isOverdue']) && array_key_exists('dueLabel', $by['Z-RESOLVED']) && $by['Z-RESOLVED']['dueLabel'] === null, 'App-Schnittstelle: gelöstes Ticket genauso');
|
||||
// Echtes Schließen über die Weboberfläche räumt auf
|
||||
$t = $csrf('/staff/ticket_view.php?n=Z-OPEN', $jar);
|
||||
[, $v0] = $call('GET', '/staff/ticket_view.php?n=Z-OPEN', [], $jar);
|
||||
$call('POST', '/staff/ticket_view.php?n=Z-OPEN', ['csrf_token' => $t, 'action' => 'update_props', 'status' => 'closed', 'resolution_code' => 'fixed', 'resolution_note' => 'ok', 'priority_id' => (string) $pdo()->query("SELECT priority_id FROM tickets WHERE ticket_number = 'Z-OPEN'")->fetchColumn(), 'department_id' => (string) $dept, 'assigned_staff_id' => ''], $jar);
|
||||
$st = $pdo()->query("SELECT status, is_overdue FROM tickets WHERE ticket_number = 'Z-OPEN'")->fetch();
|
||||
$check($st['status'] === 'closed' && (int) $st['is_overdue'] === 0, 'Nach dem Schließen in der Oberfläche ist das Ticket geschlossen und nicht mehr überfällig');
|
||||
@unlink($jar);
|
||||
} finally {
|
||||
if (is_resource($webProc)) { proc_terminate($webProc); proc_close($webProc); }
|
||||
$rm = function (string $d) use (&$rm): void { foreach (glob($d . '/{,.}*', GLOB_BRACE) ?: [] as $e) { $b = basename($e); if ($b === '.' || $b === '..') continue; is_dir($e) ? $rm($e) : @unlink($e); } @rmdir($d); };
|
||||
$rm($work);
|
||||
}
|
||||
|
||||
if ($failures) {
|
||||
fwrite(STDERR, "Fehlgeschlagen:\n - " . implode("\n - ", $failures) . "\n");
|
||||
exit(1);
|
||||
}
|
||||
echo "Ticket done web checks passed ({$total} Prüfungen).\n";
|
||||
@@ -45,6 +45,10 @@ $z = new ZipArchive(); $z->open($zip, ZipArchive::CREATE); $z->addFromString('ve
|
||||
$check($code === 0 && is_file($zip . '.sig'), 'Signier-Werkzeug erzeugt die .sig-Datei: ' . $out);
|
||||
$sig = (string) @file_get_contents($zip . '.sig');
|
||||
$check(update_verify_package('1.0.7', $zip, $sig) === null, 'Gültige Signatur wird akzeptiert');
|
||||
foreach (['docs/DeskHub-Handbuch.pdf' => false, 'docs/video/demo.mp4' => true, 'docs/anderes.pdf' => true, 'plugins/x/plugin.php' => true, 'uploads/a.png' => true, 'backups/b.zip' => true,
|
||||
'app_downloads/DeskHub.apk' => true, 'tools/x.php' => true, 'tests/x.php' => true, 'staff/help.php' => false, 'includes/mailer.php' => false, 'assets/js/passkey.js' => false, 'docs/DeskHub-Handbuch.pdf.bak' => true, 'config/config.php' => true, 'Config/Config.php' => true, 'config/config.example.php' => true, 'config/.htaccess' => true, './config/config.php' => true, '.env' => true, 'configure.php' => false, 'tools/fix-permissions.sh' => false, 'tools/release-sign.php' => true] as $path => $skipped) {
|
||||
$check(update_path_is_skipped($path) === $skipped, 'Update-Pfad ' . $path . ($skipped ? ' wird übersprungen' : ' wird installiert'));
|
||||
}
|
||||
$check(update_verify_package('v1.0.7', $zip, $sig) === null, 'Version mit führendem "v" passt ebenfalls');
|
||||
$check(update_verify_package('1.0.8', $zip, $sig) !== null, 'Signatur einer anderen Version wird abgelehnt (kein Herunterstufen/Umetikettieren)');
|
||||
$check(update_verify_package('1.0.7', $zip, '') !== null, 'Fehlende Signatur wird abgelehnt');
|
||||
@@ -100,6 +104,47 @@ $check(update_http_headers() === [] && update_token_info() === ['', ''], 'ohne T
|
||||
putenv('DESKHUB_CATALOG_DEFAULTS=' . __DIR__ . '/fixtures/no-catalog.php');
|
||||
$check(function_exists('plugin_publisher_key'), 'Hilfsfunktion vorhanden');
|
||||
|
||||
putenv('DESKHUB_CATALOG_DEFAULTS=' . $tmp . '/defaults.php'); // wieder mit offiziellem Katalog
|
||||
// ── Updates von der Plugin-Webseite ──────────────────────────────────────────
|
||||
$check(update_publisher_endpoint(['list' => 1]) === 'https://plugins.example.test/app-release.php?platform=server&list=1', 'Update-Adresse liegt neben dem Katalog: ' . update_publisher_endpoint(['list' => 1]));
|
||||
$GLOBALS['deskhub_catalog_defaults'] = ['plugins_url' => '', 'themes_url' => '', 'publisher_key' => $pub];
|
||||
$check(update_publisher_endpoint([]) === '' && update_publisher_releases() === [], 'Ohne offiziellen Katalog gibt es keine Update-Adresse');
|
||||
unset($GLOBALS['deskhub_catalog_defaults']);
|
||||
$pubZip = $tmp . '/pub.zip';
|
||||
$z = new ZipArchive(); $z->open($pubZip, ZipArchive::CREATE); $z->addFromString('version.php', "<?php define('DESKHUB_VERSION', '1.0.9');"); $z->close();
|
||||
$pubSha = hash_file('sha256', $pubZip);
|
||||
openssl_sign(update_signature_message('1.0.9', $pubSha), $pubSigRaw, $k, OPENSSL_ALGO_SHA256);
|
||||
$entry = ['platform' => 'server', 'version' => '1.0.9', 'sha256' => $pubSha, 'size' => filesize($pubZip), 'notes' => 'Neu', 'created_at' => '2026-11-05 10:00:00', 'signature' => base64_encode($pubSigRaw)];
|
||||
$older = ['platform' => 'server', 'version' => '1.0.8', 'sha256' => str_repeat('a', 64), 'size' => 10, 'notes' => '', 'created_at' => '2026-10-01 10:00:00', 'signature' => 'x'];
|
||||
$served = [$older, $entry];
|
||||
$tamper = false;
|
||||
$GLOBALS['deskhub_http_get'] = function (string $url, int $max, array $headers = []) use (&$served, &$tamper, $pubZip): array {
|
||||
if (str_contains($url, 'list=1')) return [200, json_encode(['platform' => 'server', 'releases' => $served]), null];
|
||||
if (str_contains($url, 'version=1.0.9') && str_contains($url, 'download=1')) return [200, ($tamper ? 'X' : '') . file_get_contents($pubZip), null];
|
||||
return [404, null, 'HTTP 404'];
|
||||
};
|
||||
$rels = update_publisher_releases();
|
||||
$check(count($rels) === 2 && $rels[0]['tag_name'] === '1.0.9' && $rels[0]['source'] === 'publisher' && $rels[0]['body'] === 'Neu', 'Plugin-Webseite: Versionen werden geladen, neueste zuerst');
|
||||
$check(update_publisher_find('v1.0.9')['sha256'] === $pubSha && update_publisher_find('9.9.9') === null, 'Version wird mit oder ohne „v“ gefunden');
|
||||
[$got, $err] = update_publisher_fetch($rels[0]);
|
||||
$check($err === null && $got !== null && is_file($got), 'Gültiges Paket wird geladen und geprüft: ' . var_export($err, true));
|
||||
if ($got) @unlink($got);
|
||||
$bad = $rels[0]; $bad['signature'] = base64_encode('falsch');
|
||||
[$got, $err] = update_publisher_fetch($bad);
|
||||
$check($got === null && $err !== null, 'Falsche Signatur wird abgelehnt');
|
||||
$bad = $rels[0]; $bad['signature'] = '';
|
||||
[$got, $err] = update_publisher_fetch($bad);
|
||||
$check($got === null && $err !== null, 'Fehlende Signatur wird abgelehnt (Seite ohne Signierschlüssel)');
|
||||
$tamper = true;
|
||||
[$got, $err] = update_publisher_fetch($rels[0]);
|
||||
$check($got === null && str_contains((string) $err, 'Prüfsumme'), 'Verändertes Paket scheitert an der Prüfsumme');
|
||||
$tamper = false;
|
||||
$served = [['version' => '../../x', 'sha256' => $pubSha], ['version' => '2.0.0', 'sha256' => 'kurz'], 'kaputt'];
|
||||
$check(update_publisher_releases() === [], 'Einträge mit ungültiger Version oder Prüfsumme werden ignoriert');
|
||||
$GLOBALS['deskhub_http_get'] = fn(string $url, int $max, array $h = []): array => [500, null, 'HTTP 500'];
|
||||
$check(update_publisher_releases() === [], 'Fällt die Plugin-Webseite aus, ist die Liste leer (dann greift Gitea)');
|
||||
unset($GLOBALS['deskhub_http_get']);
|
||||
|
||||
// Aufräumen
|
||||
foreach (glob($tmp . '/*') ?: [] as $f) @unlink($f);
|
||||
@rmdir($tmp);
|
||||
|
||||
@@ -15,6 +15,7 @@ require_once dirname(__DIR__) . '/includes/push.php';
|
||||
$failures = [];
|
||||
$check = function (bool $ok, string $what) use (&$failures): void { if (!$ok) $failures[] = $what; };
|
||||
$pdo = get_db();
|
||||
set_setting('sla_notify_mode', 'assignee'); // diese Prüfungen betreffen den Modus „Bearbeiter bzw. Abteilung“ (Standard ist „nur Administratoren“)
|
||||
$now = db_now();
|
||||
$pdo->prepare('INSERT INTO departments (name, email) VALUES (?,?)')->execute(['Support', 'support@firma.example']);
|
||||
$dept = (int) $pdo->lastInsertId();
|
||||
|
||||
@@ -108,8 +108,8 @@ try {
|
||||
// ── Welche Seiten sieht wer? ───────────────────────────────────────────
|
||||
$keys = fn(string $t): array => array_column(($api('GET', '/api/staff/v1/web_pages.php', null, $t)[1]['pages'] ?? []), 'key');
|
||||
$kc = $keys($chef); $ka = $keys($agent); $kp = $keys($pfleger);
|
||||
$check(count($kc) === 20 && in_array('settings', $kc, true) && in_array('backup', $kc, true) && in_array('assistant', $kc, true), 'Administrator sieht alle 20 Seiten');
|
||||
$check($ka === ['mentions', 'agent_directory', 'apps'], 'Mitarbeiter ohne Rechte sehen nur Erwähnungen, Verzeichnis und die Seite „Apps und Downloads“ ' . json_encode($ka));
|
||||
$check(count($kc) === 27 && in_array('settings', $kc, true) && in_array('backup', $kc, true) && in_array('assistant', $kc, true), 'Administrator sieht alle 27 Seiten');
|
||||
$check($ka === ['mentions', 'agent_directory', 'apps', 'banlist'], 'Mitarbeiter ohne Rechte sehen nur Erwähnungen, Verzeichnis, Apps und die Sperrliste (Standardrecht) ' . json_encode($ka));
|
||||
$check(in_array('assistant', $kp, true) && !in_array('settings', $kp, true) && !in_array('backup', $kp, true) && !in_array('plugins', $kp, true), 'Mit dem Recht „Assistent“ kommt genau diese Seite dazu, aber keine Systemseiten ' . json_encode($kp));
|
||||
|
||||
// ── Übergabe ───────────────────────────────────────────────────────────
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
#!/bin/sh
|
||||
# DeskHub: setzt die Datei-Rechte so, dass der Webserver alles lesen und das System-Update alles schreiben kann.
|
||||
#
|
||||
# Aufruf (im DeskHub-Hauptordner oder mit Pfad):
|
||||
# sh tools/fix-permissions.sh [--check] [--group-write] [BENUTZER[:GRUPPE]] [PFAD]
|
||||
#
|
||||
# --check nur anzeigen, was nicht stimmt, nichts ändern
|
||||
# --group-write Ordner 775 und Dateien 664 (wenn Webserver und FTP-Benutzer nur über eine gemeinsame Gruppe schreiben)
|
||||
# BENUTZER neuer Besitzer aller Dateien, nötig nur wenn sie jemand anderem gehören (z. B. root). Ändern darf das nur root:
|
||||
# beim Hoster als „Geplante Aufgabe“ (Plesk: Tools & Einstellungen → Geplante Aufgaben → Befehl ausführen) oder über den Support
|
||||
# PFAD Ordner von DeskHub (Standard: der Ordner über „tools“)
|
||||
#
|
||||
# Ergebnis: Ordner 755, Dateien 644, Konfiguration und Datenbank 640 (nur Besitzer und Gruppe lesen), Skripte unter tools 755.
|
||||
# Nichts wird gelöscht und keine Datei verändert, nur Besitzer und Rechte.
|
||||
set -u
|
||||
|
||||
CHECK=0; GROUPW=0; OWNER=""; ROOT=""
|
||||
for a in "$@"; do
|
||||
case "$a" in
|
||||
--check) CHECK=1 ;;
|
||||
--group-write) GROUPW=1 ;;
|
||||
--*) echo "Unbekannte Option: $a" >&2; exit 2 ;;
|
||||
*) if [ -z "$OWNER" ] && [ ! -d "$a" ]; then OWNER="$a"; else ROOT="$a"; fi ;;
|
||||
esac
|
||||
done
|
||||
[ -n "$ROOT" ] || ROOT=$(cd "$(dirname "$0")/.." && pwd)
|
||||
[ -f "$ROOT/version.php" ] && [ -d "$ROOT/includes" ] || { echo "Das ist kein DeskHub-Ordner: $ROOT (version.php oder includes fehlt)" >&2; exit 1; }
|
||||
|
||||
DMODE=755; FMODE=644; SMODE=640
|
||||
if [ "$GROUPW" = 1 ]; then DMODE=775; FMODE=664; SMODE=660; fi
|
||||
ME=$(id -u)
|
||||
echo "DeskHub-Ordner: $ROOT"
|
||||
echo "Angemeldet als: $(id -un) (uid $ME) · Ordner $DMODE, Dateien $FMODE, Konfiguration/Datenbank $SMODE"
|
||||
|
||||
# Läuft das Skript als root ohne Benutzerangabe, wird der Besitzer von index.php (sonst des Ordners) übernommen, sofern das nicht root ist
|
||||
if [ -z "$OWNER" ] && [ "$ME" = 0 ]; then
|
||||
guess=$(stat -c %U "$ROOT/index.php" 2>/dev/null || true)
|
||||
[ -n "$guess" ] && [ "$guess" != root ] || guess=$(stat -c %U "$ROOT" 2>/dev/null || true)
|
||||
if [ -n "$guess" ] && [ "$guess" != root ]; then OWNER="$guess"; echo "Besitzer automatisch erkannt: $OWNER"
|
||||
else echo "Als root bitte den Besitzer angeben: sh tools/fix-permissions.sh BENUTZER" >&2; exit 1; fi
|
||||
fi
|
||||
|
||||
# Dateien, die nicht dem Zielbenutzer gehören, kann nur root ändern
|
||||
if [ -n "$OWNER" ]; then
|
||||
if [ "$CHECK" = 1 ]; then
|
||||
n=$(find "$ROOT" ! -user "${OWNER%%:*}" 2>/dev/null | wc -l)
|
||||
echo "Gehören nicht ${OWNER%%:*}: $n Einträge"
|
||||
find "$ROOT" ! -user "${OWNER%%:*}" 2>/dev/null | head -15 | sed 's/^/ /'
|
||||
elif [ "$ME" = 0 ]; then
|
||||
chown -R "$OWNER" "$ROOT" && echo "Besitzer gesetzt: $OWNER"
|
||||
else
|
||||
echo "Hinweis: Den Besitzer kann nur root ändern (Geplante Aufgabe beim Hoster oder Support). Es werden nur die Rechte gesetzt." >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
fix() { # Art(d|f) Modus [weitere find-Argumente...]
|
||||
kind=$1; mode=$2; shift 2
|
||||
if [ "$CHECK" = 1 ]; then
|
||||
n=$(find "$ROOT" -type "$kind" ! -perm "$mode" "$@" 2>/dev/null | wc -l)
|
||||
echo " $([ "$kind" = d ] && echo Ordner || echo Dateien) mit anderen Rechten als $mode: $n"
|
||||
else
|
||||
find "$ROOT" -type "$kind" ! -perm "$mode" "$@" -exec chmod "$mode" {} + 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
echo "Ordner:"
|
||||
fix d "$DMODE" ! -path "*/.git/*"
|
||||
echo "Dateien:"
|
||||
fix f "$FMODE" ! -path "*/.git/*" ! -path "*/config/config.php" ! -path "*/database/*.sqlite" ! -path "*/database/*.sqlite-*" ! -path "*/database/*.db" ! -name .env ! -path "*/tools/*.sh"
|
||||
echo "Konfiguration und Datenbank:"
|
||||
for f in "$ROOT/config/config.php" "$ROOT"/database/*.sqlite "$ROOT"/database/*.sqlite-* "$ROOT"/database/*.db "$ROOT/.env"; do
|
||||
[ -f "$f" ] || continue
|
||||
if [ "$CHECK" = 1 ]; then
|
||||
[ "$(stat -c %a "$f" 2>/dev/null)" = "$SMODE" ] || echo " $f hat andere Rechte als $SMODE"
|
||||
else
|
||||
chmod "$SMODE" "$f" 2>/dev/null
|
||||
fi
|
||||
done
|
||||
for f in "$ROOT"/tools/*.sh; do [ -f "$f" ] && { [ "$CHECK" = 1 ] || chmod 755 "$f" 2>/dev/null; }; done
|
||||
|
||||
# Zum Schluss: Was kann der Webserver später noch nicht ändern?
|
||||
echo "Prüfung der Schreibbarkeit (für den Benutzer, der dieses Skript ausführt):"
|
||||
bad=0
|
||||
for d in "" config database backups uploads plugins app_downloads assets cron includes staff api docs; do
|
||||
p="$ROOT${d:+/$d}"
|
||||
[ -d "$p" ] || continue
|
||||
if [ ! -w "$p" ]; then echo " NICHT beschreibbar: ${d:-.}"; bad=1; fi
|
||||
done
|
||||
[ -f "$ROOT/version.php" ] && [ ! -w "$ROOT/version.php" ] && { echo " NICHT beschreibbar: version.php"; bad=1; }
|
||||
if [ "$bad" = 0 ]; then echo " alles in Ordnung"; else echo " Besitzer ändern (nur root): sh tools/fix-permissions.sh BENUTZER"; fi
|
||||
[ "$CHECK" = 1 ] && echo "(--check: nichts wurde geändert)"
|
||||
exit 0
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
/**
|
||||
* tools/mail_preview.php: zeigt die wichtigsten System-Mails mit Beispieldaten in einer Seite (zum Prüfen des Mail-Designs).
|
||||
* DESKHUB_TESTS_FORCE_SQLITE=1 DESKHUB_TESTING=1 DESKHUB_SQLITE_PATH=:memory: php tools/mail_preview.php > vorschau.html
|
||||
* Es wird nichts versendet und nichts gespeichert; alle Daten sind erfunden.
|
||||
*/
|
||||
putenv('DESKHUB_TESTING=1');
|
||||
putenv('DESKHUB_SQLITE_PATH=:memory:');
|
||||
require_once dirname(__DIR__) . '/includes/bootstrap.php';
|
||||
set_setting('display_name', 'Muster Support');
|
||||
set_setting('site_url', 'https://support.example.de');
|
||||
|
||||
$vars = [
|
||||
'ticket_number' => '261031-6188', 'subject' => 'Rechnung für März fehlt', 'customer_name' => 'Erika Muster', 'customer_email' => 'erika.muster@kunde.example',
|
||||
'department_name' => 'Buchhaltung', 'ticket_url' => 'https://support.example.de/ticket_view.php?n=261031-6188', 'lookup_url' => 'https://support.example.de/ticket_lookup.php',
|
||||
'message' => '<p style="margin:0 0 10px">Hallo Frau Muster,</p><p style="margin:0 0 10px">die Rechnung für März haben wir Ihnen soeben noch einmal als PDF an Ihre Adresse geschickt. Bitte prüfen Sie auch den Spam-Ordner.</p><p style="margin:0">Viele Grüße<br>Lena Hartmann</p>',
|
||||
'close_note' => email_p('Wir schließen das Ticket automatisch in <strong>3 Tagen</strong>, wenn wir nichts mehr von Ihnen hören.'),
|
||||
'transcript' => email_chat_transcript_row('Erika Muster', 'Hallo, ich suche die Rechnung für März.', '14:02', false) . email_chat_transcript_row('Lena Hartmann', 'Kein Problem, ich schicke sie Ihnen gleich.', '14:03', true),
|
||||
];
|
||||
$samples = [
|
||||
'Ticket erstellt (Kunde)' => render_email_template('ticket_created', $vars, 'x', email_ticket_created_default_body())[1],
|
||||
'Neue Antwort (Kunde)' => render_email_template('new_reply', $vars, 'x', email_new_reply_default_body())[1],
|
||||
'Neues Ticket (Abteilung)' => render_email_template('new_ticket_department', $vars, 'x', email_new_ticket_department_default_body())[1],
|
||||
'SLA überschritten' => render_email_template('ticket_overdue', $vars, 'x', email_ticket_overdue_default_body())[1],
|
||||
'Chat-Verlauf' => render_email_template('live_chat_transcript', $vars, 'x', email_live_chat_transcript_default_body())[1],
|
||||
'Erinnerung an Kunden' => render_email_template('customer_reminder', $vars, 'x', email_customer_reminder_default_body())[1],
|
||||
'Passwort zurücksetzen' => null,
|
||||
];
|
||||
?><!doctype html><html lang="de"><head><meta charset="utf-8"><title>Mail-Vorschau</title>
|
||||
<style>body{margin:0;background:#cbd5e1;font-family:Arial,sans-serif}h2{margin:24px 12px 6px;font-size:14px;color:#334155}.frame{max-width:680px;margin:0 auto 10px}</style></head><body>
|
||||
<?php foreach ($samples as $title => $body): ?>
|
||||
<h2><?= e($title) ?></h2>
|
||||
<div class="frame"><?= $body === null ? email_password_reset_body('https://support.example.de/reset?token=beispiel') : email_wrap($body) ?></div>
|
||||
<?php endforeach; ?>
|
||||
</body></html>
|
||||
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
tools/manual/build_manual.py: setzt das Handbuch zusammen (Kapitel aus tools/manual/handbuch/ch*.html + Bilder).
|
||||
|
||||
python build_manual.py <Bilder-Web> <Bilder-Windows> <Ausgabeordner> [Seitenzahlen.json]
|
||||
|
||||
Erzeugt <Ausgabeordner>/handbuch.html (und img/). Die PDF-Datei entsteht danach mit print_pdf.js (Electron).
|
||||
Kapitel-Dateien nutzen: <h1>, <h2>, <h3> (Nummern und Inhaltsverzeichnis entstehen automatisch)
|
||||
<fig n="bildname">Bildunterschrift</fig> (Bild aus den Ordnern der Aufnahmen)
|
||||
<div class="tip|warn|note">…</div>, <table>, <kbd>
|
||||
"""
|
||||
import html
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
from PIL import Image
|
||||
|
||||
here = os.path.dirname(os.path.abspath(__file__))
|
||||
web_dir, win_dir, out_dir = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
pages = {}
|
||||
if len(sys.argv) > 4 and os.path.exists(sys.argv[4]):
|
||||
pages = json.load(io.open(sys.argv[4], encoding='utf-8'))
|
||||
img_out = os.path.join(out_dir, 'img')
|
||||
os.makedirs(img_out, exist_ok=True)
|
||||
|
||||
VERSION = 'Server 1.1.0 · Windows-Programm 2.12.3 · Android-App 3.5.3 · Chrome-Erweiterung 2.1.1 · WordPress-Plugin 1.3.0 · Minecraft-Plugin 1.2.0'
|
||||
used = set()
|
||||
missing = []
|
||||
fig_no = [0]
|
||||
|
||||
|
||||
def prep_image(name):
|
||||
"""Bild kopieren (als JPEG, zu hohe Bilder werden unten abgeschnitten)."""
|
||||
src = os.path.join(win_dir if name.startswith('windows-') else web_dir, name + '.png')
|
||||
if not os.path.exists(src):
|
||||
missing.append(name)
|
||||
return None
|
||||
dst = os.path.join(img_out, name + '.jpg')
|
||||
if not os.path.exists(dst) or os.path.getmtime(dst) < os.path.getmtime(src):
|
||||
im = Image.open(src).convert('RGB')
|
||||
if im.height > 1650 and im.width > 300:
|
||||
im = im.crop((0, 0, im.width, 1650))
|
||||
im.save(dst, 'JPEG', quality=86, optimize=True)
|
||||
used.add(name)
|
||||
return 'img/' + name + '.jpg'
|
||||
|
||||
|
||||
def fig_sub(m):
|
||||
name, cap = m.group(1), m.group(2).strip()
|
||||
path = prep_image(name)
|
||||
if not path:
|
||||
return '<div class="warn">[Bild fehlt: %s]</div>' % html.escape(name)
|
||||
fig_no[0] += 1
|
||||
narrow = ' narrow' if Image.open(os.path.join(img_out, name + '.jpg')).width < 500 else ''
|
||||
return ('<figure class="%s"><img src="%s" alt=""><figcaption><b>Abbildung %d:</b> %s</figcaption></figure>'
|
||||
% (narrow.strip(), path, fig_no[0], cap))
|
||||
|
||||
|
||||
chapters = []
|
||||
for fn in sorted(os.listdir(os.path.join(here, 'handbuch'))):
|
||||
if fn.startswith('ch') and fn.endswith('.html'):
|
||||
chapters.append(io.open(os.path.join(here, 'handbuch', fn), encoding='utf-8').read())
|
||||
body = '\n'.join(chapters)
|
||||
body = re.sub(r'<fig n="([^"]+)">(.*?)</fig>', fig_sub, body, flags=re.S)
|
||||
|
||||
# Nummerieren, Anker und Inhaltsverzeichnis
|
||||
toc = []
|
||||
c1 = [0]
|
||||
c2 = [0]
|
||||
|
||||
|
||||
def head_sub(m):
|
||||
lvl, text = int(m.group(1)), m.group(2).strip()
|
||||
if lvl == 1:
|
||||
c1[0] += 1
|
||||
c2[0] = 0
|
||||
num = str(c1[0])
|
||||
elif lvl == 2:
|
||||
c2[0] += 1
|
||||
num = '%d.%d' % (c1[0], c2[0])
|
||||
else:
|
||||
return '<h3>%s</h3>' % text
|
||||
anchor = 'k' + num.replace('.', '_')
|
||||
plain = html.unescape(re.sub(r'<[^>]+>', '', text))
|
||||
toc.append((lvl, num, plain, anchor))
|
||||
return '<h%d id="%s"><span class="num">%s</span> %s</h%d>' % (lvl, anchor, num, text, lvl)
|
||||
|
||||
|
||||
body = re.sub(r'<h([12])>(.*?)</h\1>', head_sub, body, flags=re.S)
|
||||
|
||||
toc_html = ['<div class="toc"><h1 class="plain">Inhaltsverzeichnis</h1>']
|
||||
for lvl, num, plain, anchor in toc:
|
||||
p = pages.get(anchor, '')
|
||||
toc_html.append('<a class="t%d" href="#%s"><span class="tn">%s</span><span class="tt">%s</span><span class="tl"></span><span class="tp">%s</span></a>'
|
||||
% (lvl, anchor, num, html.escape(plain), p))
|
||||
toc_html.append('</div>')
|
||||
|
||||
css = io.open(os.path.join(here, 'handbuch', 'manual.css'), encoding='utf-8').read()
|
||||
cover = io.open(os.path.join(here, 'handbuch', 'cover.html'), encoding='utf-8').read().replace('{{VERSION}}', VERSION)
|
||||
front = io.open(os.path.join(here, 'handbuch', 'front.html'), encoding='utf-8').read()
|
||||
doc = ('<!doctype html><html lang="de"><head><meta charset="utf-8"><title>DeskHub Handbuch</title><style>%s</style></head><body>'
|
||||
'%s%s%s<main>%s</main></body></html>') % (css, cover, front, '\n'.join(toc_html), body)
|
||||
io.open(os.path.join(out_dir, 'handbuch.html'), 'w', encoding='utf-8').write(doc)
|
||||
io.open(os.path.join(out_dir, 'cover.html'), 'w', encoding='utf-8').write('<!doctype html><html lang="de"><head><meta charset="utf-8"><style>%s</style></head><body>%s</body></html>' % (css, cover))
|
||||
json.dump([{'level': l, 'num': n, 'title': t, 'anchor': a} for l, n, t, a in toc], io.open(os.path.join(out_dir, 'toc.json'), 'w', encoding='utf-8'), ensure_ascii=False)
|
||||
print('Kapitel/Abschnitte: %d, Abbildungen: %d, verwendete Bilder: %d, fehlend: %s' % (len(toc), fig_no[0], len(used), missing or 'keine'))
|
||||
@@ -0,0 +1,85 @@
|
||||
'use strict';
|
||||
/**
|
||||
* tools/manual/capture_extra.js: nimmt die neuen Bilder für das Handbuch auf (Passkeys, Anmeldeseite, Mitarbeiter-Menü, Anmelde-Wächter) (Demo-Installation mit erfundenen Daten, siehe seed_manual.php).
|
||||
* 1. php tools/demo-video/seed_demo.php <Ordner> && php tools/manual/seed_manual.php <Ordner> && php tools/manual/install_plugins.php <Ordner> <Plugin-ZIPs>
|
||||
* 2. DESKHUB_CATALOG_DEFAULTS=tests/fixtures/no-catalog.php php -S 127.0.0.1:8097 -t <Ordner>
|
||||
* 3. electron tools/manual/capture_manual.js <Bilderordner>
|
||||
*/
|
||||
const { app, BrowserWindow } = require('electron');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const BASE = 'http://127.0.0.1:8097';
|
||||
const OUT = path.resolve(process.argv[2] || path.join(__dirname, 'shots'));
|
||||
const W = 1180; // schmaler als ein Bildschirm: Die Bilder werden auf A4 verkleinert und müssen lesbar bleiben
|
||||
const SIDEBAR = 240; // Breite der Seitenleiste; sie wird aus den Bildern der Seiten herausgeschnitten (eigenes Bild vom Menü)
|
||||
const H = 860;
|
||||
const MAXH = 2400;
|
||||
fs.mkdirSync(OUT, { recursive: true });
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
let win;
|
||||
const run = (js) => win.webContents.executeJavaScript(js, true);
|
||||
const report = { ok: [], fail: [] };
|
||||
|
||||
/** Seite laden und alles entfernen, was nur zur Demo-Installation gehört (SQLite-Hinweis, Zähler der Sitzung). */
|
||||
async function load(url, wait = 700) {
|
||||
await win.loadURL(BASE + url);
|
||||
await sleep(wait);
|
||||
await run(`(() => {
|
||||
document.getElementById('session-timer-bar')?.remove();
|
||||
document.querySelectorAll('div,p,section,aside').forEach((e) => { if (e.children.length < 8 && /SQLite-Modus aktiv/.test(e.textContent) && e.textContent.length < 400) e.remove(); });
|
||||
})()`);
|
||||
}
|
||||
|
||||
/** Bild der Seite speichern: höchstens MAXH hoch; mit full = false nur der sichtbare Teil. */
|
||||
async function shot(name, { full = true, clip = null, keepSidebar = false } = {}) {
|
||||
let h = H;
|
||||
if (full) {
|
||||
h = await run('Math.max(document.documentElement.scrollHeight, document.body.scrollHeight)');
|
||||
h = Math.min(Math.max(h, 500), MAXH);
|
||||
}
|
||||
win.setContentSize(W, h);
|
||||
await sleep(350);
|
||||
const hasSidebar = await run(`(() => { const s = document.getElementById('sidebar'); return !!s && s.getBoundingClientRect().width > 100; })()`);
|
||||
let img = await win.webContents.capturePage();
|
||||
if (clip) img = img.crop(clip);
|
||||
else if (hasSidebar && !keepSidebar) img = img.crop({ x: SIDEBAR, y: 0, width: W - SIDEBAR, height: h });
|
||||
fs.writeFileSync(path.join(OUT, name + '.png'), img.toPNG());
|
||||
win.setContentSize(W, H);
|
||||
report.ok.push(name);
|
||||
}
|
||||
|
||||
async function page(name, url, opts = {}) {
|
||||
try {
|
||||
await load(url, opts.wait);
|
||||
if (opts.prep) { await run(opts.prep); await sleep(opts.prepWait || 400); }
|
||||
await shot(name, opts);
|
||||
} catch (e) {
|
||||
report.fail.push(name + ': ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function login(email, pass, loginUrl) {
|
||||
await load(loginUrl, 600);
|
||||
await run(`(() => { const f = document.querySelector('form'); f.querySelector('[name=email]').value = ${JSON.stringify(email)}; f.querySelector('[name=password]').value = ${JSON.stringify(pass)}; f.submit(); })()`);
|
||||
await sleep(1500);
|
||||
}
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
win = new BrowserWindow({ width: W, height: H, show: true, useContentSize: true, webPreferences: { contextIsolation: true, backgroundThrottling: false } });
|
||||
try {
|
||||
// Anmeldeseite mit dem Passkey-Knopf (im Electron-Fenster ohne Passkey-Unterstützung wird er von Hand eingeblendet)
|
||||
await page('staff-login', '/staff/login.php', { full: false, prep: `(() => { const b = document.getElementById('passkey-login'); if (b) b.style.display = 'block'; })()` });
|
||||
await login('lena@demo.example', 'Demo-Passwort-1234', '/staff/login.php');
|
||||
await page('profil-sicherheit', '/staff/profile.php?tab=security', { prep: `(() => { const a = document.getElementById('passkey-add'); if (a && !a.getAttribute('data-off')) a.style.display = 'block'; const u = document.getElementById('pk_unsupported'); if (u) u.style.display = 'none'; })()` });
|
||||
await page('admin-anmelde-waechter', '/staff/login_log.php');
|
||||
await page('admin-mitarbeiter-passkeys', '/staff/staff_manage.php?edit=3', { prep: `(() => { const h = [...document.querySelectorAll('h3')].find((x) => x.textContent.includes('Passkeys von')); if (!h) return; const card = h.closest('.card'); let n = card.previousElementSibling; while (n) { const p = n.previousElementSibling; if (n.tagName !== 'STYLE') n.style.display = 'none'; n = p; } })()` });
|
||||
await page('admin-menue-aktionen', '/staff/staff_manage.php', { full: false, prep: `(() => { const row = [...document.querySelectorAll('#sm-table tbody tr')].find((r) => r.textContent.includes('Tim')); row?.querySelector('.sm-menu summary')?.click(); })()` });
|
||||
} catch (e) {
|
||||
report.fail.push('FATAL ' + e.stack);
|
||||
}
|
||||
fs.writeFileSync(path.join(OUT, '_bericht_extra.json'), JSON.stringify(report, null, 1));
|
||||
console.log(`Aufgenommen: ${report.ok.length}, Fehler: ${report.fail.length}`);
|
||||
report.fail.forEach((f) => console.log(' - ' + f));
|
||||
app.quit();
|
||||
});
|
||||
@@ -0,0 +1,176 @@
|
||||
'use strict';
|
||||
/**
|
||||
* tools/manual/capture_manual.js: nimmt die Bilder für das Handbuch auf (Demo-Installation mit erfundenen Daten, siehe seed_manual.php).
|
||||
* 1. php tools/demo-video/seed_demo.php <Ordner> && php tools/manual/seed_manual.php <Ordner> && php tools/manual/install_plugins.php <Ordner> <Plugin-ZIPs>
|
||||
* 2. DESKHUB_CATALOG_DEFAULTS=tests/fixtures/no-catalog.php php -S 127.0.0.1:8097 -t <Ordner>
|
||||
* 3. electron tools/manual/capture_manual.js <Bilderordner>
|
||||
*/
|
||||
const { app, BrowserWindow } = require('electron');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const BASE = 'http://127.0.0.1:8097';
|
||||
const OUT = path.resolve(process.argv[2] || path.join(__dirname, 'shots'));
|
||||
const W = 1180; // schmaler als ein Bildschirm: Die Bilder werden auf A4 verkleinert und müssen lesbar bleiben
|
||||
const SIDEBAR = 240; // Breite der Seitenleiste; sie wird aus den Bildern der Seiten herausgeschnitten (eigenes Bild vom Menü)
|
||||
const H = 860;
|
||||
const MAXH = 2400;
|
||||
fs.mkdirSync(OUT, { recursive: true });
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
let win;
|
||||
const run = (js) => win.webContents.executeJavaScript(js, true);
|
||||
const report = { ok: [], fail: [] };
|
||||
|
||||
/** Seite laden und alles entfernen, was nur zur Demo-Installation gehört (SQLite-Hinweis, Zähler der Sitzung). */
|
||||
async function load(url, wait = 700) {
|
||||
await win.loadURL(BASE + url);
|
||||
await sleep(wait);
|
||||
await run(`(() => {
|
||||
document.getElementById('session-timer-bar')?.remove();
|
||||
document.querySelectorAll('div,p,section,aside').forEach((e) => { if (e.children.length < 8 && /SQLite-Modus aktiv/.test(e.textContent) && e.textContent.length < 400) e.remove(); });
|
||||
})()`);
|
||||
}
|
||||
|
||||
/** Bild der Seite speichern: höchstens MAXH hoch; mit full = false nur der sichtbare Teil. */
|
||||
async function shot(name, { full = true, clip = null, keepSidebar = false } = {}) {
|
||||
let h = H;
|
||||
if (full) {
|
||||
h = await run('Math.max(document.documentElement.scrollHeight, document.body.scrollHeight)');
|
||||
h = Math.min(Math.max(h, 500), MAXH);
|
||||
}
|
||||
win.setContentSize(W, h);
|
||||
await sleep(350);
|
||||
const hasSidebar = await run(`(() => { const s = document.getElementById('sidebar'); return !!s && s.getBoundingClientRect().width > 100; })()`);
|
||||
let img = await win.webContents.capturePage();
|
||||
if (clip) img = img.crop(clip);
|
||||
else if (hasSidebar && !keepSidebar) img = img.crop({ x: SIDEBAR, y: 0, width: W - SIDEBAR, height: h });
|
||||
fs.writeFileSync(path.join(OUT, name + '.png'), img.toPNG());
|
||||
win.setContentSize(W, H);
|
||||
report.ok.push(name);
|
||||
}
|
||||
|
||||
async function page(name, url, opts = {}) {
|
||||
try {
|
||||
await load(url, opts.wait);
|
||||
if (opts.prep) { await run(opts.prep); await sleep(opts.prepWait || 400); }
|
||||
await shot(name, opts);
|
||||
} catch (e) {
|
||||
report.fail.push(name + ': ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function login(email, pass, loginUrl) {
|
||||
await load(loginUrl, 600);
|
||||
await run(`(() => { const f = document.querySelector('form'); f.querySelector('[name=email]').value = ${JSON.stringify(email)}; f.querySelector('[name=password]').value = ${JSON.stringify(pass)}; f.submit(); })()`);
|
||||
await sleep(1500);
|
||||
}
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
win = new BrowserWindow({ width: W, height: H, show: true, useContentSize: true, webPreferences: { contextIsolation: true, backgroundThrottling: false } });
|
||||
const tab = (id) => `document.querySelector('[data-tab="${id}"]')?.click()`;
|
||||
try {
|
||||
// ── Kundenportal ──────────────────────────────────────────────────────────
|
||||
await page('portal-start', '/home.php', { full: false });
|
||||
await page('portal-neues-ticket', '/ticket_new.php');
|
||||
await page('portal-status-pruefen', '/ticket_lookup.php', { full: false });
|
||||
await page('portal-login', '/login.php', { full: false });
|
||||
await page('portal-registrieren', '/register.php', { full: false });
|
||||
await page('portal-passwort-vergessen', '/forgot_password.php', { full: false });
|
||||
await page('portal-wissensdatenbank', '/kb.php');
|
||||
await page('portal-artikel', '/kb_article.php?slug=versandzeiten');
|
||||
await page('portal-seite-impressum', '/page.php?slug=impressum', { full: false });
|
||||
await page('portal-statusseite', '/status.php');
|
||||
await page('portal-assistent', '/home.php', { full: false, wait: 1000, prepWait: 1800, prep: `(() => { document.getElementById('as-bubble')?.click(); const i = document.getElementById('as-input'); if (i) { i.value = 'Wie lange dauert der Versand?'; document.getElementById('as-form').dispatchEvent(new Event('submit', { cancelable: true })); } })()` });
|
||||
await login('erika.muster@kunde.example', 'Demo-Passwort-1234', '/login.php');
|
||||
await page('portal-meine-tickets', '/dashboard.php');
|
||||
await page('portal-ticket-ansicht', '/ticket_view.php?n=260107');
|
||||
await page('portal-kunden-startseite', '/home.php', { full: false });
|
||||
await load('/logout.php');
|
||||
|
||||
// ── Mitarbeiter-Anmeldung ─────────────────────────────────────────────────
|
||||
await page('staff-login', '/staff/login.php', { full: false });
|
||||
await page('staff-passwort-vergessen', '/staff/forgot_password.php', { full: false });
|
||||
await login('lena@demo.example', 'Demo-Passwort-1234', '/staff/login.php');
|
||||
// Gesamtansicht mit Seitenleiste und die Seitenleiste allein (Adminportal aufgeklappt)
|
||||
await page('staff-gesamtansicht', '/staff/reports.php', { full: false, keepSidebar: true });
|
||||
await page('staff-seitenleiste', '/staff/reports.php', { full: false, clip: { x: 0, y: 0, width: SIDEBAR, height: 2000 }, prep: `(() => { const b = document.getElementById('admin-block'); if (b) b.style.display = 'block'; document.getElementById('sidebar').style.height = 'auto'; document.getElementById('sidebar').style.overflow = 'visible'; })()` });
|
||||
|
||||
// ── Arbeitsalltag ─────────────────────────────────────────────────────────
|
||||
await page('uebersicht-ueberblick', '/staff/reports.php');
|
||||
await page('uebersicht-verlauf', '/staff/reports.php?tab=activity');
|
||||
await page('uebersicht-auswertung', '/staff/reports.php?tab=analysis');
|
||||
await page('uebersicht-zufriedenheit', '/staff/reports.php?tab=csat');
|
||||
await page('meine-woche', '/staff/my_week.php');
|
||||
await page('tickets-liste', '/staff/tickets.php');
|
||||
await page('tickets-suche', '/staff/tickets.php?q=Rechnung', { full: false });
|
||||
await page('tickets-ansicht-speichern', '/staff/tickets.php?tab=open', { full: false });
|
||||
await page('tickets-wiedervorlage', '/staff/tickets.php?tab=snoozed', { full: false });
|
||||
await page('tickets-tastenkuerzel', '/staff/tickets.php', { full: false, prep: `document.dispatchEvent(new KeyboardEvent('keydown', { key: '?' }))` });
|
||||
await page('ticket-neu', '/staff/ticket_create.php');
|
||||
await page('ticket-ansicht', '/staff/ticket_view.php?n=260101');
|
||||
await page('ticket-ansicht-details', '/staff/ticket_view.php?n=260101', { prep: tab('details') });
|
||||
await page('ticket-ansicht-organisation', '/staff/ticket_view.php?n=260107', { prep: tab('manage') });
|
||||
await page('ticket-ansicht-mehr', '/staff/ticket_view.php?n=260101', { prep: tab('more') });
|
||||
await page('ticket-anhang', '/staff/ticket_view.php?n=260105');
|
||||
await page('ticket-antwortformular', '/staff/ticket_view.php?n=260102', { full: false, prep: `(() => { document.getElementById('message-field')?.scrollIntoView({block:'start'}); window.scrollBy(0,-80); document.querySelectorAll('details').forEach((d) => { if (/Später senden/.test(d.textContent)) d.open = true; }); })()` });
|
||||
await page('ticket-geplante-antwort', '/staff/ticket_view.php?n=260103');
|
||||
await page('ticket-zeiterfassung', '/staff/ticket_view.php?n=260108', { prep: tab('manage') });
|
||||
await page('ticket-tastenkuerzel', '/staff/ticket_view.php?n=260101', { full: false, prep: `document.dispatchEvent(new KeyboardEvent('keydown', { key: '?' }))` });
|
||||
await page('ticket-drucken', '/staff/ticket_print.php?n=260101');
|
||||
await page('aufgaben', '/staff/tasks.php');
|
||||
await page('sperrliste', '/staff/banlist.php');
|
||||
await page('verzeichnis', '/staff/agent_directory.php');
|
||||
await page('erwaehnungen', '/staff/mentions.php');
|
||||
await page('interner-chat', '/staff/chat.php?with=3', { wait: 1200 });
|
||||
await page('live-chat', '/staff/live_chat.php');
|
||||
await page('kunden-profil', '/staff/customer_profile.php?id=1');
|
||||
await page('apps', '/staff/apps.php');
|
||||
|
||||
// ── Profil ────────────────────────────────────────────────────────────────
|
||||
await page('profil-konto', '/staff/profile.php?tab=account');
|
||||
await page('profil-einstellungen', '/staff/profile.php?tab=settings');
|
||||
await page('profil-sicherheit', '/staff/profile.php?tab=security');
|
||||
await page('profil-signatur', '/staff/profile.php?tab=signature');
|
||||
await page('verfuegbarkeit', '/staff/availability.php');
|
||||
|
||||
// ── Berichte ──────────────────────────────────────────────────────────────
|
||||
await page('bericht-baukasten', '/staff/report_builder.php?dimension=department');
|
||||
await page('bericht-zeitplan', '/staff/report_builder.php#versand', { prep: `document.getElementById('versand')?.setAttribute('open','')` });
|
||||
|
||||
// ── Adminportal ───────────────────────────────────────────────────────────
|
||||
const admin = [
|
||||
['admin-abteilungen', '/staff/departments.php'], ['admin-abteilung-bearbeiten', '/staff/department_form.php?dept=1'], ['admin-prioritaeten', '/staff/priorities.php'],
|
||||
['admin-sla-plaene', '/staff/sla_plans.php'], ['admin-hilfethemen', '/staff/help_topics.php'], ['admin-ticket-felder', '/staff/custom_fields.php'],
|
||||
['admin-listen', '/staff/custom_lists.php'], ['admin-vorlagen', '/staff/canned_responses.php'], ['admin-seiten', '/staff/site_pages.php'],
|
||||
['admin-sprachen', '/staff/languages.php'], ['admin-ticket-vorlagen', '/staff/ticket_templates.php'], ['admin-statusseite', '/staff/status_page.php'],
|
||||
['admin-mail-warteschlange', '/staff/mail_queue.php'], ['admin-anmelde-waechter', '/staff/login_log.php'], ['admin-email-adressen', '/staff/email_addresses.php'],
|
||||
['admin-postfaecher', '/staff/imap_accounts.php'], ['admin-email-vorlagen', '/staff/email_templates.php'], ['admin-wissensdatenbank', '/staff/kb_manage.php'],
|
||||
['admin-assistent-einstellungen', '/staff/assistant.php?tab=settings'], ['admin-assistent-antworten', '/staff/assistant.php?tab=answers'], ['admin-assistent-log', '/staff/assistant.php?tab=log'],
|
||||
['admin-plugins-installiert', '/staff/plugins.php?tab=installed'], ['admin-plugins-hinzufuegen', '/staff/plugins.php?tab=add'], ['admin-plugins-katalog', '/staff/plugins.php?tab=settings'],
|
||||
['admin-api-schluessel', '/staff/api_keys.php'], ['admin-webhooks', '/staff/webhooks.php'], ['admin-kundenverzeichnis', '/staff/customers.php'],
|
||||
['admin-organisationen', '/staff/organizations.php'], ['admin-mitarbeiter', '/staff/staff_manage.php'], ['admin-mitarbeiter-bearbeiten', '/staff/staff_manage.php?edit=2'],
|
||||
['admin-mitarbeiter-zugriff', '/staff/staff_manage.php?edit=2&tab=access'], ['admin-teams', '/staff/teams.php'], ['admin-rollen', '/staff/roles.php'],
|
||||
['admin-sicherheitspruefung', '/staff/security_check.php'], ['admin-protokoll', '/staff/audit_log.php'], ['admin-ticket-regeln', '/staff/ticket_rules.php'],
|
||||
['admin-eskalationsregeln', '/staff/escalation_rules.php'], ['admin-wiederkehrende-tickets', '/staff/recurring_tickets.php'], ['admin-zeiterfassung', '/staff/time_entries.php'],
|
||||
['admin-geschaeftszeiten', '/staff/business_hours.php'], ['admin-einstellungen-allgemein', '/staff/settings.php?tab=allgemein'], ['admin-einstellungen-design', '/staff/settings.php?tab=design'],
|
||||
['admin-einstellungen-email', '/staff/settings.php?tab=email'], ['admin-einstellungen-benachrichtigungen', '/staff/settings.php?tab=benachrichtigungen'],
|
||||
['admin-einstellungen-uploads', '/staff/settings.php?tab=uploads'], ['admin-einstellungen-footer', '/staff/settings.php?tab=footer'], ['admin-einstellungen-cron', '/staff/settings.php?tab=cron'],
|
||||
['admin-einstellungen-datenschutz', '/staff/settings.php?tab=datenschutz'], ['admin-backup', '/staff/backup.php'], ['admin-system-update', '/staff/update.php'],
|
||||
['admin-osticket-import', '/staff/import_osticket.php'], ['admin-probleme', '/staff/problems.php'],
|
||||
];
|
||||
for (const [name, url] of admin) await page(name, url);
|
||||
await page('admin-menue-aktionen', '/staff/staff_manage.php', { full: false, prep: `document.querySelector('.sm-menu summary')?.click()` });
|
||||
|
||||
// ── Plugin-Seiten ─────────────────────────────────────────────────────────
|
||||
const plugins = [['tagesbericht', 'einstellungen'], ['tagesbericht', 'persoenlich'], ['schichtuebergabe', 'schichtuebergabe'], ['rundmail', 'rundmail'], ['spamfilter', 'spamfilter'],
|
||||
['virenscan', 'virenscan'], ['ueberwachung', 'ueberwachung'], ['datenschutz', 'datenschutz'], ['vertretung', 'vertretung'], ['community-bot', 'community-bot'], ['authentik', 'einstellungen'],
|
||||
['authentik', 'konto'], ['nas-backup', 'einstellungen'], ['virustotal', 'virustotal'], ['deskhub-cache-manager', 'status'], ['code-issue-sync', 'manage'], ['cloudflare-protection', 'status']];
|
||||
for (const [slug, pg] of plugins) await page(`plugin-${slug}-${pg}`, `/staff/plugin.php?p=${slug}&page=${pg}`);
|
||||
} catch (e) {
|
||||
report.fail.push('FATAL ' + e.stack);
|
||||
}
|
||||
fs.writeFileSync(path.join(OUT, '_bericht.json'), JSON.stringify(report, null, 1));
|
||||
console.log(`Aufgenommen: ${report.ok.length}, Fehler: ${report.fail.length}`);
|
||||
report.fail.forEach((f) => console.log(' - ' + f));
|
||||
app.quit();
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
'use strict';
|
||||
/**
|
||||
* tools/manual/capture_roles.js: nimmt die neuen Bilder für das Handbuch auf (Rollen und Rechte) (Demo-Installation mit erfundenen Daten, siehe seed_manual.php).
|
||||
* 1. php tools/demo-video/seed_demo.php <Ordner> && php tools/manual/seed_manual.php <Ordner> && php tools/manual/install_plugins.php <Ordner> <Plugin-ZIPs>
|
||||
* 2. DESKHUB_CATALOG_DEFAULTS=tests/fixtures/no-catalog.php php -S 127.0.0.1:8097 -t <Ordner>
|
||||
* 3. electron tools/manual/capture_manual.js <Bilderordner>
|
||||
*/
|
||||
const { app, BrowserWindow } = require('electron');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const BASE = 'http://127.0.0.1:8097';
|
||||
const OUT = path.resolve(process.argv[2] || path.join(__dirname, 'shots'));
|
||||
const W = 1180; // schmaler als ein Bildschirm: Die Bilder werden auf A4 verkleinert und müssen lesbar bleiben
|
||||
const SIDEBAR = 240; // Breite der Seitenleiste; sie wird aus den Bildern der Seiten herausgeschnitten (eigenes Bild vom Menü)
|
||||
const H = 860;
|
||||
const MAXH = 2400;
|
||||
fs.mkdirSync(OUT, { recursive: true });
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
let win;
|
||||
const run = (js) => win.webContents.executeJavaScript(js, true);
|
||||
const report = { ok: [], fail: [] };
|
||||
|
||||
/** Seite laden und alles entfernen, was nur zur Demo-Installation gehört (SQLite-Hinweis, Zähler der Sitzung). */
|
||||
async function load(url, wait = 700) {
|
||||
await win.loadURL(BASE + url);
|
||||
await sleep(wait);
|
||||
await run(`(() => {
|
||||
document.getElementById('session-timer-bar')?.remove();
|
||||
document.querySelectorAll('div,p,section,aside').forEach((e) => { if (e.children.length < 8 && /SQLite-Modus aktiv/.test(e.textContent) && e.textContent.length < 400) e.remove(); });
|
||||
})()`);
|
||||
}
|
||||
|
||||
/** Bild der Seite speichern: höchstens MAXH hoch; mit full = false nur der sichtbare Teil. */
|
||||
async function shot(name, { full = true, clip = null, keepSidebar = false } = {}) {
|
||||
let h = H;
|
||||
if (full) {
|
||||
h = await run('Math.max(document.documentElement.scrollHeight, document.body.scrollHeight)');
|
||||
h = Math.min(Math.max(h, 500), MAXH);
|
||||
}
|
||||
win.setContentSize(W, h);
|
||||
await sleep(350);
|
||||
const hasSidebar = await run(`(() => { const s = document.getElementById('sidebar'); return !!s && s.getBoundingClientRect().width > 100; })()`);
|
||||
let img = await win.webContents.capturePage();
|
||||
if (clip) img = img.crop(clip);
|
||||
else if (hasSidebar && !keepSidebar) img = img.crop({ x: SIDEBAR, y: 0, width: W - SIDEBAR, height: h });
|
||||
fs.writeFileSync(path.join(OUT, name + '.png'), img.toPNG());
|
||||
win.setContentSize(W, H);
|
||||
report.ok.push(name);
|
||||
}
|
||||
|
||||
async function page(name, url, opts = {}) {
|
||||
try {
|
||||
await load(url, opts.wait);
|
||||
if (opts.prep) { await run(opts.prep); await sleep(opts.prepWait || 400); }
|
||||
await shot(name, opts);
|
||||
} catch (e) {
|
||||
report.fail.push(name + ': ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function login(email, pass, loginUrl) {
|
||||
await load(loginUrl, 600);
|
||||
await run(`(() => { const f = document.querySelector('form'); f.querySelector('[name=email]').value = ${JSON.stringify(email)}; f.querySelector('[name=password]').value = ${JSON.stringify(pass)}; f.submit(); })()`);
|
||||
await sleep(1500);
|
||||
}
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
win = new BrowserWindow({ width: W, height: H, show: true, useContentSize: true, webPreferences: { contextIsolation: true, backgroundThrottling: false } });
|
||||
try {
|
||||
await login('lena@demo.example', 'Demo-Passwort-1234', '/staff/login.php');
|
||||
await page('admin-rollen', '/staff/roles.php', { prep: `(() => { const d = document.querySelector('details'); if (d) d.open = true; })()` });
|
||||
await page('admin-rollen-rechte', '/staff/roles.php?new=1', { prep: `(() => { document.querySelector('[data-tab="perms"]')?.click(); })()` });
|
||||
} catch (e) {
|
||||
report.fail.push('FATAL ' + e.stack);
|
||||
}
|
||||
console.log(`Aufgenommen: ${report.ok.length}, Fehler: ${report.fail.length}`);
|
||||
report.fail.forEach((f) => console.log(' - ' + f));
|
||||
app.quit();
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
'use strict';
|
||||
/**
|
||||
* tools/manual/capture_windows.js: nimmt die Bilder vom Windows-Programm für das Handbuch auf. Es nutzt die Test-Attrappe
|
||||
* desktop/test/mock.html (erfundene Beispieldaten, kein Server nötig).
|
||||
* electron tools/manual/capture_windows.js <Bilderordner>
|
||||
*/
|
||||
const { app, BrowserWindow } = require('electron');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const OUT = path.resolve(process.argv[2] || path.join(__dirname, 'shots-windows'));
|
||||
const MOCK = path.resolve(__dirname, '../../../desktop/test/mock.html');
|
||||
fs.mkdirSync(OUT, { recursive: true });
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const report = { ok: [], fail: [] };
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
const win = new BrowserWindow({ width: 1280, height: 800, show: true, useContentSize: true, webPreferences: { contextIsolation: false, nodeIntegration: false, backgroundThrottling: false } });
|
||||
const run = (js) => win.webContents.executeJavaScript(js, true);
|
||||
const shot = async (name) => {
|
||||
await sleep(700);
|
||||
fs.writeFileSync(path.join(OUT, name + '.png'), (await win.webContents.capturePage()).toPNG());
|
||||
report.ok.push(name);
|
||||
};
|
||||
const step = async (name, js, wait = 900) => {
|
||||
try { if (js) await run(js); await sleep(wait); await shot(name); } catch (e) { report.fail.push(name + ': ' + e.message); }
|
||||
};
|
||||
try {
|
||||
await win.loadFile(MOCK);
|
||||
await sleep(1500);
|
||||
await step('windows-uebersicht', `go('dashboard')`);
|
||||
await step('windows-tickets', `go('tickets')`, 1400);
|
||||
await step('windows-ticket', `go('ticket', 'T-1001')`, 1400);
|
||||
await step('windows-neues-ticket', `go('newticket')`);
|
||||
await step('windows-aufgaben', `go('tasks')`);
|
||||
await step('windows-kunden', `go('customers')`);
|
||||
await step('windows-chat', `go('chat')`);
|
||||
await step('windows-berichte', `go('reports')`);
|
||||
await step('windows-live-chat', `go('livechat')`);
|
||||
await step('windows-verfuegbarkeit', `go('availability')`);
|
||||
await step('windows-verwaltung', `go('admin')`);
|
||||
await step('windows-einstellungen', `go('settings')`);
|
||||
await run(`document.documentElement.setAttribute('data-theme','dark'); try { window.deskhub.setSettings({ theme: 'dark' }).then((p) => { state.prefs = p; applyTheme(); }); } catch (e) {}`);
|
||||
await step('windows-dunkel', `go('tickets')`, 1200);
|
||||
await run(`try { window.deskhub.setSettings({ theme: 'light' }).then((p) => { state.prefs = p; applyTheme(); }); } catch (e) {}`);
|
||||
await step('windows-anmeldung', `(typeof showLogin === 'function') && showLogin()`, 900);
|
||||
} catch (e) {
|
||||
report.fail.push('FATAL ' + e.stack);
|
||||
}
|
||||
fs.writeFileSync(path.join(OUT, '_bericht.json'), JSON.stringify(report, null, 1));
|
||||
console.log(`Windows-Programm: ${report.ok.length} Bilder, ${report.fail.length} Fehler`);
|
||||
report.fail.forEach((f) => console.log(' - ' + f));
|
||||
app.quit();
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env python3
|
||||
"""tools/manual/find_pages.py <pass.pdf> <toc.json> <pages.json>: sucht die Seitenzahl jeder Überschrift für das Inhaltsverzeichnis."""
|
||||
import io
|
||||
import json
|
||||
import sys
|
||||
|
||||
import pymupdf
|
||||
|
||||
pdf, toc_file, out = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
toc = json.load(io.open(toc_file, encoding='utf-8'))
|
||||
doc = pymupdf.open(pdf)
|
||||
heads = [] # (Seite, Text, Größe)
|
||||
for pno in range(len(doc)):
|
||||
for b in doc[pno].get_text('dict')['blocks']:
|
||||
for l in b.get('lines', []):
|
||||
txt = ''.join(s['text'] for s in l['spans']).strip()
|
||||
size = max((s['size'] for s in l['spans']), default=0)
|
||||
if size >= 12 and txt:
|
||||
heads.append((pno + 1, txt, size))
|
||||
pages = {}
|
||||
cursor = 0
|
||||
missing = []
|
||||
for t in toc:
|
||||
key = t['title'].strip()
|
||||
found = None
|
||||
for i in range(cursor, len(heads)):
|
||||
p, txt, size = heads[i]
|
||||
# Überschrift = "<Nr> <Titel>"; lange Titel brechen um, darum Anfang vergleichen
|
||||
flat = txt.replace(' ', ' ')
|
||||
if flat.endswith(key) or flat == t['num'] + ' ' + key or key.startswith(flat.split(' ', 1)[-1][:25]) and flat.startswith(t['num'] + ' '):
|
||||
if flat.startswith(t['num'] + ' ') or flat.startswith(t['num']):
|
||||
found = (i, p)
|
||||
break
|
||||
if found:
|
||||
cursor = found[0] + 1
|
||||
pages['k' + t['num'].replace('.', '_')] = found[1]
|
||||
else:
|
||||
missing.append(t['num'] + ' ' + key)
|
||||
json.dump(pages, io.open(out, 'w', encoding='utf-8'))
|
||||
print('gefunden: %d von %d; fehlend: %s' % (len(pages), len(toc), missing[:10]))
|
||||
@@ -0,0 +1,37 @@
|
||||
<h1>Einführung und Überblick</h1>
|
||||
|
||||
<h2>Was ist DeskHub?</h2>
|
||||
<p><b>DeskHub</b> ist ein vollständiges, selbst gehostetes Helpdesk-System. Kunden stellen Supportanfragen über ein individuell gestaltbares <b>Kundenportal</b>, per E-Mail, über den digitalen Assistenten, den Live-Chat oder über angebundene Systeme (WordPress, Minecraft, eigene Programme über die Schnittstelle). Das Support-Team bearbeitet alles zentral als <b>Tickets</b> in der Weboberfläche, im Windows-Programm, in der Android-App oder in der Chrome-Erweiterung.</p>
|
||||
<p>Alle Daten bleiben auf eurem eigenen Server. Fragen an den digitalen Assistenten, Tickets und Anhänge verlassen ihn nicht. DeskHub braucht keinen externen KI- oder Cloud-Dienst.</p>
|
||||
<fig n="portal-start">Die Startseite des Kundenportals (Demo-Daten).</fig>
|
||||
|
||||
<h2>Die drei Bereiche</h2>
|
||||
<table>
|
||||
<tr><th>Bereich</th><th>Für wen?</th><th>Adresse</th></tr>
|
||||
<tr><td><b>Kundenportal</b></td><td>Eure Kunden</td><td><code>eure-domain.de/</code></td></tr>
|
||||
<tr><td><b>Mitarbeiterbereich</b> (mit <b>Adminportal</b>)</td><td>Support-Team und Administratoren</td><td><code>eure-domain.de/staff/</code></td></tr>
|
||||
<tr><td><b>Apps</b></td><td>Support-Team und Administratoren am PC und unterwegs</td><td>Windows-Programm, Android-App, Chrome-Erweiterung; WordPress-Plugin und Minecraft-Plugin zum Anbinden von Webseiten und Spielservern</td></tr>
|
||||
</table>
|
||||
<p>Ein Administrator-Konto ist ein normales Mitarbeiter-Konto mit der Rolle „Admin“. Beim Anmelden gibt es keinen Unterschied, nur bei dem, was man danach sieht und darf.</p>
|
||||
|
||||
<h2>Der Weg eines Tickets</h2>
|
||||
<ol class="steps">
|
||||
<li><b>Eingang:</b> Ein Kunde schreibt über das Portal, per E-Mail (Abruf aus einem Postfach), über den Assistenten, den Live-Chat, die Schnittstelle (API) oder ein Plugin. Bei Mail-Antworten ordnet DeskHub die Nachricht dem passenden Ticket zu.</li>
|
||||
<li><b>Einsortieren:</b> Ticket-Regeln, die Abteilung und die automatische Zuweisung legen Abteilung, Priorität und Bearbeiter fest. Der SLA-Plan berechnet die Fristen für erste Antwort und Lösung.</li>
|
||||
<li><b>Bearbeiten:</b> Ein Mitarbeiter antwortet, schreibt interne Notizen, setzt den Status, verknüpft Tickets, erfasst Zeit oder legt das Ticket auf Wiedervorlage.</li>
|
||||
<li><b>Abschluss:</b> Das Ticket wird gelöst und später geschlossen. Der Kunde kann per Sternen bewerten (Zufriedenheitsabfrage). Antwortet er später, öffnet sich das Ticket wieder.</li>
|
||||
<li><b>Auswertung:</b> Übersicht, Berichte und der Bericht-Baukasten zeigen Aufkommen, Antwortzeiten, SLA-Einhaltung und Zufriedenheit.</li>
|
||||
</ol>
|
||||
|
||||
<h2>Funktionsüberblick</h2>
|
||||
<table>
|
||||
<tr><th>Bereich</th><th>Funktionen</th></tr>
|
||||
<tr><td>Kundenportal</td><td>Tickets erstellen und verfolgen (auch ohne Konto), Wissensdatenbank, frei gestaltbare Seiten, digitaler Assistent, Statusseite, Zufriedenheitsabfrage, Sprachwechsel</td></tr>
|
||||
<tr><td>Tickets</td><td>Listen mit Filtern und gespeicherten Ansichten, Wiedervorlage, Beobachter, verknüpfte Tickets, geplante Antworten, Textbausteine, Signaturen, Screenshot einfügen, Zeiterfassung, Zusammenführen und Teilen, Ticket-Sperre, Tastenkürzel</td></tr>
|
||||
<tr><td>Automatik</td><td>Zuweisung beim Eingang oder Öffnen, Ticket-Regeln, Eskalationsregeln, wiederkehrende Tickets, SLA-Pläne mit Geschäftszeiten, automatisches Schließen und Erinnerungen, Status „Offline“ bei Inaktivität</td></tr>
|
||||
<tr><td>E-Mail</td><td>Versand über SMTP (auch je Absenderadresse), Abruf über IMAP aus mehreren Postfächern, Mail-Warteschlange mit Wiederholungen, Vorlagen für alle automatischen Mails</td></tr>
|
||||
<tr><td>Team</td><td>Abteilungen, Teams, Rollen mit Rechten (auch je Abteilung), Chat, Aufgaben, Erwähnungen, Verfügbarkeit und Schichten, „Meine Woche“</td></tr>
|
||||
<tr><td>Auswertung</td><td>Übersicht mit vier Reitern, Berichte, Bericht-Baukasten mit CSV-Export und Versand per E-Mail</td></tr>
|
||||
<tr><td>Integration</td><td>API-Schlüssel und Ticket-API, Webhooks, WordPress-Plugin, Minecraft-Plugin, Chrome-Erweiterung, Plugins</td></tr>
|
||||
<tr><td>Betrieb</td><td>System-Update und Datenbank-Update, Backup mit Prüfung, Anmelde-Wächter mit Passkeys, Sicherheitsprüfung, osTicket-Import, Datenschutz-Werkzeuge</td></tr>
|
||||
</table>
|
||||
@@ -0,0 +1,52 @@
|
||||
<h1>Installation und erste Schritte</h1>
|
||||
|
||||
<h2>Voraussetzungen</h2>
|
||||
<ul>
|
||||
<li>PHP 8.2 oder neuer</li>
|
||||
<li>Eine MySQL-Datenbank (für kleine Installationen und Tests genügt SQLite)</li>
|
||||
<li>PHP-Erweiterung <code>imap</code>, wenn E-Mails automatisch abgerufen werden sollen</li>
|
||||
<li>PHP-Erweiterung <code>zip</code> für Theme-Upload, Plugin-Installation und System-Update per ZIP</li>
|
||||
<li>Empfohlen: <code>curl</code>, <code>openssl</code> und HTTPS</li>
|
||||
</ul>
|
||||
|
||||
<h2>Der Installationsassistent</h2>
|
||||
<ol class="steps">
|
||||
<li>Alle DeskHub-Dateien auf den Server hochladen (zum Beispiel per FTP).</li>
|
||||
<li>Im Browser <code>eure-domain.de/install/</code> aufrufen.</li>
|
||||
<li>Dem Assistenten folgen: Datenbank-Zugangsdaten, Website-Name, erstes Admin-Konto.</li>
|
||||
<li>Danach unter <code>eure-domain.de/staff/login.php</code> mit dem neuen Konto anmelden.</li>
|
||||
</ol>
|
||||
<p>Der Assistent prüft zuerst die Voraussetzungen (PHP-Version, Datenbanktreiber, <code>openssl</code>, beschreibbare Ordner). Das Admin-Passwort braucht mindestens 10 Zeichen mit Groß- und Kleinbuchstaben, Ziffer und Sonderzeichen. Zum Schluss spielt er alle Datenbank-Änderungen ein, erzeugt den Verschlüsselungsschlüssel und sperrt sich selbst. Fehler bei der Datenbankverbindung erklärt er verständlich (falsches Passwort, Server nicht erreichbar, Datenbank fehlt).</p>
|
||||
<div class="warn">Löscht nach der Installation den Ordner <code>install</code> vom Server (oder sperrt ihn). Der Assistent überschreibt eine bestehende Installation nie: Existiert schon eine <code>config/config.php</code> mit Datenbank und Mitarbeiterkonten, meldet er „Bereits installiert“.</div>
|
||||
|
||||
<h2>Schnellstart: die ersten fünf Schritte</h2>
|
||||
<p>Direkt nach der Installation läuft DeskHub, kann aber noch keine E-Mails verschicken oder abrufen. Arbeitet diese Liste einmal ab:</p>
|
||||
<table>
|
||||
<tr><th>#</th><th>Was</th><th>Wo</th><th>Warum</th></tr>
|
||||
<tr><td>1</td><td>Abteilungen anlegen (z. B. „Technik“, „Vertrieb“)</td><td>Adminportal → Abteilungen</td><td>Jedes Ticket gehört zu genau einer Abteilung</td></tr>
|
||||
<tr><td>2</td><td>SMTP einrichten (E-Mail-Versand)</td><td>Adminportal → Einstellungen → E-Mail</td><td>Damit Kunden Bestätigungen bekommen</td></tr>
|
||||
<tr><td>3</td><td>Postfach einrichten (E-Mail-Abruf, optional)</td><td>Adminportal → Postfächer (Abruf)</td><td>Damit eingehende Mails zu Tickets werden</td></tr>
|
||||
<tr><td>4</td><td>Automatisierung prüfen</td><td>Adminportal → Einstellungen → Cron-Jobs</td><td>Damit Versand, Abruf und Prüfungen von selbst laufen</td></tr>
|
||||
<tr><td>5</td><td>Mitarbeiter anlegen</td><td>Adminportal → Mitarbeiter</td><td>Damit nicht nur ihr allein Tickets bearbeitet</td></tr>
|
||||
</table>
|
||||
|
||||
<h2>Automatisierung (Cron-Jobs)</h2>
|
||||
<p>Einige Aufgaben müssen regelmäßig von selbst laufen: E-Mail-Abruf (alle 5 Minuten), SLA-Prüfung (alle 15 Minuten), Eskalationsregeln (stündlich), wiederkehrende Tickets (täglich), Backups (nach Zeitplan), Datenanonymisierung (täglich), außerdem Wiedervorlagen, geplante Antworten, Berichtsversand und Mail-Warteschlange. Unter <b>Einstellungen → Cron-Jobs</b> gibt es drei Wege:</p>
|
||||
<table>
|
||||
<tr><th>Weg</th><th>Beschreibung</th></tr>
|
||||
<tr><td><b>A: Hintergrund-Betrieb (Standard)</b></td><td>Von Anfang an aktiv. Sobald jemand die Website, das Backend oder eine App nutzt, prüft DeskHub, ob etwas fällig ist, und führt es aus. Reicht bei regelmäßigen Besuchern.</td></tr>
|
||||
<tr><td><b>B: Echter Server-Cronjob</b></td><td>Mit eigenem Server und Shell-Zugriff trägt DeskHub per Klick auf „Jetzt installieren“ die nötigen Cronjobs selbst ein. Läuft zuverlässig zur exakten Zeit, auch nachts.</td></tr>
|
||||
<tr><td><b>C: Manuell</b></td><td>Jede Aufgabe hat einen Knopf „Jetzt ausführen“.</td></tr>
|
||||
</table>
|
||||
<fig n="admin-einstellungen-cron">Einstellungen → Cron-Jobs: Aufgaben mit Zeitplan, letztem Lauf und „Jetzt ausführen“.</fig>
|
||||
<div class="tip">Bei einem eigenen Server richtet Weg B ein und schaltet danach den eingebauten Ersatz ab: Lange Jobs (Backup, Mail-Abruf) belegen sonst einen Webserver-Prozess, und hinter Cloudflare kann das zur Fehlerseite „524“ führen.</div>
|
||||
|
||||
<h2>Betrieb absichern (Kurzfassung)</h2>
|
||||
<ol>
|
||||
<li><b>HTTPS</b> einrichten und die Website-URL in den Einstellungen auf <code>https://</code> stellen.</li>
|
||||
<li><b>Interne Ordner sperren</b> (auf Apache erledigt die mitgelieferte <code>.htaccess</code> das; auf nginx tragt ihr die Regeln aus der README ein).</li>
|
||||
<li>Hinter einem <b>Reverse-Proxy</b> (nginx, Traefik, Caddy, Cloudflare) die vertrauenswürdigen Proxys in der <code>config.php</code> eintragen, damit die echte Besucher-IP erkannt wird.</li>
|
||||
<li>Den <b>Ordner <code>install/</code> löschen</b>, PHP-Fehleranzeige abschalten, <b>Zwei-Faktor-Anmeldung</b> für alle Administratoren aktivieren.</li>
|
||||
<li><b>Backups</b> einrichten und prüfen, den Ordner <code>backups/</code> nie öffentlich machen.</li>
|
||||
<li>Die <b>Sicherheitsprüfung</b> im Adminportal einmal ausführen (siehe Kapitel 17).</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,59 @@
|
||||
<h1>Das Kundenportal</h1>
|
||||
<p>Das Kundenportal ist das, was eure Kunden sehen. Es läuft unter der Hauptadresse eurer Installation und lässt sich mit Logo, Farben, Themes, eigenen Seiten und Sprachen anpassen (Kapitel 11 und 13).</p>
|
||||
|
||||
<h2>Startseite</h2>
|
||||
<p>Die Startseite bietet vier Wege: <b>Neues Ticket eröffnen</b>, <b>Ticketstatus überprüfen</b>, die <b>Wissensdatenbank</b> (wenn eingeschaltet) und – für angemeldete Kunden – <b>Meine Tickets</b>. Oben in der Leiste stehen Anmelden/Registrieren, die Sprachflaggen und, falls aktiv, der Link „Status“ zur Statusseite. Zusätzliche Seiten wie Impressum oder AGB erscheinen in der Fußzeile.</p>
|
||||
<fig n="portal-start">Startseite des Kundenportals.</fig>
|
||||
|
||||
<h2>Konto, Anmeldung und Passwort</h2>
|
||||
<p>Kunden können mit Konto oder ganz ohne Konto arbeiten. Mit Konto sehen sie alle ihre Tickets an einem Ort. Ein Kundenkonto hat keinen Zugang zum Mitarbeiterbereich und umgekehrt.</p>
|
||||
<ul>
|
||||
<li><b>Registrieren:</b> Name, E-Mail-Adresse und Passwort angeben. Gesperrte Adressen (Sperrliste) werden abgewiesen.</li>
|
||||
<li><b>Anmelden:</b> mit E-Mail und Passwort; mit dem Plugin „authentik-Anmeldung“ zusätzlich über den Anmeldedienst der Firma.</li>
|
||||
<li><b>Passwort vergessen:</b> Der Kunde bekommt einen Link per E-Mail, mit dem er ein neues Passwort vergibt.</li>
|
||||
</ul>
|
||||
<fig n="portal-login">Kundenanmeldung.</fig>
|
||||
<fig n="portal-registrieren">Registrierung eines Kundenkontos.</fig>
|
||||
<fig n="portal-passwort-vergessen">„Passwort vergessen“ im Kundenportal.</fig>
|
||||
|
||||
<h2>Ein Ticket erstellen</h2>
|
||||
<ol class="steps">
|
||||
<li>Auf der Startseite <b>„Neues Ticket eröffnen“</b> wählen.</li>
|
||||
<li>Hilfethema wählen (wenn eingerichtet), Name, E-Mail-Adresse, Betreff und Beschreibung eintragen, optional Dateien anhängen.</li>
|
||||
<li>Absenden. Die <b>Ticketnummer</b> erscheint auf der Seite und kommt per E-Mail.</li>
|
||||
</ol>
|
||||
<p>Je nach Hilfethema oder Abteilung zeigt das Formular zusätzliche Felder (Text, Mehrzeilig, Auswahl, Checkbox, Zahl, Datum, teils Pflicht) und eigene Überschriften. Name und E-Mail werden immer abgefragt. Angemeldete Kunden brauchen sie nicht erneut einzugeben. Verdächtige Massen-Einsendungen fängt ein verstecktes Spam-Feld ab.</p>
|
||||
<fig n="portal-neues-ticket">Das Formular „Neues Ticket“ im Kundenportal.</fig>
|
||||
|
||||
<h2>Meine Tickets und Ticketansicht</h2>
|
||||
<p>Angemeldete Kunden sehen unter <b>Meine Tickets</b> alle eigenen Tickets mit Nummer, Betreff, Status und letzter Änderung. Ein Klick öffnet das Ticket: Der Verlauf zeigt alle öffentlichen Nachrichten (interne Notizen sieht der Kunde nie). Darunter kann der Kunde antworten, Dateien anhängen und, wenn das Ticket gelöst ist, es wieder öffnen.</p>
|
||||
<fig n="portal-meine-tickets">Liste „Meine Tickets“.</fig>
|
||||
<fig n="portal-ticket-ansicht">Ein Ticket aus Sicht des Kunden mit Verlauf und Antwortfeld.</fig>
|
||||
<fig n="portal-kunden-startseite">Startseite eines angemeldeten Kunden.</fig>
|
||||
|
||||
<h2>Ticketstatus ohne Konto prüfen</h2>
|
||||
<p>Mit Ticketnummer und E-Mail-Adresse lässt sich unter <b>„Ticketstatus überprüfen“</b> jederzeit nachsehen, ob es eine Antwort gibt, auch ohne Konto.</p>
|
||||
<fig n="portal-status-pruefen">Statusabfrage mit Ticketnummer.</fig>
|
||||
|
||||
<h2>Wissensdatenbank</h2>
|
||||
<p>Die Wissensdatenbank besteht aus Artikeln, die ihr im Adminportal pflegt (Kapitel 11). Im Portal sind sie nach Kategorien gegliedert und durchsuchbar. Kunden finden dort Antworten, ohne ein Ticket zu eröffnen; der digitale Assistent nutzt dieselben Artikel.</p>
|
||||
<fig n="portal-wissensdatenbank">Wissensdatenbank mit Suche und Kategorien.</fig>
|
||||
<fig n="portal-artikel">Ein einzelner Artikel.</fig>
|
||||
|
||||
<h2>Digitaler Assistent</h2>
|
||||
<p>Der Assistent erscheint als Sprechblase unten rechts und beantwortet Fragen aus den <b>zusätzlichen Antworten</b> und den veröffentlichten <b>Artikeln der Wissensdatenbank</b> – ohne externen KI-Dienst, die Fragen verlassen den Server nicht. Findet er nichts, bietet er an, ein Ticket zu erstellen, fragt Name, E-Mail, Betreff und Beschreibung ab, zeigt eine Zusammenfassung und legt nach Bestätigung das Ticket an (mit dem Tag „Assistent“). Nach einer Antwort kann der Kunde „Das hat geholfen“ oder „Ticket erstellen“ wählen.</p>
|
||||
<p>Der Assistent ersetzt das Live-Chat-Fenster, solange der Live-Chat ausgeschaltet oder niemand erreichbar ist. Sobald ein Mitarbeiter „Verfügbar“ ist, sehen Kunden wieder den normalen Live-Chat. Missbrauchsschutz: höchstens 30 Fragen je 10 Minuten und Sitzung, höchstens 3 Tickets pro Stunde und Adresse.</p>
|
||||
<fig n="portal-assistent">Der digitale Assistent beantwortet eine Frage im Portal.</fig>
|
||||
|
||||
<h2>Statusseite</h2>
|
||||
<p>Unter <code>/status.php</code> sehen Kunden, ob eure Dienste laufen (In Betrieb, Wartung, Eingeschränkt, Ausfall) und welche Störungsmeldungen mit Verlauf es gibt. Gepflegt wird sie im Adminportal (Kapitel 11).</p>
|
||||
<fig n="portal-statusseite">Öffentliche Statusseite.</fig>
|
||||
|
||||
<h2>Eigene Seiten, Zufriedenheit und Sprachen</h2>
|
||||
<ul>
|
||||
<li><b>Seiten:</b> Frei gestaltbare Inhaltsseiten (Impressum, AGB, Datenschutz) erscheinen im Portal unter eigener Adresse.</li>
|
||||
<li><b>Zufriedenheitsabfrage:</b> Nach dem Lösen oder Schließen bekommt der Kunde (Wartezeit einstellbar) eine Mail mit fünf Sternen; ein Klick öffnet die Bewertungsseite, gespeichert wird erst mit dem Knopf (so zählen automatische Link-Prüfungen nicht).</li>
|
||||
<li><b>Sprache:</b> Über die Flaggen oben wechseln Besucher die Sprache. Deutsch ist eingebaut, weitere Sprachen kommen als Sprachpaket-Plugin (z. B. English).</li>
|
||||
<li><b>Erinnerung:</b> Reagiert ein Kunde nach einer Antwort mehrere Tage nicht, bekommt er einmal eine freundliche Erinnerung (bei automatischem Schließen mit Restzeit).</li>
|
||||
</ul>
|
||||
<fig n="portal-seite-impressum">Eine frei gestaltete Seite (hier: Impressum mit Beispieltext).</fig>
|
||||
@@ -0,0 +1,48 @@
|
||||
<h1>Anmelden und der Mitarbeiterbereich</h1>
|
||||
|
||||
<h2>Mitarbeiter-Anmeldung</h2>
|
||||
<p>Mitarbeiter und Administratoren melden sich unter <code>/staff/login.php</code> an. Wer die Zwei-Faktor-Anmeldung eingerichtet hat, gibt danach den Code seiner Authenticator-App ein. Mit dem Plugin „authentik-Anmeldung“ erscheint zusätzlich der Knopf „Mit authentik anmelden“.</p>
|
||||
<p><b>Mit Passkey anmelden (ab Server 1.0.77):</b> Unter dem Anmeldeformular steht „🔑 Mit Passkey anmelden“. Ein Klick öffnet die Abfrage des Geräts (Fingerabdruck, Gesicht oder PIN); E-Mail-Adresse, Passwort und 2FA-Code entfallen. Der Knopf erscheint nur, wenn der Browser Passkeys unterstützt und die Verbindung per HTTPS läuft. Passkeys richtet jeder im Profil unter „Sicherheit“ ein (Kapitel 9). Die Apps (Windows, Android, Chrome-Erweiterung) melden sich weiter mit Passwort an.</p>
|
||||
<fig n="staff-login">Anmeldeseite des Mitarbeiterbereichs.</fig>
|
||||
<ul>
|
||||
<li><b>Passwort vergessen:</b> Ein Link per E-Mail führt zu einem neuen Passwort. Das hebt auch eine eventuelle Anmeldesperre auf, weil sich der Mitarbeiter damit als Postfach-Besitzer ausgewiesen hat.</li>
|
||||
<li><b>Schutz vor Raten:</b> Nach zu vielen Fehlversuchen (Standard 5) wird das Konto 15 Minuten gesperrt; zusätzlich gilt ein Limit je IP-Adresse. Bekannte Geräte (vom Anmelde-Wächter gemerkt) bleiben mit richtigem Passwort anmeldbar, so kann niemand den echten Mitarbeiter durch Fehlversuche aussperren. Administratoren können eine Sperre aufheben (Kapitel 12).</li>
|
||||
<li><b>Neues Gerät:</b> Meldet sich jemand von einem unbekannten Gerät oder Ort an, kommt eine Warn-Mail mit dem Link „Das war ich nicht“, der das Konto sichert (Kapitel 12).</li>
|
||||
</ul>
|
||||
<fig n="staff-passwort-vergessen">„Passwort vergessen“ im Mitarbeiterbereich.</fig>
|
||||
|
||||
<h2>Aufbau der Oberfläche</h2>
|
||||
<p>Links steht die Seitenleiste mit allen Bereichen, rechts der Inhalt. Oben rechts zeigt ein Zähler die Restzeit bis zur automatischen Abmeldung. Ganz unten in der Seitenleiste wechselt ihr die Sprache (Flaggen) und den eigenen Status.</p>
|
||||
<fig n="staff-gesamtansicht">Mitarbeiterbereich mit Seitenleiste (Beispiel: Berichte).</fig>
|
||||
|
||||
<h3>Die Seitenleiste</h3>
|
||||
<table>
|
||||
<tr><th>Menüpunkt</th><th>Inhalt</th></tr>
|
||||
<tr><td>Übersicht</td><td>Kennzahlen und Auswertungen in vier Reitern</td></tr>
|
||||
<tr><td>Tickets</td><td>Ticketliste, Suche, Ansichten; „Neues Ticket“</td></tr>
|
||||
<tr><td>Aufgaben</td><td>Eigene To-dos, unabhängig von Tickets</td></tr>
|
||||
<tr><td>Chat / Live-Chat</td><td>Interner Team-Chat und Gespräche mit Besuchern der Webseite</td></tr>
|
||||
<tr><td>Meine Woche</td><td>Persönliche Wochenstatistik</td></tr>
|
||||
<tr><td>Erwähnungen</td><td>Wo euch jemand mit @ erwähnt hat</td></tr>
|
||||
<tr><td>Berichte</td><td>Standardauswertungen und Bericht-Baukasten</td></tr>
|
||||
<tr><td>Sperrliste, Verzeichnis</td><td>Gesperrte Absender; Kontaktliste aller Kollegen</td></tr>
|
||||
<tr><td>Verfügbarkeit & Schichten</td><td>Status des Teams</td></tr>
|
||||
<tr><td>Apps</td><td>Downloads der Programme mit Kurzanleitung</td></tr>
|
||||
<tr><td>Adminportal</td><td>Alle Verwaltungsbereiche (nur Administratoren und Co-Admins mit Einzelrechten); einklappbar</td></tr>
|
||||
<tr><td>Plugin-Punkte</td><td>Menüpunkte installierter Plugins</td></tr>
|
||||
</table>
|
||||
<fig n="staff-seitenleiste">Die Seitenleiste mit aufgeklapptem Adminportal.</fig>
|
||||
<div class="note">Sieht ein Administrator den Punkt „Adminportal“ nicht, hilft einmal Ab- und Anmelden; außerdem prüfen, ob der Bereich eingeklappt ist (Pfeil daneben).</div>
|
||||
|
||||
<h2>Automatische Abmeldung und Status</h2>
|
||||
<p><b>Abmeldung:</b> Im Browser wird ein Mitarbeiter nach der eingestellten Zeit ohne Bedienung abgemeldet (Einstellungen → Allgemein → Sitzungs-Timeout, Standard 30 Minuten). Als Bedienung zählen Klicks, Tastatureingaben, Mausbewegungen und selbst ausgelöste Seitenaufrufe; Hintergrundabfragen einer unbeachtet offenen Seite zählen nicht. Mehrere Browser-Tabs teilen sich die Zeit. Windows-Programm und Android-App sind nicht betroffen.</p>
|
||||
<p><b>Status:</b> Jeder Mitarbeiter stellt seinen Status ein: <b>Verfügbar</b>, <b>Beschäftigt</b>, <b>Pause</b> oder <b>Offline</b>. Wer 30 Minuten lang nichts tut, geht automatisch auf Offline und bekommt seinen früheren Status zurück, sobald er wieder aktiv ist (die Zeit stellt ein Administrator ein; 0 = aus). Ein von Hand gesetzter Status wird nicht von selbst zurückgesetzt. Der Live-Chat im Kundenportal zeigt Offline-Mitarbeiter nicht als erreichbar.</p>
|
||||
<fig n="verfuegbarkeit">Verfügbarkeit & Schichten: Status aller Mitarbeiter.</fig>
|
||||
|
||||
<h2>Tastenkürzel</h2>
|
||||
<p>In jeder Mitarbeiterseite funktionieren Tastenkürzel (nicht in Eingabefeldern). <kbd>?</kbd> zeigt die Übersicht und erlaubt das Abschalten (gilt nur für diesen Browser). Die Tabelle steht auch im Anhang.</p>
|
||||
<fig n="tickets-tastenkuerzel">Die Kürzel-Übersicht (Taste <kbd>?</kbd>).</fig>
|
||||
|
||||
<h2>Apps und Downloads</h2>
|
||||
<p>Unter <b>Apps</b> laden angemeldete Mitarbeiter Windows-Programm, Android-App, Chrome-Erweiterung, WordPress-Plugin und (wenn eingeblendet) das Minecraft-Plugin herunter und sehen Version, Größe, Prüfsumme, Kurzanleitung und die Serveradresse zum Kopieren. Ohne Anmeldung sind die Dateien nicht erreichbar.</p>
|
||||
<fig n="apps">Die Seite „Apps“.</fig>
|
||||
@@ -0,0 +1,35 @@
|
||||
<h1>Übersicht, Auswertung und „Meine Woche“</h1>
|
||||
|
||||
<h2>Die Übersicht in vier Reitern</h2>
|
||||
<p>Die Übersicht ist die Startseite des Mitarbeiterbereichs. Oben wählt ihr den <b>Berichtszeitraum</b>; er gilt für die Reiter „Verlauf“, „Auswertung“ und „Zufriedenheit“. Jeder sieht nur Zahlen aus Abteilungen, auf die er Zugriff hat.</p>
|
||||
|
||||
<h3>Reiter „Überblick“</h3>
|
||||
<p>Zeigt offene Tickets, euch zugewiesene, nicht zugewiesene und überfällige Tickets, euren Status und den des Teams sowie Gesamtzahlen. Darunter stehen die zuletzt aktualisierten Tickets. Ein Klick auf eine Kachel öffnet die passend gefilterte Liste.</p>
|
||||
<fig n="uebersicht-ueberblick">Überblick: Kennzahlen, Teamstatus und letzte Tickets.</fig>
|
||||
|
||||
<h3>Reiter „Verlauf“</h3>
|
||||
<p>Ticketaktivität über die Zeit (neu, gelöst), SLA-Einhaltung sowie Verteilung nach Status und Priorität.</p>
|
||||
<fig n="uebersicht-verlauf">Verlauf: Aktivität, SLA und Verteilungen.</fig>
|
||||
|
||||
<h3>Reiter „Auswertung“</h3>
|
||||
<p>Tickets nach Abteilung, Hilfethema und Mitarbeiter mit durchschnittlichen Zeiten; Export als CSV (Excel-kompatibel).</p>
|
||||
<fig n="uebersicht-auswertung">Auswertung nach Abteilung, Thema und Mitarbeiter.</fig>
|
||||
|
||||
<h3>Reiter „Zufriedenheit“</h3>
|
||||
<p>Erscheint, wenn die Zufriedenheitsabfrage genutzt wird: Durchschnittsbewertung, Rücklaufquote, Verteilung der Sterne, Auswertung nach Mitarbeiter und die letzten Kommentare.</p>
|
||||
<fig n="uebersicht-zufriedenheit">Zufriedenheit der Kunden.</fig>
|
||||
|
||||
<h2>Meine Woche</h2>
|
||||
<p>„Meine Woche“ ist eure persönliche Statistik – sichtbar nur für euch selbst. Zeiträume: diese und letzte Woche, dieser und letzter Monat.</p>
|
||||
<ul>
|
||||
<li><b>Gelöste Tickets</b> und <b>gesendete Antworten</b> (ohne interne Notizen)</li>
|
||||
<li><b>Durchschnittliche Zeit bis zur ersten Antwort</b> (nur Tickets, bei denen die erste öffentliche Antwort von euch kam; bei eingerichteten Geschäftszeiten in Arbeitszeit)</li>
|
||||
<li><b>Pünktlich gelöst</b>: Anteil der Tickets, die vor Ablauf der Lösungsfrist gelöst wurden</li>
|
||||
<li><b>Bewertung</b> eurer Tickets, wenn die Zufriedenheitsabfrage eingeschaltet ist</li>
|
||||
<li>Je ein <b>Vergleich</b> mit dem gleich langen Zeitraum davor, ein Balkendiagramm „Gelöst pro Tag“ und was gerade bei euch offen ist</li>
|
||||
</ul>
|
||||
<fig n="meine-woche">Meine Woche mit Vergleich und Tagesdiagramm (Beispieldaten).</fig>
|
||||
|
||||
<h2>Erwähnungen</h2>
|
||||
<p>Schreibt ein Kollege in einer internen Notiz <code>@Name</code>, erscheint die Erwähnung bei euch unter <b>Erwähnungen</b> (und als Benachrichtigung per Mail oder Push). Ein Klick öffnet das Ticket an der Stelle der Notiz; gelesene Erwähnungen verschwinden aus dem Zähler.</p>
|
||||
<fig n="erwaehnungen">Liste der Erwähnungen.</fig>
|
||||
@@ -0,0 +1,109 @@
|
||||
<h1>Tickets bearbeiten</h1>
|
||||
|
||||
<h2>Die Ticketliste</h2>
|
||||
<p>Unter <b>Tickets</b> steht die komplette Liste, sortiert nach der <b>letzten Aktivität</b> (zuletzt bearbeitete Tickets oben). Beim Darüberfahren in der Spalte „Letzte Aktivität“ erscheint der Eingang. Überfällige Tickets sind rot markiert; gelöste und geschlossene Tickets zeigen nie „überfällig“.</p>
|
||||
<fig n="tickets-liste">Die Ticketliste mit Reitern, Filtern und Tabelle.</fig>
|
||||
<ul>
|
||||
<li><b>Reiter:</b> <i>Offen</i>, <i>Pausiert</i>, <i>Gelöst</i>, <i>Geschlossen</i>, <i>Wiedervorlage</i>, <i>Gesperrt</i> (Tickets, die gerade jemand bearbeitet) und <i>Alle</i>; die Zahl am Reiter zeigt die Menge. Über <b>Schnellfilter</b> lassen sich weitere Vorgaben einblenden.</li>
|
||||
<li><b>Suche und Filter:</b> Suchfeld (mit „Auch Nachrichten durchsuchen“), Abteilung, Status, E-Mail (normale Tickets oder Spam), Priorität, Zuweisung, Tag und SLA-Status. <b>CSV exportieren</b> speichert das Ergebnis für Excel.</li>
|
||||
<li><b>E-Mails jetzt abrufen:</b> Der Knopf oben (sichtbar, sobald ein Postfach eingerichtet ist) startet denselben Abruf wie der Cronjob und meldet, wie viele Mails verarbeitet wurden. Ein zweiter Klick innerhalb von 20 Sekunden bekommt nur den Hinweis „lief gerade eben schon“.</li>
|
||||
<li><b>Sammelaktionen:</b> Tickets ankreuzen und im Block „Auswahl“ <b>zuweisen</b>, die <b>Priorität setzen</b>, den <b>Status setzen</b> (mit Abschlussgrund), den <b>Absender sperren</b>, <b>löschen</b> oder die <b>Abteilung ändern</b> (die SLA-Fristen werden nach den Regeln der neuen Abteilung neu berechnet; nur mit dem Recht „Tickets übertragen“ und nur in Abteilungen mit Zugriff).</li>
|
||||
</ul>
|
||||
<fig n="tickets-suche">Suche und Filter in der Ticketliste.</fig>
|
||||
|
||||
<h3>Gespeicherte Ansichten</h3>
|
||||
<p>Aktuelle Filter lassen sich über <b>„Aktuelle Filter als Ansicht speichern“</b> als Schnellauswahl sichern – für euch oder das ganze Team. Löschen können der Ersteller und alle Administratoren (✕ neben der Ansicht). Die Ansichten sind in Web, Windows-Programm und Android-App gleich.</p>
|
||||
<fig n="tickets-ansicht-speichern">Eine Ansicht speichern.</fig>
|
||||
|
||||
<h3>Wiedervorlage-Reiter</h3>
|
||||
<fig n="tickets-wiedervorlage">Tickets, die auf später gelegt wurden.</fig>
|
||||
|
||||
<h2>Ein Ticket anlegen</h2>
|
||||
<p>Mit <b>+ Ticket erstellen</b> (oder <kbd>n</kbd>) legen Mitarbeiter ein Ticket im Namen eines Kunden an: Kundendaten, Abteilung, Hilfethema, Priorität, Zuweisung, Betreff, Text und Anhänge. Eine <b>Ticket-Vorlage</b> füllt Betreff, Text, Abteilung und Priorität vor (Verwaltung in Kapitel 10). Von Hand angelegte Tickets bleiben so zugewiesen, wie ihr sie angelegt habt.</p>
|
||||
<fig n="ticket-neu">Das Formular „Neues Ticket“ für Mitarbeiter.</fig>
|
||||
|
||||
<h2>Die Ticketansicht</h2>
|
||||
<p>Links stehen Verlauf und Antwortfeld, rechts die Karten in drei Reitern. Der gewählte Reiter bleibt beim Neuladen erhalten.</p>
|
||||
<fig n="ticket-ansicht">Die Ticketansicht: Verlauf links, Karten rechts.</fig>
|
||||
|
||||
<h3>Verlauf</h3>
|
||||
<p>Alle Nachrichten in zeitlicher Folge: Kundennachrichten, Antworten, <b>interne Notizen</b> (nur das Team sieht sie, gelb markiert) und Systemereignisse (Statuswechsel, Zuweisung, SLA, automatische Aktionen). Bei E-Mail-Tickets zeigt DeskHub die Nachricht formatiert (Absätze, Listen, Tabellen, Bilder). Der <b>zitierte Verlauf</b> früherer Mails ist hinter „▶ Verlauf anzeigen“ eingeklappt. Fremde Bilder werden aus Datenschutzgründen nicht automatisch geladen; „Bilder jetzt laden“ lädt sie nur für diese Ansicht. Fehlt ein Anhang aus einer älteren Mail, holt „📎 Anhänge und Bilder aus E-Mail laden“ ihn aus dem Postfach nach.</p>
|
||||
|
||||
<h3>Reiter „Details“</h3>
|
||||
<p>Eigenschaften (Status, Zuweisung, Priorität, Abteilung, Hilfethema, Fälligkeit), SLA-Fristen mit Verlauf, Absenderdaten, Zusatzfelder und die Kundenbewertung.</p>
|
||||
<fig n="ticket-ansicht-details">Karten „Eigenschaften“, „SLA“ und „Absender“.</fig>
|
||||
|
||||
<h3>Reiter „Organisation“</h3>
|
||||
<p>Zugehörige Probleme, <b>Wiedervorlage und Beobachter</b>, Mitleser (Cc) und Kollaboratoren, <b>verknüpfte Tickets</b> und die <b>Zeiterfassung</b>.</p>
|
||||
<fig n="ticket-ansicht-organisation">Wiedervorlage, Beobachter, Verknüpfungen und Zeiterfassung.</fig>
|
||||
|
||||
<h3>Reiter „Mehr“</h3>
|
||||
<p>Passende Artikel der Wissensdatenbank (mit „Link kopieren“) und <b>ähnliche gelöste Tickets</b> („Dazu passend“), Zusammenführen, Teilen, Drucken, Löschen sowie Karten von Plugins (zum Beispiel Spam-Bewertung).</p>
|
||||
<fig n="ticket-ansicht-mehr">Karte „Dazu passend“ sowie Zusammenführen und Löschen.</fig>
|
||||
|
||||
<h2>Antworten</h2>
|
||||
<p>Das Antwortformular steht unter dem Verlauf. Zwischen <b>Antwort an Kunden</b> und <b>Interne Notiz</b> wählt ihr oben im Formular.</p>
|
||||
<fig n="ticket-antwortformular">Das Antwortformular.</fig>
|
||||
<ul>
|
||||
<li><b>Textbausteine (Vorlagen):</b> „Vorlage wählen“ zeigt nur Vorlagen der Abteilung des Tickets und die für alle Abteilungen, gruppiert.</li>
|
||||
<li><b>Signatur:</b> Unter dem Textfeld wählt ihr <i>Meine Signatur</i>, <i>Abteilung „…“</i> oder <i>Keine Signatur</i>; die Vorschau zeigt, was angehängt wird. Signaturen gehen nur an öffentliche Antworten, nie an interne Notizen.</li>
|
||||
<li><b>Formatierung und Bilder:</b> Fett, Kursiv, Listen, Links und Bilder. <b>Screenshot einfügen:</b> <kbd>Strg</kbd> + <kbd>V</kbd> setzt ein kopiertes Bild direkt in die Antwort; auch Ziehen einer Bilddatei in das Feld geht (PNG, JPG, GIF, bis 5 MB). Enthält die Zwischenablage Text, bleibt das normale Einfügen.</li>
|
||||
<li><b>Anhänge:</b> Dateien hochladen; erlaubte Endungen und Größe stellt der Administrator ein.</li>
|
||||
<li><b>Status nach dem Senden:</b> Auswahl unter dem Textfeld, Standard „Beantwortet“; möglich sind Offen, Wartet auf Kunde, Beantwortet, Pausiert, Gelöst und Geschlossen (mit Abschlussgrund).</li>
|
||||
<li><b>Senden:</b> Mit dem Knopf oder <kbd>Strg</kbd> + <kbd>Enter</kbd>.</li>
|
||||
</ul>
|
||||
|
||||
<h3>Antwort einplanen („Später senden“)</h3>
|
||||
<p>Unter dem Antwortformular wählt ihr Datum und Uhrzeit (Serverzeit, höchstens 90 Tage voraus). Bis dahin steht die Antwort als <b>Geplante Antwort</b> im Ticket; Verfasser und Administratoren können sie abbrechen. Beim Senden gelten Status, Abschlussgrund und Signatur wie beim sofortigen Antworten. Anhänge sind beim Einplanen nicht möglich. Ist das Ticket gelöscht oder der Verfasser deaktiviert, wird nichts gesendet.</p>
|
||||
<fig n="ticket-geplante-antwort">Eine eingeplante Antwort.</fig>
|
||||
|
||||
<h2>Bilder und Anhänge ansehen</h2>
|
||||
<p>Ein Klick auf ein Bild im Ticket öffnet die <b>Bildvorschau</b>: groß, ohne Download. Pfeiltasten blättern, <kbd>Esc</kbd> schließt, ein Knopf lädt herunter. Andere Anhänge sind als Dateien aufgeführt.</p>
|
||||
<fig n="ticket-anhang">Anhänge und Bildvorschau im Ticket.</fig>
|
||||
|
||||
<h2>Status, Zuweisung und Ticket-Sperre</h2>
|
||||
<ul>
|
||||
<li><b>Status:</b> Offen, Wartet auf Kunde, Beantwortet, Pausiert (die SLA-Uhr steht), Gelöst und Geschlossen; dazu „Spam“ und „Zusammengeführt“ als Sonderfälle. Antwortet der Kunde auf ein gelöstes oder geschlossenes Ticket, öffnet es sich wieder. Dabei <b>startet die SLA neu</b>: Die Lösungsfrist wird ab jetzt berechnet, „überfällig“ verschwindet, der SLA-Verlauf zeigt „SLA neu gestartet“.</li>
|
||||
<li><b>Gelöst und Geschlossen sind beide „erledigt“ (ab Server 1.0.78):</b> Für ein erledigtes Ticket läuft keine SLA mehr. Die Spalte „Fällig“ zeigt „–“, in der Ticketansicht steht „✓ erledigt“, und es erscheint nie „überfällig“. Beim Erledigen werden der Überfällig-Merker und eine Wiedervorlage entfernt. Es kommen keine Meldungen mehr wegen SLA-Überschreitung, Eskalation, wartender Antwort oder Wiedervorlage. Antwortet der Kunde später, öffnet sich das Ticket wieder und die SLA beginnt neu.</li>
|
||||
<li><b>Zuweisung:</b> Von Hand an einen Mitarbeiter oder ein Team. <b>Beim Öffnen automatisch zuweisen:</b> Öffnet ein Mitarbeiter ein noch niemandem zugewiesenes Ticket, wird es ihm zugewiesen (abschaltbar; nicht bei geschlossenen, gelösten, zusammengeführten oder Spam-Tickets, nicht ohne Antwortrecht).</li>
|
||||
<li><b>Ticket-Sperre:</b> Wer ein Ticket öffnet, sperrt es 5 Minuten für andere. Normale Mitarbeiter sehen den Namen des Bearbeiters; <b>Administratoren</b> können trotzdem lesen und bearbeiten und die Sperre mit „Sperre aufheben und übernehmen“ übernehmen (steht im Verlauf).</li>
|
||||
<li><b>Erinnerung bei wartenden Tickets:</b> Wartet ein zugewiesenes Ticket seit einstellbar vielen Arbeitsstunden (Standard 4) auf eure Antwort und ist die SLA-Frist noch nicht gerissen, bekommt der Bearbeiter einen Push (Android und Browser) und auf Wunsch eine Mail – einmal pro Kundennachricht.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Wiedervorlage („Auf später legen“)</h2>
|
||||
<p>Rechts auf der Ticketseite: <b>Auf später legen</b> – in 1 Stunde, 3 Stunden, morgen früh, in 3 Tagen, 1 Woche oder zu einem eigenen Zeitpunkt. Das Ticket verschwindet aus „Offen“ und „Alle“ und steht im Reiter „💤 Wiedervorlage“. Zur gewählten Zeit kommt es zurück und der Mitarbeiter bekommt eine Benachrichtigung („Wiedervorlage fällig“). Antwortet der Kunde früher, kommt es sofort zurück.</p>
|
||||
|
||||
<h2>Beobachter und Mitleser</h2>
|
||||
<p><b>Ticket beobachten</b> macht euch zum Beobachter: Ihr bekommt Benachrichtigungen bei Kundenantworten und Antworten anderer Mitarbeiter, ohne dass das Ticket euch zugewiesen ist (in den Push-Einstellungen abschaltbar). <b>Cc-Empfänger</b> (E-Mail-Adressen) bekommen die öffentlichen Antworten in Kopie.</p>
|
||||
|
||||
<h2>Verknüpfte Tickets</h2>
|
||||
<p>Im Reiter „Organisation“ verknüpft ihr beliebige Tickets per Nummer als <b>verwandt</b>, <b>doppelt</b> oder <b>blockiert</b>. Beide Tickets zeigen die Verknüpfung mit passender Bezeichnung („Doppelt zu“ / „Hat Duplikat“, „Blockiert“ / „Blockiert durch“); ein offener Blocker erscheint als Hinweis. Mitarbeiter sehen nur Tickets ihrer Abteilungen. Beim Löschen eines Tickets verschwinden seine Verknüpfungen.</p>
|
||||
|
||||
<h2>Zeiterfassung</h2>
|
||||
<p>Im Reiter „Organisation“ startet und stoppt ihr eine Zeitmessung oder tragt Minuten nachträglich mit Beschreibung ein. Alle erfassten Zeiten sieht ein Administrator gesammelt unter Adminportal → Zeiterfassung.</p>
|
||||
<fig n="ticket-zeiterfassung">Zeiterfassung im Ticket.</fig>
|
||||
|
||||
<h2>Zusammenführen, Teilen, Drucken, Löschen</h2>
|
||||
<ul>
|
||||
<li><b>Zusammenführen:</b> Doppelte Tickets lassen sich in ein Ticket vereinen (Suche nach Nummer); das andere gilt danach als „Zusammengeführt“.</li>
|
||||
<li><b>Teilen:</b> Eine Nachricht kann in ein neues Ticket abgespalten werden.</li>
|
||||
<li><b>Drucken:</b> Eine druckfreundliche Fassung des Tickets.</li>
|
||||
<li><b>Löschen:</b> Nur mit entsprechendem Recht; löscht das Ticket samt Nachrichten und Verknüpfungen.</li>
|
||||
</ul>
|
||||
<fig n="ticket-drucken">Druckansicht eines Tickets.</fig>
|
||||
|
||||
<h2>Tastenkürzel in Liste und Ticket</h2>
|
||||
<table>
|
||||
<tr><th>Wo</th><th>Taste</th><th>Wirkung</th></tr>
|
||||
<tr><td>Liste</td><td><kbd>j</kbd> / <kbd>k</kbd></td><td>nächstes / vorheriges Ticket wählen</td></tr>
|
||||
<tr><td>Liste</td><td><kbd>Enter</kbd> oder <kbd>o</kbd></td><td>Ticket öffnen</td></tr>
|
||||
<tr><td>Liste</td><td><kbd>r</kbd></td><td>öffnen und gleich antworten</td></tr>
|
||||
<tr><td>Liste</td><td><kbd>x</kbd></td><td>Ticket ankreuzen</td></tr>
|
||||
<tr><td>Liste</td><td><kbd>/</kbd></td><td>Suche</td></tr>
|
||||
<tr><td>Liste</td><td><kbd>n</kbd></td><td>Neues Ticket</td></tr>
|
||||
<tr><td>Ticket</td><td><kbd>j</kbd> / <kbd>k</kbd></td><td>durch die Nachrichten springen</td></tr>
|
||||
<tr><td>Ticket</td><td><kbd>r</kbd></td><td>Antwort schreiben</td></tr>
|
||||
<tr><td>Ticket</td><td><kbd>u</kbd></td><td>zurück zur Liste</td></tr>
|
||||
<tr><td>Ticket</td><td><kbd>Strg</kbd> + <kbd>Enter</kbd></td><td>Antwort senden</td></tr>
|
||||
<tr><td>überall</td><td><kbd>?</kbd></td><td>Übersicht ein-/ausschalten</td></tr>
|
||||
</table>
|
||||
@@ -0,0 +1,28 @@
|
||||
<h1>Aufgaben, Chat, Kunden und weitere Werkzeuge</h1>
|
||||
|
||||
<h2>Aufgaben</h2>
|
||||
<p>Unter <b>Aufgaben</b> führt jeder Mitarbeiter eigene To-dos – unabhängig von Tickets. Eine Aufgabe hat Titel, optional Beschreibung und Fälligkeit; erledigte Aufgaben werden abgehakt. Aufgaben gibt es auch im Windows-Programm, in der Android-App und in der Chrome-Erweiterung.</p>
|
||||
<fig n="aufgaben">Die Aufgabenliste.</fig>
|
||||
|
||||
<h2>Interner Team-Chat</h2>
|
||||
<p>Der <b>Chat</b> verbindet das Team untereinander: links die Kollegen, rechts das Gespräch mit dem gewählten Kollegen. Neue Nachrichten zeigen einen Zähler im Menü und lösen eine Benachrichtigung aus. Der Chat ist auch in den Apps verfügbar.</p>
|
||||
<fig n="interner-chat">Team-Chat mit einem Kollegen.</fig>
|
||||
|
||||
<h2>Live-Chat</h2>
|
||||
<p>Ist der Live-Chat eingeschaltet und ein Mitarbeiter „Verfügbar“, sehen Besucher des Kundenportals ein Chat-Fenster. Im Mitarbeiterbereich unter <b>Live-Chat</b> nehmen Mitarbeiter Gespräche an und beantworten sie; jedes Gespräch wird als Ticket (Quelle „Live-Chat“) gespeichert. Ein Live-Chat sperrt Administratoren nie aus. Ist niemand erreichbar, übernimmt der digitale Assistent.</p>
|
||||
<fig n="live-chat">Live-Chat-Bereich für Mitarbeiter.</fig>
|
||||
|
||||
<h2>Kundenverzeichnis und Kundenprofil</h2>
|
||||
<p>Im <b>Kundenprofil</b> (aus einem Ticket oder dem Kundenverzeichnis) stehen Kontaktdaten, Organisation, alle Tickets des Kunden und interne Notizen zum Kunden. Mitarbeiter sehen Kundenprofile nur für ihre Abteilungen. Sind die <b>Datenschutz-Werkzeuge</b> eingeschaltet (Einstellungen → Datenschutz), bieten Administratoren hier „Alle Daten exportieren (JSON)“ und „Kundendaten löschen“ an (Kapitel 17).</p>
|
||||
<fig n="kunden-profil">Ein Kundenprofil.</fig>
|
||||
|
||||
<h2>Sperrliste</h2>
|
||||
<p>Die <b>Sperrliste</b> blockiert einzelne E-Mail-Adressen oder ganze Mail-Anbieter (Domains). Mails von dort erzeugen kein Ticket mehr; gesperrte Adressen können sich nicht registrieren. Eintragen könnt ihr direkt in der Liste oder aus einem Ticket („Absender sperren“).</p>
|
||||
<fig n="sperrliste">Die Sperrliste.</fig>
|
||||
|
||||
<h2>Verzeichnis der Kollegen</h2>
|
||||
<p>Das <b>Verzeichnis</b> ist die Kontaktliste aller Mitarbeiter mit Abteilung, E-Mail und Status – hilfreich, um den richtigen Ansprechpartner zu finden.</p>
|
||||
<fig n="verzeichnis">Das Mitarbeiterverzeichnis.</fig>
|
||||
|
||||
<h2>Verfügbarkeit & Schichten</h2>
|
||||
<p>Hier sieht das ganze Team, wer verfügbar, beschäftigt, in Pause oder offline ist. Jeder ändert seinen eigenen Status; <b>Administratoren</b> können den Status jedes Mitarbeiters ändern (das steht im Aktivitätsprotokoll) und stellen die Zeit bis zum automatischen „Offline“ ein. Die automatische Zuweisung „Reihum“ überspringt Mitarbeiter, die offline oder außerhalb ihrer Schicht sind.</p>
|
||||
@@ -0,0 +1,33 @@
|
||||
<h1>Berichte und Bericht-Baukasten</h1>
|
||||
|
||||
<h2>Standardberichte</h2>
|
||||
<p>Unter <b>Berichte</b> findet ihr die Standardauswertungen zu Antwortzeiten, Ticketaufkommen und SLA-Einhaltung. Jeder sieht nur Zahlen aus Abteilungen, auf die er Zugriff hat. Die Übersicht (Kapitel 5) zeigt dieselben Daten kompakt.</p>
|
||||
|
||||
<h2>Der Bericht-Baukasten</h2>
|
||||
<p>Der Baukasten erlaubt eigene Auswertungen. Ihr stellt zusammen:</p>
|
||||
<table>
|
||||
<tr><th>Bereich</th><th>Auswahl</th></tr>
|
||||
<tr><td><b>Gruppieren nach</b></td><td>Abteilung, Hilfethema, Mitarbeiter, Team, Priorität, Status, Quelle, Tag, Woche oder Monat</td></tr>
|
||||
<tr><td><b>Kennzahlen</b></td><td>Tickets gesamt, offen, gelöst/geschlossen, überfällig, durchschnittliche erste Antwort, durchschnittliche Lösungszeit, SLA-Erfüllung bei erster Antwort und bei Lösung</td></tr>
|
||||
<tr><td><b>Zeitraum und Filter</b></td><td>Von/Bis nach Erstellungsdatum; Filter nach Abteilung, Priorität, Mitarbeiter, Hilfethema und Quelle</td></tr>
|
||||
<tr><td><b>Sortierung</b></td><td>nach Gruppe oder Kennzahl, auf- oder absteigend</td></tr>
|
||||
</table>
|
||||
<fig n="bericht-baukasten">Der Bericht-Baukasten mit Ergebnis „Tickets je Abteilung“.</fig>
|
||||
<ul>
|
||||
<li><b>Speichern und teilen:</b> Über „Diesen Bericht speichern“ vergebt ihr einen Namen; gespeicherte Berichte erscheinen oben als Schnellauswahl, privat oder mit dem Team geteilt.</li>
|
||||
<li><b>Export:</b> „CSV exportieren“ liefert eine mit Excel kompatible Datei.</li>
|
||||
<li>Der Baukasten steht auch in Windows-Programm und Android-App zur Verfügung.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Berichte per E-Mail (Zeitplan)</h2>
|
||||
<p>Im Baukasten gibt es unten <b>„Versand per E-Mail (Zeitplan)“</b>: Ein gespeicherter Bericht wird täglich, wöchentlich oder monatlich als Tabelle per Mail gesendet – für einen relativen Zeitraum (gestern, letzte 7 oder 30 Tage, letzte Kalenderwoche, letzter Monat). Berechnet wird mit den Abteilungsrechten dessen, der den Zeitplan eingerichtet hat.</p>
|
||||
<fig n="bericht-zeitplan">Zeitplan für den E-Mail-Versand eines Berichts.</fig>
|
||||
<ul>
|
||||
<li><b>Empfänger:</b> Nicht-Administratoren dürfen nur an aktive Mitarbeiter senden, Administratoren auch an externe Adressen (höchstens 10 Empfänger).</li>
|
||||
<li><b>Weniger Mails:</b> Drei Schalter je Zeitplan – <i>Nur senden, wenn der Bericht Daten enthält</i>, <i>Nur senden, wenn sich etwas geändert hat</i> und (bei „Täglich“) <i>Nur Mo–Fr</i>. Die ersten beiden sind standardmäßig an. Warum nichts kam, steht in der Spalte „Zuletzt“ („Übersprungen: …“).</li>
|
||||
<li><b>Test senden</b> verschickt sofort und ignoriert die Schalter.</li>
|
||||
<li>Voraussetzung ist ein eingerichteter E-Mail-Versand; sonst steht „Kein Empfänger erreicht“.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Zufriedenheit</h2>
|
||||
<p>Die Auswertung der Kundenbewertungen (Durchschnitt, Rücklaufquote, Verteilung, nach Mitarbeiter, letzte Kommentare) steht auf der Übersicht im Reiter „Zufriedenheit“, im Windows-Programm unter Berichte und im Dashboard der Android-App.</p>
|
||||
@@ -0,0 +1,32 @@
|
||||
<h1>Mein Profil</h1>
|
||||
<p>Das Profil erreicht ihr über euren Namen in der Seitenleiste. Es hat vier Reiter.</p>
|
||||
|
||||
<h2>Konto</h2>
|
||||
<ul>
|
||||
<li><b>Profilbild:</b> JPG, PNG oder WEBP bis 2 MB; ohne Bild zeigt DeskHub den Anfangsbuchstaben.</li>
|
||||
<li><b>Kontodaten:</b> Name, E-Mail-Adresse und optional Telefonnummer.</li>
|
||||
<li><b>Passwort ändern:</b> aktuelles Passwort und neues Passwort (mindestens 10 Zeichen mit Groß-/Kleinbuchstaben, Ziffer und Sonderzeichen), zweimal eingeben.</li>
|
||||
<li><b>Browser-Push-Benachrichtigungen:</b> Meldungen im Browser aktivieren (setzt eine passende Serverkonfiguration voraus).</li>
|
||||
<li><b>Android-App: Welche Popups?</b> Ihr wählt, wofür die App ein Popup zeigt: neues Ticket, neue Kundenantwort, Ticket mir zugewiesen, neuer Live-Chat, SLA überschritten, Ticket wartet auf meine Antwort, ich wurde erwähnt, Wiedervorlage fällig, beobachtetes Ticket aktualisiert. Dazu kommen stille Zeiten (Serverzeit, auch über Mitternacht). Jedem erscheinen nur die Arten, die für ihn in Frage kommen.</li>
|
||||
</ul>
|
||||
<fig n="profil-konto">Profil → Konto.</fig>
|
||||
|
||||
<h2>Einstellungen</h2>
|
||||
<ul>
|
||||
<li><b>Abwesenheit / Urlaub:</b> Mit „Urlaubsmodus aktiv“ bekommt ihr keine Mails, Push- oder App-Meldungen; stattdessen bekommt eure <b>Vertretung</b> (auch über Ketten, ohne Doppelte) die Benachrichtigungen. Ohne Vertretung bekommt niemand etwas. Optional übergebt ihr eure offenen Tickets an die Vertretung; nach dem Urlaub kommen sie zurück. Die Vertretung sieht und bearbeitet währenddessen auch eure Abteilungen. Sicherheitsmails bleiben davon unberührt.</li>
|
||||
<li><b>Übersicht:</b> Rolle, Mitglied seit, zugewiesene Tickets.</li>
|
||||
</ul>
|
||||
<fig n="profil-einstellungen">Profil → Einstellungen.</fig>
|
||||
|
||||
<h2>Signatur</h2>
|
||||
<p>Der Text, der an eure öffentlichen Antworten angehängt wird: Fett, Kursiv, Liste, Link und Bilder (🖼️). Die Bildgröße ändert ihr durch Anklicken des Bildes und Ziehen am runden Anfasser oder über 25 / 50 / 75 % / Original. Bilder in Signaturen müssen über eine https-Adresse erreichbar sein (die Website-URL muss mit <code>https://</code> beginnen), sonst blendet der Sicherheitsfilter sie aus. Beim Antworten wählt ihr pro Ticket zwischen eurer Signatur, der Abteilungs-Signatur und keiner.</p>
|
||||
<fig n="profil-signatur">Profil → Signatur mit Editor.</fig>
|
||||
|
||||
<h2>Sicherheit</h2>
|
||||
<ul>
|
||||
<li><b>Zwei-Faktor-Authentifizierung (2FA):</b> Authenticator-App (z. B. Google Authenticator oder Authy) installieren, QR-Code scannen oder den Schlüssel manuell eingeben, den 6-stelligen Code eintragen und „2FA aktivieren“ klicken. Für Administratoren dringend empfohlen.</li>
|
||||
<li><b>Passkeys (ab Server 1.0.77):</b> Mit einem Passkey meldet ihr euch im Browser ohne Passwort an – per Fingerabdruck, Gesicht oder Geräte-PIN (Windows Hello, Handy, Sicherheitsschlüssel). Der Passkey ersetzt Passwort und 2FA-Code; beides bleibt als Rückfall bestehen. Zum Einrichten tragt ihr einen Namen (z. B. „Laptop“) und euer <b>Passwort</b> ein und klickt „Passkey einrichten“; danach bestätigt das Gerät die Einrichtung. Ihr könnt mehrere Passkeys anlegen (Laptop, Handy, Sicherheitsschlüssel). Die Liste zeigt Name, Datum und „zuletzt benutzt“; <b>Entfernen</b> verlangt wieder das Passwort. Verliert ihr ein Gerät, kann ein Administrator eure Passkeys entfernen. Voraussetzung ist, dass die Website-URL mit <code>https://</code> beginnt. Unter der Karte steht, für welche <b>Domain</b> die Passkeys gelten – bei einer Subdomain (zum Beispiel <code>support.beispiel.de</code>) ist das genau diese Adresse. Den Namen und das Symbol im Dialog bestimmt der Browser oder euer Passwortmanager (zum Beispiel Bitwarden), nicht DeskHub; angezeigt wird dort die Domain. Ein Passkey gilt nur für die Domain, unter der er angelegt wurde. Ein Administrator kann Passkeys im Adminportal ganz abschalten (Kapitel 12).</li>
|
||||
<li><b>Von allen Geräten abmelden:</b> Beendet alle Anmeldungen in Browser und Apps (Android, Windows, Chrome-Erweiterung) und stoppt die Push-Benachrichtigungen auf diesen Geräten. <i>Alle anderen Geräte abmelden</i> lässt die aktuelle Sitzung bestehen, <i>Von allen Geräten abmelden (auch hier)</i> beendet sie ebenfalls. Das Passwort bleibt unverändert; angemeldete Apps werden aufgelistet. Nützlich bei einem verlorenen Gerät.</li>
|
||||
<li><b>Meine Geräte und Anmeldungen:</b> Liste der Geräte, mit denen ihr euch angemeldet habt (Gerät, letzter Ort, zuletzt gesehen). Unbekannte Geräte entfernt ihr mit „Entfernen“. Darunter stehen die letzten Anmeldungen.</li>
|
||||
</ul>
|
||||
<fig n="profil-sicherheit">Profil → Sicherheit: Karte „Passkeys“ mit zwei eingerichteten Passkeys (Beispieldaten), darunter 2FA, Abmelden und Geräteliste.</fig>
|
||||
@@ -0,0 +1,59 @@
|
||||
<h1>Adminportal: Tickets und Automatik einrichten</h1>
|
||||
<p>Das Adminportal ist der Verwaltungsbereich in der Seitenleiste. Es ist für echte Administratoren immer sichtbar und für <b>Co-Admins</b> mit passenden Einzelrechten (Kapitel 12). Gegliedert ist es in <b>Verwalten</b>, <b>Kunden</b>, <b>Agenten</b> und <b>System</b>. Dieses und die folgenden Kapitel führen durch alle Bereiche.</p>
|
||||
|
||||
<h2>Abteilungen</h2>
|
||||
<p>Abteilungen sind die Themenbereiche, denen Tickets zugeordnet werden (z. B. Technik, Buchhaltung). Die Liste zeigt Status, Abteilungsleiter, Mitglieder und SLA-Plan.</p>
|
||||
<fig n="admin-abteilungen">Liste der Abteilungen.</fig>
|
||||
<p>Im Formular einer Abteilung stellt ihr ein:</p>
|
||||
<ul>
|
||||
<li>Name, Beschreibung, Status (aktiv/inaktiv), Abteilungsleiter, E-Mail-Adresse der Abteilung (DeskHub legt dazu automatisch einen Eintrag unter „E-Mail-Adressen“ an), <b>SLA-Plan</b> und die <b>Signatur</b> der Abteilung.</li>
|
||||
<li><b>Neue Tickets automatisch zuweisen:</b> <i>Nicht automatisch</i>, <i>Immer an einen bestimmten Mitarbeiter</i> (bei Urlaub an die Vertretung) oder <i>Reihum</i> an die Mitarbeiter der Abteilung (Urlaub, Offline und Schichtende werden übersprungen). Gilt für Formular, E-Mail, API und Live-Chat; eine Ticket-Regel „Mitarbeiter zuweisen“ hat Vorrang.</li>
|
||||
<li><b>Eigenes Ticket-Formular:</b> Im Reiter „Ticket-Formular“ bekommt jede Abteilung ein eigenes Formular mit eigenen Feldern, Überschrift, Einleitung und Beschriftungen. Betreff und Anhang lassen sich ausblenden.</li>
|
||||
<li><b>Mitglieder:</b> Mitarbeiter der Abteilung, jeweils mit eigener Rolle.</li>
|
||||
</ul>
|
||||
<fig n="admin-abteilung-bearbeiten">Das Abteilungsformular.</fig>
|
||||
|
||||
<h2>Prioritäten</h2>
|
||||
<p>Dringlichkeitsstufen (z. B. Niedrig, Normal, Hoch, Dringend) mit Farbe und Zeiten für <b>Reaktion</b> und <b>Lösung</b>. Sie gelten als Grundlage, wenn weder Hilfethema noch Abteilung einen SLA-Plan haben.</p>
|
||||
<fig n="admin-prioritaeten">Prioritäten mit Zeiten.</fig>
|
||||
|
||||
<h2>SLA-Pläne</h2>
|
||||
<p>Ein SLA-Plan legt fest, wie schnell reagiert und gelöst werden soll. Pläne weist ihr Abteilungen oder Hilfethemen zu. Welche Zeiten gelten, entscheidet diese Reihenfolge: (1) SLA-Plan des <b>Hilfethemas</b>, (2) SLA-Plan der <b>Abteilung</b>, (3) Zeiten der <b>Priorität</b>. Geschäftszeiten und Feiertage gelten weiterhin. Bei Überschreitung gibt es Meldungen (siehe Einstellungen → Allgemein); standardmäßig geht die Mail „SLA überschritten“ nur an die Administratoren. Wieder geöffnete Tickets starten die SLA neu. Für gelöste und geschlossene Tickets läuft keine SLA mehr (siehe Kapitel 6).</p>
|
||||
<fig n="admin-sla-plaene">SLA-Pläne.</fig>
|
||||
|
||||
<h2>Hilfethemen</h2>
|
||||
<p>Hilfethemen sind die Auswahl, die Kunden beim Ticket-Erstellen sehen. Jedes Thema ist mit Abteilung, Priorität, optional Team und SLA-Plan verknüpft und kann eigene Formularfelder mitbringen. Themen lassen sich öffentlich oder nur intern anbieten.</p>
|
||||
<fig n="admin-hilfethemen">Hilfethemen.</fig>
|
||||
|
||||
<h2>Ticket-Felder und Listen</h2>
|
||||
<p><b>Ticket-Felder</b> sind eigene Zusatzfelder für Tickets (Text, Mehrzeilig, Auswahl, Checkbox, Zahl, Datum; mit Pflicht und Reihenfolge). <b>Benutzerdefinierte Listen</b> liefern die Auswahlwerte für Auswahlfelder und lassen sich mehrfach verwenden.</p>
|
||||
<fig n="admin-ticket-felder">Ticket-Felder.</fig>
|
||||
<fig n="admin-listen">Benutzerdefinierte Listen.</fig>
|
||||
|
||||
<h2>Vorlagen (Textbausteine)</h2>
|
||||
<p>Vorlagen sind wiederverwendbare Antworttexte für das Antwortformular. Ordnet sie einer Abteilung zu oder lasst die Abteilung leer („für alle“). Im Formular erscheinen sie nach Abteilung gruppiert.</p>
|
||||
<fig n="admin-vorlagen">Textbausteine.</fig>
|
||||
|
||||
<h2>Ticket-Vorlagen</h2>
|
||||
<p>Eine Ticket-Vorlage füllt beim Anlegen eines Tickets Name, Betreff, Text, Abteilung, Hilfethema und Priorität vor. Mit „für Kunden sichtbar“ erscheint sie auch auf der Seite „Neues Ticket“ im Portal. Mitarbeiter wählen sie in Web, Windows-Programm und App; gepflegt werden sie nur im Web.</p>
|
||||
<fig n="admin-ticket-vorlagen">Ticket-Vorlagen.</fig>
|
||||
|
||||
<h2>Ticket-Regeln</h2>
|
||||
<p>Ticket-Regeln führen bei neuen Tickets automatisch Aktionen aus. <b>Bedingungen</b> prüfen Betreff, Nachricht, Absender-E-Mail, Absender-Name oder Kanal (web/email) mit „enthält“, „enthält nicht“, „ist genau“, „beginnt mit“ oder „endet mit“; ihr wählt, ob <i>alle</i> (UND) oder <i>mindestens eine</i> (ODER) zutreffen muss. <b>Aktionen:</b> Abteilung, Priorität oder Hilfethema setzen, <b>Mitarbeiter zuweisen</b>, Tag hinzufügen, Auto-Antwort (Textbaustein) senden, Ticket sofort schließen oder verwerfen (z. B. Spam). Die Reihenfolge der Regeln ist einstellbar. Die Seite bleibt echten Administratoren vorbehalten.</p>
|
||||
<fig n="admin-ticket-regeln">Ticket-Regeln.</fig>
|
||||
|
||||
<h2>Eskalationsregeln</h2>
|
||||
<p>Eskalationsregeln wirken auf bestehende Tickets (Prüfung stündlich). <b>Auslöser:</b> keine Mitarbeiter-Antwort seit X Stunden, kein Update seit X Stunden, Ticket offen seit X Stunden oder Ticket überfällig (SLA überschritten). <b>Bedingungen</b> (optional): Status, Priorität, Abteilung. <b>Aktionen:</b> Priorität setzen, einem Mitarbeiter zuweisen, Status setzen, Administratoren und/oder Mitarbeiter benachrichtigen. Mitarbeiter mit dem Recht „Eskalationsregeln verwalten“ erhalten auch SLA-Push-Meldungen.</p>
|
||||
<fig n="admin-eskalationsregeln">Eskalationsregeln.</fig>
|
||||
|
||||
<h2>Wiederkehrende Tickets</h2>
|
||||
<p>Regelmäßig neu erstellte Tickets, zum Beispiel eine monatliche Checkliste: Betreff, Text, Abteilung, Priorität, Zuweisung und das Intervall in Tagen (z. B. alle 30 Tage) mit nächstem Termin werden hinterlegt; der tägliche Hintergrundjob legt die Tickets an.</p>
|
||||
<fig n="admin-wiederkehrende-tickets">Wiederkehrende Tickets.</fig>
|
||||
|
||||
<h2>Zeiterfassung (Auswertung)</h2>
|
||||
<p>Alle in Tickets erfassten Bearbeitungszeiten, mit Mitarbeiter, Ticket und Summen, filterbar nach Zeitraum.</p>
|
||||
<fig n="admin-zeiterfassung">Erfasste Bearbeitungszeiten.</fig>
|
||||
|
||||
<h2>Geschäftszeiten und Feiertage</h2>
|
||||
<p>Die Zeitfenster, in denen die SLA-Uhr läuft, je Wochentag, plus Feiertage. Außerhalb dieser Zeiten verstreicht keine SLA-Zeit; auch die Erinnerung an wartende Tickets und „Meine Woche“ rechnen in Arbeitszeit.</p>
|
||||
<fig n="admin-geschaeftszeiten">Geschäftszeiten mit Feiertagen.</fig>
|
||||
@@ -0,0 +1,70 @@
|
||||
<h1>Adminportal: E-Mail, Inhalte und Kundenservice</h1>
|
||||
|
||||
<h2>E-Mail-Adressen</h2>
|
||||
<p>Hier verwaltet ihr Absender- und Eingangsadressen. Jede Adresse gehört zu einer Abteilung, kann ein <b>Hilfethema</b> vorgeben (Tickets über diese Adresse bekommen es automatisch) und optional einen <b>eigenen SMTP-Server</b> (Reiter „Abgehend (SMTP)“) sowie einen eigenen Abruf. Mehrere Adressen lassen sich auf einmal anlegen (eine pro Zeile). <b>„Testen“</b> prüft Abruf und Versand jeder Adresse einzeln. Gibt es für eine Adresse keinen eigenen Server, gilt der allgemeine.</p>
|
||||
<fig n="admin-email-adressen">E-Mail-Adressen mit Abteilung und Test.</fig>
|
||||
<div class="warn">Die Absenderadresse einer Abteilung muss bei eurem Mail-Anbieter existieren und freigeschaltet sein, sonst blockt der Anbieter den Versand. Im Zweifel die Adresse deaktivieren oder löschen; dann gilt die Standard-Absenderadresse.</div>
|
||||
|
||||
<h2>Postfächer (E-Mail-Abruf)</h2>
|
||||
<p>Damit eine Mail an <code>support@eure-domain.de</code> automatisch als Ticket erscheint, ruft DeskHub das Postfach per <b>IMAP</b> ab.</p>
|
||||
<ol class="steps">
|
||||
<li>Adminportal → <b>Postfächer (Abruf)</b> → „Neues Postfach“.</li>
|
||||
<li>Server, Port, Benutzername und Passwort eintragen (beim Anbieter meist unter „IMAP-Zugang“).</li>
|
||||
<li>Optional eine <b>Standardabteilung</b> wählen, sonst entscheidet die Empfängeradresse.</li>
|
||||
<li>Speichern und auf <b>„Testen“</b> klicken.</li>
|
||||
</ol>
|
||||
<fig n="admin-postfaecher">Postfächer für den Abruf.</fig>
|
||||
<ul>
|
||||
<li><b>Mehrere Postfächer:</b> Jedes bekommt eine eigene Standardabteilung; DeskHub ruft alle aktiven nacheinander ab.</li>
|
||||
<li><b>Nach dem Abruf</b> legt ihr je Postfach fest: als gelesen markieren (Voreinstellung, am sichersten), in einen Ordner verschieben oder löschen.</li>
|
||||
<li><b>Zuordnung von Antworten:</b> Eine Mail wird einem Ticket zugeordnet, wenn die DeskHub-Nummer im Betreff steht, eine andere im System vorhandene Nummer im Betreff steht oder die Kopfzeilen <code>In-Reply-To</code>/<code>References</code> auf eine DeskHub-Mail zeigen (so klappt es auch bei geändertem Betreff). Bei den beiden letzten Wegen muss der Absender zum Ticket gehören.</li>
|
||||
<li><b>Anhänge und Formatierung:</b> Anhänge werden dem Ticket zugefügt (erlaubte Endungen und Größe wie beim Hochladen, mit einer eigenen Zusatzliste für eingehende Mails). HTML-Mails erscheinen formatiert, eingebettete Bilder werden gespeichert, fremde Bilder nicht automatisch geladen.</li>
|
||||
<li><b>Überwachung:</b> Schlägt der Abruf mehrfach fehl, erscheint ein rotes Banner im Mitarbeiterbereich und in den Apps, und die Administratoren bekommen eine Mail (höchstens einmal in 24 Stunden) und eine weitere, sobald es wieder klappt.</li>
|
||||
</ul>
|
||||
|
||||
<h2>E-Mail-Vorlagen</h2>
|
||||
<p>Die Texte aller automatisch verschickten Mails – Eingangsbestätigung, Antwort, Zuweisung, Erinnerung an Kunden (<code>customer_reminder</code>), SLA-Meldung u. a. – lassen sich ersetzen. Platzhalter wie <code>{customer_name}</code>, <code>{ticket_number}</code>, <code>{subject}</code> und <code>{ticket_url}</code> werden beim Versand gefüllt.</p>
|
||||
<fig n="admin-email-vorlagen">E-Mail-Vorlagen.</fig>
|
||||
|
||||
<h3>Das Mail-Design (ab Server 1.0.80)</h3>
|
||||
<p>Alle automatischen Mails haben denselben Rahmen: Kopf mit Name oder <b>Logo</b> (Einstellungen → Design), Überschrift, die Ticketnummer als Karte mit Farbbalken, der Antworttext im Zitatkasten, ein großer Knopf und eine ruhige Fußzeile. Der Aufbau ist für Outlook, Gmail, Apple Mail und Handys gebaut (feste Breite 600 px, die auf kleinen Bildschirmen schrumpft; in Outlook ist der Knopf ein echter Knopf). Texte, die ihr unter E-Mail-Vorlagen angepasst habt, bekommen den Rahmen automatisch. Mails von Plugins behalten ihr eigenes Aussehen. Eine Vorschau mit Beispieldaten erzeugt <code>tools/mail_preview.php</code>.</p>
|
||||
<fig n="mail-ticket-erstellt">Bestätigungsmail an den Kunden.</fig>
|
||||
<fig n="mail-neue-antwort">Mail „Neue Antwort zu Ihrem Ticket“.</fig>
|
||||
<fig n="mail-sla">Mail „SLA-Frist überschritten“ an die Administratoren.</fig>
|
||||
|
||||
<h2>Mail-Warteschlange</h2>
|
||||
<p>Scheitert der Versand (Server nicht erreichbar, falscher Port, Störung), geht die Mail <b>nicht verloren</b>: DeskHub speichert sie und versucht es erneut – nach 5, 10, 20, 40 und 60 Minuten, danach alle 2 bis 6 Stunden, höchstens 10-mal und höchstens 48 Stunden lang; dann gilt sie als aufgegeben.</p>
|
||||
<fig n="admin-mail-warteschlange">Mail-Warteschlange mit Empfänger, Versuchen und letztem Fehler.</fig>
|
||||
<ul>
|
||||
<li>Pro Mail: „Jetzt senden“ oder „Löschen“; außerdem „Alle wartenden jetzt senden“ und „Aufgegebene erneut versuchen“.</li>
|
||||
<li>Wartende Mails zeigt die Statusleiste als Warnung, aufgegebene als Fehler. Erledigte und aufgegebene Einträge löscht DeskHub nach 30 Tagen.</li>
|
||||
<li>Test-Mails und Adressen mit der Endung <code>.invalid</code> kommen nicht in die Warteschlange. Mit dem Schalter lässt sich die Funktion ausschalten.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Seiten</h2>
|
||||
<p>Frei gestaltbare Inhaltsseiten für das Kundenportal (Impressum, AGB, Datenschutz, Hilfe). Jede Seite hat Titel, Adresse, Inhalt im Editor und Sichtbarkeit; veröffentlichte Seiten erscheinen im Portal, meist in der Fußzeile.</p>
|
||||
<fig n="admin-seiten">Seiten für das Kundenportal.</fig>
|
||||
|
||||
<h2>Wissensdatenbank</h2>
|
||||
<p>Öffentliche Hilfe-Artikel mit Kategorien, Titel, Text, Status (veröffentlicht/Entwurf) und Suche. Veröffentlichte Artikel erscheinen im Portal, in den Antworten des Assistenten und als Vorschlag in der Ticketansicht („Dazu passend“).</p>
|
||||
<fig n="admin-wissensdatenbank">Verwaltung der Wissensdatenbank.</fig>
|
||||
|
||||
<h2>Digitaler Assistent (Verwaltung)</h2>
|
||||
<p>Adminportal → <b>Digitaler Assistent</b> mit drei Bereichen:</p>
|
||||
<ul>
|
||||
<li><b>Einstellungen:</b> ein-/ausschalten, Name, Begrüßung, Text bei fehlender Antwort, Abteilung und Priorität erzeugter Tickets, Anzeigemodus (auch „Immer (zum Testen)“), <b>Gleichbedeutende Wörter</b> (Gruppen wie „Zugang, Einladung, beitreten“) und <b>„Frage ausprobieren“</b>.</li>
|
||||
<li><b>Zusätzliche Antworten:</b> Frage, Stichwörter und Text; sie haben Vorrang vor der Wissensdatenbank.</li>
|
||||
<li><b>Fragen-Log:</b> Jede Kundenfrage mit Ergebnis (eigene Antwort, Wissensdatenbank, keine Antwort, Ticket gewünscht/erstellt) und 👍/👎. Filter nach Ergebnis, Zeitraum und Text; „Häufige Fragen ohne Antwort“ fasst gleiche Fragen zusammen und legt mit einem Klick eine neue Antwort an. Gespeichert werden nur Frage und Ergebnis (kein Name, keine E-Mail); Aufbewahrung einstellbar (Standard 90 Tage, 0 = nichts speichern); „Log leeren“ löscht alles.</li>
|
||||
</ul>
|
||||
<p>Das Recht „Digitalen Assistenten verwalten“ lässt sich auch einer Rolle geben; die Verwaltung gibt es ebenso in Windows-Programm und Android-App.</p>
|
||||
<fig n="admin-assistent-einstellungen">Assistent: Einstellungen.</fig>
|
||||
<fig n="admin-assistent-antworten">Assistent: zusätzliche Antworten.</fig>
|
||||
<fig n="admin-assistent-log">Assistent: Fragen-Log.</fig>
|
||||
|
||||
<h2>Statusseite (Verwaltung)</h2>
|
||||
<p>Dienste mit Zustand <i>In Betrieb</i>, <i>Wartung</i>, <i>Eingeschränkt</i> oder <i>Ausfall</i> und <b>Störungsmeldungen</b> mit Updates. Eine Meldung kann den Dienst gleich umstellen; „Behoben“ setzt ihn zurück, solange keine andere Meldung offen ist. Dienste mit Prüfziel (Domain oder IP mit Port, auch ein Minecraft-Server) werden automatisch überwacht; fällt einer aus (zwei Prüfungen hintereinander) oder läuft wieder, bekommen die Administratoren eine Mail und eine Push-Nachricht (höchstens eine je Dienst und Richtung in 10 Minuten; abschaltbar). Mit „Intern machen“ erscheint ein Dienst nur für Mitarbeiter und nicht auf der öffentlichen Seite.</p>
|
||||
<fig n="admin-statusseite">Verwaltung der Statusseite.</fig>
|
||||
|
||||
<h2>Sprachen</h2>
|
||||
<p>Deutsch ist eingebaut; jede weitere Sprache ist ein Plugin („Sprachpaket“) und braucht keine Änderung am Server. Adminportal → <b>Sprachen</b> zeigt installierte Sprachen, die Standardsprache und eine Tabelle, welches Plugin für welche Sprache Texte mitbringt. Mit dem <b>Sammelmodus</b> für Übersetzer merkt sich DeskHub die Texte besuchter Seiten, „Vorlage laden“ liefert eine fertige <code>strings.json</code>. Besucher wechseln die Sprache über die Flaggen. Mitgeliefert ist English (rund 3400 Einträge).</p>
|
||||
<fig n="admin-sprachen">Sprachen und Sprachpakete.</fig>
|
||||
@@ -0,0 +1,104 @@
|
||||
<h1>Adminportal: Kunden, Mitarbeiter und Sicherheit</h1>
|
||||
|
||||
<h2>Kundenverzeichnis</h2>
|
||||
<p>Alle Kunden-Konten im Überblick mit Name, E-Mail, Organisation und Anzahl Tickets. Von hier öffnet ihr das Kundenprofil, legt Konten an oder bearbeitet sie. Nur echte Administratoren haben Zugriff.</p>
|
||||
<fig n="admin-kundenverzeichnis">Das Kundenverzeichnis.</fig>
|
||||
|
||||
<h2>Organisationen</h2>
|
||||
<p>Mehrere Kunden werden zu einer Firma gebündelt. Tickets lassen sich so gemeinsam betrachten, und Kollegen eines Kunden sehen gegenseitig ihre Tickets, wenn die Organisation das erlaubt.</p>
|
||||
<fig n="admin-organisationen">Organisationen.</fig>
|
||||
|
||||
<h2>Mitarbeiter</h2>
|
||||
<p>Die Mitarbeiterverwaltung zeigt je Person Avatar oder Initialen, Name und E-Mail, die <b>Rolle</b> (Administrator, eigene Rolle oder Mitarbeiter, dazu „🔒 2FA an“), <b>Abteilung</b> samt weiteren Abteilungen und Teams, die Zahl der <b>offenen Tickets</b>, <b>zuletzt aktiv</b> (grüner Punkt bis 5 Minuten) und den <b>Status</b> (Aktiv, Deaktiviert, Im Urlaub, „Anmeldung zeitweise gesperrt“). Oben stehen Filter (Alle, Administratoren, Mitarbeiter, Deaktiviert, Gesperrt – jeweils mit Zahl) und ein Suchfeld nach Name, E-Mail, Abteilung und Rolle.</p>
|
||||
<fig n="admin-mitarbeiter">Die Mitarbeiterübersicht.</fig>
|
||||
<p>Rechts gibt es <b>Bearbeiten</b> und das Menü <b>⋯</b>:</p>
|
||||
<ul>
|
||||
<li><b>Sperre aufheben</b> (nur bei gesperrter Anmeldung): löscht die Fehlversuch-Zähler für Web, App und 2FA dieses Kontos; die IP-Sperre bleibt.</li>
|
||||
<li><b>Passkeys entfernen (n):</b> Erscheint nur, wenn der Mitarbeiter Passkeys hat (in der Zeile steht „🔑 n Passkeys“). Löscht <b>alle</b> Passkeys dieses Mitarbeiters – etwa bei verlorenem Gerät oder Verdacht auf Missbrauch. Der Mitarbeiter bekommt eine Mail, der Vorgang steht im Anmelde-Wächter („Passkey entfernt“) und im Admin-Protokoll. Die Anmeldung mit Passwort (und 2FA) bleibt möglich; neue Passkeys richtet der Mitarbeiter selbst ein. Das Recht „Mitarbeiter verwalten“ genügt.</li>
|
||||
<li><b>Überall abmelden:</b> beendet alle Anmeldungen der Person (Browser, Android, Windows, Chrome-Erweiterung), löscht deren Push-Geräte, gibt Ticket-Sperren und laufende Zeiterfassung frei; das Passwort bleibt. Der Mitarbeiter bekommt eine Mail, der Vorgang steht im Anmelde-Wächter und im Admin-Protokoll.</li>
|
||||
<li><b>Löschen</b> (nicht beim eigenen Konto).</li>
|
||||
</ul>
|
||||
<fig n="admin-menue-aktionen">Das Menü ⋯ einer Zeile.</fig>
|
||||
<p>Bei Mitarbeitern mit Passkeys zeigt die Liste „🔑 n Passkeys“, und das Menü enthält „Passkeys entfernen (n)“.</p>
|
||||
<h3>Konto bearbeiten und Zugriff</h3>
|
||||
<p>Im Bearbeiten-Formular stellt ihr Name, E-Mail, Passwort, Hauptabteilung, <b>weitere Abteilungen mit jeweils eigener Rolle</b>, Teams, Urlaubsstatus und Vertretung ein; ein Mitarbeiter kann so „Teamleitung“ in Technik und „Agent“ im Vertrieb sein. Im Reiter „Zugriff“ legt ihr fest, ob das Konto aktiv ist und welche Adminportal-Bereiche (als Co-Admin) freigeschaltet sind.</p>
|
||||
<fig n="admin-mitarbeiter-bearbeiten">Mitarbeiter-Konto bearbeiten.</fig>
|
||||
<fig n="admin-mitarbeiter-zugriff">Zugriff und Rollen je Abteilung.</fig>
|
||||
<p>Unter dem Formular steht die Karte <b>„Passkeys von …“</b>: Alle Passkeys der Person mit Name, Datum und „zuletzt benutzt“. Mit <b>Entfernen</b> löscht ihr einen einzelnen Passkey, mit <b>„Alle Passkeys entfernen“</b> alle. Der Mitarbeiter bekommt eine Mail und kann sich weiter mit Passwort (und 2FA) anmelden.</p>
|
||||
<fig n="admin-mitarbeiter-passkeys">Karte „Passkeys von …“ in der Bearbeiten-Ansicht (Beispieldaten).</fig>
|
||||
|
||||
<h2>Teams</h2>
|
||||
<p>Teams sind Gruppen von Mitarbeitern, an die Tickets zugewiesen oder Benachrichtigungen gerichtet werden können (z. B. „Nachtschicht“).</p>
|
||||
<fig n="admin-teams">Teams.</fig>
|
||||
|
||||
<h2>Rollen und Rechte</h2>
|
||||
<p>Rollen sind Sammlungen von Rechten. Jeder Mitarbeiter hat entweder keine Rolle (dann gelten die <b>Standardrechte</b>) oder eine eigene Rolle; je Abteilung kann er eine andere haben. <b>Administratoren</b> dürfen immer alles. Ein Recht für einen Adminportal-Bereich macht eine Person zum <b>Co-Admin</b> genau für diesen Bereich: Sie sieht das Adminportal im Menü, aber nur die Punkte, für die sie ein Recht hat.</p>
|
||||
<fig n="admin-rollen">Rollen mit der Rechte-Übersicht „Wer darf was?“ (Beispieldaten).</fig>
|
||||
<h3>Vordefinierte Rollen</h3>
|
||||
<p>Mit dem Update legt DeskHub elf fertige Rollen an. Sie sind ganz normale Rollen: Ihr könnt Rechte ändern, die Rolle umbenennen oder löschen. Vorhandene Rollen werden nie überschrieben. Eine gelöschte Rolle legt der Knopf <b>„Fehlende Rollen anlegen“</b> auf der Rollen-Seite wieder an (nur Administratoren). Auf der Rollen-Seite tragen sie das Kennzeichen „vordefiniert“.</p>
|
||||
<table>
|
||||
<tr><th>Rolle</th><th>Für wen / was sie darf</th></tr>
|
||||
<tr><td><b>Co-Administrator</b></td><td>Alle Rechte, die sich vergeben lassen. Kein echter Administrator: Einstellungen, Backup, System-Update, Plugins, API-Schlüssel, Webhooks, Import und Sicherheitsprüfung bleiben gesperrt.</td></tr>
|
||||
<tr><td><b>Support-Manager</b></td><td>Leitet den Support: Tickets inklusive Löschen und Export, Einrichtung (Abteilungen, Prioritäten, Themen, Regeln, Felder), Mitarbeiter, Teams, Protokolle, Mail-Warteschlange. Keine Rollenverwaltung.</td></tr>
|
||||
<tr><td><b>IT / Technik</b></td><td>Betrieb: Tickets bearbeiten, dazu Postfächer und E-Mail-Adressen, Mail-Warteschlange, Anmelde-Wächter, Statusseite, Regeln, Eskalationen, Protokoll, Probleme, Ticket-Felder, Hilfe.</td></tr>
|
||||
<tr><td><b>Teamleitung</b></td><td>Wie Mitarbeiter, dazu Löschen, Export, Zeiterfassung, Textbausteine, Wissensdatenbank, Teams und Schichten.</td></tr>
|
||||
<tr><td><b>Support 2nd Level</b></td><td>Erfahrene Mitarbeiter: Tickets bearbeiten, Probleme verwalten, Export und Zeiterfassung.</td></tr>
|
||||
<tr><td><b>Support 1st Level</b></td><td>Einstieg: antworten, Notizen, zuweisen, Tickets anlegen und verknüpfen, E-Mails abrufen. Kein Schließen, Verschieben, Zusammenführen oder Sperren.</td></tr>
|
||||
<tr><td><b>Redaktion (Wissen und Inhalte)</b></td><td>Pflegt Wissensdatenbank, digitalen Assistenten, Textbausteine, Ticket- und E-Mail-Vorlagen, Seiten und Sprachen.</td></tr>
|
||||
<tr><td><b>Auswertung / Controlling</b></td><td>Berichte (auch per E-Mail), Export, Zeiterfassung, Protokoll. Bearbeitet keine Tickets.</td></tr>
|
||||
<tr><td><b>Teamverwaltung / Schichtplanung</b></td><td>Mitarbeiter, Teams, Status und Schichten, Geschäftszeiten, Anmelde-Wächter ansehen.</td></tr>
|
||||
<tr><td><b>Aushilfe / Praktikant</b></td><td>Nur antworten, Notizen schreiben und Kundenprofile ansehen.</td></tr>
|
||||
<tr><td><b>Nur lesen</b></td><td>Tickets, Kundenprofile und Berichte ansehen, nichts ändern.</td></tr>
|
||||
</table>
|
||||
<p class="small">Ohne Rolle gelten die Standardrechte eines Mitarbeiters. Im Formular steht „Mitarbeiter“ zusätzlich als Vorlage zum Laden bereit.</p>
|
||||
|
||||
<h3>Eine Rolle anlegen</h3>
|
||||
<ol class="steps">
|
||||
<li>Adminportal → <b>Rollen</b> → „Neue Rolle hinzufügen“, Namen vergeben (z. B. „Teamleitung Technik“).</li>
|
||||
<li>Im Reiter <b>Berechtigungen</b> eine <b>Vorlage laden</b> (alle vordefinierten Rollen und „Mitarbeiter“) und danach einzelne Rechte anpassen. Je Gruppe gibt es „Alle“ und „Keine“; oben zählt „n von 44 Rechten“ mit.</li>
|
||||
<li>Speichern und unter Mitarbeiter die Rolle zuweisen.</li>
|
||||
</ol>
|
||||
<fig n="admin-rollen-rechte">Berechtigungen einer Rolle, gruppiert, mit Erklärung und Vorlagen.</fig>
|
||||
<p>Die <b>Rechte-Übersicht</b> auf der Rollen-Seite zeigt in einer Tabelle, welche Rolle welches Recht hat. Wer kein Administrator ist, kann in einer Rolle <b>keine Rechte vergeben, die er selbst nicht hat</b>; so lässt sich niemand zum Administrator hochstufen.</p>
|
||||
|
||||
<h3>Alle Rechte im Überblick</h3>
|
||||
<table>
|
||||
<tr><th>Gruppe</th><th>Rechte (Standard für Mitarbeiter ohne Rolle in Klammern: ✓ an, – aus)</th></tr>
|
||||
<tr><td><b>Tickets bearbeiten</b></td><td>Tickets beantworten ✓ · Interne Notizen ✓ · Schließen/lösen ✓ · Zuweisen ✓ · Übertragen (Abteilung ändern) ✓ · Tickets erstellen ✓ · <b>Zusammenführen und teilen</b> ✓ · <b>Verknüpfen</b> ✓ · Löschen – (gilt auch für die Sammelaktion)</td></tr>
|
||||
<tr><td><b>Kunden und Sperren</b></td><td>Kundenprofile ansehen ✓ · <b>Kundenverzeichnis verwalten</b> – · Organisationen verwalten – · <b>Sperrliste und Absender sperren</b> ✓</td></tr>
|
||||
<tr><td><b>Auswertung und Export</b></td><td>Berichte einsehen ✓ · <b>Berichte per E-Mail versenden</b> ✓ (externe Empfänger nur Administratoren) · <b>Tickets als CSV exportieren</b> – · Zeiterfassung einsehen – · Admin-Protokoll einsehen –</td></tr>
|
||||
<tr><td><b>E-Mail</b></td><td><b>E-Mails jetzt abrufen</b> ✓ · E-Mail-Adressen und Postfächer – · E-Mail-Vorlagen – · <b>Mail-Warteschlange</b> –</td></tr>
|
||||
<tr><td><b>Inhalte und Vorlagen</b></td><td>Textbausteine – · <b>Ticket-Vorlagen</b> – · Wissensdatenbank – · Digitaler Assistent – · <b>Seiten im Portal</b> – · <b>Sprachen</b> – · <b>Statusseite</b> – · <b>Ansichten mit dem Team teilen</b> ✓</td></tr>
|
||||
<tr><td><b>Einrichtung</b></td><td>Abteilungen – · Prioritäten/SLA – · Hilfethemen – · <b>Ticket-Felder und Listen</b> – · <b>Ticket-Regeln und wiederkehrende Tickets</b> – · Eskalationsregeln – · Geschäftszeiten – · <b>Problemmanagement</b> –</td></tr>
|
||||
<tr><td><b>Team und Sicherheit</b></td><td>Mitarbeiter verwalten (auch Sperre aufheben, Überall abmelden, Passkeys entfernen) – · Teams – · Rollen – · <b>Status und Schichten der Kollegen ändern</b> – · <b>Anmelde-Wächter ansehen</b> – · <b>Hilfe und Handbuch</b> –</td></tr>
|
||||
</table>
|
||||
<p class="small">Fett gesetzte Rechte sind neu ab Server 1.0.85.</p>
|
||||
<div class="note"><b>Weiterhin nur für echte Administratoren</b> (nicht delegierbar): Einstellungen, Backup, System-Update, Plugins, API-Schlüssel, Webhooks, osTicket-Import, Sicherheitsprüfung, die Einstellungen des Anmelde-Wächters (inklusive „Passkeys erlauben“) und die Datenschutz-Löschfunktionen. Die Sammelaktionen in der Ticketliste prüfen die Rechte je Ticket und Abteilung.</div>
|
||||
<div class="tip"><b>Nichts geht verloren:</b> Bestehende Rollen behalten ihre Rechte. Für neue Rechte gilt der Standardwert: Was bisher jeder Mitarbeiter durfte (zum Beispiel zusammenführen, Sperrliste, E-Mails abrufen), bleibt erlaubt, neue Adminportal-Bereiche sind aus, bis ihr sie vergebt. Die Rechte gelten überall gleich (Web, Windows-Programm ab 2.12.3, Android-App ab 3.5.3, Chrome-Erweiterung). Die Rollen-Editoren der beiden Apps zeigen alle Rechte gruppiert mit den Beschriftungen vom Server; „E-Mails abrufen“, das Teilen von Ansichten, Sperrliste, Automatisierung und der Status der Kollegen erscheinen nur mit dem passenden Recht. Beim Speichern einer Rolle in einer älteren App bleiben die neuen Rechte unverändert.</div>
|
||||
|
||||
<h2>Anmelde-Wächter</h2>
|
||||
<p>Der Wächter schützt die Anmeldung der Mitarbeiter (Web <b>und</b> Apps) und ist fest eingebaut.</p>
|
||||
<fig n="admin-anmelde-waechter">Anmelde-Wächter mit Einstellungen und Protokoll.</fig>
|
||||
<ul>
|
||||
<li><b>Passkeys erlauben:</b> Der Schalter schaltet die Anmeldung mit Passkey für alle ein oder aus (Standard: an). Ausgeschaltet verschwindet „Mit Passkey anmelden“ und Anmeldungen mit Passkey werden abgelehnt; vorhandene Passkeys bleiben gespeichert und gelten wieder, sobald ihr einschaltet.</li>
|
||||
<li><b>Geräte und Orte merken:</b> Ein Gerät ist ein Browser (Cookie <code>dh_device</code>) oder eine App. Ein Ort ist das Land (wenn Cloudflare es mitschickt) oder das Netz der IP-Adresse. Die erste Anmeldung eines Kontos wird still gemerkt.</li>
|
||||
<li><b>Warnung per Mail</b> bei neuem Gerät (und abschaltbar bei neuem Ort) mit Zeit, Gerät, Ort, IP und dem Link <b>„Das war ich nicht“</b> (7 Tage, einmalig): Er macht das Passwort ungültig, beendet alle App- und Browser-Sitzungen, vergisst die gemerkten Geräte und informiert die Administratoren.</li>
|
||||
<li><b>Sperre nach Fehlversuchen:</b> Standard 5 Fehlversuche, dann 15 Minuten (einstellbar: 3 bis 20 Versuche, 5 bis 240 Minuten); gilt auch für falsche 2FA-Codes. Zusätzlich ein Limit je IP-Adresse. Bekannte Geräte bleiben mit richtigem Passwort anmeldbar. Die Sperre wird protokolliert und per Mail an Kontoinhaber und Administratoren gemeldet; Administratoren heben sie mit „Sperre aufheben“ auf.</li>
|
||||
<li><b>Protokoll:</b> jede Anmeldung (auch per Passkey), jeder Fehlversuch, jede Sperre, „Konto gesichert“ (löscht auch die Passkeys), „Überall abgemeldet“, „Passkey hinzugefügt/entfernt“ und „Sperre aufgehoben“ mit Zeit, Konto, Gerät, Ort, IP und Kanal; Zähler der letzten 24 Stunden, Filter und Suche; Einträge werden nach 180 Tagen gelöscht (einstellbar).</li>
|
||||
<li><b>Grenzen:</b> Ohne Fremddienst gibt es keinen genauen Standort. Wer Cookies löscht, gilt als neues Gerät.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Admin-Protokoll</h2>
|
||||
<p>Alle wichtigen Änderungen und Ereignisse: wer was wann geändert hat (Einstellungen, Rollen, Mitarbeiter, Backups, Plugin-Aktivierungen, Anmeldungen über Plugins, Übergaben an die Weboberfläche) sowie fehlgeschlagene automatische E-Mails mit der Fehlermeldung des Anbieters.</p>
|
||||
<fig n="admin-protokoll">Das Admin-Protokoll.</fig>
|
||||
|
||||
<h2>Sicherheitsprüfung</h2>
|
||||
<p>Prüft die Einrichtung der Installation und ruft interne Adressen selbst von außen ab, um zu sehen, ob Konfiguration, Datenbank, Backups oder Programmcode erreichbar wären. Nach der Installation und nach jeder Änderung am Webserver einmal ausführen; jeder Punkt zeigt „in Ordnung“ oder was zu tun ist.</p>
|
||||
<fig n="admin-sicherheitspruefung">Ergebnis der Sicherheitsprüfung.</fig>
|
||||
|
||||
<h2>Hilfe und Handbuch</h2>
|
||||
<p>Adminportal → <b>Hilfe</b> (Recht „Hilfe und Handbuch“; standardmäßig nur Administratoren) enthält das <b>Handbuch als PDF</b> zum Herunterladen mit Größe, Stand, Version und Prüfsumme, eine Kurzhilfe („Erste Schritte“ und „Wenn etwas nicht funktioniert“, jeweils mit Sprungmarken) und die Daten dieser Installation. Das Handbuch kommt mit jedem Update im Programmpaket mit (<code>docs/DeskHub-Handbuch.pdf</code>); einen Upload gibt es nicht.</p>
|
||||
|
||||
<h2>Problemmanagement</h2>
|
||||
<p>Ein <b>Problem</b> bündelt mehrere Tickets mit derselben Ursache (z. B. „Mailserver-Ausfall“). Ihr erfasst das Problem mit Titel und Beschreibung, verknüpft Tickets und seht im Ticket die zugehörigen Probleme (Reiter „Organisation“). So lässt sich die Ursache einmal beheben und alle betroffenen Tickets gemeinsam abschließen.</p>
|
||||
<fig n="admin-probleme">Problemmanagement.</fig>
|
||||
@@ -0,0 +1,69 @@
|
||||
<h1>Einstellungen</h1>
|
||||
<p>Adminportal → <b>Einstellungen</b> bündelt die Grundkonfiguration in acht Reitern: Allgemein, Design, E-Mail, Benachrichtigungen, Uploads, Footer, Cron-Jobs und Datenschutz. Oben rechts führt „osTicket Import“ zum Import-Assistenten (Kapitel 17).</p>
|
||||
|
||||
<h2>Allgemein</h2>
|
||||
<p>Die Karte <b>System</b> enthält die Schalter für das tägliche Verhalten:</p>
|
||||
<table>
|
||||
<tr><th>Einstellung</th><th>Wirkung</th></tr>
|
||||
<tr><td>Helpdesk-Status, Offline-Nachricht</td><td>Online/Offline des Supports; bei „Offline“ sehen Kunden die Offline-Nachricht</td></tr>
|
||||
<tr><td>Standardabteilung</td><td>Abteilung für Tickets ohne erkennbare Zuordnung</td></tr>
|
||||
<tr><td>Wissensdatenbank im Kundenportal anzeigen</td><td>Blendet die Artikel im Portal und für den Assistenten ein</td></tr>
|
||||
<tr><td>Minecraft-Plugin auf der Apps-Seite anzeigen</td><td>Zeigt die Karte mit dem Download (Standard: aus; ausgeblendet ist auch der Download gesperrt)</td></tr>
|
||||
<tr><td>Ticket-Bewertung aktivieren</td><td>Kunden können nach Schließen 1–5 Sterne vergeben</td></tr>
|
||||
<tr><td>Automatische Zeiterfassung</td><td>Misst automatisch, wie lange ein Mitarbeiter ein Ticket geöffnet hat</td></tr>
|
||||
<tr><td>Beim Öffnen automatisch zuweisen</td><td>Nicht zugewiesene Tickets gehen an den, der sie öffnet</td></tr>
|
||||
<tr><td>Tickets automatisch schließen</td><td>Schließt Tickets, wenn der Kunde nach einer Antwort nicht mehr reagiert (nach X Tagen)</td></tr>
|
||||
<tr><td>Bei überschrittener SLA-Frist benachrichtigen</td><td><i>Nur die Administratoren (per E-Mail)</i> – Standard – oder <i>Bearbeiter bzw. Abteilung (E-Mail und Push in der App)</i></td></tr>
|
||||
<tr><td>Bearbeiter erinnern</td><td>Erinnert, wenn ein Ticket nach X Arbeitsstunden noch auf Antwort wartet; zusätzlich per E-Mail wählbar</td></tr>
|
||||
<tr><td>Kunden erinnern</td><td>Eine freundliche Mail, wenn der Kunde nach X Tagen nicht reagiert</td></tr>
|
||||
<tr><td>Zufriedenheitsabfrage</td><td>Mail mit Sternen nach dem Schließen oder Lösen (Wartezeit in Minuten)</td></tr>
|
||||
<tr><td>Live-Chat im Kundenportal</td><td>Chat-Widget ein/aus, Abteilung für Live-Chat-Tickets, Chatverlauf per E-Mail an den Kunden</td></tr>
|
||||
<tr><td>Automatischer Logout nach Inaktivität</td><td>Minuten (5–480), Countdown oben rechts</td></tr>
|
||||
</table>
|
||||
<p>Die Karte <b>Anzeigename & URL</b> legt Name (Browser-Tab, Mails, Web-App), <b>Website-URL</b> (für Links in Mails, ohne abschließenden Schrägstrich, am besten <code>https://</code>) und Zeitzone fest. „E-Mail-Versand aktiv (PHP mail) Fallback“ nutzt die PHP-Funktion, wenn kein SMTP eingerichtet ist; „Bei schweren Fehlern eine E-Mail senden“ meldet Probleme an die Administratoren (höchstens 10 Mails pro Stunde).</p>
|
||||
<fig n="admin-einstellungen-allgemein">Einstellungen → Allgemein.</fig>
|
||||
|
||||
<h2>Design</h2>
|
||||
<ul>
|
||||
<li><b>Firmenlogo:</b> PNG, JPG, GIF oder SVG bis 2 MB; erscheint im Kundenportal, auf den Anmeldeseiten und im Mitarbeiterbereich. „Name neben dem Logo ausblenden“ zeigt nur das Logo. Helle Logos sind auf hellen Portalseiten schwer erkennbar.</li>
|
||||
<li><b>Favicon:</b> das Symbol im Browser-Tab (ICO, PNG, JPG, GIF, WebP oder SVG, höchstens 1 MB, am besten quadratisch; ohne eigenes Favicon gilt das DeskHub-Symbol).</li>
|
||||
<li><b>Titelbild (Hero-Banner):</b> Hintergrundbild im dunklen Kopfbereich des Portals (mindestens 1600×500 px, höchstens 5 MB).</li>
|
||||
<li><b>Startseiten-Inhalte:</b> Schalter für zusätzliche Bereiche der Portal-Startseite, zum Beispiel „Beliebte Artikel“.</li>
|
||||
<li><b>Themes für das Kundenportal:</b> Themes als ZIP hochladen, aktivieren und löschen. Ein aktives Theme gilt für das ganze Portal; „Standarddesign nutzen“ deaktiviert es. Fertige Themes (u. a. <i>Halloween Nacht</i> und <i>Weihnachten Winter</i>) liegen im Ordner <code>theme/</code>. Aus dem Katalog der Plugin-Webseite („Themes aus dem Katalog“) werden Prüfsumme und Signatur vor der Installation geprüft; aktiviert wird ein Theme erst, wenn ihr es selbst aktiviert.</li>
|
||||
</ul>
|
||||
<fig n="admin-einstellungen-design">Einstellungen → Design.</fig>
|
||||
|
||||
<h2>E-Mail</h2>
|
||||
<h3>SMTP-Versand</h3>
|
||||
<p>Server, Port, Verschlüsselung, Benutzername, Passwort, Absendername und Absenderadresse eures Anbieters eintragen und mit <b>„Test-Mail senden“</b> prüfen. „STARTTLS“ gehört zu Port <b>587</b>, „SSL/TLS“ zu Port <b>465</b>; beim Umschalten setzt DeskHub den Port selbst, wenn dort noch ein Standardport steht. Passwörter werden verschlüsselt gespeichert. Kommt eine bestimmte automatische Mail trotz erfolgreicher Test-Mail nicht an, zeigt das Admin-Protokoll die Fehlermeldung des Anbieters.</p>
|
||||
<h3>E-Mail-Abruf (IMAP)</h3>
|
||||
<p>Die rechte Karte zeigt, wie viele Postfächer eingerichtet sind, wann zuletzt abgerufen wurde, und führt mit „Postfächer verwalten“ zur Verwaltung (Kapitel 11).</p>
|
||||
<fig n="admin-einstellungen-email">Einstellungen → E-Mail.</fig>
|
||||
|
||||
<h2>Benachrichtigungen</h2>
|
||||
<ul>
|
||||
<li><b>Discord & Telegram:</b> Mitarbeiter werden bei neuen Tickets und Antworten über einen Discord-Webhook oder einen Telegram-Bot (Bot-Token und Chat-ID) benachrichtigt. Webhook-URL und Bot-Token werden verschlüsselt gespeichert; leer lassen = unverändert, Wert = löschen.</li>
|
||||
<li><b>Android-App: Push-Benachrichtigungen:</b> Push senden an/aus, optional „Betreff und Kundenname in der Benachrichtigung anzeigen“ (Standard aus: nur „Neues Ticket #T-123“), App-Name in der Meldung und <b>„Testnachricht an meine Geräte“</b>. Der Push läuft standardmäßig über den Push-Dienst des Herausgebers (nur Geräte-Token und Ticketnummer); ein eigenes Firebase-Projekt lässt sich in der <code>config.php</code> eintragen.</li>
|
||||
</ul>
|
||||
<fig n="admin-einstellungen-benachrichtigungen">Einstellungen → Benachrichtigungen.</fig>
|
||||
|
||||
<h2>Uploads</h2>
|
||||
<p>Die Liste der erlaubten <b>Dateiendungen</b> (kommagetrennt; im Beispielbild steht in der Demo nur <code>png</code>), die <b>maximale Dateigröße</b> in MB und die Liste <b>„Zusätzlich erlaubt für Anhänge eingehender E-Mails“</b> (Standard: <code>doc, xls, ppt, pptx, odt, ods, odp, csv, rtf, msg, heic, webp</code>; gilt nur für Mails, nicht fürs Formular). Gefährliche Typen (Skripte, ausführbare Dateien) bleiben immer gesperrt.</p>
|
||||
<fig n="admin-einstellungen-uploads">Einstellungen → Uploads.</fig>
|
||||
|
||||
<h2>Footer</h2>
|
||||
<p>Die Fußzeile des Kundenportals enthält die Verweise <b>Datenschutzerklärung</b> und <b>Impressum</b>. Für jeden legt ihr die Beschriftung im Footer fest und entweder einen <b>externen Link</b> oder einen <b>HTML-Inhalt</b>, der als Popup erscheint (ist ein Link gesetzt, wird das HTML-Feld ignoriert).</p>
|
||||
<fig n="admin-einstellungen-footer">Einstellungen → Footer.</fig>
|
||||
|
||||
<h2>Cron-Jobs</h2>
|
||||
<p>Zeigt alle automatischen Aufgaben mit Zeitplan und letztem Lauf, den Schalter für den eingebauten Hintergrund-Betrieb, die Installation echter Cronjobs und „Jetzt ausführen“ für jede Aufgabe (Kapitel 2).</p>
|
||||
<fig n="admin-einstellungen-cron">Einstellungen → Cron-Jobs.</fig>
|
||||
|
||||
<h2>Datenschutz</h2>
|
||||
<ul>
|
||||
<li><b>Automatische Datenlöschung:</b> Geschlossene und gelöste Tickets werden nach der gewählten Zeit (Monate nach Schließung) unwiderruflich anonymisiert: Name, E-Mail-Adresse und IP werden unkenntlich gemacht; Ticketnummer, Betreff, Status und Nachrichten bleiben für Statistiken erhalten. Die Funktion ist <b>standardmäßig aus</b>.</li>
|
||||
<li><b>IP-Adressen sofort kürzen:</b> beim Speichern wird das letzte Oktett entfernt.</li>
|
||||
<li><b>Auskunft und Löschung auf Anfrage:</b> blendet im Kundenprofil für Administratoren „Alle Daten exportieren (JSON)“ und „Kundendaten löschen“ ein.</li>
|
||||
<li>Die rechte Karte zeigt <b>Status & Vorschau</b> (wie viele Tickets aktuell betroffen wären), was anonymisiert wird, den <b>Crontab-Eintrag</b> und „Jetzt ausführen“.</li>
|
||||
</ul>
|
||||
<fig n="admin-einstellungen-datenschutz">Einstellungen → Datenschutz.</fig>
|
||||
@@ -0,0 +1,76 @@
|
||||
<h1>Die Apps</h1>
|
||||
<p>DeskHub gibt es als <b>Windows-Programm</b>, <b>Android-App</b> und <b>Chrome-Erweiterung</b>. Alle drei sind für Mitarbeiter und Administratoren gedacht, sprechen mit eurem Server über die Mitarbeiter-Schnittstelle und zeigen nur, was die Rolle erlaubt. Dazu kommen das <b>WordPress-Plugin</b> und das <b>Minecraft-Plugin</b> (Kapitel 16).</p>
|
||||
|
||||
<h2>Einrichtung (alle Apps)</h2>
|
||||
<ol class="steps">
|
||||
<li>App installieren (Download im Mitarbeiterbereich unter „Apps“).</li>
|
||||
<li>Beim ersten Start die Adresse eurer DeskHub-Installation eingeben, zum Beispiel <code>https://eure-domain.de</code>. Die App merkt sie sich.</li>
|
||||
<li>Mit dem Mitarbeiter-Konto anmelden (bei aktivierter 2FA mit Code).</li>
|
||||
</ol>
|
||||
|
||||
<h2>Windows-Programm</h2>
|
||||
<p>Das Windows-Programm hat ein helles und ein dunkles Design (umschaltbar oder nach System), eine eigene Titelleiste und die Sprachen Deutsch und English (Flaggen in der Kopfleiste und auf der Anmeldung). Es läuft auch unter Linux (AppImage/tar.gz); für den Mac muss es selbst gebaut werden. Die Bilder in diesem Abschnitt stammen aus einer Test-Attrappe mit Beispieldaten.</p>
|
||||
<fig n="windows-anmeldung">Anmeldung: Serveradresse, E-Mail und Passwort.</fig>
|
||||
<fig n="windows-uebersicht">Übersicht mit Kennzahlen und zuletzt aktualisierten Tickets.</fig>
|
||||
<h3>Tickets</h3>
|
||||
<p>Reiter und Filter wie im Web: <i>Alle aktiven</i>, <i>Meine</i>, <i>Nicht zugewiesen</i>, <i>Überfällig</i>, <i>Bald fällig</i>, <i>Pausiert</i>, <i>Wiedervorlage</i>, <i>Beobachtet</i>, <i>Erledigt</i>, <i>Alle</i>; Suche; gespeicherte Ansichten; Sammelaktionen; <b>„📥 E-Mails abrufen“</b> in der Kopfleiste.</p>
|
||||
<fig n="windows-tickets">Ticketliste.</fig>
|
||||
<p>Im Ticket stehen Verlauf, Antwortfeld mit <b>Textbausteinen der Abteilung</b>, <b>Signatur-Auswahl</b> mit Vorschau, Status, Zuweisung, Priorität, <b>Auf später</b> (1 Stunde bis 1 Woche oder eigener Zeitpunkt), <b>Beobachten</b>, die Karte <b>„Dazu passend“</b>, <b>Beobachter und Cc</b>, die Kundenbewertung und bei E-Mail-Tickets <b>„📎 Anhänge laden“</b>. „Im Browser“ öffnet die Ticketseite für Zusammenführen, Teilen und Drucken. Ein von jemand anderem bearbeitetes Ticket ist gesperrt; Administratoren können es mit „Sperre aufheben und übernehmen“ übernehmen.</p>
|
||||
<fig n="windows-ticket">Ein Ticket im Windows-Programm.</fig>
|
||||
<fig n="windows-neues-ticket">Neues Ticket mit Ticket-Vorlage.</fig>
|
||||
<h3>Weitere Bereiche</h3>
|
||||
<fig n="windows-aufgaben">Aufgaben.</fig>
|
||||
<fig n="windows-kunden">Kundensuche.</fig>
|
||||
<fig n="windows-chat">Team-Chat.</fig>
|
||||
<fig n="windows-live-chat">Live-Chat mit Kunden.</fig>
|
||||
<fig n="windows-berichte">Berichte: Zufriedenheit und Bericht-Baukasten.</fig>
|
||||
<fig n="windows-verfuegbarkeit">Verfügbarkeit und Schichten.</fig>
|
||||
<fig n="windows-verwaltung">Verwaltung (Kacheln je nach Rechten).</fig>
|
||||
<fig n="windows-einstellungen">Einstellungen: Darstellung, Windows (Autostart, Verhalten beim Schließen) und Benachrichtigungen.</fig>
|
||||
<fig n="windows-dunkel">Dunkles Design.</fig>
|
||||
<div class="note">In den Bildern erscheinen Hinweise wie „Mock: … nicht vorhanden“. Das sind Platzhalter der Test-Attrappe für Bereiche, die in dieser Demo keine Daten haben; im echten Programm sind diese Bereiche mit dem Server verbunden.</div>
|
||||
<ul>
|
||||
<li><b>Zähler und Benachrichtigungen:</b> Der Zähler zeigt, wie viele Tickets <i>jetzt</i> Aufmerksamkeit brauchen (offen, unbeantwortet, euch oder niemandem zugewiesen, nicht zurückgestellt, kein Spam, seit der letzten Kundennachricht von euch nicht geöffnet). Öffnen, Beantworten, Schließen, Zuweisen oder Zurückstellen – auch anderswo – nimmt ein Ticket beim nächsten Abruf (Standard alle 30 Sekunden, einstellbar) aus dem Zähler. Windows-Benachrichtigungen erscheinen nur für neue Tickets.</li>
|
||||
<li><b>Windows-Einstellungen:</b> Mit Windows starten, beim Schließen in den Infobereich wechseln, Benachrichtigungen und deren Abfrage-Intervall, Testbenachrichtigung.</li>
|
||||
<li><b>Update-Hinweis:</b> Beim Start und alle 6 Stunden prüft die App die neueste Version. Das Banner „Update verfügbar“ bietet <b>Jetzt aktualisieren</b> (Setup laden, SHA-256-Prüfsumme kontrollieren, starten) und <b>Später</b>.</li>
|
||||
<li><b>Verbindung:</b> Ein Banner zeigt fehlende Verbindung zum Server mit automatischem Wiederholen und dem Knopf „Jetzt erneut versuchen“.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Android-App</h2>
|
||||
<div class="note">Für die Android-App liegen in diesem Handbuch keine Bildschirmfotos vor; die Beschreibung folgt dem Funktionsumfang der Version 3.5.2.</div>
|
||||
<p>Die Android-App (Version 3.x) hat ein neues Design mit hellen, runden Karten in Blau/Türkis und einem Dunkelmodus. Unten schwebt die Navigation: <b>Start</b>, <b>Tickets</b>, <b>Chat</b>, <b>Kunden</b> und <b>Mehr</b>. „Mehr“ enthält Kacheln für Aufgaben, Status, Benachrichtigungen, Sprache (Deutsch/English) und die <b>Verwaltung</b> nach Rechten.</p>
|
||||
<ul>
|
||||
<li><b>Tickets:</b> Liste mit Reitern, Suche, gespeicherten Ansichten; im Ticket Antworten (mit Textbausteinen der Abteilung und Signatur-Auswahl), interne Notizen, Status, Zuweisung, <b>Auf später</b>, <b>Beobachten</b>, Karte „Dazu passend“, Beobachter und Cc, Kundenbewertung, „📥 E-Mails abrufen“ und „📎 Anhänge laden“.</li>
|
||||
<li><b>Start (Dashboard):</b> Kennzahlen, Zufriedenheit und eure offenen Tickets.</li>
|
||||
<li><b>Chat, Live-Chat, Kunden, Aufgaben, Status</b> wie im Web.</li>
|
||||
<li><b>Verwaltung (Admins und Co-Admins):</b> Abteilungen, Prioritäten, SLA-Pläne, Hilfethemen, E-Mail-Adressen (mit Test), Wissensdatenbank, E-Mail-Vorlagen, Organisationen, Mitarbeiter, Teams, Rollen, Admin-Protokoll, Eskalationsregeln, Zeiterfassung, Geschäftszeiten, Sperrliste, Statusseite, digitaler Assistent, Bericht-Baukasten. Systemnahe Bereiche (Backup, System-Update, Plugins, API-Schlüssel, Webhooks, Postfächer, Ticket-Felder, Seiten, Einstellungen) gibt es bewusst nur im Web.</li>
|
||||
<li><b>Push-Benachrichtigungen:</b> Popups bei neuen Tickets, Kundenantworten, Zuweisungen, Live-Chats, SLA-Überschreitungen, Erwähnungen, Wiedervorlagen und wartenden Tickets – auch bei geschlossener App. Auf Kundenantworten lässt sich direkt in der Benachrichtigung antworten. Pro Mitarbeiter sind bis zu 10 Geräte möglich. Standardmäßig steht nur „Neues Ticket #T-123“ in der Nachricht; Betreff und Kundenname verlassen den Server nicht.</li>
|
||||
<li><b>Welche Popups?</b> Im Profil und in der App unter Verwaltung → Benachrichtigungen wählt jeder die Arten und stille Zeiten; „Testnachricht“ prüft die Einrichtung. Ein Popup bekommt nur, wer das Ticket sehen darf.</li>
|
||||
<li><b>Widget (ab 3.1):</b> Auf dem Startbildschirm in drei Größen. <i>Klein</i>: Meine offenen und Überfällige. <i>Mittel</i>: vier Kennzahlen und die drei wichtigsten Tickets als Nummern. <i>Groß</i>: zusätzlich „Neues Ticket“, „Alle Tickets“ und der eigene Status zum Umschalten. Es zeigt bewusst keine Betreffs oder Kundennamen, wird rot bei Überfälligem und aktualisiert sich etwa alle 30 Minuten, beim Tippen auf ⟳, beim Öffnen der App und bei jeder Push-Nachricht.</li>
|
||||
<li><b>Update-Hinweis:</b> Wie beim Windows-Programm; die APK wird geladen und geprüft, dann öffnet sich der Android-Installer (einmalige Erlaubnis „Aus dieser Quelle installieren“). Bei Play-Store-Installationen aktualisiert Google selbst.</li>
|
||||
<li><b>Absturzmeldung:</b> Stürzt die App ab, meldet sie das beim nächsten Start an den eigenen Server (Aktivitätsprotokoll); es gehen keine Ticketinhalte und nichts an Dritte.</li>
|
||||
</ul>
|
||||
<div class="warn">Eine neue Version wird nur über die alte installiert, wenn sie mit demselben Signaturschlüssel erstellt wurde und eine höhere Versionsnummer hat. Nach einem Update die App einmal ganz schließen und neu öffnen.</div>
|
||||
|
||||
<h2>Chrome-Erweiterung</h2>
|
||||
<div class="note">Für die Chrome-Erweiterung liegen in diesem Handbuch keine Bildschirmfotos vor.</div>
|
||||
<p>Die Erweiterung läuft in Chrome, Edge und Brave (Manifest V3) auf Windows, Mac und Linux und ist ein kleiner Arbeitsplatz in der Browserleiste:</p>
|
||||
<ul>
|
||||
<li><b>Start</b> mit eigenem Status und Kennzahlen, <b>Ticketliste</b> mit Filtern und Suche, <b>Ticket-Ansicht</b> zum Lesen und <b>Antworten</b> (Textbausteine, Signatur, Status danach), interne Notiz, Status, Zuweisung, Wiedervorlage und Beobachten.</li>
|
||||
<li><b>Aufgaben</b>, <b>Ticket anlegen</b> (auch per Rechtsklick auf markierten Text) und ein <b>Zähler mit Benachrichtigungen</b> (Abfrage alle 30 Sekunden, ältere Chrome-Versionen jede Minute).</li>
|
||||
<li>Läuft als Popup, in Chromes <b>Seitenleiste</b> oder als eigenes Fenster; Tastenkürzel <kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>N</kbd> und <kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>D</kbd>, Adressleisten-Suche „dh“.</li>
|
||||
</ul>
|
||||
<h3>Installieren</h3>
|
||||
<ol class="steps">
|
||||
<li>ZIP aus „Apps“ laden und entpacken.</li>
|
||||
<li><code>chrome://extensions</code> öffnen, Entwicklermodus einschalten, „Entpackte Erweiterung laden“, Ordner wählen (eine ältere Version vorher entfernen).</li>
|
||||
<li>Anmelden mit Server-Adresse, E-Mail und Passwort (bei Authenticator mit Code).</li>
|
||||
</ol>
|
||||
<ul>
|
||||
<li><b>Sicherheit:</b> nur HTTPS, keine Cookies, keine Weiterleitungen; das Token liegt nur im lokalen Speicher, Abmelden sperrt es auf dem Server und löscht lokale Entwürfe.</li>
|
||||
<li><b>Update-Hinweis:</b> alle 6 Stunden; bei neuer Version erscheint eine Benachrichtigung und ein Hinweis mit „Download-Seite öffnen“. Aktualisieren bleibt Handarbeit (ZIP laden, in denselben Ordner entpacken, unter <code>chrome://extensions</code> neu laden).</li>
|
||||
<li><b>Grenzen:</b> Meldungen nur bei geöffnetem Browser; Anhänge werden angezeigt, zum Herunterladen das Ticket im Browser öffnen. Ein geöffnetes Ticket ist für andere gesperrt (wie im Web).</li>
|
||||
</ul>
|
||||
|
||||
<h2>Push-Benachrichtigungen im Browser und Sicherheit der Apps</h2>
|
||||
<p>Alle Apps melden sich mit eigenem Token an. <b>Von allen Geräten abmelden</b> (Profil → Sicherheit oder durch einen Administrator) löscht diese Tokens; die Apps verlangen danach eine neue Anmeldung. Der Anmelde-Wächter schützt App-Anmeldungen wie die im Browser.</p>
|
||||
@@ -0,0 +1,102 @@
|
||||
<h1>Plugins</h1>
|
||||
<p>Mit Plugins rüstet ihr Funktionen nach, ohne DeskHub selbst zu verändern. Alles läuft unter Adminportal → <b>Plugins</b> (nur echte Administratoren).</p>
|
||||
|
||||
<h2>Die Plugin-Verwaltung</h2>
|
||||
<p>Die Seite hat drei Reiter:</p>
|
||||
<ul>
|
||||
<li><b>Installiert:</b> jedes Plugin als Zeile mit Zustand (Aktiv, Inaktiv, Fehler, Update), Beschreibung, Version, Berechtigungen und den Aktionen Aktivieren/Deaktivieren, Öffnen, Aktualisieren und Entfernen; oben Filter mit Zahlen (Alle, Aktiv, Inaktiv, Mit Fehler, Update verfügbar) und eine Suche.</li>
|
||||
<li><b>Plugin hinzufügen:</b> der Katalog als Kacheln (mit Suche) und der ZIP-Upload für eigene Plugins.</li>
|
||||
<li><b>Katalog & Schlüssel:</b> Katalog-Adresse und Herausgeber-Schlüssel.</li>
|
||||
</ul>
|
||||
<fig n="admin-plugins-installiert">Installierte Plugins.</fig>
|
||||
<fig n="admin-plugins-hinzufuegen">Plugin hinzufügen (ZIP-Upload mit Prüfsumme und Quellenbestätigung).</fig>
|
||||
<fig n="admin-plugins-katalog">Katalog und Herausgeber-Schlüssel.</fig>
|
||||
|
||||
<h2>Installieren, Berechtigungen, Updates</h2>
|
||||
<ol class="steps">
|
||||
<li>Plugin als ZIP bereithalten; wenn der Anbieter eine Prüfsumme (SHA-256) oder Signatur nennt, tragt sie ein.</li>
|
||||
<li>ZIP hochladen und bestätigen, dass ihr der Quelle vertraut. Das Plugin ist danach <b>noch inaktiv</b>.</li>
|
||||
<li>Mit <b>„Aktivieren“</b> einschalten und vorher die angeforderten <b>Berechtigungen</b> bestätigen. Dabei entsteht automatisch ein Backup; das Plugin legt bei Bedarf eigene Tabellen an.</li>
|
||||
</ol>
|
||||
<p><b>Berechtigungen</b> nennen, welche Erweiterungspunkte ein Plugin nutzt: Ticket-Ereignisse (<code>hooks.tickets</code>), Hintergrundläufe (<code>hooks.cron</code>), Menüpunkte (<code>ui.menu</code>), Seitenleistenbausteine (<code>ui.sidebar</code>), eigene Datenbank-Tabellen (<code>db.migrations</code>), öffentliche Adressen (<code>api.public</code>), Anmeldung für Mitarbeiter (<code>auth.login</code>) und Kunden (<code>auth.customer</code>). Fordert eine neue Version zusätzliche Berechtigungen an, wird das Plugin automatisch angehalten, bis ihr sie bestätigt habt – auch wenn jemand Dateien per FTP austauscht.</p>
|
||||
<ul>
|
||||
<li><b>Geprüfter Modus:</b> Bringt eure DeskHub-Version einen offiziellen Katalog mit, müssen Katalog-Plugins mit dem Schlüssel des Herausgebers <b>signiert</b> sein. Der Katalog lädt automatisch (alle 30 Minuten im Hintergrund); ein verfügbares Update zeigt ein Hinweis für Admins, installiert wird es nie von selbst. Eigene Plugins lassen sich zusätzlich hochladen und sind als „nicht vom Herausgeber geprüft“ markiert.</li>
|
||||
<li><b>Verwalten:</b> deaktivieren, durch neuere Version ersetzen, entfernen (mit Wahl, ob die Daten des Plugins gelöscht werden).</li>
|
||||
<li><b>Fehlerverhalten:</b> Ein Fehler legt das System nicht lahm; nach drei Fehlern wird das Plugin automatisch deaktiviert, der Fehlertext steht in der Liste.</li>
|
||||
<li><b>Notausschalter:</b> Eine leere Datei <code>.disabled</code> im Ordner <code>plugins</code> verhindert, dass irgendein Plugin geladen wird.</li>
|
||||
</ul>
|
||||
<div class="warn">Ein Plugin ist Programmcode und läuft mit allen Rechten des Systems, auch mit Zugriff auf die gesamte Datenbank. Die Berechtigungen begrenzen nur die Erweiterungspunkte. Installiert nur Plugins aus vertrauenswürdigen Quellen und aktiviert „Nur signierte Plugins installieren“.</div>
|
||||
|
||||
<h2>Mitgelieferte Plugins</h2>
|
||||
<p>Alle außer dem Discord/Telegram-Bot laufen auf jedem DeskHub ohne Änderung am Server. Jedes Plugin bringt eine eigene Seite im Menü mit.</p>
|
||||
<table>
|
||||
<tr><th>Plugin</th><th>Was es tut</th></tr>
|
||||
<tr><td><b>Tagesbericht per E-Mail</b></td><td>Morgens eine Übersicht: offene, unzugewiesene, überfällige und eigene Tickets, Zu- und Abgänge der letzten 24 Stunden, die fünf ältesten offenen Tickets</td></tr>
|
||||
<tr><td><b>Anhänge-Virenscan</b></td><td>Prüft Anhänge und sperrt Gefährliches (Quarantäne, Notiz, Tag „Virus-Verdacht“, Mail)</td></tr>
|
||||
<tr><td><b>VirusTotal-Anhangsprüfung</b></td><td>Hält Anhänge zurück, bis VirusTotal sie anhand des Fingerabdrucks geprüft hat</td></tr>
|
||||
<tr><td><b>Spam- und Missbrauchsfilter</b></td><td>Bewertet neue Tickets mit Punkten; legt Spam automatisch ab</td></tr>
|
||||
<tr><td><b>Discord- und Telegram-Bot</b></td><td>Mitglieder legen per <code>/ticket</code> Tickets an und werden benachrichtigt</td></tr>
|
||||
<tr><td><b>Urlaub und Vertretung</b></td><td>Abwesenheit mit Vertretung; Tickets wandern zur Vertretung und zurück</td></tr>
|
||||
<tr><td><b>Schichtübergabe</b></td><td>Notizen für die nächste Schicht, sichtbar nach der Anmeldung</td></tr>
|
||||
<tr><td><b>Störungsmitteilung und Rundmail</b></td><td>Eine Nachricht an viele Kunden gleichzeitig</td></tr>
|
||||
<tr><td><b>Website- und Server-Überwachung</b></td><td>Prüft Webseiten, Ports und Zertifikate; legt bei Ausfall Tickets an</td></tr>
|
||||
<tr><td><b>Datenschutz</b></td><td>Anonymisiert Kundendaten nach einer wählbaren Frist; löscht auf Antrag</td></tr>
|
||||
<tr><td><b>authentik-Anmeldung</b></td><td>Anmeldung von Mitarbeitern und Kunden über authentik (OpenID Connect)</td></tr>
|
||||
<tr><td><b>Synology NAS Backup</b></td><td>Kopiert Backups automatisch auf eine Synology NAS (WebDAV)</td></tr>
|
||||
<tr><td><b>Cloudflare-Schutz</b></td><td>Prüft Cloudflare-Anfragen und gleicht die Cloudflare-Proxy-Netze ab, damit Besucher-IP und HTTPS sicher erkannt werden</td></tr>
|
||||
<tr><td><b>Cache & Performance</b></td><td>Beschleunigt die öffentliche Wissensdatenbank mit einem einstellbaren Datenbankcache und zeigt den PHP-OPcache-Status</td></tr>
|
||||
<tr><td><b>Code-Issue-Synchronisierung</b></td><td>Gibt einzelne Git-Repositories frei, importiert Issues als Tickets (mit gewählter Abteilung und Priorität) und gleicht den Ticketstatus ab</td></tr>
|
||||
<tr><td><b>Sprachpakete</b> (<code>lang-en</code>, <code>lang-de</code>)</td><td>Weitere Sprachen für Portal und Mitarbeiterbereich</td></tr>
|
||||
</table>
|
||||
<p>Die folgenden Abschnitte zeigen die Seiten der Plugins mit Beispieldaten.</p>
|
||||
|
||||
<h3>Tagesbericht</h3>
|
||||
<p>Im Menü „Tagesbericht“ schaltet ihr den Versand ein, legt Uhrzeit, <b>Rhythmus</b> (täglich, nur Werktage oder einmal pro Woche mit Wochentag) und Empfänger fest und schickt einen <b>Testbericht</b>. Standardmäßig kommt der Bericht nur, wenn es etwas zu tun gibt (unzugewiesene oder überfällige Tickets). Jeder Mitarbeiter wählt unter <b>„Mein Tagesbericht“</b>: <i>wie eingestellt</i>, <i>nur wenn Tickets überfällig sind</i>, <i>nur einmal pro Woche (montags)</i> oder <i>gar nicht mehr</i>; jede Mail enthält dazu einen Link. Jeder sieht nur Tickets seiner Abteilungen.</p>
|
||||
<fig n="plugin-tagesbericht-einstellungen">Tagesbericht: Einstellungen (Administrator).</fig>
|
||||
<fig n="plugin-tagesbericht-persoenlich">Tagesbericht: „Mein Tagesbericht“.</fig>
|
||||
|
||||
<h3>Virenscan und VirusTotal</h3>
|
||||
<p>Der <b>Virenscan</b> braucht nichts auf dem Server: Die eingebaute Analyse erkennt Programme, Skripte, getarnte Dateien (eine .exe als PDF), doppelte Endungen, Office-Makros, PDF-Tricks, Skripte in HTML/SVG und Archive mit solchen Dateien. Optional gleicht er per Hash-Abfrage bei VirusTotal ab, und ClamAV wird mitgenutzt, falls vorhanden. Er erkennt keine ganz neuen Viren in normal aussehenden Dokumenten.</p>
|
||||
<p>Die <b>VirusTotal-Anhangsprüfung</b> sperrt dagegen zuerst: Jeder neue Anhang bleibt <code>[IN PRÜFUNG]</code>, bis der SHA-256-Fingerabdruck abgefragt wurde – sauber: frei; ab der Schwelle (Standard 3 Scanner) Schadsoftware: gesperrt mit Tag, Notiz und Mail; unbekannt: zurückgehalten, bis ein Administrator freigibt oder löscht. Standardmäßig verlässt keine Datei den Server. Der kostenlose API-Schlüssel erlaubt 4 Abfragen pro Minute und 500 pro Tag; das Plugin hält sich daran.</p>
|
||||
<fig n="plugin-virenscan-virenscan">Virenscan.</fig>
|
||||
<fig n="plugin-virustotal-virustotal">VirusTotal-Anhangsprüfung.</fig>
|
||||
|
||||
<h3>Spam- und Missbrauchsfilter</h3>
|
||||
<p>Bewertet jedes neue Ticket (Phishing-Wendungen, Marken-Nachahmung im Absendernamen, Link-Verkürzer, Wegwerf-Adressen, Wiederholungen; bekannte Absender bekommen Abzug). Ab 70 Punkten wird das Ticket als Spam abgelegt, ab 35 erhält es den Tag „Spam-Verdacht“. Freigabe- und Sperrliste, eigene Wörter, Protokoll mit „Kein Spam“ / „Doch Spam“ und eine Bewertung in der Seitenleiste des Tickets.</p>
|
||||
<fig n="plugin-spamfilter-spamfilter">Spam- und Missbrauchsfilter.</fig>
|
||||
|
||||
<h3>Discord- und Telegram-Bot</h3>
|
||||
<p>Mitglieder legen mit <code>/ticket</code> im Discord oder in Telegram ein Ticket an, bekommen die Nummer zurück und werden bei Antworten und Lösung benachrichtigt; weitere Befehle <code>/status</code> und <code>/antwort</code>. Der Server nimmt die Anfragen direkt entgegen (kein eigener Bot-Server); sie werden per Signatur (Discord) bzw. Geheimtoken (Telegram) geprüft, je Person gilt eine Stundenobergrenze. Braucht Server 1.0.30 oder neuer, für Discord die PHP-Erweiterung <code>sodium</code> und die Berechtigung <code>api.public</code>.</p>
|
||||
<fig n="plugin-community-bot-community-bot">Community-Bot.</fig>
|
||||
|
||||
<h3>Urlaub und Vertretung, Schichtübergabe, Rundmail</h3>
|
||||
<p><b>Vertretung:</b> Abwesenheit mit Vertretung eintragen; zum Beginn gehen offene und neue Tickets an die Vertretung, am Ende kommen die noch offenen zurück; die Vertretung sieht währenddessen auch die Abteilungen des Abwesenden (Ketten bis drei Stufen). <b>Schichtübergabe:</b> Am Schichtende eine kurze Notiz (für alle oder eine Abteilung); die nächste Schicht sieht sie als Hinweis oben auf jeder Seite (rot bei wichtigen), mit „neu“-Zähler im Menü; der Verfasser sieht, wer sie gelesen hat. <b>Rundmail:</b> eine Nachricht an Kunden mit offenen Tickets, einer Abteilung oder Organisation, mit Vorlagen, Empfängerprüfung, Probemail und Protokoll; jede Mail geht einzeln raus (höchstens 200 Empfänger, 5 Rundmails pro Stunde).</p>
|
||||
<fig n="plugin-vertretung-vertretung">Urlaub und Vertretung.</fig>
|
||||
<fig n="plugin-schichtuebergabe-schichtuebergabe">Schichtübergabe.</fig>
|
||||
<fig n="plugin-rundmail-rundmail">Störungsmitteilung und Rundmail.</fig>
|
||||
|
||||
<h3>Überwachung und Datenschutz</h3>
|
||||
<p>Die <b>Website- und Server-Überwachung</b> prüft Webseiten, Ports und Zertifikate; bei Ausfall (nach 2 Fehlschlägen in Folge) oder einem Zertifikat, das in 14 Tagen abläuft, legt das Plugin ein Ticket an und schickt eine Mail; nach der Behebung kommt die Entwarnung und das Ticket wird gelöst. Das <b>Datenschutz-Plugin</b> anonymisiert Kundendaten aus Tickets ohne Registrierung nach einer wählbaren Frist (Name, E-Mail oder beides, auch IP, Mitleser, Zusatzfelder, Nachrichten, Anhänge); die automatische Anonymisierung ist nach der Installation <b>aus</b>. Daten registrierter Kunden löscht der Administrator auf Antrag mit einem Klick.</p>
|
||||
<fig n="plugin-ueberwachung-ueberwachung">Website- und Server-Überwachung.</fig>
|
||||
<fig n="plugin-datenschutz-datenschutz">Datenschutz-Plugin.</fig>
|
||||
|
||||
<h3>authentik-Anmeldung</h3>
|
||||
<p>Mitarbeiter <b>und Kunden</b> melden sich mit authentik an (OpenID Connect mit PKCE). In authentik legt ihr einen OAuth2/OpenID-Provider an und tragt die Rückkehr-Adresse <code>https://<DeskHub>/staff/plugin_login.php?p=authentik&e=callback</code> ein; in DeskHub tragt ihr Issuer-Adresse, Client-ID und Geheimnis ein und prüft mit „Verbindung testen“. Mitarbeiter verknüpfen ihr Konto selbst („Mein authentik-Konto“); Mitarbeiterkonten werden nie automatisch angelegt, Kundenkonten bei Bedarf schon. E-Mail und Passwort funktionieren weiter, auch wenn authentik ausfällt. Braucht Server 1.0.52.</p>
|
||||
<fig n="plugin-authentik-einstellungen">authentik: Einstellungen.</fig>
|
||||
<fig n="plugin-authentik-konto">authentik: „Mein Konto“.</fig>
|
||||
|
||||
<h3>Synology NAS Backup</h3>
|
||||
<p>Kopiert die Backups des Servers automatisch per WebDAV auf eine Synology NAS (oder einen anderen WebDAV-Server) und erscheint als Reiter „Synology NAS Backup“ auf der Seite „Backup“. Einstellbar: Verbindung (Adresse, Port 5005/5006, Benutzer, Passwort, HTTPS, Zielverzeichnis) mit „Verbindung testen“, Aufbewahrung (maximales Alter und maximale Anzahl), einzelnes Verschieben von Backups, eigene Backups mit Zeitplan und – für große Backups hinter Proxys mit Größengrenze – Hochladen in Stücken. Jedes Backup wird nach dem Hochladen auf vollständige Größe geprüft; das Passwort wird verschlüsselt gespeichert.</p>
|
||||
<fig n="plugin-nas-backup-einstellungen">Synology NAS Backup.</fig>
|
||||
|
||||
<h3>Cloudflare-Schutz, Cache & Performance, Code-Issue-Synchronisierung</h3>
|
||||
<fig n="plugin-cloudflare-protection-status">Cloudflare-Schutz: Status.</fig>
|
||||
<fig n="plugin-deskhub-cache-manager-status">Cache-Manager: Status.</fig>
|
||||
<fig n="plugin-code-issue-sync-manage">Code-Issue-Sync: Verwaltung.</fig>
|
||||
<p><b>Cloudflare-Schutz</b> und <b>Cache & Performance</b> sind reine Betriebs-Zusätze mit je einer Statusseite. Die <b>Code-Issue-Synchronisierung</b> braucht mehrere Berechtigungen (u. a. <code>api.public</code>, <code>hooks.cron</code>), weil sie Issues aus Git-Repositories abholt und Statusänderungen zurückmeldet.</p>
|
||||
|
||||
<h2>Eigene Plugins entwickeln (Kurzüberblick)</h2>
|
||||
<p>Ein Plugin ist ein ZIP mit <code>plugin.json</code> (Name, Version, Berechtigungen, optional Sprachpaket-Angaben), <code>plugin.php</code> (Code), optionalen Seiten, Migrationen und <code>lang/<Code>.json</code> für Übersetzungen. Es registriert Menüpunkte, Seiten, Ereignisse (Hooks), Hintergrundjobs, API-Endpunkte oder Anmeldeverfahren – jeweils nur mit der entsprechenden Berechtigung. Pakete lassen sich mit dem Herausgeber-Schlüssel signieren. Die technische Dokumentation steht im Abschnitt „Plugins entwickeln“ der README.</p>
|
||||
|
||||
<h2>Die Plugin- und Theme-Webseite</h2>
|
||||
<p>Die Plugin-Webseite ist der Katalog für Plugins, Themes und App-Dateien. Betreiber veröffentlichen dort signierte Pakete; DeskHub-Installationen lesen den Katalog, prüfen Prüfsumme und Signatur und installieren per Klick. Dort liegen auch die Apps (Windows, Android, Chrome-Erweiterung, WordPress- und Minecraft-Plugin), die der Update-Hinweis der Apps abfragt, und der Push-Dienst für die Android-Benachrichtigungen.</p>
|
||||
@@ -0,0 +1,63 @@
|
||||
<h1>Integrationen und Schnittstellen</h1>
|
||||
|
||||
<h2>API-Schlüssel</h2>
|
||||
<p>Ein API-Schlüssel erlaubt einem anderen System, Tickets in DeskHub anzulegen oder zu lesen (Adminportal → <b>API-Schlüssel</b>, nur echte Administratoren).</p>
|
||||
<fig n="admin-api-schluessel">API-Schlüssel mit Rechten und IP-Beschränkung.</fig>
|
||||
<ul>
|
||||
<li>Beim Erstellen wird der Schlüssel (Format <code>dh_</code> plus 48 Hexzeichen) <b>nur einmal</b> angezeigt – sofort notieren. In der Datenbank liegt nur eine Prüfsumme.</li>
|
||||
<li>Pro Schlüssel legt ihr die <b>Rechte</b> fest („Tickets anlegen“ und/oder „Tickets lesen“) und optional die <b>erlaubten IP-Adressen</b>.</li>
|
||||
<li>Schlüssel lassen sich jederzeit deaktivieren. Es gilt eine Begrenzung auf 60 über die API angelegte Tickets pro Minute (gemeinsam für alle Schlüssel).</li>
|
||||
</ul>
|
||||
|
||||
<h2>Webhooks</h2>
|
||||
<p>Webhooks melden Ereignisse sofort an eine Adresse eures Systems (Adminportal → <b>Webhooks</b>): Name, URL und gewünschte Ereignisse wählen; das <b>Secret</b> zur Signaturprüfung legt ihr selbst fest oder lasst es erzeugen (es wird nur verschlüsselt gespeichert).</p>
|
||||
<fig n="admin-webhooks">Webhooks mit Zustellstatus.</fig>
|
||||
<table>
|
||||
<tr><th>Ereignis</th><th>Wann</th></tr>
|
||||
<tr><td><code>ticket.created</code></td><td>Neues Ticket (Portal, E-Mail, Chat, Mitarbeiter oder API)</td></tr>
|
||||
<tr><td><code>ticket.replied</code></td><td>Antwort oder interne Notiz (Feld <code>author</code>, bei Mitarbeitern auch <code>internal</code>)</td></tr>
|
||||
<tr><td><code>ticket.status_changed</code></td><td>Status geändert (Feld <code>previousStatus</code>)</td></tr>
|
||||
<tr><td><code>ticket.assigned</code></td><td>Zuweisung geändert</td></tr>
|
||||
</table>
|
||||
<ul>
|
||||
<li><b>Anfrage:</b> <code>POST</code> mit JSON und den Headern <code>X-DeskHub-Event</code> und <code>X-DeskHub-Signature</code> (<code>sha256=</code> plus HMAC-SHA256 des rohen Körpers mit dem Secret). Gesendet werden nur Ticketdaten, nicht der Nachrichtentext.</li>
|
||||
<li><b>Zustellung:</b> Eure Adresse muss binnen 5 Sekunden mit <code>2xx</code> antworten. Es gibt <b>keine automatische Wiederholung</b>. Nach <b>10 Fehlern in Folge</b> wird der Webhook automatisch deaktiviert; Status, Fehlertext und Zeitpunkt stehen in der Liste.</li>
|
||||
<li>Erlaubt sind <code>http://</code> und <code>https://</code>, aber keine internen Ziele (localhost, private Netze). Nutzt <code>https://</code>.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Die Ticket-API für Entwickler</h2>
|
||||
<p>Basis-Adresse: <code>https://support.example.de/api/public/v1/tickets.php</code> (Beispieladresse). Jeder Aufruf braucht einen Schlüssel im Header <code>X-API-Key</code> oder <code>Authorization: Bearer</code>. Anfragen und Antworten sind JSON.</p>
|
||||
<table>
|
||||
<tr><th>Aufruf</th><th>Recht</th><th>Zweck</th></tr>
|
||||
<tr><td><code>POST /api/public/v1/tickets.php</code></td><td>Tickets anlegen</td><td>Ticket anlegen mit <code>name</code>, <code>email</code>, <code>subject</code>, <code>message</code> und optional <code>helpTopicId</code>, <code>departmentId</code>, <code>priorityId</code>, <code>fields</code> (Formularfelder) und <code>attachments</code> (bis 5 Dateien, Base64). Antwort <code>201</code> mit <code>{"number":"T-1042","status":"open"}</code></td></tr>
|
||||
<tr><td><code>GET …tickets.php?topics=1</code></td><td>Tickets anlegen</td><td>Öffentliche Hilfethemen</td></tr>
|
||||
<tr><td><code>GET …tickets.php?form=1&helpTopicId=3</code></td><td>Tickets anlegen</td><td>Das Formular eines Themas oder einer Abteilung (Felder, Texte, Anhangsgrenzen)</td></tr>
|
||||
<tr><td><code>GET …tickets.php?number=T-1042&email=…</code></td><td>Tickets lesen</td><td>Status und öffentliche Nachrichten (nie interne Notizen); falsche E-Mail ergibt 404</td></tr>
|
||||
</table>
|
||||
<p>Fehler haben immer die Form <code>{"error":{"code":"INVALID_EMAIL","message":"…"}}</code>. Wichtige Codes: <code>UNAUTHENTICATED</code> (401), <code>IP_NOT_ALLOWED</code> (401), <code>FORBIDDEN</code> (403), <code>BANNED</code>/<code>REJECTED</code> (403), <code>NOT_FOUND</code> (404), <code>TOO_LARGE</code> (413), <code>INVALID_*</code> (422), <code>RATE_LIMITED</code> (429), <code>NO_DEPARTMENT</code> (503). Das Anlegen hat keine Wiederholungssicherung: Zweimal gesendet ergibt zwei Tickets.</p>
|
||||
|
||||
<h2>WordPress-Plugin „DeskHub Tickets“</h2>
|
||||
<p>Besucher einer WordPress-Seite legen damit Tickets in DeskHub an (<code>[deskhub_ticket]</code>) und fragen den Stand ab (<code>[deskhub_status]</code>). Das Plugin nutzt die öffentliche Ticket-API und braucht nur einen <b>API-Schlüssel</b> (Recht „Tickets anlegen“, für die Statusabfrage zusätzlich „Tickets lesen“).</p>
|
||||
<ul>
|
||||
<li><b>Formular</b> mit den eigenen Feldern und Texten des Hilfethemas aus DeskHub (auch dem Formular der Abteilung), Einwilligungs-Kästchen, Anhänge (bis 5 Dateien, zusammen 10 MB).</li>
|
||||
<li><b>Statusabfrage</b> mit Verlauf.</li>
|
||||
<li><b>Spamschutz:</b> Falle-Feld, Mindestzeit, Mengenbegrenzung, optional Cloudflare Turnstile. Verbindungstest, Deutsch und Englisch.</li>
|
||||
<li><b>Design:</b> Farben übernimmt das Formular vom Theme; die Akzentfarbe lässt sich fest vorgeben.</li>
|
||||
<li><b>Sicherheit:</b> Der Schlüssel liegt nur auf dem WordPress-Server (oder in der <code>wp-config.php</code>), nie im Browser.</li>
|
||||
</ul>
|
||||
<div class="note">Das Plugin wurde mit einem Nachbau der WordPress-Funktionen und gegen einen echten DeskHub-Server getestet, noch nicht in einer echten WordPress-Installation.</div>
|
||||
|
||||
<h2>Minecraft-Plugin</h2>
|
||||
<p>Eine Jar für <b>Spigot, Paper und BungeeCord</b>, die in DeskHub automatisch ein Ticket anlegt, wenn ein Minecraft-Server offline geht – und es nach der Rückkehr als gelöst markiert.</p>
|
||||
<table>
|
||||
<tr><th>Wo es liegt</th><th>Erkennung</th><th>Ticket</th></tr>
|
||||
<tr><td>BungeeCord-Proxy (empfohlen)</td><td>Prüft die Server von außen per Ping alle 30 Sekunden</td><td>nach 3 Fehlschlägen in Folge und wenn der Server länger als die Schonfrist (5 Minuten) fehlt</td></tr>
|
||||
<tr><td>Spigot/Paper</td><td>Schreibt Lebenszeichen und markiert normales Beenden</td><td>beim nächsten Start nach einem Absturz oder harten Ende</td></tr>
|
||||
</table>
|
||||
<ul>
|
||||
<li><b>Installation:</b> API-Schlüssel mit Recht „Tickets anlegen“ (mit IP-Beschränkung auf den Minecraft-Server) erzeugen; Jar in den Ordner <code>plugins</code>; <code>config.yml</code> ausfüllen (<code>url</code>, <code>api-key</code>, <code>requester-email</code>, optional Abteilung und Priorität); <code>/deskhubmc reload</code>; mit <code>/deskhubmc test</code> ein Testticket anlegen.</li>
|
||||
<li><b>Befehle:</b> <code>/deskhubmc test</code>, <code>/deskhubmc status</code>, <code>/deskhubmc reload</code> (Alias <code>/dhmc</code>, Recht <code>deskhubmc.admin</code>).</li>
|
||||
<li><b>Geplante Neustarts</b> lassen sich dem Proxy signiert melden (längere Schonfrist, Standard 15 Minuten). Gegen Ticket-Fluten: pro Ausfall höchstens ein Ticket, pro Server frühestens nach 30 Minuten wieder eines.</li>
|
||||
<li>Betreff und Text der Tickets sind anpassbar (Platzhalter <code>{server}</code>, <code>{time}</code>, <code>{downtime}</code> u. a.).</li>
|
||||
</ul>
|
||||
<fig n="apps">Auf der Seite „Apps“ steht das Minecraft-Plugin bereit, wenn es unter Einstellungen → Allgemein eingeblendet ist.</fig>
|
||||
Reference in New Issue
Block a user