asdsad
This commit is contained in:
@@ -42,17 +42,23 @@ foreach ($fields as $field) {
|
||||
}
|
||||
$isFxRatesSetup = $moduleName === 'fx-rates';
|
||||
$current = modules()->settings($moduleName);
|
||||
$intervalTaskStatuses = modules()->intervalTaskStatuses($moduleName);
|
||||
$intervalTaskStatuses = [];
|
||||
$cronTaskDefinitions = modules()->cronTasks($moduleName);
|
||||
$cronTaskStatuses = modules()->cronTaskStatuses($moduleName);
|
||||
$cronTaskStatuses = [];
|
||||
$cronTaskStatusGroups = [];
|
||||
foreach ($cronTaskStatuses as $cronTaskStatus) {
|
||||
$cronGroupName = trim((string) ($cronTaskStatus['job_name'] ?? $cronTaskStatus['name'] ?? ''));
|
||||
if ($cronGroupName === '') {
|
||||
continue;
|
||||
$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;
|
||||
}
|
||||
$cronTaskStatusGroups[$cronGroupName][] = $cronTaskStatus;
|
||||
}
|
||||
};
|
||||
$refreshSchedulerState();
|
||||
$setupActions = modules()->hasFunction($moduleName, 'setup_actions')
|
||||
? (array) module_fn($moduleName, 'setup_actions')
|
||||
: [];
|
||||
@@ -134,6 +140,8 @@ $driverOptions = [
|
||||
'sqlite' => 'SQLite',
|
||||
];
|
||||
|
||||
$timezoneOptions = modules()->timezones();
|
||||
|
||||
$describeDbConfig = static function (array $dbConfig): string {
|
||||
$driver = (string)($dbConfig['driver'] ?? '');
|
||||
$host = (string)($dbConfig['host'] ?? '');
|
||||
@@ -281,18 +289,72 @@ $normalizeDriver = static function (mixed $value): mixed {
|
||||
};
|
||||
};
|
||||
|
||||
$formatRunTimestamp = static function (?string $value): string {
|
||||
$formatRunTimestamp = static function (?string $value, ?string $timezone = null): string {
|
||||
$value = trim((string) $value);
|
||||
if ($value === '') {
|
||||
return '-';
|
||||
}
|
||||
|
||||
$ts = strtotime($value);
|
||||
if ($ts === false) {
|
||||
return $value;
|
||||
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 date('Y-m-d H:i:s', $ts);
|
||||
return $schedulerJobs;
|
||||
};
|
||||
|
||||
$cronWeekdays = [
|
||||
@@ -338,6 +400,8 @@ $renderField = function (array $field) use (&$current, $getNested, $driverOption
|
||||
<textarea name="<?= e($postKey) ?>" rows="3" <?= $required ? 'required' : '' ?>><?= e($value) ?></textarea>
|
||||
<?php elseif ($type === 'checkbox'): ?>
|
||||
<input type="checkbox" name="<?= e($postKey) ?>" value="1" <?= $value === '1' ? 'checked' : '' ?>>
|
||||
<?php elseif ($name === 'schedule_timezone' || str_ends_with($name, '_timezone') || str_ends_with($name, '.timezone')): ?>
|
||||
<input type="text" name="<?= e($postKey) ?>" value="<?= e($value) ?>" list="timezone-options" autocomplete="off" <?= $required ? 'required' : '' ?>>
|
||||
<?php else: ?>
|
||||
<input type="<?= e($type) ?>" name="<?= e($postKey) ?>" value="<?= e($value) ?>" <?= $required ? 'required' : '' ?>>
|
||||
<?php endif; ?>
|
||||
@@ -349,6 +413,7 @@ $renderField = function (array $field) use (&$current, $getNested, $driverOption
|
||||
};
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$isSchedulerAutosave = isset($_POST['scheduler_autosave']) && (string) $_POST['scheduler_autosave'] === '1';
|
||||
$payload = [];
|
||||
|
||||
foreach ($fields as $field) {
|
||||
@@ -393,53 +458,25 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
|
||||
if ($cronTaskDefinitions !== []) {
|
||||
$postedSchedulerJobs = is_array($_POST['scheduler_jobs'] ?? null) ? $_POST['scheduler_jobs'] : [];
|
||||
$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];
|
||||
}
|
||||
$schedulerJobs = $extractSchedulerJobs($postedSchedulerJobs, $cronTaskDefinitions, $current);
|
||||
$current['scheduler_jobs'] = $schedulerJobs;
|
||||
}
|
||||
|
||||
if ($isSchedulerAutosave) {
|
||||
modules()->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);
|
||||
return;
|
||||
}
|
||||
|
||||
$postedTestGroup = (string)($_POST['test_db'] ?? '');
|
||||
$postedResetGroup = (string)($_POST['reset_db'] ?? '');
|
||||
$postedSetupAction = trim((string)($_POST['module_setup_action'] ?? ''));
|
||||
@@ -524,6 +561,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
module_fn($moduleName, 'save_runtime_settings', $payload);
|
||||
$current = modules()->settings($moduleName);
|
||||
}
|
||||
$refreshSchedulerState();
|
||||
if (empty($payload['debug_enabled'])) {
|
||||
module_debug_clear($moduleName);
|
||||
}
|
||||
@@ -554,6 +592,11 @@ $activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups)
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" class="setup-form">
|
||||
<datalist id="timezone-options">
|
||||
<?php foreach ($timezoneOptions as $timezoneOption): ?>
|
||||
<option value="<?= e((string) $timezoneOption['value']) ?>"><?= e((string) $timezoneOption['label']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</datalist>
|
||||
<?php if ($isFxRatesSetup): ?>
|
||||
<?php
|
||||
$fxCatalog = is_array($current['currency_catalog'] ?? null) ? $current['currency_catalog'] : [];
|
||||
@@ -686,7 +729,7 @@ $activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups)
|
||||
<div class="setup-grid">
|
||||
<label class="setup-field muted">
|
||||
<span>Scheduler-Zeitzone</span>
|
||||
<input type="text" name="schedule_timezone" value="<?= e((string) ($current['schedule_timezone'] ?? 'Europe/Berlin')) ?>">
|
||||
<input type="text" name="schedule_timezone" value="<?= e((string) ($current['schedule_timezone'] ?? 'Europe/Berlin')) ?>" list="timezone-options" autocomplete="off">
|
||||
<small class="muted">Diese Zeitzone wird fuer Cron-Jobs und die lokale Zeitberechnung verwendet.</small>
|
||||
</label>
|
||||
</div>
|
||||
@@ -852,10 +895,9 @@ $activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups)
|
||||
<input type="number" min="1" max="31" name="scheduler_jobs[<?= e($cronName) ?>][entries][<?= e((string) $entryIndex) ?>][builder][month_day]" value="<?= e((string) ($builder['month_day'] ?? 1)) ?>" data-cron-builder-month-day>
|
||||
<input type="number" min="1" max="23" name="scheduler_jobs[<?= e($cronName) ?>][entries][<?= e((string) $entryIndex) ?>][builder][interval_hours]" value="<?= e((string) ($builder['interval_hours'] ?? 6)) ?>" data-cron-builder-interval-hours>
|
||||
</div>
|
||||
<small class="muted">Letzter Start: <?= e($formatRunTimestamp((string) ($task['state']['last_started_at'] ?? ''))) ?></small>
|
||||
<small class="muted">Letzter Erfolg: <?= e($formatRunTimestamp((string) ($task['state']['last_success_at'] ?? ''))) ?></small>
|
||||
<small class="muted">Naechster Lauf UTC: <?= e($formatRunTimestamp((string) ($task['next_due_at'] ?? ''))) ?></small>
|
||||
<small class="muted">Naechster Lauf lokal: <?= e((string) (($task['next_due_at_local'] ?? '') !== '' ? $task['next_due_at_local'] : '-')) ?></small>
|
||||
<small class="muted">Letzter Start: <?= e($formatRunTimestamp((string) ($task['state']['last_started_at'] ?? ''), (string) ($task['timezone'] ?? 'UTC'))) ?></small>
|
||||
<small class="muted">Letzter Erfolg: <?= e($formatRunTimestamp((string) ($task['state']['last_success_at'] ?? ''), (string) ($task['timezone'] ?? 'UTC'))) ?></small>
|
||||
<small class="muted">Naechster Lauf lokal: <?= e(!empty($task['enabled']) ? (string) (($task['next_due_at_local'] ?? '') !== '' ? $task['next_due_at_local'] : '-') : '-') ?></small>
|
||||
<small class="muted">Status: <?= e((string) (($task['state']['last_status'] ?? '') !== '' ? $task['state']['last_status'] : '-')) ?></small>
|
||||
<?php if (trim((string) ($task['parse_error'] ?? '')) !== ''): ?>
|
||||
<small class="muted" style="color:#b42318;">Cron-Fehler: <?= e((string) $task['parse_error']) ?></small>
|
||||
@@ -1228,10 +1270,9 @@ $activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups)
|
||||
<input type="number" min="1" max="31" name="scheduler_jobs[<?= e($cronName) ?>][entries][<?= e((string) $entryIndex) ?>][builder][month_day]" value="<?= e((string) ($builder['month_day'] ?? 1)) ?>" data-cron-builder-month-day>
|
||||
<input type="number" min="1" max="23" name="scheduler_jobs[<?= e($cronName) ?>][entries][<?= e((string) $entryIndex) ?>][builder][interval_hours]" value="<?= e((string) ($builder['interval_hours'] ?? 6)) ?>" data-cron-builder-interval-hours>
|
||||
</div>
|
||||
<small class="muted">Letzter Start: <?= e($formatRunTimestamp((string) ($task['state']['last_started_at'] ?? ''))) ?></small>
|
||||
<small class="muted">Letzter Erfolg: <?= e($formatRunTimestamp((string) ($task['state']['last_success_at'] ?? ''))) ?></small>
|
||||
<small class="muted">Naechster Lauf UTC: <?= e($formatRunTimestamp((string) ($task['next_due_at'] ?? ''))) ?></small>
|
||||
<small class="muted">Naechster Lauf lokal: <?= e((string) (($task['next_due_at_local'] ?? '') !== '' ? $task['next_due_at_local'] : '-')) ?></small>
|
||||
<small class="muted">Letzter Start: <?= e($formatRunTimestamp((string) ($task['state']['last_started_at'] ?? ''), (string) ($task['timezone'] ?? 'UTC'))) ?></small>
|
||||
<small class="muted">Letzter Erfolg: <?= e($formatRunTimestamp((string) ($task['state']['last_success_at'] ?? ''), (string) ($task['timezone'] ?? 'UTC'))) ?></small>
|
||||
<small class="muted">Naechster Lauf lokal: <?= e(!empty($task['enabled']) ? (string) (($task['next_due_at_local'] ?? '') !== '' ? $task['next_due_at_local'] : '-') : '-') ?></small>
|
||||
<small class="muted">Status: <?= e((string) (($task['state']['last_status'] ?? '') !== '' ? $task['state']['last_status'] : '-')) ?></small>
|
||||
<?php if (trim((string) ($task['parse_error'] ?? '')) !== ''): ?>
|
||||
<small class="muted" style="color:#b42318;">Cron-Fehler: <?= e((string) $task['parse_error']) ?></small>
|
||||
@@ -1410,7 +1451,7 @@ $activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups)
|
||||
</label>
|
||||
<label class="setup-field muted">
|
||||
<span>Zeitzone</span>
|
||||
<input type="text" value="UTC" data-modal-timezone>
|
||||
<input type="text" value="UTC" data-modal-timezone list="timezone-options" autocomplete="off">
|
||||
</label>
|
||||
|
||||
<div class="scheduler-builder">
|
||||
@@ -1517,6 +1558,7 @@ $activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups)
|
||||
minute: '00',
|
||||
weekday: '1',
|
||||
};
|
||||
const form = modal.closest('form');
|
||||
|
||||
const modalFields = {
|
||||
enabled: modal.querySelector('[data-modal-enabled]'),
|
||||
@@ -1642,6 +1684,78 @@ $activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups)
|
||||
};
|
||||
|
||||
const getEntryField = (entry, selector) => entry.querySelector(selector);
|
||||
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);
|
||||
if (node) {
|
||||
node.textContent = `${label}: ${value}`;
|
||||
}
|
||||
};
|
||||
|
||||
const applyStatusUpdates = (statuses) => {
|
||||
if (!Array.isArray(statuses)) return;
|
||||
const statusMap = new Map();
|
||||
statuses.forEach((status) => {
|
||||
statusMap.set(`${status.job_name}#${status.entry_index}`, status);
|
||||
});
|
||||
|
||||
jobs.forEach((job) => {
|
||||
const jobName = job.dataset.jobName || '';
|
||||
job.querySelectorAll('[data-scheduler-entry]').forEach((entry) => {
|
||||
const key = `${jobName}#${entry.dataset.entryIndex || '0'}`;
|
||||
const status = statusMap.get(key);
|
||||
if (!status) return;
|
||||
const enabledNode = getEntryField(entry, '[data-enabled]');
|
||||
if (enabledNode) {
|
||||
enabledNode.checked = !!status.enabled;
|
||||
}
|
||||
setStatusText(entry, 'Letzter Start', status.last_started_at_local || status.state?.last_started_at || '-');
|
||||
setStatusText(entry, 'Letzter Erfolg', status.last_success_at_local || status.state?.last_success_at || '-');
|
||||
setStatusText(entry, 'Naechster Lauf lokal', status.enabled ? (status.next_due_at_local || '-') : '-');
|
||||
setStatusText(entry, 'Status', status.state?.last_status || '-');
|
||||
updateEntrySummary(entry);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const collectSchedulerPayload = () => {
|
||||
const payload = new FormData();
|
||||
payload.append('scheduler_autosave', '1');
|
||||
jobs.forEach((job) => {
|
||||
job.querySelectorAll('[data-scheduler-entry]').forEach((entry) => {
|
||||
entry.querySelectorAll('[name]').forEach((field) => {
|
||||
if (field instanceof HTMLInputElement && field.type === 'checkbox') {
|
||||
if (field.checked) {
|
||||
payload.append(field.name, field.value || '1');
|
||||
}
|
||||
return;
|
||||
}
|
||||
payload.append(field.name, field.value);
|
||||
});
|
||||
});
|
||||
});
|
||||
return payload;
|
||||
};
|
||||
|
||||
const persistScheduler = async () => {
|
||||
if (!form) return true;
|
||||
const response = await fetch(window.location.href, {
|
||||
method: 'POST',
|
||||
body: collectSchedulerPayload(),
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Scheduler konnte nicht gespeichert werden (${response.status}).`);
|
||||
}
|
||||
const data = await response.json();
|
||||
if (!data || data.ok !== true) {
|
||||
throw new Error(data?.message || 'Scheduler konnte nicht gespeichert werden.');
|
||||
}
|
||||
applyStatusUpdates(data.statuses || []);
|
||||
return true;
|
||||
};
|
||||
|
||||
const updateEntrySummary = (entry) => {
|
||||
const enabled = getEntryField(entry, '[data-enabled]')?.checked ?? false;
|
||||
@@ -1683,11 +1797,11 @@ $activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups)
|
||||
entry.appendChild(statusNode);
|
||||
}
|
||||
|
||||
const lastStart = Array.from(entry.querySelectorAll('small')).find((node) => node.textContent.includes('Letzter Start'));
|
||||
const lastSuccess = Array.from(entry.querySelectorAll('small')).find((node) => node.textContent.includes('Letzter Erfolg'));
|
||||
const nextLocal = Array.from(entry.querySelectorAll('small')).find((node) => node.textContent.includes('Naechster Lauf lokal'));
|
||||
const status = Array.from(entry.querySelectorAll('small')).find((node) => node.textContent.includes('Status:'));
|
||||
const parseError = Array.from(entry.querySelectorAll('small')).find((node) => node.textContent.includes('Cron-Fehler'));
|
||||
const lastStart = findStatusNode(entry, 'Letzter Start');
|
||||
const lastSuccess = findStatusNode(entry, 'Letzter Erfolg');
|
||||
const nextLocal = findStatusNode(entry, 'Naechster Lauf lokal');
|
||||
const status = findStatusNode(entry, 'Status');
|
||||
const parseError = findStatusNode(entry, 'Cron-Fehler');
|
||||
|
||||
summaryNode.innerHTML = `
|
||||
<strong>${summary}</strong>
|
||||
@@ -1755,7 +1869,7 @@ $activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups)
|
||||
document.body.classList.remove('scheduler-modal-open');
|
||||
};
|
||||
|
||||
const saveModal = () => {
|
||||
const saveModal = async () => {
|
||||
const entry = modalState.entry;
|
||||
if (!entry) return;
|
||||
|
||||
@@ -1794,7 +1908,12 @@ $activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups)
|
||||
}
|
||||
|
||||
updateEntrySummary(entry);
|
||||
closeModal();
|
||||
try {
|
||||
await persistScheduler();
|
||||
closeModal();
|
||||
} catch (error) {
|
||||
alert(error instanceof Error ? error.message : 'Scheduler konnte nicht gespeichert werden.');
|
||||
}
|
||||
};
|
||||
|
||||
const createEntry = (job, values = {}) => {
|
||||
@@ -1850,6 +1969,7 @@ $activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups)
|
||||
const reindexJob = (job) => {
|
||||
const jobName = job.dataset.jobName || 'job';
|
||||
job.querySelectorAll('[data-scheduler-entry]').forEach((entry, index) => {
|
||||
entry.dataset.entryIndex = String(index);
|
||||
const setName = (selector, suffix) => {
|
||||
const node = entry.querySelector(selector);
|
||||
if (node) {
|
||||
@@ -1886,9 +2006,14 @@ $activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups)
|
||||
}
|
||||
|
||||
actions.querySelector('[data-entry-edit]')?.addEventListener('click', () => openModal(entry));
|
||||
actions.querySelector('[data-remove-scheduler-entry]')?.addEventListener('click', () => {
|
||||
actions.querySelector('[data-remove-scheduler-entry]')?.addEventListener('click', async () => {
|
||||
entry.remove();
|
||||
reindexJob(job);
|
||||
try {
|
||||
await persistScheduler();
|
||||
} catch (error) {
|
||||
alert(error instanceof Error ? error.message : 'Scheduler konnte nicht gespeichert werden.');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1997,7 +2122,7 @@ $activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups)
|
||||
.scheduler-modal__backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(10, 18, 28, 0.45);
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.scheduler-modal__dialog {
|
||||
|
||||
@@ -10,13 +10,16 @@ final class BaseSchema
|
||||
$driver = (string)$pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
|
||||
if ($driver === 'pgsql') {
|
||||
self::ensurePgsql($pdo);
|
||||
self::seedTimezones($pdo);
|
||||
return;
|
||||
}
|
||||
if ($driver === 'sqlite') {
|
||||
self::ensureSqlite($pdo);
|
||||
self::seedTimezones($pdo);
|
||||
return;
|
||||
}
|
||||
self::ensureGeneric($pdo);
|
||||
self::seedTimezones($pdo);
|
||||
}
|
||||
|
||||
private static function ensurePgsql(\PDO $pdo): void
|
||||
@@ -136,6 +139,15 @@ final class BaseSchema
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
)"
|
||||
);
|
||||
|
||||
$pdo->exec(
|
||||
"CREATE TABLE IF NOT EXISTS nexus_timezones (
|
||||
identifier TEXT PRIMARY KEY,
|
||||
label TEXT NOT NULL,
|
||||
group_name TEXT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
)"
|
||||
);
|
||||
}
|
||||
|
||||
private static function ensureSqlite(\PDO $pdo): void
|
||||
@@ -255,6 +267,15 @@ final class BaseSchema
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)"
|
||||
);
|
||||
|
||||
$pdo->exec(
|
||||
"CREATE TABLE IF NOT EXISTS nexus_timezones (
|
||||
identifier TEXT PRIMARY KEY,
|
||||
label TEXT NOT NULL,
|
||||
group_name TEXT NULL,
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)"
|
||||
);
|
||||
}
|
||||
|
||||
private static function ensureGeneric(\PDO $pdo): void
|
||||
@@ -374,5 +395,66 @@ final class BaseSchema
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)"
|
||||
);
|
||||
|
||||
$pdo->exec(
|
||||
"CREATE TABLE IF NOT EXISTS nexus_timezones (
|
||||
identifier VARCHAR(190) PRIMARY KEY,
|
||||
label VARCHAR(255) NOT NULL,
|
||||
group_name VARCHAR(190) NULL,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)"
|
||||
);
|
||||
}
|
||||
|
||||
private static function seedTimezones(\PDO $pdo): void
|
||||
{
|
||||
try {
|
||||
$count = (int) $pdo->query("SELECT COUNT(*) FROM nexus_timezones")->fetchColumn();
|
||||
} catch (\Throwable) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($count > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$driver = (string) $pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
|
||||
$sql = match ($driver) {
|
||||
'pgsql' => "INSERT INTO nexus_timezones (identifier, label, group_name, updated_at)
|
||||
VALUES (:identifier, :label, :group_name, NOW())
|
||||
ON CONFLICT (identifier) DO UPDATE SET
|
||||
label = EXCLUDED.label,
|
||||
group_name = EXCLUDED.group_name,
|
||||
updated_at = NOW()",
|
||||
'sqlite' => "INSERT INTO nexus_timezones (identifier, label, group_name, updated_at)
|
||||
VALUES (:identifier, :label, :group_name, datetime('now'))
|
||||
ON CONFLICT(identifier) DO UPDATE SET
|
||||
label = excluded.label,
|
||||
group_name = excluded.group_name,
|
||||
updated_at = datetime('now')",
|
||||
default => "INSERT INTO nexus_timezones (identifier, label, group_name, updated_at)
|
||||
VALUES (:identifier, :label, :group_name, CURRENT_TIMESTAMP)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
label = VALUES(label),
|
||||
group_name = VALUES(group_name),
|
||||
updated_at = CURRENT_TIMESTAMP",
|
||||
};
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
foreach (timezone_identifiers_list() as $identifier) {
|
||||
$parts = explode('/', $identifier, 2);
|
||||
$group = $parts[0] ?? 'Other';
|
||||
$label = str_replace('_', ' ', $identifier);
|
||||
if ($identifier === 'UTC') {
|
||||
$group = 'UTC';
|
||||
$label = 'UTC';
|
||||
}
|
||||
|
||||
$stmt->execute([
|
||||
'identifier' => $identifier,
|
||||
'label' => $label,
|
||||
'group_name' => $group,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,15 +75,16 @@ final class ModuleCronScheduler
|
||||
|
||||
try {
|
||||
$cron = CronExpression::parse($expression);
|
||||
$previousDueUtc = $cron->previousRun($nowUtc, $timezone);
|
||||
$lastScheduledFor = $this->parseUtc((string) ($state['last_scheduled_for'] ?? ''));
|
||||
$isDue = !empty($config['enabled'])
|
||||
&& !$isLocked
|
||||
&& $previousDueUtc instanceof DateTimeImmutable
|
||||
&& ($lastScheduledFor === null || $previousDueUtc > $lastScheduledFor);
|
||||
$nextDueUtc = $isDue
|
||||
? $previousDueUtc
|
||||
: $cron->nextRun($nowUtc, $timezone);
|
||||
if (!empty($config['enabled'])) {
|
||||
$previousDueUtc = $cron->previousRun($nowUtc, $timezone);
|
||||
$lastScheduledFor = $this->parseUtc((string) ($state['last_scheduled_for'] ?? ''));
|
||||
$isDue = !$isLocked
|
||||
&& $previousDueUtc instanceof DateTimeImmutable
|
||||
&& ($lastScheduledFor === null || $previousDueUtc > $lastScheduledFor);
|
||||
$nextDueUtc = $isDue
|
||||
? $previousDueUtc
|
||||
: $cron->nextRun($nowUtc, $timezone);
|
||||
}
|
||||
} catch (\Throwable $exception) {
|
||||
$parseError = $exception->getMessage();
|
||||
}
|
||||
@@ -94,6 +95,8 @@ final class ModuleCronScheduler
|
||||
'state_key' => $stateKey,
|
||||
'config' => $config,
|
||||
'state' => $state,
|
||||
'last_started_at_local' => ($this->parseUtc((string) ($state['last_started_at'] ?? '')))?->setTimezone($timezone)->format('Y-m-d H:i:s'),
|
||||
'last_success_at_local' => ($this->parseUtc((string) ($state['last_success_at'] ?? '')))?->setTimezone($timezone)->format('Y-m-d H:i:s'),
|
||||
'enabled' => !empty($config['enabled']),
|
||||
'cron_expression' => $expression,
|
||||
'timezone' => $timezone->getName(),
|
||||
|
||||
@@ -128,6 +128,42 @@ final class ModuleManager
|
||||
]);
|
||||
}
|
||||
|
||||
public function timezones(): array
|
||||
{
|
||||
if (!$this->basePdo) {
|
||||
return $this->fallbackTimezones();
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $this->basePdo->query(
|
||||
"SELECT identifier, label, group_name
|
||||
FROM nexus_timezones
|
||||
ORDER BY group_name ASC, identifier ASC"
|
||||
);
|
||||
$rows = $stmt ? $stmt->fetchAll(\PDO::FETCH_ASSOC) : [];
|
||||
if (!is_array($rows) || $rows === []) {
|
||||
return $this->fallbackTimezones();
|
||||
}
|
||||
|
||||
return array_values(array_filter(array_map(
|
||||
static function (array $row): ?array {
|
||||
$identifier = trim((string) ($row['identifier'] ?? ''));
|
||||
if ($identifier === '') {
|
||||
return null;
|
||||
}
|
||||
return [
|
||||
'value' => $identifier,
|
||||
'label' => trim((string) ($row['label'] ?? $identifier)),
|
||||
'group' => trim((string) ($row['group_name'] ?? '')),
|
||||
];
|
||||
},
|
||||
$rows
|
||||
)));
|
||||
} catch (\Throwable) {
|
||||
return $this->fallbackTimezones();
|
||||
}
|
||||
}
|
||||
|
||||
public function modulePdo(string $name, array $fallback = []): ?\PDO
|
||||
{
|
||||
$settings = $this->settings($name);
|
||||
@@ -358,6 +394,20 @@ final class ModuleManager
|
||||
return $enabledByDefault;
|
||||
}
|
||||
|
||||
private function fallbackTimezones(): array
|
||||
{
|
||||
$result = [];
|
||||
foreach (timezone_identifiers_list() as $identifier) {
|
||||
$parts = explode('/', $identifier, 2);
|
||||
$result[] = [
|
||||
'value' => $identifier,
|
||||
'label' => $identifier === 'UTC' ? 'UTC' : str_replace('_', ' ', $identifier),
|
||||
'group' => $parts[0] ?? 'Other',
|
||||
];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function saveAuth(string $name, array $auth): array
|
||||
{
|
||||
if (!preg_match('/^[a-zA-Z0-9_\-]+$/', $name)) {
|
||||
|
||||
Reference in New Issue
Block a user