get($moduleName); $error = null; $notice = null; $testGroup = null; $dbTestMessages = []; require_admin(); if (!$module) { http_response_code(404); echo '
Modul nicht gefunden.
'; return; } $fields = (array)($module['setup']['fields'] ?? []); $hasGlobalDebugField = false; foreach ($fields as $field) { if ((string)($field['name'] ?? '') === 'debug_enabled') { $hasGlobalDebugField = true; break; } } if (!$hasGlobalDebugField) { $fields[] = [ 'name' => 'debug_enabled', 'label' => 'Modul-Debug aktivieren', 'type' => 'checkbox', 'help' => 'Wenn aktiv, darf das Modul Debug-Daten sammeln und den Debug-Bereich anzeigen.', ]; } $fieldTypes = []; $fieldMeta = []; foreach ($fields as $field) { $fname = (string)($field['name'] ?? ''); if ($fname === '') { continue; } $fieldTypes[$fname] = (string)($field['type'] ?? 'text'); $fieldMeta[$fname] = $field; } $isFxRatesSetup = $moduleName === 'fx-rates'; $current = modules()->settings($moduleName); $intervalTaskStatuses = []; $cronTaskDefinitions = modules()->cronTasks($moduleName); $cronTaskStatuses = []; $cronTaskStatusGroups = []; $refreshSchedulerState = static function () use ($moduleName, &$intervalTaskStatuses, &$cronTaskStatuses, &$cronTaskStatusGroups): void { $intervalTaskStatuses = modules()->intervalTaskStatuses($moduleName); $cronTaskStatuses = modules()->cronTaskStatuses($moduleName); $cronTaskStatusGroups = []; foreach ($cronTaskStatuses as $cronTaskStatus) { $cronGroupName = trim((string) ($cronTaskStatus['job_name'] ?? $cronTaskStatus['name'] ?? '')); if ($cronGroupName === '') { continue; } $cronTaskStatusGroups[$cronGroupName][] = $cronTaskStatus; } }; $refreshSchedulerState(); $setupActions = modules()->hasFunction($moduleName, 'setup_actions') ? (array) module_fn($moduleName, 'setup_actions') : []; $defaults = $module['db_defaults'] ?? []; if (empty($current['db']) && is_array($defaults)) { $current['db'] = $defaults; } $metadataDefaults = $module['metadata_db_defaults'] ?? []; if (empty($current['metadata_db']) && is_array($metadataDefaults)) { $current['metadata_db'] = $metadataDefaults; } $dbDefaultsByGroup = [ 'db' => is_array($defaults) ? $defaults : [], 'metadata_db' => is_array($metadataDefaults) ? $metadataDefaults : [], ]; $setNested = function (array &$target, string $path, mixed $value): void { $parts = explode('.', $path); $last = array_pop($parts); $node = &$target; foreach ($parts as $part) { if (!isset($node[$part]) || !is_array($node[$part])) { $node[$part] = []; } $node = &$node[$part]; } if ($last !== null && $last !== '') { $node[$last] = $value; } }; $getNested = function (array $source, string $path): mixed { $node = $source; foreach (explode('.', $path) as $part) { if (!is_array($node) || !array_key_exists($part, $node)) { return null; } $node = $node[$part]; } return $node; }; $dbGroups = []; $fieldsByDbGroup = []; $generalFields = []; foreach ($fields as $field) { $name = (string)($field['name'] ?? ''); if (!str_contains($name, '.')) { continue; } [$group, $key] = explode('.', $name, 2); if ($key !== 'driver') { continue; } $label = (string)($field['label'] ?? $group); $label = trim(preg_replace('/\s+DB\s+Driver$/i', ' DB', $label) ?? $label); $label = $label !== '' ? $label : $group; $dbGroups[$group] = $label; } foreach ($fields as $field) { $name = (string)($field['name'] ?? ''); if (str_contains($name, '.')) { [$group] = explode('.', $name, 2); if (array_key_exists($group, $dbGroups)) { $fieldsByDbGroup[$group][] = $field; continue; } } $generalFields[] = $field; } $driverOptions = [ 'pgsql' => 'PostgreSQL', 'mysql' => 'MySQL / MariaDB', 'sqlite' => 'SQLite', ]; $timezoneOptions = modules()->timezones(); $describeDbConfig = static function (array $dbConfig): string { $driver = (string)($dbConfig['driver'] ?? ''); $host = (string)($dbConfig['host'] ?? ''); $port = (string)($dbConfig['port'] ?? ''); return 'Aktive Einstellung: Treiber ' . ($driver !== '' ? $driver : 'nicht gesetzt') . ', Host ' . ($host !== '' ? $host : 'nicht gesetzt') . ', Port ' . ($port !== '' ? $port : 'nicht gesetzt') . '.'; }; $describeDbDefaults = static function (array $dbDefaults): string { $driver = (string)($dbDefaults['driver'] ?? ''); $host = (string)($dbDefaults['host'] ?? ''); $port = (string)($dbDefaults['port'] ?? ''); if ($driver === '' && $host === '' && $port === '') { return ''; } return 'Standard: Treiber ' . ($driver !== '' ? $driver : 'nicht gesetzt') . ', Host ' . ($host !== '' ? $host : 'nicht gesetzt') . ', Port ' . ($port !== '' ? $port : 'nicht gesetzt') . '.'; }; $dbConfigWarning = static function (array $dbConfig): ?string { $driver = (string)($dbConfig['driver'] ?? ''); $port = (string)($dbConfig['port'] ?? ''); if ($driver === 'pgsql' && $port === '3306') { return 'Port 3306 ist typisch fuer MySQL/MariaDB, aber als Treiber ist PostgreSQL ausgewaehlt. Bitte den Treiber auf MySQL / MariaDB stellen.'; } if ($driver === 'mysql' && $port === '5432') { return 'Port 5432 ist typisch fuer PostgreSQL, aber als Treiber ist MySQL / MariaDB ausgewaehlt. Bitte Port oder Treiber pruefen.'; } return null; }; $dbConfigHint = static function (string $group, array $dbConfig, array $dbDefaults): ?string { if ($group !== 'metadata_db') { return null; } $driver = (string)($dbConfig['driver'] ?? ''); $host = (string)($dbConfig['host'] ?? ''); $port = (string)($dbConfig['port'] ?? ''); $defaultDriver = (string)($dbDefaults['driver'] ?? ''); $defaultHost = (string)($dbDefaults['host'] ?? ''); $defaultPort = (string)($dbDefaults['port'] ?? ''); if ($driver !== $defaultDriver || $host !== $defaultHost || $port !== $defaultPort) { return 'Diese Verbindung ist fuer Nexus-eigene DHCP-Zusatzinfos gedacht, nicht fuer die Nexus-App/Base-DB. Erwartet wird normalerweise: ' . ($defaultDriver !== '' ? $defaultDriver : 'Treiber offen') . ' auf ' . ($defaultHost !== '' ? $defaultHost : 'Host offen') . ':' . ($defaultPort !== '' ? $defaultPort : 'Port offen') . '.'; } return null; }; $fetchJsonWithApiKey = static function (string $url, string $apiKey, int $timeout = 10): array { $headers = [ 'Accept: application/json', 'x-api-key: ' . $apiKey, ]; $responseBody = null; $httpCode = 0; $curlError = ''; if (function_exists('curl_init')) { $ch = curl_init($url); if ($ch !== false) { curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_TIMEOUT => $timeout, CURLOPT_CONNECTTIMEOUT => min(5, $timeout), CURLOPT_HTTPHEADER => $headers, ]); $responseBody = curl_exec($ch); $curlError = curl_error($ch); $httpCode = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE); curl_close($ch); } } if (!is_string($responseBody) || $responseBody === '') { $context = stream_context_create([ 'http' => [ 'method' => 'GET', 'timeout' => $timeout, 'header' => implode("\r\n", $headers) . "\r\n", ], ]); $responseBody = @file_get_contents($url, false, $context); } if (!is_string($responseBody) || $responseBody === '') { return [ 'ok' => false, 'message' => 'Abruf fehlgeschlagen.' . ($curlError !== '' ? ' ' . $curlError : '') . ($httpCode > 0 ? ' HTTP ' . $httpCode : ''), ]; } $decoded = json_decode($responseBody, true); if (!is_array($decoded)) { return [ 'ok' => false, 'message' => 'Antwort ist kein gueltiges JSON.', ]; } foreach (['error', 'message', 'detail'] as $errorKey) { if (isset($decoded[$errorKey]) && is_string($decoded[$errorKey]) && trim($decoded[$errorKey]) !== '') { return [ 'ok' => false, 'message' => trim((string) $decoded[$errorKey]), ]; } } return [ 'ok' => true, 'data' => $decoded, ]; }; $normalizeDriver = static function (mixed $value): mixed { if (!is_string($value)) { return $value; } $normalized = strtolower(trim($value)); return match ($normalized) { 'postgres', 'postgresql' => 'pgsql', 'mariadb', 'mysql/mariadb', 'mysql / mariadb' => 'mysql', default => $normalized, }; }; $formatRunTimestamp = static function (?string $value, ?string $timezone = null): string { $value = trim((string) $value); if ($value === '') { return '-'; } try { $dt = new DateTimeImmutable($value, new DateTimeZone('UTC')); $targetTz = trim((string) $timezone) !== '' ? new DateTimeZone((string) $timezone) : new DateTimeZone(date_default_timezone_get()); return $dt->setTimezone($targetTz)->format('Y-m-d H:i:s'); } catch (\Throwable) { $ts = strtotime($value); if ($ts === false) { return $value; } return date('Y-m-d H:i:s', $ts); } }; $extractSchedulerJobs = static function (array $postedSchedulerJobs, array $cronTaskDefinitions, array $current): array { $schedulerJobs = []; foreach ($cronTaskDefinitions as $cronTask) { if (!is_array($cronTask)) { continue; } $cronName = trim((string) ($cronTask['name'] ?? '')); if ($cronName === '') { continue; } $jobPayload = is_array($postedSchedulerJobs[$cronName] ?? null) ? $postedSchedulerJobs[$cronName] : []; $entriesPayload = is_array($jobPayload['entries'] ?? null) ? $jobPayload['entries'] : []; $entries = []; foreach (array_values($entriesPayload) as $entryPayload) { if (!is_array($entryPayload)) { continue; } $cronExpression = trim((string) ($entryPayload['cron_expression'] ?? '')); $timezone = trim((string) ($entryPayload['timezone'] ?? ($current['schedule_timezone'] ?? 'UTC'))); if ($cronExpression === '' && $timezone === '') { continue; } $entries[] = [ 'enabled' => !empty($entryPayload['enabled']), 'cron_expression' => $cronExpression, 'timezone' => $timezone !== '' ? $timezone : 'UTC', 'builder' => [ 'mode' => trim((string) ($entryPayload['builder']['mode'] ?? 'builder')), 'kind' => trim((string) ($entryPayload['builder']['kind'] ?? 'daily')), 'time' => trim((string) ($entryPayload['builder']['time'] ?? '18:00')), 'interval_days' => max(1, (int) ($entryPayload['builder']['interval_days'] ?? 2)), 'weekday' => trim((string) ($entryPayload['builder']['weekday'] ?? '1')), 'month_day' => max(1, min(31, (int) ($entryPayload['builder']['month_day'] ?? 1))), 'interval_hours' => max(1, min(23, (int) ($entryPayload['builder']['interval_hours'] ?? 6))), ], ]; } if ((string) ($cronTask['mode'] ?? 'single') !== 'multi' && $entries !== []) { $entries = [array_values($entries)[0]]; } $schedulerJobs[$cronName] = ['entries' => $entries]; } return $schedulerJobs; }; $cronWeekdays = [ '0' => 'Sonntag', '1' => 'Montag', '2' => 'Dienstag', '3' => 'Mittwoch', '4' => 'Donnerstag', '5' => 'Freitag', '6' => 'Samstag', ]; $renderField = function (array $field) use (&$current, $getNested, $driverOptions): void { $name = (string)($field['name'] ?? ''); if ($name === '') { return; } $label = (string)($field['label'] ?? $name); $type = (string)($field['type'] ?? 'text'); $required = !empty($field['required']); $help = (string)($field['help'] ?? $field['description'] ?? ''); $postKey = str_replace('.', '_', $name); $value = ''; if ($name === 'kea_auto_init') { $value = !empty($current[$name]) ? '1' : '0'; } elseif (str_contains($name, '.')) { $value = (string)($getNested($current, $name) ?? ''); } else { $value = (string)($current[$name] ?? ''); } ?> saveSettings($moduleName, $current); $current = modules()->settings($moduleName); $refreshSchedulerState(); header('Content-Type: application/json; charset=utf-8'); echo json_encode([ 'ok' => true, 'message' => 'Scheduler gespeichert.', 'scheduler_jobs' => $current['scheduler_jobs'] ?? [], 'statuses' => $cronTaskStatuses, ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); exit; } foreach ($fields as $field) { $name = (string)($field['name'] ?? ''); if ($name === '') { continue; } $type = (string)($field['type'] ?? 'text'); $postKey = str_replace('.', '_', $name); $value = $_POST[$postKey] ?? null; if ($type === 'checkbox') { $value = isset($_POST[$postKey]) ? '1' : '0'; } if (is_array($value) && $type === 'multiselect') { $value = array_values(array_filter(array_map( static fn (mixed $item): string => trim((string) $item), $value ), static fn (string $item): bool => $item !== '')); } elseif (is_array($value)) { continue; } $value = is_string($value) ? trim($value) : $value; if (str_ends_with($name, '.driver')) { $value = $normalizeDriver($value); } if ($name === 'kea_auto_init') { $payload[$name] = $value === '1'; continue; } if (str_contains($name, '.')) { $setNested($payload, $name, $value); continue; } $payload[$name] = $value; } $current = array_replace_recursive($current, $payload); if ($cronTaskDefinitions !== []) { $postedSchedulerJobs = is_array($_POST['scheduler_jobs'] ?? null) ? $_POST['scheduler_jobs'] : []; $schedulerJobs = $extractSchedulerJobs($postedSchedulerJobs, $cronTaskDefinitions, $current); $current['scheduler_jobs'] = $schedulerJobs; } $postedTestGroup = (string)($_POST['test_db'] ?? ''); $postedResetGroup = (string)($_POST['reset_db'] ?? ''); $postedSetupAction = trim((string)($_POST['module_setup_action'] ?? '')); if ($postedSetupAction !== '') { if (!modules()->hasFunction($moduleName, 'run_setup_action')) { $error = 'Diese Setup-Aktion wird vom Modul nicht unterstuetzt.'; } else { try { $actionResult = module_fn($moduleName, 'run_setup_action', $postedSetupAction); $current = modules()->settings($moduleName); $notice = 'Setup-Aktion ausgefuehrt.'; if (is_array($actionResult)) { if (isset($actionResult['message']) && is_string($actionResult['message']) && trim($actionResult['message']) !== '') { $notice = trim((string) $actionResult['message']); } elseif (isset($actionResult['synced_count'])) { $notice = 'Waehrungskatalog synchronisiert. ' . (int) $actionResult['synced_count'] . ' Waehrungen verarbeitet.'; } } } catch (\Throwable $e) { $error = $e->getMessage(); } } } elseif ($postedResetGroup !== '') { $testGroup = $postedResetGroup; if (!array_key_exists($postedResetGroup, $dbGroups)) { $error = 'Unbekannte Datenbank-Konfiguration.'; } elseif (($dbDefaultsByGroup[$postedResetGroup] ?? []) === []) { $dbTestMessages[$postedResetGroup] = [ 'type' => 'error', 'text' => 'Fuer diese Datenbank sind keine Standardwerte hinterlegt.', ]; } else { $current[$postedResetGroup] = $dbDefaultsByGroup[$postedResetGroup]; $dbTestMessages[$postedResetGroup] = [ 'type' => 'success', 'text' => 'Standardwerte geladen. Bitte pruefen und speichern.', ]; } } elseif ($postedTestGroup !== '') { $testGroup = $postedTestGroup; if (!array_key_exists($postedTestGroup, $dbGroups)) { $error = 'Unbekannte Datenbank-Konfiguration.'; } else { $dbConfig = $getNested($current, $postedTestGroup); if (!is_array($dbConfig)) { $dbTestMessages[$postedTestGroup] = [ 'type' => 'error', 'text' => 'Datenbank-Konfiguration ist unvollstaendig.', ]; } else { $warning = $dbConfigWarning($dbConfig); if ($warning !== null) { $dbTestMessages[$postedTestGroup] = [ 'type' => 'error', 'text' => $warning . ' ' . $describeDbConfig($dbConfig), ]; } else { try { $dbConfig['options'] = array_replace([ \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC, ], (array)($dbConfig['options'] ?? [])); $testPdo = \App\Database::createFromArray($dbConfig); $testPdo->query('SELECT 1')->fetchColumn(); $dbTestMessages[$postedTestGroup] = [ 'type' => 'success', 'text' => 'Verbindung erfolgreich. ' . $describeDbConfig($dbConfig), ]; } catch (\Throwable $e) { $dbTestMessages[$postedTestGroup] = [ 'type' => 'error', 'text' => 'Verbindung fehlgeschlagen. ' . $describeDbConfig($dbConfig) . ' ' . $e->getMessage(), ]; } } } } } else { modules()->saveSettings($moduleName, $current); if ($isFxRatesSetup && modules()->hasFunction($moduleName, 'save_runtime_settings')) { module_fn($moduleName, 'save_runtime_settings', $payload); $current = modules()->settings($moduleName); } $refreshSchedulerState(); if (empty($payload['debug_enabled'])) { module_debug_clear($moduleName); } $notice = 'Setup gespeichert.'; $module = modules()->get($moduleName) ?: $module; } } $moduleStatusPanel = null; $activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups) ? $testGroup : (array_key_first($dbGroups) ?? ''); ?>
Setup

– Einrichtung

Trage die benötigten Informationen für das Modul ein.

['code' => $code, 'name' => $name], array_values($fxCatalogOptions), array_keys($fxCatalogOptions) ), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>
API

Provider und Abruf

Waehrungen

Waehrungseinstellungen

Die Auswahl wird erst verfuegbar, nachdem der Waehrungskatalog synchronisiert wurde.

Noch kein Waehrungskatalog vorhanden. Fuehre zuerst unten die Modulaktion Waehrungskatalog synchronisieren aus.
Scheduler

Cron-Zeitzone

DB

Datenbank

>
Aktionen

Modulaktionen

Waehrungssynch Laedt die verfuegbaren Waehrungen einmalig aus dem konfigurierten FX-Provider.
Letzter Sync:
Automationen

Intervall-Aufgaben

Diese Aufgaben werden beim ersten gueltigen Modulaufruf nach Ablauf des Intervalls automatisch ausgefuehrt.

Intervall: Stunden Letzter Start: Letzter Erfolg: Naechster Lauf:
Automationen

Cron-Jobs

Diese Jobs werden ueber den zentralen Nexus-Scheduler ausgefuehrt. Der System-Cron sollte den CLI-Runner jede Minute starten.

$task): ?>
Cron-Syntax: Minute Stunde Tag Monat Wochentag
Letzter Start: Letzter Erfolg: Naechster Lauf lokal: Status: Cron-Fehler:
Allgemein

Moduleinstellungen

Automationen

Intervall-Aufgaben

Diese Aufgaben werden beim ersten gueltigen Modulaufruf nach Ablauf des Intervalls automatisch ausgefuehrt.

Intervall: Stunden Letzter Start: Letzter Erfolg: Naechster Lauf: Status: Meldung:
Automationen

Cron-Jobs

Diese Jobs werden ueber den zentralen Nexus-Scheduler ausgefuehrt. Der System-Cron sollte den CLI-Runner jede Minute starten.

$task): ?>
Cron-Syntax: Minute Stunde Tag Monat Wochentag
Letzter Start: Letzter Erfolg: Naechster Lauf lokal: Status: Cron-Fehler:
Aktionen

Modulaktionen

Seltene Wartungsaktionen koennen direkt hier aus dem Setup ausgefuehrt werden.

Status

Der Status wird beim Aufruf der Setup-Seite automatisch geprueft.

Datenbanken

Verbindungen

Jede Verbindung kann getrennt konfiguriert und getestet werden.

$label): ?>
$label): ?>
>

konfigurieren