asdas
All checks were successful
Deploy / deploy-staging (push) Successful in 6s
Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-05-11 01:57:23 +02:00
parent ebbdb52f93
commit 0f8f9567fe
6 changed files with 306 additions and 57 deletions

View File

@@ -515,7 +515,7 @@ $mm->registerFunction($moduleName, 'alpha_vantage_request', static function (
}); });
$mm->registerFunction($moduleName, 'display_timezone', static function (): \DateTimeZone { $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 { $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 ''; return '';
} }
$displayTimezone = new \DateTimeZone('Europe/Berlin'); $displayTimezone = new \DateTimeZone(nexus_display_timezone_name());
$source = trim((string) $source); $source = trim((string) $source);
if (str_starts_with($source, 'bavest:') || str_starts_with($source, 'alphavantage:')) { if (str_starts_with($source, 'bavest:') || str_starts_with($source, 'alphavantage:')) {

View File

@@ -14,6 +14,7 @@ $modules = array_values(array_filter(
<div style="display:flex; gap:10px; flex-wrap:wrap;"> <div style="display:flex; gap:10px; flex-wrap:wrap;">
<a class="nav-link" href="/modules">Module verwalten</a> <a class="nav-link" href="/modules">Module verwalten</a>
<?php if (auth_is_admin()): ?> <?php if (auth_is_admin()): ?>
<a class="nav-link" href="/settings">Nexus Einstellungen</a>
<a class="nav-link" href="/exports/database.sql">SQL-Export</a> <a class="nav-link" href="/exports/database.sql">SQL-Export</a>
<?php endif; ?> <?php endif; ?>
</div> </div>

View File

@@ -165,6 +165,7 @@ foreach ($fields as $field) {
$generalSetupFields = []; $generalSetupFields = [];
$databaseSetupFields = []; $databaseSetupFields = [];
$cronSetupFields = []; $cronSetupFields = [];
$cronTimezoneField = null;
$customSetupFields = []; $customSetupFields = [];
foreach ($generalFields as $field) { foreach ($generalFields as $field) {
$fieldName = (string)($field['name'] ?? ''); $fieldName = (string)($field['name'] ?? '');
@@ -177,7 +178,7 @@ foreach ($generalFields as $field) {
continue; continue;
} }
if ($fieldName === 'schedule_timezone') { if ($fieldName === 'schedule_timezone') {
$cronSetupFields[] = $field; $cronTimezoneField = $field;
continue; continue;
} }
$customSetupFields[] = $field; $customSetupFields[] = $field;
@@ -190,6 +191,10 @@ $driverOptions = [
]; ];
$timezoneOptions = modules()->timezones(); $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 { $describeDbConfig = static function (array $dbConfig): string {
$driver = (string)($dbConfig['driver'] ?? ''); $driver = (string)($dbConfig['driver'] ?? '');
@@ -346,7 +351,7 @@ $formatRunTimestamp = static function (?string $value, ?string $timezone = null)
try { try {
$dt = new DateTimeImmutable($value, new DateTimeZone('UTC')); $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'); return $dt->setTimezone($targetTz)->format('Y-m-d H:i:s');
} catch (\Throwable) { } catch (\Throwable) {
$ts = strtotime($value); $ts = strtotime($value);
@@ -376,7 +381,7 @@ $extractSchedulerJobs = static function (array $postedSchedulerJobs, array $cron
continue; continue;
} }
$cronExpression = trim((string) ($entryPayload['cron_expression'] ?? '')); $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 === '') { if ($cronExpression === '' && $timezone === '') {
continue; continue;
} }
@@ -523,7 +528,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$payload = []; $payload = [];
$sectionFieldNames = match ($submittedSetupSection) { $sectionFieldNames = match ($submittedSetupSection) {
'general' => array_map(static fn (array $field): string => (string) ($field['name'] ?? ''), $generalSetupFields), '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), 'custom' => array_map(static fn (array $field): string => (string) ($field['name'] ?? ''), $customSetupFields),
'database' => array_values(array_filter(array_merge( 'database' => array_values(array_filter(array_merge(
array_map(static fn (array $field): string => (string) ($field['name'] ?? ''), $databaseSetupFields), array_map(static fn (array $field): string => (string) ($field['name'] ?? ''), $databaseSetupFields),
@@ -583,6 +591,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$postKey = str_replace('.', '_', $name); $postKey = str_replace('.', '_', $name);
$value = $_POST[$postKey] ?? null; $value = $_POST[$postKey] ?? null;
if ($submittedSetupSection === 'cron' && $name === 'schedule_timezone') {
if (!isset($_POST['schedule_timezone_custom'])) {
$payload[$name] = '';
continue;
}
}
if ($type === 'checkbox') { if ($type === 'checkbox') {
$value = isset($_POST[$postKey]) ? '1' : '0'; $value = isset($_POST[$postKey]) ? '1' : '0';
} }
@@ -930,12 +945,31 @@ $GLOBALS['layout_header_context'] = 'Setup / ' . ($sectionTitles[$currentSection
<p class="muted">Hier liegen die zeitbezogenen Modul-Einstellungen, Intervall-Tasks und Cron-Jobs.</p> <p class="muted">Hier liegen die zeitbezogenen Modul-Einstellungen, Intervall-Tasks und Cron-Jobs.</p>
</div> </div>
</div> </div>
<?php if ($cronSetupFields !== []): ?> <?php if ($cronTimezoneField !== null || $cronSetupFields !== []): ?>
<div class="setup-grid"> <div class="setup-grid">
<?php if ($cronTimezoneField !== null): ?>
<div class="setup-field muted">
<span>Standard-Zeitzone für dieses Modul</span>
<label style="display:flex; align-items:center; gap:10px;">
<input type="checkbox" name="schedule_timezone_custom" value="1" <?= $moduleCronTimezoneOverride !== '' ? 'checked' : '' ?> data-module-cron-tz-toggle>
<span>Custom-Zeitzone verwenden</span>
</label>
<div class="setup-db-message setup-db-message--hint">Aktiv: <?= e($effectiveModuleCronTimezone) ?></div>
<small class="muted">Wenn deaktiviert, wird die globale Nexus-Cron-Zeitzone verwendet: <?= e($globalCronTimezone) ?>.</small>
</div>
<?php endif; ?>
<?php foreach ($cronSetupFields as $field): ?> <?php foreach ($cronSetupFields as $field): ?>
<?php $renderField($field); ?> <?php $renderField($field); ?>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
<?php if ($cronTimezoneField !== null): ?>
<div class="setup-grid" data-module-cron-tz-panel <?= $moduleCronTimezoneOverride !== '' ? '' : 'hidden' ?>>
<label class="setup-field muted">
<span>Custom-Zeitzone für dieses Modul</span>
<input type="text" name="schedule_timezone" value="<?= e($moduleCronTimezoneOverride !== '' ? $moduleCronTimezoneOverride : $globalCronTimezone) ?>" list="timezone-options" autocomplete="off">
</label>
</div>
<?php endif; ?>
<?php else: ?> <?php else: ?>
<p class="muted">Dieses Modul hat keine eigenen Zeitzonenfelder. Intervall-Tasks und Cron-Jobs koennen trotzdem weiter unten verwaltet werden.</p> <p class="muted">Dieses Modul hat keine eigenen Zeitzonenfelder. Intervall-Tasks und Cron-Jobs koennen trotzdem weiter unten verwaltet werden.</p>
<?php endif; ?> <?php endif; ?>
@@ -987,7 +1021,7 @@ $GLOBALS['layout_header_context'] = 'Setup / ' . ($sectionTitles[$currentSection
$cronMode = (string) ($cronDefinition['mode'] ?? 'single'); $cronMode = (string) ($cronDefinition['mode'] ?? 'single');
$cronEntries = $cronTaskStatusGroups[$cronName] ?? []; $cronEntries = $cronTaskStatusGroups[$cronName] ?? [];
?> ?>
<div class="setup-field muted" data-scheduler-job data-job-name="<?= e($cronName) ?>" data-job-mode="<?= e($cronMode) ?>" data-job-label="<?= e((string) ($cronDefinition['label'] ?? $cronName)) ?>" data-job-callback="<?= e((string) ($cronDefinition['callback'] ?? '')) ?>"> <div class="setup-field muted" data-scheduler-job data-job-name="<?= e($cronName) ?>" data-job-mode="<?= e($cronMode) ?>" data-job-label="<?= e((string) ($cronDefinition['label'] ?? $cronName)) ?>" data-job-callback="<?= e((string) ($cronDefinition['callback'] ?? '')) ?>" data-default-timezone="<?= e($effectiveModuleCronTimezone) ?>">
<span><?= e((string) ($cronDefinition['label'] ?? $cronName)) ?></span> <span><?= e((string) ($cronDefinition['label'] ?? $cronName)) ?></span>
<?php if (trim((string) ($cronDefinition['help'] ?? '')) !== ''): ?> <?php if (trim((string) ($cronDefinition['help'] ?? '')) !== ''): ?>
<small class="muted"><?= e((string) $cronDefinition['help']) ?></small> <small class="muted"><?= e((string) $cronDefinition['help']) ?></small>
@@ -1002,7 +1036,7 @@ $GLOBALS['layout_header_context'] = 'Setup / ' . ($sectionTitles[$currentSection
<label><input type="checkbox" name="scheduler_jobs[<?= e($cronName) ?>][entries][<?= e((string) $entryIndex) ?>][enabled]" value="1" data-enabled <?= !empty($cronConfig['enabled']) ? 'checked' : '' ?>> Aktiv</label> <label><input type="checkbox" name="scheduler_jobs[<?= e($cronName) ?>][entries][<?= e((string) $entryIndex) ?>][enabled]" value="1" data-enabled <?= !empty($cronConfig['enabled']) ? 'checked' : '' ?>> Aktiv</label>
<input type="text" name="scheduler_jobs[<?= e($cronName) ?>][entries][<?= e((string) $entryIndex) ?>][cron_expression]" value="<?= e((string) ($cronConfig['cron_expression'] ?? '')) ?>" data-cron-expression> <input type="text" name="scheduler_jobs[<?= e($cronName) ?>][entries][<?= e((string) $entryIndex) ?>][cron_expression]" value="<?= e((string) ($cronConfig['cron_expression'] ?? '')) ?>" data-cron-expression>
<small class="muted">Cron-Syntax: Minute Stunde Tag Monat Wochentag</small> <small class="muted">Cron-Syntax: Minute Stunde Tag Monat Wochentag</small>
<input type="text" name="scheduler_jobs[<?= e($cronName) ?>][entries][<?= e((string) $entryIndex) ?>][timezone]" value="<?= e((string) ($cronConfig['timezone'] ?? 'UTC')) ?>" data-cron-timezone> <input type="text" name="scheduler_jobs[<?= e($cronName) ?>][entries][<?= e((string) $entryIndex) ?>][timezone]" value="<?= e((string) (($cronConfig['timezone'] ?? '') !== '' ? $cronConfig['timezone'] : '')) ?>" data-cron-timezone>
<select name="scheduler_jobs[<?= e($cronName) ?>][entries][<?= e((string) $entryIndex) ?>][builder][mode]" data-cron-builder-mode> <select name="scheduler_jobs[<?= e($cronName) ?>][entries][<?= e((string) $entryIndex) ?>][builder][mode]" data-cron-builder-mode>
<option value="builder" <?= (string) ($builder['mode'] ?? 'builder') === 'builder' ? 'selected' : '' ?>>Builder</option> <option value="builder" <?= (string) ($builder['mode'] ?? 'builder') === 'builder' ? 'selected' : '' ?>>Builder</option>
<option value="manual" <?= (string) ($builder['mode'] ?? 'builder') === 'manual' ? 'selected' : '' ?>>Cron-Syntax</option> <option value="manual" <?= (string) ($builder['mode'] ?? 'builder') === 'manual' ? 'selected' : '' ?>>Cron-Syntax</option>
@@ -1267,6 +1301,14 @@ $GLOBALS['layout_header_context'] = 'Setup / ' . ($sectionTitles[$currentSection
</label> </label>
<label class="setup-field muted"> <label class="setup-field muted">
<span>Zeitzone</span> <span>Zeitzone</span>
<label style="display:flex; align-items:center; gap:10px;">
<input type="checkbox" value="1" data-modal-timezone-custom>
<span>Custom-Zeitzone verwenden</span>
</label>
<div class="setup-db-message setup-db-message--hint" data-modal-timezone-default>Standard: UTC</div>
</label>
<label class="setup-field muted" data-modal-timezone-wrap hidden>
<span>Custom-Zeitzone</span>
<input type="text" value="UTC" data-modal-timezone list="timezone-options" autocomplete="off"> <input type="text" value="UTC" data-modal-timezone list="timezone-options" autocomplete="off">
</label> </label>
@@ -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 jobs = document.querySelectorAll('[data-scheduler-job]');
const modal = document.querySelector('[data-scheduler-modal]'); const modal = document.querySelector('[data-scheduler-modal]');
if (!jobs.length || !modal) return; if (!jobs.length || !modal) return;
@@ -1424,6 +1476,9 @@ $GLOBALS['layout_header_context'] = 'Setup / ' . ($sectionTitles[$currentSection
const modalFields = { const modalFields = {
enabled: modal.querySelector('[data-modal-enabled]'), 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]'), timezone: modal.querySelector('[data-modal-timezone]'),
intervalHours: modal.querySelector('[data-modal-interval-hours]'), intervalHours: modal.querySelector('[data-modal-interval-hours]'),
monthDay: modal.querySelector('[data-modal-month-day]'), monthDay: modal.querySelector('[data-modal-month-day]'),
@@ -1497,20 +1552,22 @@ $GLOBALS['layout_header_context'] = 'Setup / ' . ($sectionTitles[$currentSection
const buildSummary = (tab) => { const buildSummary = (tab) => {
const enabled = modalFields.enabled.value === '1' ? 'Aktiv' : 'Inaktiv'; 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); const time = formatTime(modalState.hour, modalState.minute);
switch (tab) { switch (tab) {
case 'hourly': 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': case 'weekly':
return `${enabled}, woechentlich ${weekdayMap[modalState.weekday || '1']} um ${time}, ${timezone}`; return `${enabled}, woechentlich ${weekdayMap[modalState.weekday || '1']} um ${time}, ${timezoneLabel}`;
case 'monthly': 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': case 'custom':
return `${enabled}, benutzerdefinierte Cron-Syntax, ${timezone}`; return `${enabled}, benutzerdefinierte Cron-Syntax, ${timezoneLabel}`;
case 'daily': case 'daily':
default: 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 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 findStatusNode = (entry, label) => Array.from(entry.querySelectorAll('small')).find((node) => node.textContent.includes(label));
const setStatusText = (entry, label, value) => { const setStatusText = (entry, label, value) => {
const node = findStatusNode(entry, label); const node = findStatusNode(entry, label);
@@ -1696,7 +1754,8 @@ $GLOBALS['layout_header_context'] = 'Setup / ' . ($sectionTitles[$currentSection
const updateEntrySummary = (entry) => { const updateEntrySummary = (entry) => {
const enabled = getEntryField(entry, '[data-enabled]')?.checked ?? false; const enabled = getEntryField(entry, '[data-enabled]')?.checked ?? false;
const expression = getEntryField(entry, '[data-cron-expression]')?.value || ''; 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 builderMode = getEntryField(entry, '[data-cron-builder-mode]')?.value || 'builder';
const builderKind = getEntryField(entry, '[data-cron-builder-kind]')?.value || 'daily'; const builderKind = getEntryField(entry, '[data-cron-builder-kind]')?.value || 'daily';
const time = getEntryField(entry, '[data-cron-builder-time]')?.value || '18:00'; 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'; let summary = enabled ? 'Aktiv' : 'Inaktiv';
if (builderMode === 'manual') { if (builderMode === 'manual') {
summary += `, Custom, ${timezone}`; summary += `, Custom, ${timezoneLabel}`;
} else if (builderKind === 'every_x_hours') { } else if (builderKind === 'every_x_hours') {
summary += `, alle ${intervalHours} Stunden, ${timezone}`; summary += `, alle ${intervalHours} Stunden, ${timezoneLabel}`;
} else if (builderKind === 'weekly') { } else if (builderKind === 'weekly') {
summary += `, ${weekdayMap[weekday] || weekday} ${time}, ${timezone}`; summary += `, ${weekdayMap[weekday] || weekday} ${time}, ${timezoneLabel}`;
} else if (builderKind === 'monthly_day') { } else if (builderKind === 'monthly_day') {
summary += `, monatlich am ${monthDay}. ${time}, ${timezone}`; summary += `, monatlich am ${monthDay}. ${time}, ${timezoneLabel}`;
} else { } else {
summary += `, taeglich ${time}, ${timezone}`; summary += `, taeglich ${time}, ${timezoneLabel}`;
} }
let summaryNode = entry.querySelector('[data-entry-summary]'); 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(':'); const [hour = '18', minute = '00'] = time.split(':');
modalFields.enabled.value = getEntryField(entry, '[data-enabled]')?.checked ? '1' : '0'; 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.intervalHours.value = getEntryField(entry, '[data-cron-builder-interval-hours]')?.value || '6';
modalFields.monthDay.value = getEntryField(entry, '[data-cron-builder-month-day]')?.value || '1'; modalFields.monthDay.value = getEntryField(entry, '[data-cron-builder-month-day]')?.value || '1';
modalFields.expression.value = getEntryField(entry, '[data-cron-expression]')?.value || ''; modalFields.expression.value = getEntryField(entry, '[data-cron-expression]')?.value || '';
@@ -1813,7 +1878,7 @@ $GLOBALS['layout_header_context'] = 'Setup / ' . ($sectionTitles[$currentSection
tab: modalState.tab, tab: modalState.tab,
entryIndex: entry.dataset.entryIndex || null, entryIndex: entry.dataset.entryIndex || null,
enabled: modalFields.enabled.value, enabled: modalFields.enabled.value,
timezone: modalFields.timezone.value, timezone: modalFields.timezoneCustom.checked ? modalFields.timezone.value : '',
}); });
const modeNode = getEntryField(entry, '[data-cron-builder-mode]'); 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'; 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); timeNode.value = formatTime(modalState.hour, modalState.minute);
weekdayNode.value = modalState.weekday; weekdayNode.value = modalState.weekday;
monthDayNode.value = String(Math.max(1, Math.min(31, Number(modalFields.monthDay.value || 1)))); 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-enabled]').checked = Boolean(values.enabled);
getEntryField(entry, '[data-cron-expression]').value = values.cron_expression || '0 18 * * *'; 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-mode]').value = values.builderMode || 'builder';
getEntryField(entry, '[data-cron-builder-kind]').value = values.builderKind || 'daily'; getEntryField(entry, '[data-cron-builder-kind]').value = values.builderKind || 'daily';
getEntryField(entry, '[data-cron-builder-time]').value = values.time || '18:00'; 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('input', refreshPreview);
node?.addEventListener('change', refreshPreview); node?.addEventListener('change', refreshPreview);
}); });
modalFields.timezoneCustom?.addEventListener('change', () => {
modalFields.timezoneWrap.hidden = !modalFields.timezoneCustom.checked;
refreshPreview();
});
modal.querySelectorAll('[data-scheduler-close]').forEach((button) => { modal.querySelectorAll('[data-scheduler-close]').forEach((button) => {
button.addEventListener('click', closeModal); button.addEventListener('click', closeModal);
}); });
@@ -2035,7 +2105,7 @@ $GLOBALS['layout_header_context'] = 'Setup / ' . ($sectionTitles[$currentSection
addButton?.addEventListener('click', () => { addButton?.addEventListener('click', () => {
const container = job.querySelector('[data-scheduler-entries]'); const container = job.querySelector('[data-scheduler-entries]');
if (!container) return; if (!container) return;
const entry = createEntry(job, { timezone: container.querySelector('[data-cron-timezone]')?.value || 'UTC' }); const entry = createEntry(job, { timezone: '' });
container.appendChild(entry); container.appendChild(entry);
bindEntry(job, entry); bindEntry(job, entry);
reindexJob(job); reindexJob(job);

View File

@@ -8,9 +8,35 @@ $themes = [
require_auth(); require_auth();
$current = user_theme(); $current = user_theme();
$timezoneOptions = modules()->timezones();
$nexusSettings = nexus_settings();
$isAdmin = auth_is_admin();
$notice = null; $notice = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$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'); $theme = (string)($_POST['theme'] ?? 'light');
if (!isset($themes[$theme])) { if (!isset($themes[$theme])) {
$theme = 'light'; $theme = 'light';
@@ -19,20 +45,38 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$current = $theme; $current = $theme;
$notice = 'Theme gespeichert.'; $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'] ?? ''));
?> ?>
<div class="card"> <div class="module-shell"><div class="module-page-bg"><div class="module-page-stack">
<div class="pill">Einstellungen</div> <section class="section-box">
<h1 style="margin-top:.75rem;">User-Design</h1> <h1>Nexus Einstellungen</h1>
<p class="muted">Wähle deine persönliche Farbpalette.</p> <p class="muted">Persönliche Anzeige und systemweite Standardwerte.</p>
<?php if ($notice): ?> <?php if ($notice): ?>
<div class="card" style="margin-top:1rem; border-color:var(--accent-2);"> <div class="section-box" style="margin-top:1rem; border-color:var(--accent-2);">
<?= e($notice) ?> <?= e($notice) ?>
</div> </div>
<?php endif; ?> <?php endif; ?>
<form method="post" style="margin-top:1rem; display:grid; gap:12px; max-width:360px;"> <div class="setup-form" style="margin-top:1rem;">
<label class="muted" style="display:grid; gap:6px;"> <section class="section-box setup-panel">
<div class="setup-panel__head">
<div>
<span class="pill">Benutzer</span>
<h2>Persönliches Design</h2>
</div>
</div>
<form method="post" class="setup-form">
<input type="hidden" name="settings_section" value="theme">
<div class="setup-grid">
<label class="setup-field muted">
<span>Farbpalette</span> <span>Farbpalette</span>
<select name="theme"> <select name="theme">
<?php foreach ($themes as $key => $label): ?> <?php foreach ($themes as $key => $label): ?>
@@ -42,6 +86,77 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
</label> </label>
<button class="cta-button" type="submit">Speichern</button>
</form>
</div> </div>
<div class="setup-actions setup-actions--footer">
<button class="cta-button" type="submit">Design speichern</button>
</div>
</form>
</section>
<?php if ($isAdmin): ?>
<section class="section-box setup-panel">
<div class="setup-panel__head">
<div>
<span class="pill">Nexus</span>
<h2>Standard-Zeitzonen</h2>
<p class="muted">Diese Werte werden von Modulen als Default übernommen, sofern dort kein eigener Override gesetzt ist.</p>
</div>
</div>
<form method="post" class="setup-form">
<input type="hidden" name="settings_section" value="nexus">
<datalist id="nexus-timezone-options">
<?php foreach ($timezoneOptions as $timezoneOption): ?>
<option value="<?= e((string) $timezoneOption['value']) ?>"><?= e((string) $timezoneOption['label']) ?></option>
<?php endforeach; ?>
</datalist>
<div class="setup-grid">
<div class="setup-field muted">
<span>System-Zeitzone</span>
<div><?= e($systemTimezone) ?></div>
<small class="muted">Diese Zeitzone wird genutzt, wenn keine globale Anzeige-Zeitzone gesetzt ist.</small>
</div>
<div class="setup-field muted">
<span>Anzeige-Zeitzone</span>
<label style="display:flex; align-items:center; gap:10px;">
<input type="checkbox" name="display_timezone_custom" value="1" <?= $displayTimezoneCustom ? 'checked' : '' ?> data-global-display-tz-toggle>
<span>Custom-Zeitzone verwenden</span>
</label>
<div class="setup-db-message setup-db-message--hint">Aktiv: <?= e($effectiveDisplayTimezone) ?></div>
</div>
</div>
<div class="setup-grid" data-global-display-tz-panel <?= $displayTimezoneCustom ? '' : 'hidden' ?>>
<label class="setup-field muted">
<span>Custom Anzeige-Zeitzone</span>
<input type="text" name="display_timezone" value="<?= e($savedDisplayTimezone !== '' ? $savedDisplayTimezone : $effectiveDisplayTimezone) ?>" list="nexus-timezone-options" autocomplete="off">
</label>
</div>
<div class="setup-grid">
<label class="setup-field muted">
<span>Standard-Zeitzone für Crons</span>
<input type="text" name="cron_timezone" value="<?= e($savedCronTimezone !== '' ? $savedCronTimezone : $effectiveCronTimezone) ?>" list="nexus-timezone-options" autocomplete="off">
<small class="muted">Wird in Modul-Crons als Standard verwendet. Einzelne Module oder Einträge können das übersteuern.</small>
</label>
</div>
<div class="setup-actions setup-actions--footer">
<button class="cta-button" type="submit">Nexus-Einstellungen speichern</button>
</div>
</form>
</section>
<?php endif; ?>
</div>
</section>
</div></div></div>
<?php if ($isAdmin): ?>
<script>
(() => {
const toggle = document.querySelector('[data-global-display-tz-toggle]');
const panel = document.querySelector('[data-global-display-tz-panel]');
if (!toggle || !panel) return;
const sync = () => {
panel.hidden = !toggle.checked;
};
toggle.addEventListener('change', sync);
sync();
})();
</script>
<?php endif; ?>

View File

@@ -306,6 +306,9 @@ final class ModuleCronScheduler
$job = $jobExists ? $jobs[$definition['name']] : []; $job = $jobExists ? $jobs[$definition['name']] : [];
$timezoneSetting = trim((string) ($definition['timezone_setting'] ?? '')); $timezoneSetting = trim((string) ($definition['timezone_setting'] ?? ''));
$fallbackTimezone = $timezoneSetting !== '' ? trim((string) ($settings[$timezoneSetting] ?? '')) : ''; $fallbackTimezone = $timezoneSetting !== '' ? trim((string) ($settings[$timezoneSetting] ?? '')) : '';
if ($fallbackTimezone === '' && function_exists('nexus_cron_timezone_name')) {
$fallbackTimezone = trim((string) nexus_cron_timezone_name());
}
$defaultEntry = [ $defaultEntry = [
'enabled' => (bool) $definition['default_enabled'], 'enabled' => (bool) $definition['default_enabled'],
'cron_expression' => trim((string) ($definition['default_cron'] ?? '0 * * * *')), 'cron_expression' => trim((string) ($definition['default_cron'] ?? '0 * * * *')),
@@ -336,7 +339,7 @@ final class ModuleCronScheduler
$result[] = [ $result[] = [
'enabled' => array_key_exists('enabled', $entry) ? $this->settingBool($entry['enabled'], (bool) $defaultEntry['enabled']) : (bool) $defaultEntry['enabled'], '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'])), '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'] : [], 'builder' => is_array($entry['builder'] ?? null) ? $entry['builder'] : [],
]; ];
} }

View File

@@ -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 function module_debug_enabled(string $module): bool
{ {
if (preg_match('/[^a-zA-Z0-9_\-]/', $module)) { if (preg_match('/[^a-zA-Z0-9_\-]/', $module)) {