ddsfds
This commit is contained in:
@@ -414,9 +414,10 @@ $renderField = function (array $field) use (&$current, $getNested, $driverOption
|
|||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
$isSchedulerAutosave = isset($_POST['scheduler_autosave']) && (string) $_POST['scheduler_autosave'] === '1';
|
$isSchedulerAutosave = isset($_POST['scheduler_autosave']) && (string) $_POST['scheduler_autosave'] === '1';
|
||||||
|
$isSchedulerTest = isset($_POST['scheduler_test']) && (string) $_POST['scheduler_test'] === '1';
|
||||||
$payload = [];
|
$payload = [];
|
||||||
|
|
||||||
if ($isSchedulerAutosave) {
|
if ($isSchedulerAutosave || $isSchedulerTest) {
|
||||||
if ($cronTaskDefinitions !== []) {
|
if ($cronTaskDefinitions !== []) {
|
||||||
$postedSchedulerJobs = is_array($_POST['scheduler_jobs'] ?? null) ? $_POST['scheduler_jobs'] : [];
|
$postedSchedulerJobs = is_array($_POST['scheduler_jobs'] ?? null) ? $_POST['scheduler_jobs'] : [];
|
||||||
$current['scheduler_jobs'] = $extractSchedulerJobs($postedSchedulerJobs, $cronTaskDefinitions, $current);
|
$current['scheduler_jobs'] = $extractSchedulerJobs($postedSchedulerJobs, $cronTaskDefinitions, $current);
|
||||||
@@ -425,15 +426,26 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$current = modules()->settings($moduleName);
|
$current = modules()->settings($moduleName);
|
||||||
$refreshSchedulerState();
|
$refreshSchedulerState();
|
||||||
|
|
||||||
|
$testResult = null;
|
||||||
|
if ($isSchedulerTest) {
|
||||||
|
$jobName = trim((string) ($_POST['scheduler_job_name'] ?? ''));
|
||||||
|
$entryIndex = max(0, (int) ($_POST['scheduler_entry_index'] ?? 0));
|
||||||
|
$testResult = modules()->runCronTaskNow($moduleName, $jobName, $entryIndex);
|
||||||
|
$refreshSchedulerState();
|
||||||
|
}
|
||||||
|
|
||||||
while (ob_get_level() > 0) {
|
while (ob_get_level() > 0) {
|
||||||
ob_end_clean();
|
ob_end_clean();
|
||||||
}
|
}
|
||||||
header('Content-Type: application/json; charset=utf-8');
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
'ok' => true,
|
'ok' => true,
|
||||||
'message' => 'Scheduler gespeichert.',
|
'message' => $isSchedulerTest
|
||||||
|
? (string) ($testResult['message'] ?? 'Cron-Test ausgefuehrt.')
|
||||||
|
: 'Scheduler gespeichert.',
|
||||||
'scheduler_jobs' => $current['scheduler_jobs'] ?? [],
|
'scheduler_jobs' => $current['scheduler_jobs'] ?? [],
|
||||||
'statuses' => $cronTaskStatuses,
|
'statuses' => $cronTaskStatuses,
|
||||||
|
'test_result' => $testResult,
|
||||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -906,6 +918,7 @@ $activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups)
|
|||||||
<small class="muted">Letzter Erfolg: <?= e($formatRunTimestamp((string) ($task['state']['last_success_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">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>
|
<small class="muted">Status: <?= e((string) (($task['state']['last_status'] ?? '') !== '' ? $task['state']['last_status'] : '-')) ?></small>
|
||||||
|
<small class="muted">Meldung: <?= e((string) (($task['state']['last_message'] ?? '') !== '' ? $task['state']['last_message'] : '-')) ?></small>
|
||||||
<?php if (trim((string) ($task['parse_error'] ?? '')) !== ''): ?>
|
<?php if (trim((string) ($task['parse_error'] ?? '')) !== ''): ?>
|
||||||
<small class="muted" style="color:#b42318;">Cron-Fehler: <?= e((string) $task['parse_error']) ?></small>
|
<small class="muted" style="color:#b42318;">Cron-Fehler: <?= e((string) $task['parse_error']) ?></small>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
@@ -1281,6 +1294,7 @@ $activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups)
|
|||||||
<small class="muted">Letzter Erfolg: <?= e($formatRunTimestamp((string) ($task['state']['last_success_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">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>
|
<small class="muted">Status: <?= e((string) (($task['state']['last_status'] ?? '') !== '' ? $task['state']['last_status'] : '-')) ?></small>
|
||||||
|
<small class="muted">Meldung: <?= e((string) (($task['state']['last_message'] ?? '') !== '' ? $task['state']['last_message'] : '-')) ?></small>
|
||||||
<?php if (trim((string) ($task['parse_error'] ?? '')) !== ''): ?>
|
<?php if (trim((string) ($task['parse_error'] ?? '')) !== ''): ?>
|
||||||
<small class="muted" style="color:#b42318;">Cron-Fehler: <?= e((string) $task['parse_error']) ?></small>
|
<small class="muted" style="color:#b42318;">Cron-Fehler: <?= e((string) $task['parse_error']) ?></small>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
@@ -1754,6 +1768,15 @@ $activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups)
|
|||||||
return payload;
|
return payload;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const collectSchedulerTestPayload = (jobName, entryIndex) => {
|
||||||
|
const payload = collectSchedulerPayload();
|
||||||
|
payload.delete('scheduler_autosave');
|
||||||
|
payload.append('scheduler_test', '1');
|
||||||
|
payload.append('scheduler_job_name', jobName);
|
||||||
|
payload.append('scheduler_entry_index', String(entryIndex));
|
||||||
|
return payload;
|
||||||
|
};
|
||||||
|
|
||||||
const persistScheduler = async () => {
|
const persistScheduler = async () => {
|
||||||
if (!form) return true;
|
if (!form) return true;
|
||||||
const url = form.action || window.location.href;
|
const url = form.action || window.location.href;
|
||||||
@@ -1799,6 +1822,35 @@ $activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups)
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const testScheduler = async (jobName, entryIndex) => {
|
||||||
|
if (!form) return null;
|
||||||
|
const url = form.action || window.location.href;
|
||||||
|
console.log('[scheduler] test start', { jobName, entryIndex, url });
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
body: collectSchedulerTestPayload(jobName, entryIndex),
|
||||||
|
credentials: 'same-origin',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const raw = await response.text();
|
||||||
|
console.log('[scheduler] test raw response', raw);
|
||||||
|
let data = null;
|
||||||
|
try {
|
||||||
|
data = JSON.parse(raw);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[scheduler] test json parse failed', error);
|
||||||
|
throw new Error(`Cron-Test Antwort war kein JSON: ${raw.slice(0, 160)}`);
|
||||||
|
}
|
||||||
|
if (!response.ok || !data || data.ok !== true) {
|
||||||
|
throw new Error(data?.message || `Cron-Test fehlgeschlagen (${response.status}).`);
|
||||||
|
}
|
||||||
|
applyStatusUpdates(data.statuses || []);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
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 || '';
|
||||||
@@ -1855,6 +1907,7 @@ $activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups)
|
|||||||
<small class="muted">${lastSuccess ? lastSuccess.textContent : 'Letzter Erfolg: -'}</small>
|
<small class="muted">${lastSuccess ? lastSuccess.textContent : 'Letzter Erfolg: -'}</small>
|
||||||
<small class="muted">${nextLocal ? nextLocal.textContent : 'Naechster Lauf lokal: -'}</small>
|
<small class="muted">${nextLocal ? nextLocal.textContent : 'Naechster Lauf lokal: -'}</small>
|
||||||
<small class="muted">${status ? status.textContent : 'Status: -'}</small>
|
<small class="muted">${status ? status.textContent : 'Status: -'}</small>
|
||||||
|
<small class="muted">${findStatusNode(entry, 'Meldung') ? findStatusNode(entry, 'Meldung').textContent : 'Meldung: -'}</small>
|
||||||
${parseError ? `<small class="muted scheduler-entry__error">${parseError.textContent}</small>` : ''}
|
${parseError ? `<small class="muted scheduler-entry__error">${parseError.textContent}</small>` : ''}
|
||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
@@ -2012,6 +2065,7 @@ $activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups)
|
|||||||
<small class="muted">Letzter Erfolg: -</small>
|
<small class="muted">Letzter Erfolg: -</small>
|
||||||
<small class="muted">Naechster Lauf lokal: -</small>
|
<small class="muted">Naechster Lauf lokal: -</small>
|
||||||
<small class="muted">Status: -</small>
|
<small class="muted">Status: -</small>
|
||||||
|
<small class="muted">Meldung: -</small>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
getEntryField(entry, '[data-enabled]').checked = Boolean(values.enabled);
|
getEntryField(entry, '[data-enabled]').checked = Boolean(values.enabled);
|
||||||
@@ -2061,12 +2115,34 @@ $activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups)
|
|||||||
actions.dataset.entryActions = '';
|
actions.dataset.entryActions = '';
|
||||||
actions.innerHTML = `
|
actions.innerHTML = `
|
||||||
<button class="nav-link" type="button" data-entry-edit>Bearbeiten</button>
|
<button class="nav-link" type="button" data-entry-edit>Bearbeiten</button>
|
||||||
|
<button class="nav-link" type="button" data-entry-test>Cron testen</button>
|
||||||
${job.dataset.jobMode === 'multi' ? '<button class="nav-link" type="button" data-remove-scheduler-entry>Entfernen</button>' : ''}
|
${job.dataset.jobMode === 'multi' ? '<button class="nav-link" type="button" data-remove-scheduler-entry>Entfernen</button>' : ''}
|
||||||
`;
|
`;
|
||||||
entry.appendChild(actions);
|
entry.appendChild(actions);
|
||||||
}
|
}
|
||||||
|
|
||||||
actions.querySelector('[data-entry-edit]')?.addEventListener('click', () => openModal(entry));
|
actions.querySelector('[data-entry-edit]')?.addEventListener('click', () => openModal(entry));
|
||||||
|
actions.querySelector('[data-entry-test]')?.addEventListener('click', async () => {
|
||||||
|
const testButton = actions.querySelector('[data-entry-test]');
|
||||||
|
const originalText = testButton?.textContent || 'Cron testen';
|
||||||
|
const jobName = job.dataset.jobName || '';
|
||||||
|
const entryIndex = Number(entry.dataset.entryIndex || '0');
|
||||||
|
if (testButton) {
|
||||||
|
testButton.disabled = true;
|
||||||
|
testButton.textContent = 'Teste...';
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await testScheduler(jobName, entryIndex);
|
||||||
|
alert(result?.message || 'Cron-Test erfolgreich.');
|
||||||
|
} catch (error) {
|
||||||
|
alert(error instanceof Error ? error.message : 'Cron-Test fehlgeschlagen.');
|
||||||
|
} finally {
|
||||||
|
if (testButton) {
|
||||||
|
testButton.disabled = false;
|
||||||
|
testButton.textContent = originalText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
actions.querySelector('[data-remove-scheduler-entry]')?.addEventListener('click', async () => {
|
actions.querySelector('[data-remove-scheduler-entry]')?.addEventListener('click', async () => {
|
||||||
entry.remove();
|
entry.remove();
|
||||||
reindexJob(job);
|
reindexJob(job);
|
||||||
|
|||||||
@@ -202,6 +202,101 @@ final class ModuleCronScheduler
|
|||||||
return $results;
|
return $results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function runNow(string $moduleName, string $jobName, int $entryIndex): array
|
||||||
|
{
|
||||||
|
$task = null;
|
||||||
|
foreach ($this->statuses($moduleName) as $status) {
|
||||||
|
if ((string) ($status['job_name'] ?? '') === $jobName && (int) ($status['entry_index'] ?? -1) === $entryIndex) {
|
||||||
|
$task = $status;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_array($task)) {
|
||||||
|
return [
|
||||||
|
'task' => $jobName,
|
||||||
|
'entry_index' => $entryIndex,
|
||||||
|
'ok' => false,
|
||||||
|
'message' => 'Cron-Eintrag nicht gefunden.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->modules->hasFunction($moduleName, (string) $task['callback'])) {
|
||||||
|
return [
|
||||||
|
'task' => $jobName,
|
||||||
|
'entry_index' => $entryIndex,
|
||||||
|
'ok' => false,
|
||||||
|
'message' => 'Callback nicht registriert.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->acquireLock($moduleName, (string) $task['state_key'], (int) $task['lock_minutes'])) {
|
||||||
|
return [
|
||||||
|
'task' => $jobName,
|
||||||
|
'entry_index' => $entryIndex,
|
||||||
|
'ok' => false,
|
||||||
|
'message' => 'Cron-Eintrag ist aktuell gesperrt.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$startedAt = gmdate('Y-m-d H:i:s');
|
||||||
|
$timezone = $this->safeTimezone((string) ($task['timezone'] ?? 'UTC'));
|
||||||
|
$this->persistState($moduleName, (string) $task['state_key'], [
|
||||||
|
'last_started_at' => $startedAt,
|
||||||
|
'last_status' => 'running',
|
||||||
|
'last_message' => 'Manueller Testlauf gestartet.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = $this->modules->call($moduleName, (string) $task['callback'], [
|
||||||
|
'task' => $task,
|
||||||
|
'trigger' => 'manual_test',
|
||||||
|
'started_at' => $startedAt,
|
||||||
|
'scheduled_for_utc' => null,
|
||||||
|
'scheduled_for_local' => null,
|
||||||
|
'timezone' => $timezone->getName(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$ok = !is_array($result) || !array_key_exists('ok', $result) || !empty($result['ok']);
|
||||||
|
$skipped = is_array($result) && !empty($result['skipped']);
|
||||||
|
$message = is_array($result) ? trim((string) ($result['message'] ?? '')) : '';
|
||||||
|
$finishedAt = gmdate('Y-m-d H:i:s');
|
||||||
|
|
||||||
|
$payload = [
|
||||||
|
'last_finished_at' => $finishedAt,
|
||||||
|
'last_status' => $skipped ? 'skipped' : ($ok ? 'success' : 'error'),
|
||||||
|
'last_message' => $message !== '' ? $message : ($ok ? 'Manueller Testlauf erfolgreich.' : 'Manueller Testlauf fehlgeschlagen.'),
|
||||||
|
'lock_until' => null,
|
||||||
|
];
|
||||||
|
if ($ok && !$skipped) {
|
||||||
|
$payload['last_success_at'] = $finishedAt;
|
||||||
|
}
|
||||||
|
$this->persistState($moduleName, (string) $task['state_key'], $payload);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'task' => $jobName,
|
||||||
|
'entry_index' => $entryIndex,
|
||||||
|
'ok' => $ok,
|
||||||
|
'message' => (string) $payload['last_message'],
|
||||||
|
];
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$finishedAt = gmdate('Y-m-d H:i:s');
|
||||||
|
$this->persistState($moduleName, (string) $task['state_key'], [
|
||||||
|
'last_finished_at' => $finishedAt,
|
||||||
|
'last_status' => 'error',
|
||||||
|
'last_message' => $e->getMessage(),
|
||||||
|
'lock_until' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'task' => $jobName,
|
||||||
|
'entry_index' => $entryIndex,
|
||||||
|
'ok' => false,
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private function jobConfigs(array $definition, array $settings): array
|
private function jobConfigs(array $definition, array $settings): array
|
||||||
{
|
{
|
||||||
$jobs = is_array($settings['scheduler_jobs'] ?? null)
|
$jobs = is_array($settings['scheduler_jobs'] ?? null)
|
||||||
|
|||||||
@@ -311,6 +311,11 @@ final class ModuleManager
|
|||||||
return $this->cronScheduler()->runDue($name);
|
return $this->cronScheduler()->runDue($name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function runCronTaskNow(string $name, string $jobName, int $entryIndex): array
|
||||||
|
{
|
||||||
|
return $this->cronScheduler()->runNow($name, $jobName, $entryIndex);
|
||||||
|
}
|
||||||
|
|
||||||
private function scanModules(): void
|
private function scanModules(): void
|
||||||
{
|
{
|
||||||
$this->modules = [];
|
$this->modules = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user