diff --git a/modules/boersenchecker/bootstrap.php b/modules/boersenchecker/bootstrap.php index 818490a..abf321f 100644 --- a/modules/boersenchecker/bootstrap.php +++ b/modules/boersenchecker/bootstrap.php @@ -515,7 +515,7 @@ $mm->registerFunction($moduleName, 'alpha_vantage_request', static function ( }); $mm->registerFunction($moduleName, 'display_timezone', static function (): \DateTimeZone { - return new \DateTimeZone('Europe/Berlin'); + return new \DateTimeZone(nexus_display_timezone_name()); }); $mm->registerFunction($moduleName, 'normalize_market_timestamp_utc', static function (mixed $value): string { @@ -547,7 +547,7 @@ $mm->registerFunction($moduleName, 'format_datetime_for_display', static functio return ''; } - $displayTimezone = new \DateTimeZone('Europe/Berlin'); + $displayTimezone = new \DateTimeZone(nexus_display_timezone_name()); $source = trim((string) $source); if (str_starts_with($source, 'bavest:') || str_starts_with($source, 'alphavantage:')) { diff --git a/partials/landingpages/index.php b/partials/landingpages/index.php index 4886b52..e56595a 100755 --- a/partials/landingpages/index.php +++ b/partials/landingpages/index.php @@ -14,6 +14,7 @@ $modules = array_values(array_filter(
Module verwalten + Nexus Einstellungen SQL-Export
diff --git a/partials/landingpages/modules/setup.php b/partials/landingpages/modules/setup.php index f989c8c..0f191dc 100644 --- a/partials/landingpages/modules/setup.php +++ b/partials/landingpages/modules/setup.php @@ -165,6 +165,7 @@ foreach ($fields as $field) { $generalSetupFields = []; $databaseSetupFields = []; $cronSetupFields = []; +$cronTimezoneField = null; $customSetupFields = []; foreach ($generalFields as $field) { $fieldName = (string)($field['name'] ?? ''); @@ -177,7 +178,7 @@ foreach ($generalFields as $field) { continue; } if ($fieldName === 'schedule_timezone') { - $cronSetupFields[] = $field; + $cronTimezoneField = $field; continue; } $customSetupFields[] = $field; @@ -190,6 +191,10 @@ $driverOptions = [ ]; $timezoneOptions = modules()->timezones(); +$globalCronTimezone = nexus_cron_timezone_name(); +$globalDisplayTimezone = nexus_display_timezone_name(); +$moduleCronTimezoneOverride = trim((string) ($current['schedule_timezone'] ?? '')); +$effectiveModuleCronTimezone = $moduleCronTimezoneOverride !== '' ? $moduleCronTimezoneOverride : $globalCronTimezone; $describeDbConfig = static function (array $dbConfig): string { $driver = (string)($dbConfig['driver'] ?? ''); @@ -346,7 +351,7 @@ $formatRunTimestamp = static function (?string $value, ?string $timezone = null) try { $dt = new DateTimeImmutable($value, new DateTimeZone('UTC')); - $targetTz = trim((string) $timezone) !== '' ? new DateTimeZone((string) $timezone) : new DateTimeZone(date_default_timezone_get()); + $targetTz = trim((string) $timezone) !== '' ? new DateTimeZone((string) $timezone) : new DateTimeZone(nexus_display_timezone_name()); return $dt->setTimezone($targetTz)->format('Y-m-d H:i:s'); } catch (\Throwable) { $ts = strtotime($value); @@ -376,7 +381,7 @@ $extractSchedulerJobs = static function (array $postedSchedulerJobs, array $cron continue; } $cronExpression = trim((string) ($entryPayload['cron_expression'] ?? '')); - $timezone = trim((string) ($entryPayload['timezone'] ?? ($current['schedule_timezone'] ?? 'UTC'))); + $timezone = trim((string) ($entryPayload['timezone'] ?? ($current['schedule_timezone'] ?? $globalCronTimezone))); if ($cronExpression === '' && $timezone === '') { continue; } @@ -523,7 +528,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $payload = []; $sectionFieldNames = match ($submittedSetupSection) { 'general' => array_map(static fn (array $field): string => (string) ($field['name'] ?? ''), $generalSetupFields), - 'cron' => array_map(static fn (array $field): string => (string) ($field['name'] ?? ''), $cronSetupFields), + 'cron' => array_values(array_filter(array_merge( + array_map(static fn (array $field): string => (string) ($field['name'] ?? ''), $cronSetupFields), + [$cronTimezoneField !== null ? (string) ($cronTimezoneField['name'] ?? '') : ''] + ), static fn (string $name): bool => $name !== '')), 'custom' => array_map(static fn (array $field): string => (string) ($field['name'] ?? ''), $customSetupFields), 'database' => array_values(array_filter(array_merge( array_map(static fn (array $field): string => (string) ($field['name'] ?? ''), $databaseSetupFields), @@ -583,6 +591,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $postKey = str_replace('.', '_', $name); $value = $_POST[$postKey] ?? null; + if ($submittedSetupSection === 'cron' && $name === 'schedule_timezone') { + if (!isset($_POST['schedule_timezone_custom'])) { + $payload[$name] = ''; + continue; + } + } + if ($type === 'checkbox') { $value = isset($_POST[$postKey]) ? '1' : '0'; } @@ -930,12 +945,31 @@ $GLOBALS['layout_header_context'] = 'Setup / ' . ($sectionTitles[$currentSection

Hier liegen die zeitbezogenen Modul-Einstellungen, Intervall-Tasks und Cron-Jobs.

- +
+ +
+ Standard-Zeitzone für dieses Modul + +
Aktiv:
+ Wenn deaktiviert, wird die globale Nexus-Cron-Zeitzone verwendet: . +
+
+ +
> + +
+

Dieses Modul hat keine eigenen Zeitzonenfelder. Intervall-Tasks und Cron-Jobs koennen trotzdem weiter unten verwaltet werden.

@@ -987,7 +1021,7 @@ $GLOBALS['layout_header_context'] = 'Setup / ' . ($sectionTitles[$currentSection $cronMode = (string) ($cronDefinition['mode'] ?? 'single'); $cronEntries = $cronTaskStatusGroups[$cronName] ?? []; ?> -
+
@@ -1002,7 +1036,7 @@ $GLOBALS['layout_header_context'] = 'Setup / ' . ($sectionTitles[$currentSection Cron-Syntax: Minute Stunde Tag Monat Wochentag - + + Custom-Zeitzone verwenden + +
Standard: UTC
+ + @@ -1399,6 +1441,16 @@ $GLOBALS['layout_header_context'] = 'Setup / ' . ($sectionTitles[$currentSection })(); (() => { + const moduleCronToggle = document.querySelector('[data-module-cron-tz-toggle]'); + const moduleCronPanel = document.querySelector('[data-module-cron-tz-panel]'); + if (moduleCronToggle && moduleCronPanel) { + const syncModuleCronTimezone = () => { + moduleCronPanel.hidden = !moduleCronToggle.checked; + }; + moduleCronToggle.addEventListener('change', syncModuleCronTimezone); + syncModuleCronTimezone(); + } + const jobs = document.querySelectorAll('[data-scheduler-job]'); const modal = document.querySelector('[data-scheduler-modal]'); if (!jobs.length || !modal) return; @@ -1424,6 +1476,9 @@ $GLOBALS['layout_header_context'] = 'Setup / ' . ($sectionTitles[$currentSection const modalFields = { enabled: modal.querySelector('[data-modal-enabled]'), + timezoneCustom: modal.querySelector('[data-modal-timezone-custom]'), + timezoneDefault: modal.querySelector('[data-modal-timezone-default]'), + timezoneWrap: modal.querySelector('[data-modal-timezone-wrap]'), timezone: modal.querySelector('[data-modal-timezone]'), intervalHours: modal.querySelector('[data-modal-interval-hours]'), monthDay: modal.querySelector('[data-modal-month-day]'), @@ -1497,20 +1552,22 @@ $GLOBALS['layout_header_context'] = 'Setup / ' . ($sectionTitles[$currentSection const buildSummary = (tab) => { const enabled = modalFields.enabled.value === '1' ? 'Aktiv' : 'Inaktiv'; - const timezone = modalFields.timezone.value.trim() || 'UTC'; + const timezoneLabel = modalFields.timezoneCustom.checked + ? `Custom ${modalFields.timezone.value.trim() || 'UTC'}` + : `Standard ${modalFields.timezoneDefault.dataset.timezone || 'UTC'}`; const time = formatTime(modalState.hour, modalState.minute); switch (tab) { case 'hourly': - return `${enabled}, alle ${modalFields.intervalHours.value || '1'} Stunden um Minute ${modalState.minute}, ${timezone}`; + return `${enabled}, alle ${modalFields.intervalHours.value || '1'} Stunden um Minute ${modalState.minute}, ${timezoneLabel}`; case 'weekly': - return `${enabled}, woechentlich ${weekdayMap[modalState.weekday || '1']} um ${time}, ${timezone}`; + return `${enabled}, woechentlich ${weekdayMap[modalState.weekday || '1']} um ${time}, ${timezoneLabel}`; case 'monthly': - return `${enabled}, monatlich am ${modalFields.monthDay.value || '1'}. um ${time}, ${timezone}`; + return `${enabled}, monatlich am ${modalFields.monthDay.value || '1'}. um ${time}, ${timezoneLabel}`; case 'custom': - return `${enabled}, benutzerdefinierte Cron-Syntax, ${timezone}`; + return `${enabled}, benutzerdefinierte Cron-Syntax, ${timezoneLabel}`; case 'daily': default: - return `${enabled}, taeglich um ${time}, ${timezone}`; + return `${enabled}, taeglich um ${time}, ${timezoneLabel}`; } }; @@ -1547,6 +1604,7 @@ $GLOBALS['layout_header_context'] = 'Setup / ' . ($sectionTitles[$currentSection }; const getEntryField = (entry, selector) => entry.querySelector(selector); + const entryDefaultTimezone = (entry) => entry.closest('[data-scheduler-job]')?.dataset.defaultTimezone || 'UTC'; const findStatusNode = (entry, label) => Array.from(entry.querySelectorAll('small')).find((node) => node.textContent.includes(label)); const setStatusText = (entry, label, value) => { const node = findStatusNode(entry, label); @@ -1696,7 +1754,8 @@ $GLOBALS['layout_header_context'] = 'Setup / ' . ($sectionTitles[$currentSection const updateEntrySummary = (entry) => { const enabled = getEntryField(entry, '[data-enabled]')?.checked ?? false; const expression = getEntryField(entry, '[data-cron-expression]')?.value || ''; - const timezone = (getEntryField(entry, '[data-cron-timezone]')?.value || 'UTC').trim() || 'UTC'; + const timezone = (getEntryField(entry, '[data-cron-timezone]')?.value || '').trim(); + const timezoneLabel = timezone !== '' ? `Custom ${timezone}` : `Standard ${entryDefaultTimezone(entry)}`; const builderMode = getEntryField(entry, '[data-cron-builder-mode]')?.value || 'builder'; const builderKind = getEntryField(entry, '[data-cron-builder-kind]')?.value || 'daily'; const time = getEntryField(entry, '[data-cron-builder-time]')?.value || '18:00'; @@ -1706,15 +1765,15 @@ $GLOBALS['layout_header_context'] = 'Setup / ' . ($sectionTitles[$currentSection let summary = enabled ? 'Aktiv' : 'Inaktiv'; if (builderMode === 'manual') { - summary += `, Custom, ${timezone}`; + summary += `, Custom, ${timezoneLabel}`; } else if (builderKind === 'every_x_hours') { - summary += `, alle ${intervalHours} Stunden, ${timezone}`; + summary += `, alle ${intervalHours} Stunden, ${timezoneLabel}`; } else if (builderKind === 'weekly') { - summary += `, ${weekdayMap[weekday] || weekday} ${time}, ${timezone}`; + summary += `, ${weekdayMap[weekday] || weekday} ${time}, ${timezoneLabel}`; } else if (builderKind === 'monthly_day') { - summary += `, monatlich am ${monthDay}. ${time}, ${timezone}`; + summary += `, monatlich am ${monthDay}. ${time}, ${timezoneLabel}`; } else { - summary += `, taeglich ${time}, ${timezone}`; + summary += `, taeglich ${time}, ${timezoneLabel}`; } let summaryNode = entry.querySelector('[data-entry-summary]'); @@ -1784,7 +1843,13 @@ $GLOBALS['layout_header_context'] = 'Setup / ' . ($sectionTitles[$currentSection const [hour = '18', minute = '00'] = time.split(':'); modalFields.enabled.value = getEntryField(entry, '[data-enabled]')?.checked ? '1' : '0'; - modalFields.timezone.value = getEntryField(entry, '[data-cron-timezone]')?.value || 'UTC'; + const savedTimezone = getEntryField(entry, '[data-cron-timezone]')?.value || ''; + const defaultTimezone = entryDefaultTimezone(entry); + modalFields.timezoneCustom.checked = savedTimezone.trim() !== ''; + modalFields.timezone.value = savedTimezone || defaultTimezone; + modalFields.timezoneDefault.dataset.timezone = defaultTimezone; + modalFields.timezoneDefault.textContent = `Standard: ${defaultTimezone}`; + modalFields.timezoneWrap.hidden = !modalFields.timezoneCustom.checked; modalFields.intervalHours.value = getEntryField(entry, '[data-cron-builder-interval-hours]')?.value || '6'; modalFields.monthDay.value = getEntryField(entry, '[data-cron-builder-month-day]')?.value || '1'; modalFields.expression.value = getEntryField(entry, '[data-cron-expression]')?.value || ''; @@ -1813,7 +1878,7 @@ $GLOBALS['layout_header_context'] = 'Setup / ' . ($sectionTitles[$currentSection tab: modalState.tab, entryIndex: entry.dataset.entryIndex || null, enabled: modalFields.enabled.value, - timezone: modalFields.timezone.value, + timezone: modalFields.timezoneCustom.checked ? modalFields.timezone.value : '', }); const modeNode = getEntryField(entry, '[data-cron-builder-mode]'); @@ -1833,7 +1898,7 @@ $GLOBALS['layout_header_context'] = 'Setup / ' . ($sectionTitles[$currentSection } enabledNode.checked = modalFields.enabled.value === '1'; - timezoneNode.value = modalFields.timezone.value.trim() || 'UTC'; + timezoneNode.value = modalFields.timezoneCustom.checked ? (modalFields.timezone.value.trim() || entryDefaultTimezone(entry)) : ''; timeNode.value = formatTime(modalState.hour, modalState.minute); weekdayNode.value = modalState.weekday; monthDayNode.value = String(Math.max(1, Math.min(31, Number(modalFields.monthDay.value || 1)))); @@ -1913,7 +1978,7 @@ $GLOBALS['layout_header_context'] = 'Setup / ' . ($sectionTitles[$currentSection getEntryField(entry, '[data-enabled]').checked = Boolean(values.enabled); getEntryField(entry, '[data-cron-expression]').value = values.cron_expression || '0 18 * * *'; - getEntryField(entry, '[data-cron-timezone]').value = values.timezone || 'UTC'; + getEntryField(entry, '[data-cron-timezone]').value = values.timezone || ''; getEntryField(entry, '[data-cron-builder-mode]').value = values.builderMode || 'builder'; getEntryField(entry, '[data-cron-builder-kind]').value = values.builderKind || 'daily'; getEntryField(entry, '[data-cron-builder-time]').value = values.time || '18:00'; @@ -2017,11 +2082,16 @@ $GLOBALS['layout_header_context'] = 'Setup / ' . ($sectionTitles[$currentSection }); }); - [modalFields.enabled, modalFields.timezone, modalFields.intervalHours, modalFields.monthDay, modalFields.expression].forEach((node) => { + [modalFields.enabled, modalFields.timezone, modalFields.intervalHours, modalFields.monthDay, modalFields.expression, modalFields.timezoneCustom].forEach((node) => { node?.addEventListener('input', refreshPreview); node?.addEventListener('change', refreshPreview); }); + modalFields.timezoneCustom?.addEventListener('change', () => { + modalFields.timezoneWrap.hidden = !modalFields.timezoneCustom.checked; + refreshPreview(); + }); + modal.querySelectorAll('[data-scheduler-close]').forEach((button) => { button.addEventListener('click', closeModal); }); @@ -2035,7 +2105,7 @@ $GLOBALS['layout_header_context'] = 'Setup / ' . ($sectionTitles[$currentSection addButton?.addEventListener('click', () => { const container = job.querySelector('[data-scheduler-entries]'); if (!container) return; - const entry = createEntry(job, { timezone: container.querySelector('[data-cron-timezone]')?.value || 'UTC' }); + const entry = createEntry(job, { timezone: '' }); container.appendChild(entry); bindEntry(job, entry); reindexJob(job); diff --git a/partials/landingpages/users/settings.php b/partials/landingpages/users/settings.php index d1d091a..ac708d6 100644 --- a/partials/landingpages/users/settings.php +++ b/partials/landingpages/users/settings.php @@ -8,40 +8,155 @@ $themes = [ require_auth(); $current = user_theme(); +$timezoneOptions = modules()->timezones(); +$nexusSettings = nexus_settings(); +$isAdmin = auth_is_admin(); $notice = null; if ($_SERVER['REQUEST_METHOD'] === 'POST') { - $theme = (string)($_POST['theme'] ?? 'light'); - if (!isset($themes[$theme])) { - $theme = 'light'; + $settingsSection = trim((string) ($_POST['settings_section'] ?? 'theme')); + if ($settingsSection === 'nexus' && $isAdmin) { + $displayTimezoneCustom = isset($_POST['display_timezone_custom']) ? '1' : '0'; + $displayTimezone = trim((string) ($_POST['display_timezone'] ?? '')); + $cronTimezone = trim((string) ($_POST['cron_timezone'] ?? '')); + + foreach (['displayTimezone' => $displayTimezone, 'cronTimezone' => $cronTimezone] as $key => $value) { + if ($value !== '') { + try { + new DateTimeZone($value); + } catch (\Throwable) { + $$key = ''; + } + } + } + + $nexusSettings['display_timezone_custom'] = $displayTimezoneCustom; + $nexusSettings['display_timezone'] = $displayTimezoneCustom === '1' ? $displayTimezone : ''; + $nexusSettings['cron_timezone'] = $cronTimezone; + nexus_save_settings($nexusSettings); + $nexusSettings = nexus_settings(); + $notice = 'Nexus-Einstellungen gespeichert.'; + } else { + $theme = (string)($_POST['theme'] ?? 'light'); + if (!isset($themes[$theme])) { + $theme = 'light'; + } + set_user_theme($theme); + $current = $theme; + $notice = 'Theme gespeichert.'; } - set_user_theme($theme); - $current = $theme; - $notice = 'Theme gespeichert.'; } + +$systemTimezone = nexus_system_timezone_name(); +$effectiveDisplayTimezone = nexus_display_timezone_name(); +$effectiveCronTimezone = nexus_cron_timezone_name(); +$displayTimezoneCustom = !empty($nexusSettings['display_timezone_custom']); +$savedDisplayTimezone = trim((string) ($nexusSettings['display_timezone'] ?? '')); +$savedCronTimezone = trim((string) ($nexusSettings['cron_timezone'] ?? '')); ?> -
-
Einstellungen
-

User-Design

-

Wähle deine persönliche Farbpalette.

+
+
+

Nexus Einstellungen

+

Persönliche Anzeige und systemweite Standardwerte.

- -
- + +
+ +
+ + +
+
+
+
+ Benutzer +

Persönliches Design

+
+
+
+ +
+ +
+ +
+
+ + +
+
+
+ Nexus +

Standard-Zeitzonen

+

Diese Werte werden von Modulen als Default übernommen, sofern dort kein eigener Override gesetzt ist.

+
+
+
+ + + + + + +
+
+ System-Zeitzone +
+ Diese Zeitzone wird genutzt, wenn keine globale Anzeige-Zeitzone gesetzt ist. +
+
+ Anzeige-Zeitzone + +
Aktiv:
+
+
+
> + +
+
+ +
+ +
+
+
- - -
- - -
-
+
+
+ + + diff --git a/src/App/ModuleCronScheduler.php b/src/App/ModuleCronScheduler.php index b319048..b505fa4 100644 --- a/src/App/ModuleCronScheduler.php +++ b/src/App/ModuleCronScheduler.php @@ -306,6 +306,9 @@ final class ModuleCronScheduler $job = $jobExists ? $jobs[$definition['name']] : []; $timezoneSetting = trim((string) ($definition['timezone_setting'] ?? '')); $fallbackTimezone = $timezoneSetting !== '' ? trim((string) ($settings[$timezoneSetting] ?? '')) : ''; + if ($fallbackTimezone === '' && function_exists('nexus_cron_timezone_name')) { + $fallbackTimezone = trim((string) nexus_cron_timezone_name()); + } $defaultEntry = [ 'enabled' => (bool) $definition['default_enabled'], 'cron_expression' => trim((string) ($definition['default_cron'] ?? '0 * * * *')), @@ -336,7 +339,7 @@ final class ModuleCronScheduler $result[] = [ 'enabled' => array_key_exists('enabled', $entry) ? $this->settingBool($entry['enabled'], (bool) $defaultEntry['enabled']) : (bool) $defaultEntry['enabled'], 'cron_expression' => trim((string) ($entry['cron_expression'] ?? $defaultEntry['cron_expression'])), - 'timezone' => trim((string) ($entry['timezone'] ?? $defaultEntry['timezone'])), + 'timezone' => (($entryTimezone = trim((string) ($entry['timezone'] ?? ''))) !== '' ? $entryTimezone : $defaultEntry['timezone']), 'builder' => is_array($entry['builder'] ?? null) ? $entry['builder'] : [], ]; } diff --git a/src/App/functions.php b/src/App/functions.php index 5bf298d..abf4b85 100644 --- a/src/App/functions.php +++ b/src/App/functions.php @@ -336,6 +336,66 @@ function nexus_debug_clear(?string $source = null): void })); } +function nexus_settings(): array +{ + try { + return modules()->settings('_nexus'); + } catch (\Throwable) { + return []; + } +} + +function nexus_save_settings(array $settings): void +{ + modules()->saveSettings('_nexus', $settings); +} + +function nexus_system_timezone_name(): string +{ + $timezone = trim((string) date_default_timezone_get()); + if ($timezone === '') { + return 'UTC'; + } + + try { + new \DateTimeZone($timezone); + return $timezone; + } catch (\Throwable) { + return 'UTC'; + } +} + +function nexus_display_timezone_name(): string +{ + $settings = nexus_settings(); + $useCustom = !empty($settings['display_timezone_custom']); + $timezone = trim((string) ($settings['display_timezone'] ?? '')); + if ($useCustom && $timezone !== '') { + try { + new \DateTimeZone($timezone); + return $timezone; + } catch (\Throwable) { + } + } + + return nexus_system_timezone_name(); +} + +function nexus_cron_timezone_name(): string +{ + $settings = nexus_settings(); + $timezone = trim((string) ($settings['cron_timezone'] ?? '')); + if ($timezone !== '') { + try { + new \DateTimeZone($timezone); + return $timezone; + } catch (\Throwable) { + } + } + + return nexus_display_timezone_name(); +} + function module_debug_enabled(string $module): bool { if (preg_match('/[^a-zA-Z0-9_\-]/', $module)) {