From a9c64c5d68fd7300b284150043d76a4e2d97a398 Mon Sep 17 00:00:00 2001 From: Lars Gebhardt-Kusche Date: Fri, 1 May 2026 02:22:43 +0200 Subject: [PATCH] ddsfds --- partials/landingpages/modules/setup.php | 80 ++++++++++++++++++++- src/App/ModuleCronScheduler.php | 95 +++++++++++++++++++++++++ src/App/ModuleManager.php | 5 ++ 3 files changed, 178 insertions(+), 2 deletions(-) diff --git a/partials/landingpages/modules/setup.php b/partials/landingpages/modules/setup.php index 15854f7..baf8583 100644 --- a/partials/landingpages/modules/setup.php +++ b/partials/landingpages/modules/setup.php @@ -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) Letzter Erfolg: Naechster Lauf lokal: Status: + Meldung: Cron-Fehler: @@ -1281,6 +1294,7 @@ $activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups) Letzter Erfolg: Naechster Lauf lokal: Status: + Meldung: Cron-Fehler: @@ -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) ${lastSuccess ? lastSuccess.textContent : 'Letzter Erfolg: -'} ${nextLocal ? nextLocal.textContent : 'Naechster Lauf lokal: -'} ${status ? status.textContent : 'Status: -'} + ${findStatusNode(entry, 'Meldung') ? findStatusNode(entry, 'Meldung').textContent : 'Meldung: -'} ${parseError ? `${parseError.textContent}` : ''} `; }; @@ -2012,6 +2065,7 @@ $activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups) Letzter Erfolg: - Naechster Lauf lokal: - Status: - + Meldung: - `; 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 = ` + ${job.dataset.jobMode === 'multi' ? '' : ''} `; 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); diff --git a/src/App/ModuleCronScheduler.php b/src/App/ModuleCronScheduler.php index 3d679d8..b319048 100644 --- a/src/App/ModuleCronScheduler.php +++ b/src/App/ModuleCronScheduler.php @@ -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) diff --git a/src/App/ModuleManager.php b/src/App/ModuleManager.php index 2b76127..dfeab61 100644 --- a/src/App/ModuleManager.php +++ b/src/App/ModuleManager.php @@ -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 = [];