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

This commit is contained in:
2026-05-01 02:22:43 +02:00
parent f7f99bd700
commit a9c64c5d68
3 changed files with 178 additions and 2 deletions

View File

@@ -414,9 +414,10 @@ $renderField = function (array $field) use (&$current, $getNested, $driverOption
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$isSchedulerAutosave = isset($_POST['scheduler_autosave']) && (string) $_POST['scheduler_autosave'] === '1';
$isSchedulerTest = isset($_POST['scheduler_test']) && (string) $_POST['scheduler_test'] === '1';
$payload = [];
if ($isSchedulerAutosave) {
if ($isSchedulerAutosave || $isSchedulerTest) {
if ($cronTaskDefinitions !== []) {
$postedSchedulerJobs = is_array($_POST['scheduler_jobs'] ?? null) ? $_POST['scheduler_jobs'] : [];
$current['scheduler_jobs'] = $extractSchedulerJobs($postedSchedulerJobs, $cronTaskDefinitions, $current);
@@ -425,15 +426,26 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$current = modules()->settings($moduleName);
$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) {
ob_end_clean();
}
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'ok' => true,
'message' => 'Scheduler gespeichert.',
'message' => $isSchedulerTest
? (string) ($testResult['message'] ?? 'Cron-Test ausgefuehrt.')
: 'Scheduler gespeichert.',
'scheduler_jobs' => $current['scheduler_jobs'] ?? [],
'statuses' => $cronTaskStatuses,
'test_result' => $testResult,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
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">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">Meldung: <?= e((string) (($task['state']['last_message'] ?? '') !== '' ? $task['state']['last_message'] : '-')) ?></small>
<?php if (trim((string) ($task['parse_error'] ?? '')) !== ''): ?>
<small class="muted" style="color:#b42318;">Cron-Fehler: <?= e((string) $task['parse_error']) ?></small>
<?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">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">Meldung: <?= e((string) (($task['state']['last_message'] ?? '') !== '' ? $task['state']['last_message'] : '-')) ?></small>
<?php if (trim((string) ($task['parse_error'] ?? '')) !== ''): ?>
<small class="muted" style="color:#b42318;">Cron-Fehler: <?= e((string) $task['parse_error']) ?></small>
<?php endif; ?>
@@ -1754,6 +1768,15 @@ $activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups)
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 () => {
if (!form) return true;
const url = form.action || window.location.href;
@@ -1799,6 +1822,35 @@ $activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups)
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 enabled = getEntryField(entry, '[data-enabled]')?.checked ?? false;
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">${nextLocal ? nextLocal.textContent : 'Naechster Lauf lokal: -'}</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>` : ''}
`;
};
@@ -2012,6 +2065,7 @@ $activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups)
<small class="muted">Letzter Erfolg: -</small>
<small class="muted">Naechster Lauf lokal: -</small>
<small class="muted">Status: -</small>
<small class="muted">Meldung: -</small>
`;
getEntryField(entry, '[data-enabled]').checked = Boolean(values.enabled);
@@ -2061,12 +2115,34 @@ $activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups)
actions.dataset.entryActions = '';
actions.innerHTML = `
<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>' : ''}
`;
entry.appendChild(actions);
}
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 () => {
entry.remove();
reindexJob(job);

View File

@@ -202,6 +202,101 @@ final class ModuleCronScheduler
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
{
$jobs = is_array($settings['scheduler_jobs'] ?? null)

View File

@@ -311,6 +311,11 @@ final class ModuleManager
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
{
$this->modules = [];