['label' => 'E-Mail-Abruf', 'file' => 'fetch_emails.php', 'fn' => 'run_fetch_emails'], 'check_sla' => ['label' => 'SLA-Prüfung', 'file' => 'check_sla.php', 'fn' => 'run_check_sla'], 'check_escalations' => ['label' => 'Eskalationsregeln', 'file' => 'check_escalations.php', 'fn' => 'run_check_escalations'], 'process_recurring' => ['label' => 'Wiederkehrende Tickets', 'file' => 'process_recurring.php', 'fn' => 'run_process_recurring'], 'process_backups' => ['label' => 'Automatische Backups', 'file' => 'process_backups.php', 'fn' => 'run_process_backups'], 'anonymize_data' => ['label' => 'Datenanonymisierung', 'file' => 'anonymize_data.php', 'fn' => 'run_anonymization'], 'auto_close_tickets'=> ['label' => 'Automatisches Schließen', 'file' => 'auto_close_tickets.php','fn' => 'run_auto_close_tickets'], 'process_snoozes' => ['label' => 'Wiedervorlagen', 'file' => 'process_snoozes.php', 'fn' => 'run_process_snoozes'], 'process_mail_queue' => ['label' => 'Mail-Warteschlange', 'file' => 'process_mail_queue.php', 'fn' => 'run_process_mail_queue'], 'remind_customers' => ['label' => 'Kundenerinnerung', 'file' => 'remind_customers.php', 'fn' => 'run_remind_customers'], 'send_csat' => ['label' => 'Zufriedenheitsabfrage', 'file' => 'send_csat.php', 'fn' => 'run_send_csat'], 'status_checks' => ['label' => 'Statusprüfungen', 'file' => 'status_checks.php', 'fn' => 'run_status_checks'], 'send_scheduled_replies' => ['label' => 'Geplante Antworten', 'file' => 'send_scheduled_replies.php', 'fn' => 'run_send_scheduled_replies'], 'send_report_schedules' => ['label' => 'Berichte per E-Mail', 'file' => 'send_report_schedules.php', 'fn' => 'run_send_report_schedules'], 'run_plugins' => ['label' => 'Plugins (Hintergrundaufgaben)', 'file' => 'run_plugins.php', 'fn' => 'run_plugins_cron'], ]; if ($_SERVER['REQUEST_METHOD'] === 'POST') { verify_csrf(); $action = $_POST['action'] ?? ''; if ($action === 'save_system') { $status = ($_POST['helpdesk_status'] ?? 'online') === 'offline' ? 'offline' : 'online'; $offlineMsg = trim($_POST['offline_message'] ?? ''); $defaultDept = (int)($_POST['default_department_id'] ?? 0); $kbEnabled = !empty($_POST['kb_enabled']) ? '1' : '0'; if (!in_array($defaultDept, array_column($departments, 'id'))) { $errors[] = 'Bitte eine gültige Standardabteilung wählen.'; } else { set_setting('helpdesk_status', $status); set_setting('offline_message', $offlineMsg ?: 'Das Supportcenter ist derzeit nicht erreichbar.'); set_setting('default_department_id', (string)$defaultDept); set_setting('kb_enabled', $kbEnabled); set_setting('minecraft_enabled', !empty($_POST['minecraft_enabled']) ? '1' : '0'); set_setting('ticket_ratings_enabled', !empty($_POST['ticket_ratings_enabled']) ? '1' : '0'); set_setting('auto_time_tracking_enabled', !empty($_POST['auto_time_tracking_enabled']) ? '1' : '0'); set_setting('auto_assign_on_open', !empty($_POST['auto_assign_on_open']) ? '1' : '0'); set_setting('live_chat_enabled', !empty($_POST['live_chat_enabled']) ? '1' : '0'); $liveChatDept = $_POST['live_chat_department_id'] ?? ''; set_setting('live_chat_department_id', ($liveChatDept !== '' && in_array((int)$liveChatDept, array_column($departments, 'id'))) ? (string)(int)$liveChatDept : ''); set_setting('live_chat_send_transcript', !empty($_POST['live_chat_send_transcript']) ? '1' : '0'); set_setting('auto_close_enabled', !empty($_POST['auto_close_enabled']) ? '1' : '0'); $autoCloseDays = max(1, min(90, (int) ($_POST['auto_close_days'] ?? 7))); set_setting('auto_close_days', (string) $autoCloseDays); set_setting('sla_notify_mode', ($_POST['sla_notify_mode'] ?? 'admins') === 'assignee' ? 'assignee' : 'admins'); set_setting('wait_reminder_enabled', !empty($_POST['wait_reminder_enabled']) ? '1' : '0'); set_setting('wait_reminder_hours', (string) max(1, min(72, (int) ($_POST['wait_reminder_hours'] ?? 4)))); set_setting('wait_reminder_email', !empty($_POST['wait_reminder_email']) ? '1' : '0'); set_setting('remind_enabled', !empty($_POST['remind_enabled']) ? '1' : '0'); set_setting('remind_after_days', (string) max(1, min(60, (int) ($_POST['remind_after_days'] ?? 3)))); csat_set_enabled(!empty($_POST['csat_enabled']), (int) ($_POST['csat_delay_minutes'] ?? 30)); $timeout = max(5, min(480, (int)($_POST['staff_session_timeout'] ?? 30))); set_setting('staff_session_timeout', (string)$timeout); audit_log($staff['id'], "Systemeinstellungen geändert"); flash('success', 'Systemeinstellungen gespeichert.'); redirect('/staff/settings.php?tab=allgemein'); } } if ($action === 'save_privacy') { $enabled = !empty($_POST['retention_enabled']) ? '1' : '0'; $months = max(1, min(120, (int)($_POST['retention_months'] ?? 24))); $ipAnon = !empty($_POST['ip_anonymize']) ? '1' : '0'; set_setting('retention_enabled', $enabled); set_setting('retention_months', (string)$months); set_setting('ip_anonymize', $ipAnon); set_setting('gdpr_tools_enabled', !empty($_POST['gdpr_tools_enabled']) ? '1' : '0'); audit_log($staff['id'], 'Datenschutzeinstellungen geändert'); flash('success', 'Datenschutzeinstellungen gespeichert.'); redirect('/staff/settings.php?tab=datenschutz'); } if ($action === 'run_anonymize_now') { define('DESKHUB_CRON_RUNNER', true); require_once __DIR__ . '/../cron/anonymize_data.php'; $result = run_anonymization(get_db(), true); audit_log($staff['id'], "Datenanonymisierung manuell ausgeführt: {$result['count']} Ticket(s)"); flash('success', "{$result['count']} Ticket(s) anonymisiert."); redirect('/staff/settings.php?tab=datenschutz'); } // Manueller Start eines Cronjobs aus dem Backend ("Jetzt ausführen"-Button, Tab Cron-Jobs). // Führt exakt die gleiche Logik aus wie der echte Cronjob, nur direkt per Klick statt per crontab. if ($action === 'run_cron_now') { $job = $_POST['job'] ?? ''; if (!isset($cronJobs[$job])) { flash('error', 'Unbekannter Cronjob.'); redirect('/staff/settings.php?tab=cron'); } $meta = $cronJobs[$job]; define('DESKHUB_CRON_RUNNER', true); require_once __DIR__ . '/../cron/' . $meta['file']; $fn = $meta['fn']; try { if ($job === 'anonymize_data') { $result = $fn(get_db(), true); $summary = "{$result['count']} Ticket(s) anonymisiert."; } elseif ($job === 'process_backups') { // Manueller Klick erzwingt ein Backup unabhängig vom Zeitplan. $result = $fn(get_db(), true, true); $summary = $result['ok'] ? ($result['ran'] ? 'Backup erstellt.' : 'Kein Backup nötig.') : 'Backup fehlgeschlagen.'; } else { $result = $fn(get_db(), true); $summary = end($result['log']) ?: 'Ausgeführt.'; } audit_log($staff['id'], "Cronjob manuell ausgeführt: {$meta['label']} – {$summary}"); flash('success', "{$meta['label']}: {$summary}"); } catch (Throwable $e) { audit_log($staff['id'], "Cronjob manuell fehlgeschlagen: {$meta['label']} – " . $e->getMessage()); flash('error', "{$meta['label']} fehlgeschlagen: " . $e->getMessage()); } redirect('/staff/settings.php?tab=cron'); } // Ein-/Ausschalten des eingebauten Auto-Cron (siehe includes/pseudo_cron.php). if ($action === 'save_pseudo_cron') { $enabled = !empty($_POST['pseudo_cron_enabled']) ? '1' : '0'; set_setting('pseudo_cron_enabled', $enabled); audit_log($staff['id'], $enabled === '1' ? 'Automatischer Hintergrund-Cron aktiviert' : 'Automatischer Hintergrund-Cron deaktiviert'); flash('success', 'Gespeichert.'); redirect('/staff/settings.php?tab=cron'); } // Echte System-Crontab installieren/entfernen (nur auf Servern mit Shell-Zugriff möglich, // siehe includes/crontab_installer.php – schreibt ausschließlich feste, fest hinterlegte Zeilen). if ($action === 'install_crontab') { $result = crontab_install(); if ($result['ok']) { audit_log($staff['id'], 'Server-Crontab installiert/aktualisiert (6 DeskHub-Cronjobs)'); flash('success', 'Crontab installiert. Die 6 Cronjobs laufen jetzt direkt über den Server-Scheduler.'); } else { audit_log($staff['id'], 'Server-Crontab-Installation fehlgeschlagen: ' . $result['error']); flash('error', 'Installation fehlgeschlagen: ' . $result['error']); } redirect('/staff/settings.php?tab=cron'); } if ($action === 'remove_crontab') { $result = crontab_uninstall(); if ($result['ok']) { audit_log($staff['id'], 'Server-Crontab-Eintrag von DeskHub entfernt'); flash('success', 'Crontab-Eintrag entfernt.'); } else { audit_log($staff['id'], 'Entfernen der Server-Crontab fehlgeschlagen: ' . $result['error']); flash('error', 'Entfernen fehlgeschlagen: ' . $result['error']); } redirect('/staff/settings.php?tab=cron'); } if ($action === 'save_name') { $name = trim($_POST['display_name'] ?? ''); $alertTo = trim((string) ($_POST['error_alert_email'] ?? '')); foreach (preg_split('/[,;\s]+/', $alertTo, -1, PREG_SPLIT_NO_EMPTY) ?: [] as $m) { if (!filter_var($m, FILTER_VALIDATE_EMAIL)) { $errors[] = 'Die Adresse für Fehler-Mails ist ungültig: ' . $m; break; } } if (!$name) { $errors[] = 'Bitte einen Anzeigenamen angeben.'; } elseif (!$errors) { set_setting('display_name', $name); $siteUrl = rtrim(trim($_POST['site_url'] ?? ''), '/'); if ($siteUrl) set_setting('site_url', $siteUrl); $tz = trim($_POST['timezone'] ?? ''); if ($tz && in_array($tz, timezone_identifiers_list(), true)) set_setting('timezone', $tz); set_setting('mail_enabled', !empty($_POST['mail_enabled']) ? '1' : '0'); set_setting('error_alert', !empty($_POST['error_alert']) ? '1' : '0'); set_setting('error_alert_email', mb_substr($alertTo, 0, 300)); audit_log($staff['id'], "Anzeigename / Basis-Einstellungen geändert: $name"); flash('success', 'Einstellungen gespeichert.'); redirect('/staff/settings.php?tab=allgemein'); } } if ($action === 'upload_logo') { $file = $_FILES['logo'] ?? []; $allowed = ['png','jpg','jpeg','gif','svg']; $ext = strtolower(pathinfo($file['name'] ?? '', PATHINFO_EXTENSION)); if (empty($file['name'])) { $errors[] = 'Bitte eine Bilddatei auswählen.'; } elseif ($file['error'] !== UPLOAD_ERR_OK) { $errors[] = 'Upload fehlgeschlagen.'; } elseif ($file['size'] > 2*1024*1024) { $errors[] = 'Max. 2 MB.'; } elseif (!in_array($ext, $allowed, true)) { $errors[] = 'Erlaubt: PNG, JPG, GIF, SVG.'; } elseif (($imgErr = favicon_validate_file($file['tmp_name'], $ext)) !== null) { $errors[] = $imgErr; } else { foreach ($allowed as $e2) @unlink($branding.'/logo.'.$e2); $dest = $branding.'/logo.'.$ext; if (!move_uploaded_file($file['tmp_name'], $dest)) { $errors[] = 'Speichern fehlgeschlagen.'; } else { set_setting('logo_path', '/assets/branding/logo.'.$ext.'?v='.time()); audit_log($staff['id'],'Logo aktualisiert'); flash('success','Logo aktualisiert.'); redirect('/staff/settings.php?tab=design'); } } } if ($action === 'upload_favicon') { $file = $_FILES['favicon'] ?? []; $ext = strtolower(pathinfo($file['name'] ?? '', PATHINFO_EXTENSION)); $err = null; if (empty($file['name'])) $err = 'Bitte eine Datei auswählen.'; elseif (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) $err = 'Upload fehlgeschlagen' . (($file['error'] ?? 0) === UPLOAD_ERR_INI_SIZE ? ' (die Datei ist größer als das Upload-Limit des Servers).' : '.'); elseif ($file['size'] > 1024 * 1024) $err = 'Das Favicon darf höchstens 1 MB groß sein.'; elseif (!isset(favicon_types()[$ext])) $err = 'Erlaubt: ICO, PNG, JPG, GIF, WebP, SVG.'; else $err = favicon_validate_file($file['tmp_name'], $ext); if ($err !== null) { $errors[] = $err; } else { foreach (array_keys(favicon_types()) as $e2) @unlink($branding . '/favicon.' . $e2); if (!move_uploaded_file($file['tmp_name'], $branding . '/favicon.' . $ext)) { $errors[] = 'Speichern fehlgeschlagen (Ordner assets/branding/ nicht beschreibbar?).'; } else { set_setting('favicon_path', '/assets/branding/favicon.' . $ext . '?v=' . time()); audit_log($staff['id'], 'Favicon aktualisiert'); flash('success', 'Favicon aktualisiert. Browser zeigen Änderungen oft erst nach Neuladen (Strg+F5) oder in einem neuen Tab.'); redirect('/staff/settings.php?tab=design'); } } } if ($action === 'remove_favicon') { foreach (array_keys(favicon_types()) as $e2) @unlink($branding . '/favicon.' . $e2); set_setting('favicon_path', null); audit_log($staff['id'], 'Favicon entfernt'); flash('success', 'Favicon entfernt.'); redirect('/staff/settings.php?tab=design'); } if ($action === 'save_logo_options') { $hide = !empty($_POST['logo_hide_name']) ? '1' : '0'; set_setting('logo_hide_name', $hide); audit_log($staff['id'], $hide === '1' ? 'Name neben dem Logo ausgeblendet' : 'Name neben dem Logo wird angezeigt'); flash('success', $hide === '1' ? 'Der Name wird neben dem Logo nicht mehr angezeigt.' : 'Der Name wird wieder neben dem Logo angezeigt.'); redirect('/staff/settings.php?tab=design'); } if ($action === 'remove_logo') { foreach (['png','jpg','jpeg','gif','svg'] as $e2) @unlink($branding.'/logo.'.$e2); set_setting('logo_path', null); audit_log($staff['id'],'Logo entfernt'); flash('success','Logo entfernt.'); redirect('/staff/settings.php?tab=design'); } if ($action === 'upload_banner') { $file = $_FILES['banner'] ?? []; $allowed = ['png','jpg','jpeg','gif']; $ext = strtolower(pathinfo($file['name'] ?? '', PATHINFO_EXTENSION)); if (empty($file['name'])) { $errors[] = 'Bitte eine Bilddatei auswählen.'; } elseif ($file['error'] !== UPLOAD_ERR_OK) { $errors[] = 'Upload fehlgeschlagen.'; } elseif ($file['size'] > 5*1024*1024) { $errors[] = 'Max. 5 MB.'; } elseif (!in_array($ext, $allowed, true)) { $errors[] = 'Erlaubt: PNG, JPG, GIF.'; } elseif (($imgErr = favicon_validate_file($file['tmp_name'], $ext)) !== null) { $errors[] = $imgErr; } else { foreach ($allowed as $e2) @unlink($branding.'/banner.'.$e2); $dest = $branding.'/banner.'.$ext; if (!move_uploaded_file($file['tmp_name'], $dest)) { $errors[] = 'Speichern fehlgeschlagen.'; } else { set_setting('banner_path', '/assets/branding/banner.'.$ext.'?v='.time()); audit_log($staff['id'],'Titelbild aktualisiert'); flash('success','Titelbild aktualisiert.'); redirect('/staff/settings.php?tab=design'); } } } if ($action === 'remove_banner') { foreach (['png','jpg','jpeg','gif'] as $e2) @unlink($branding.'/banner.'.$e2); set_setting('banner_path', null); audit_log($staff['id'],'Titelbild entfernt'); flash('success','Titelbild entfernt.'); redirect('/staff/settings.php?tab=design'); } if ($action === 'save_homepage_content') { $popularArticlesEnabled = !empty($_POST['homepage_popular_articles_enabled']) ? '1' : '0'; set_setting('homepage_popular_articles_enabled', $popularArticlesEnabled); audit_log( $staff['id'], $popularArticlesEnabled === '1' ? 'Beliebte Artikel auf der Startseite eingeblendet' : 'Beliebte Artikel auf der Startseite ausgeblendet' ); flash('success', 'Startseiten-Inhalte gespeichert.'); redirect('/staff/settings.php?tab=design'); } if ($action === 'upload_custom_theme') { try { $theme = install_custom_theme_upload($_FILES['theme_zip'] ?? []); $actionLabel = $theme['replaced'] ? 'aktualisiert' : 'hochgeladen'; audit_log($staff['id'], "Kundenportal-Theme {$actionLabel}: {$theme['name']}"); flash('success', "Theme „{$theme['name']}“ wurde {$actionLabel}."); redirect('/staff/settings.php?tab=design'); } catch (Throwable $e) { $errors[] = $e->getMessage(); } } if ($action === 'save_theme_catalog' && plugin_strict_mode()) { $errors[] = 'Im geprüften Modus ist der Katalog fest vorgegeben.'; } elseif ($action === 'save_theme_catalog') { $url = trim((string) ($_POST['theme_catalog_url'] ?? '')); if ($url !== '' && ($urlError = plugin_url_check($url))) { $errors[] = 'Katalog-Adresse: ' . $urlError; } else { set_setting('theme_catalog_url', $url); flash('success', 'Katalog-Adresse gespeichert.'); redirect('/staff/settings.php?tab=design'); } } if ($action === 'theme_catalog_install') { $res = theme_catalog_install((string) ($_POST['slug'] ?? '')); if ($res['ok']) { audit_log($staff['id'], "Kundenportal-Theme aus dem Katalog installiert: {$res['theme']['name']} (SHA-256 {$res['hash']}" . ($res['signed'] ? ', signiert' : ', nicht signiert') . ')'); flash('success', "Theme „{$res['theme']['name']}“ installiert (" . ($res['signed'] ? 'Signatur geprüft' : 'nicht signiert') . '). Es ist noch nicht aktiv.'); redirect('/staff/settings.php?tab=design'); } $errors[] = $res['error']; } if ($action === 'activate_custom_theme') { $themeId = (string) ($_POST['theme_id'] ?? ''); $theme = null; foreach (list_custom_themes() as $candidate) { if ($candidate['id'] === $themeId) { $theme = $candidate; break; } } if ($theme === null) { $errors[] = 'Das ausgewählte Theme wurde nicht gefunden.'; } else { set_setting('active_custom_theme', $themeId); audit_log($staff['id'], "Kundenportal-Theme aktiviert: {$theme['name']}"); flash('success', "Theme „{$theme['name']}“ ist jetzt aktiv."); redirect('/staff/settings.php?tab=design'); } } if ($action === 'deactivate_custom_theme') { set_setting('active_custom_theme', null); audit_log($staff['id'], 'Benutzerdefiniertes Kundenportal-Theme deaktiviert'); flash('success', 'Das Standarddesign ist jetzt aktiv.'); redirect('/staff/settings.php?tab=design'); } if ($action === 'delete_custom_theme') { $themeId = (string) ($_POST['theme_id'] ?? ''); $theme = null; foreach (list_custom_themes() as $candidate) { if ($candidate['id'] === $themeId) { $theme = $candidate; break; } } if ($theme === null || !delete_custom_theme($themeId)) { $errors[] = 'Das Theme konnte nicht gelöscht werden.'; } else { if (get_setting('active_custom_theme', '') === $themeId) { set_setting('active_custom_theme', null); } audit_log($staff['id'], "Kundenportal-Theme gelöscht: {$theme['name']}"); flash('success', "Theme „{$theme['name']}“ wurde gelöscht."); redirect('/staff/settings.php?tab=design'); } } if ($action === 'save_uploads') { $dangerous = ['php','php3','php4','php5','phtml','phar','exe','bat','cmd','sh','pl','py','asp','aspx','jsp','cgi']; $extList = array_filter(array_map('trim', explode(',', strtolower($_POST['upload_allowed_extensions'] ?? '')))); $extList = array_diff($extList, $dangerous); set_setting('upload_allowed_extensions', implode(', ', $extList)); $mailExt = array_diff(array_filter(array_map(fn($e) => ltrim(trim($e), '.'), explode(',', strtolower($_POST['mail_attachment_extra_extensions'] ?? '')))), $dangerous); set_setting('mail_attachment_extra_extensions', implode(', ', $mailExt)); set_setting('upload_max_size_mb', max(1, min(100, (int)($_POST['upload_max_size_mb'] ?? 10)))); audit_log($staff['id'], 'Upload-Einstellungen geändert'); flash('success', 'Upload-Einstellungen gespeichert.'); redirect('/staff/settings.php?tab=uploads'); } if ($action === 'save_footer_pages') { foreach (['footer_privacy_label','footer_privacy_url','footer_privacy_html','footer_imprint_label','footer_imprint_url','footer_imprint_html'] as $k) set_setting($k, trim($_POST[$k] ?? '') ?: null); audit_log($staff['id'], 'Footer-Seiten geändert'); flash('success', 'Footer-Einstellungen gespeichert.'); redirect('/staff/settings.php?tab=footer'); } if ($action === 'save_notifications') { $discordNew = trim($_POST['discord_webhook_url'] ?? ''); $tgTokenNew = trim($_POST['telegram_bot_token'] ?? ''); $tgChatId = trim($_POST['telegram_chat_id'] ?? ''); // Leer lassen = unverändert; Wert "-" = explizit löschen; sonst verschlüsseln if ($discordNew !== '') { set_setting('discord_webhook_url', $discordNew === '-' ? null : encrypt_secret($discordNew)); } if ($tgTokenNew !== '') { set_setting('telegram_bot_token', $tgTokenNew === '-' ? null : encrypt_secret($tgTokenNew)); } if ($tgChatId !== '') { set_setting('telegram_chat_id', $tgChatId === '-' ? null : $tgChatId); } audit_log($staff['id'], 'Benachrichtigungen geändert'); flash('success', 'Benachrichtigungseinstellungen gespeichert.'); redirect('/staff/settings.php?tab=benachrichtigungen'); } // Push-Benachrichtigungen für die Android-App (Firebase Cloud Messaging) if ($action === 'save_fcm') { set_setting('fcm_enabled', !empty($_POST['fcm_enabled']) ? '1' : '0'); set_setting('fcm_show_details', !empty($_POST['fcm_show_details']) ? '1' : '0'); set_setting('push_brand', fcm_clean_brand((string) ($_POST['push_brand'] ?? ''))); flash('success', 'Einstellungen für die Android-Benachrichtigungen gespeichert.'); redirect('/staff/settings.php?tab=benachrichtigungen'); } if ($action === 'remove_fcm') { set_setting('fcm_service_account', ''); set_setting('fcm_access_token', ''); audit_log($staff['id'], 'Firebase-Dienstkonto für Android-Push entfernt'); flash('success', 'Der Firebase-Schlüssel wurde vom Server entfernt.'); redirect('/staff/settings.php?tab=benachrichtigungen'); } if ($action === 'test_fcm') { if (fcm_mode() === 'none') { flash('error', 'Push ist nicht eingerichtet: Es ist kein Push-Dienst eingetragen und kein eigener Firebase-Schlüssel hinterlegt.'); redirect('/staff/settings.php?tab=benachrichtigungen'); } $t = fcm_send_test($pdo, (int) $staff['id']); if ($t['devices'] === 0) { flash('error', 'Auf deinem Konto ist noch kein Android-Gerät angemeldet. Melde dich in der App an (Benachrichtigungen erlauben) und versuche es erneut.'); redirect('/staff/settings.php?tab=benachrichtigungen'); } $t['sent'] > 0 ? flash('success', "Testnachricht an {$t['sent']} Gerät(e) gesendet. Sie sollte jetzt auf dem Handy erscheinen.") : flash('error', 'Senden fehlgeschlagen: ' . ($t['error'] !== '' ? $t['error'] : 'unbekannter Fehler')); redirect('/staff/settings.php?tab=benachrichtigungen'); } if ($action === 'test_notifications') { $sent = []; if (discord_notify('🔔 Testnachricht von DeskHub')) $sent[] = 'Discord'; if (telegram_notify('🔔 Testnachricht von DeskHub')) $sent[] = 'Telegram'; $sent ? flash('success', 'Gesendet an: '.implode(', ', $sent)) : flash('error', 'Kein Kanal konfiguriert oder Fehler.'); redirect('/staff/settings.php?tab=benachrichtigungen'); } if ($action === 'save_smtp') { set_setting('smtp_host', trim($_POST['smtp_host'] ?? '') ?: null); set_setting('smtp_port', trim($_POST['smtp_port'] ?? '') ?: null); set_setting('smtp_encryption', $_POST['smtp_encryption'] ?? 'tls'); set_setting('smtp_username', trim($_POST['smtp_username'] ?? '') ?: null); if (trim($_POST['smtp_password'] ?? '') !== '') set_setting('smtp_password', encrypt_secret(trim($_POST['smtp_password']))); set_setting('smtp_from_name', trim($_POST['smtp_from_name'] ?? '') ?: null); set_setting('smtp_from_email', trim($_POST['smtp_from_email'] ?? '') ?: null); audit_log($staff['id'], 'SMTP geändert'); flash('success', 'SMTP-Einstellungen gespeichert.'); redirect('/staff/settings.php?tab=email'); } if ($action === 'test_smtp') { $testBody = email_wrap(email_p('Diese Test-E-Mail bestätigt, dass Ihr SMTP-Versand korrekt eingerichtet ist. 🎉')); $ok = send_mail($staff['email'], 'DeskHub – Test-E-Mail', $testBody, null, false); $ok ? flash('success', 'Test-Mail an '.$staff['email'].' gesendet.') : flash('error', 'Versand fehlgeschlagen. Bitte SMTP-Daten prüfen. '.smtp_last_error()); redirect('/staff/settings.php?tab=email'); } // Einzelnes globales IMAP-Konto wurde durch die Tabelle imap_accounts abgelöst // (mehrere Postfächer möglich, siehe staff/imap_accounts.php). Die alten // Einzel-Settings (imap_host/imap_user/...) werden nur noch einmalig per // Migration php_0011_imap_accounts als erstes Postfach übernommen. } // ── Lazy-Encryption: Klartext-Credentials beim ersten Admin-Besuch sichern ── // Direkt über PDO (nicht get_setting) um den statischen Cache zu umgehen. // Idempotent: AES-GCM-Blobs (base64, min. 28 Bytes) werden nicht erneut verschlüsselt. foreach (['discord_webhook_url', 'telegram_bot_token', 'smtp_password', 'imap_pass'] as $_lek) { $stmt_le = $pdo->prepare('SELECT setting_value FROM settings WHERE setting_key = ?'); $stmt_le->execute([$_lek]); $raw_le = $stmt_le->fetchColumn(); if ($raw_le !== false && $raw_le !== '' && $raw_le !== null) { $decoded_le = base64_decode($raw_le, true); if ($decoded_le === false || strlen($decoded_le) < 28) { // Klartext gefunden → jetzt verschlüsseln $pdo->prepare('UPDATE settings SET setting_value = ? WHERE setting_key = ?') ->execute([encrypt_secret($raw_le), $_lek]); } } } unset($_lek, $stmt_le, $raw_le, $decoded_le); // ── Werte laden ─────────────────────────────────────────────────────────────── $currentLogo = get_setting('logo_path'); $currentBanner = get_setting('banner_path'); $homepagePopularArticlesEnabled = get_setting('homepage_popular_articles_enabled', '1') === '1'; $currentName = get_setting('display_name', config()['site']['name']); $helpdeskStatus = get_setting('helpdesk_status', 'online'); $offlineMessage = get_setting('offline_message', 'Das Supportcenter ist derzeit nicht erreichbar.'); $defaultDeptId = get_setting('default_department_id', (string)($departments[0]['id'] ?? '')); $kbEnabled = get_setting('kb_enabled', '1') === '1'; $discordUrl = get_setting('discord_webhook_url', ''); $telegramToken = get_setting('telegram_bot_token', ''); $telegramChatId = get_setting('telegram_chat_id', ''); $smtpHost = get_setting('smtp_host', ''); $smtpPort = get_setting('smtp_port', '587'); $smtpEncryption = get_setting('smtp_encryption', 'tls'); $smtpUsername = get_setting('smtp_username', ''); $smtpFromName = get_setting('smtp_from_name', ''); $smtpFromEmail = get_setting('smtp_from_email', ''); $footerPrivacyLabel = get_setting('footer_privacy_label', 'Datenschutz'); $footerPrivacyUrl = get_setting('footer_privacy_url', ''); $footerPrivacyHtml = get_setting('footer_privacy_html', ''); $footerImprintLabel = get_setting('footer_imprint_label', 'Impressum'); $footerImprintUrl = get_setting('footer_imprint_url', ''); $footerImprintHtml = get_setting('footer_imprint_html', ''); $uploadExtensions = get_setting('upload_allowed_extensions', implode(', ', config()['uploads']['allowed_extensions'])); $mailExtraExtensions = get_setting('mail_attachment_extra_extensions', MAIL_ATTACHMENT_EXTRA_DEFAULT); $uploadMaxSize = get_setting('upload_max_size_mb', '10'); $customThemes = list_custom_themes(); $activeCustomTheme = get_setting('active_custom_theme', ''); $cronFetch = cron_last_run_label(get_setting('cron_fetch_emails_last_run')); $cronSla = cron_last_run_label(get_setting('cron_check_sla_last_run')); $cronRecurring = cron_last_run_label(get_setting('cron_process_recurring_last_run')); $cronBackups = cron_last_run_label(get_setting('cron_process_backups_last_run')); $cronEscalations = cron_last_run_label(get_setting('cron_check_escalations_last_run')); $cronAnonymize = cron_last_run_label(get_setting('cron_anonymize_data_last_run')); $cronAutoClose = cron_last_run_label(get_setting('cron_auto_close_tickets_last_run')); $cronSnoozes = cron_last_run_label(get_setting('cron_process_snoozes_last_run')); $cronRemind = cron_last_run_label(get_setting('cron_remind_customers_last_run')); $cronPlugins = cron_last_run_label(get_setting('cron_run_plugins_last_run')); $cronCsat = cron_last_run_label(get_setting('cron_send_csat_last_run')); $tab = $_GET['tab'] ?? 'allgemein'; $pageTitle = 'Einstellungen'; $activePage = 'settings'; require __DIR__ . '/_header.php'; ?>
Wird im Browser-Tab, in E-Mails und in der Web-App angezeigt.
PNG/SVG empfohlen, max. 2 MB. Wird überall statt „DeskHub" angezeigt. Der Name der Installation steht standardmäßig daneben und lässt sich unten ausblenden.
Kein Logo hinterlegt.
ICO, PNG, JPG, GIF, WebP oder SVG, höchstens 1 MB, am besten quadratisch (z. B. 256×256). Ohne Favicon verwendet DeskHub das Logo.
Kein eigenes Favicon hinterlegt.
Hintergrundbild im dunklen Hero-Bereich. Mind. 1600×500 px, max. 5 MB.
Kein Titelbild hinterlegt.
Legen Sie fest, welche zusätzlichen Bereiche auf der Kundenportal-Startseite angezeigt werden.
Laden Sie mehrere Designs als ZIP hoch und aktivieren Sie eines davon global für alle Kunden.
Das ZIP muss im Stammverzeichnis eine theme.css enthalten. Zulässig sind CSS,
PNG, JPG, GIF, WebP, WOFF und WOFF2; maximal 10 MB.
Autor und Version können im CSS-Kopf mit Author: und Version: angegeben werden.
Der Anzeigename wird automatisch aus dem ZIP-Dateinamen übernommen, zum Beispiel
Classic-Support-Blue.zip → Classic Support Blue.
Ein erneuter Upload mit demselben Namen ersetzt das vorhandene Theme.
Noch keine eigenen Themes hochgeladen.
Fertige Designs direkt installieren. Prüfsumme und Signatur werden vor der Installation geprüft.
| Theme | Version | Signatur | |
|---|---|---|---|
= catalog_thumb('theme', $t) ?> = e($t['name']) ?> = e($t['description']) ?>
von = e($t['author']) ?> |
= e($t['version']) ?> installiert: = e($__s2['version']) ?> |
= $t['signature'] !== '' ? '✓ Signiert' : 'Nicht signiert' ?> | ✓ Installiert |
| Der Katalog enthält keine Themes. | |||
Postfächer, aus denen eingehende E-Mails als Tickets importiert werden. Es können mehrere Postfächer angelegt werden (z.B. ein eigenes je Abteilung) – jedes mit eigenem Server, Zugangsdaten und optionaler Standardabteilung.
query('SELECT COUNT(*) FROM imap_accounts WHERE is_active = 1')->fetchColumn(); ?>0): ?> ● = $imapAccountCount ?> aktive(s) Postfach/Postfächer eingerichtet ○ Noch kein Postfach eingerichtet · Letzter Abruf: = e($cronFetch) ?>
Postfächer verwalten →Mitarbeiter werden bei neuen Tickets und Antworten benachrichtigt. Ticket-Daten werden dabei an den jeweiligen Drittanbieter übermittelt.
🔒 Webhook-URL und Bot-Token werden verschlüsselt gespeichert. Leer lassen = unverändert. Wert - löscht ihn.
Popups auf dem Handy bei neuen Tickets, Kundenantworten und Zuweisungen, auch wenn die App geschlossen ist. Die Nachrichten laufen über Google (Firebase Cloud Messaging).
✓ Eigenes Firebase-Projekt · = e($__fcmSa['project_id']) ?>= $__fcmFromConfig ? ' (aus config/config.php)' : '' ?>
✓ Push ist aktiv · über den DeskHub-Push-Dienst, es ist nichts einzurichten
Nicht verfügbar. In dieser Version ist kein Push-Dienst eingetragen. Ihr könnt unten ein eigenes Firebase-Projekt verwenden.
= (int) $__fcmDevices ?> Gerät(e) angemeldet= $__fcmOk !== '' ? ' · letzter erfolgreicher Versand: ' . e($__fcmOk) : '' ?>
Letzter Fehler: = e($__fcmErr) ?>
Gefährliche Typen (PHP, EXE, SH etc.) werden immer blockiert – unabhängig von dieser Liste.
Externer Link oder HTML-Inhalt (öffnet Popup). Ist ein Link gesetzt, wird das HTML-Feld ignoriert.
Geschlossene und gelöste Tickets werden nach der gewählten Zeit automatisch anonymisiert. Name, E-Mail-Adresse und IP werden unkenntlich gemacht — das Ticket und seine Historie bleiben erhalten.
Aktuell würden = $previewCount ?> Ticket(s) anonymisiert (geschlossen/gelöst, älter als = $retMonths ?> Monate).
Anonymisiertanon_[hash]@deleted.local0.0.0.00 3 * * * php /pfad/zu/deskhub/cron/anonymize_data.php
Läuft komplett eingebaut, ganz ohne echten Server-Cronjob und ohne externen Dienst: bei jeder Anmeldung/Nutzung im Backend und in der App wird im Hintergrund geprüft, ob einer der Jobs unten fällig ist, und automatisch ausgeführt. Solange regelmäßig jemand das Backend oder die App nutzt, laufen alle Jobs von selbst dauerhaft weiter – ganz ohne Einrichtung bei Strato oder einem externen Anbieter.
Jeder Job kann hier zusätzlich jederzeit sofort manuell gestartet werden.
$cronFetch, 'check_sla' => $cronSla, 'check_escalations' => $cronEscalations, 'process_recurring' => $cronRecurring, 'process_backups' => $cronBackups, 'anonymize_data' => $cronAnonymize, 'auto_close_tickets'=> $cronAutoClose, 'process_snoozes' => $cronSnoozes, 'process_mail_queue' => cron_last_run_label(get_setting('cron_process_mail_queue_last_run')), 'remind_customers' => $cronRemind, 'run_plugins' => $cronPlugins, 'send_csat' => $cronCsat, 'status_checks' => cron_last_run_label(get_setting('cron_status_checks_last_run')), 'send_scheduled_replies' => cron_last_run_label(get_setting('cron_send_scheduled_replies_last_run')), 'send_report_schedules' => cron_last_run_label(get_setting('cron_send_report_schedules_last_run')), ]; ?>| = e($jobMeta['label']) ?> | = e($cronLastRun[$jobKey]) ?> |
✅ Dieser Server unterstützt eine automatische Installation (Shell-Zugriff erkannt). DeskHub trägt dafür einen klar markierten Block in die Crontab des Server-Benutzers ein – alle sonstigen vorhandenen Einträge bleiben unangetastet.
Status: ● Installiert ○ Nicht installiert
❌ Auf diesem Server nicht möglich: = e($crontabCheck['reason']) ?>
Typisch bei Shared-Hosting (z.B. den meisten Strato-Webhosting-Paketen). Der oben aktive
automatische Hintergrund-Betrieb deckt das in der Regel bereits ab – alternativ unten die
Zeilen manuell im Hosting-Kundenlogin eintragen, falls dort ein Cronjob-Manager verfügbar ist.
= e(crontab_marker_block()) ?>