diff --git a/CHANGELOG.md b/CHANGELOG.md index e994cd7..70f2af5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). --- +## [5.3.0] – branch: `feature/detail-reload-controls` + +### Added + +- **Job-Detail – AJAX-Reload der Ausführungshistorie:** Statt eines vollständigen `location.reload()` ruft die Seite `GET /crons/{id}?_json=1` ab und ersetzt ausschließlich den `` der Ausführungshistorie-Tabelle. Kein Seitenflackern, Scroll-Position und aufgeklappte Output-Bereiche bleiben erhalten. `CronController::show()` rendert im JSON-Modus die Tabellenzeilen serverseitig über ein ausgelagertes Partial (`_detail_history_rows.php`) und gibt `{"has_running": bool, "rows_html": "..."}` zurück. Die Session wird vor dem Agent-I/O geschlossen (`writeClose()`). +- **Job-Detail – Reload-Steuerung im Ausführungshistorie-Header:** Zwei Icon-Buttons in der Kopfzeile des Ausführungshistorie-Abschnitts: + - **Auto-Reload-Toggle** (Pause-/Play-Icon): schaltet das automatische 10-Sekunden-Polling ein und aus. Zustand wird per `sessionStorage` (Schlüssel `cm_autoreload_`) über Seitenbesuche hinweg gespeichert. Standard: ON wenn beim Laden der Seite ein Job läuft, OFF sonst. Wenn ein laufender Job abschließt, stoppt Auto-Reload automatisch. + - **Manueller Reload** (Refresh-Icon): löst sofort einen AJAX-Fetch aus. +- **`_detail_history_rows.php`:** Neues Partial-Template; enthält die gesamte ``-Rendering-Logik der Ausführungshistorie. Wird sowohl vom vollen Seitenrender als auch vom JSON-Endpoint eingebunden — keine Duplikation der Badge-/Output-Logik. + +### Changed + +- Gelber Countdown-Banner (`Job läuft – Seite aktualisiert sich in Xs`) entfernt; die Reload-Steuerung übernimmt diese Funktion. + +--- + ## [5.2.1] – branch: `fix/ansi-escape-in-ssh-output` ### Fixed diff --git a/agent/VERSION b/agent/VERSION index 26d99a2..03f488b 100644 --- a/agent/VERSION +++ b/agent/VERSION @@ -1 +1 @@ -5.2.1 +5.3.0 diff --git a/web/VERSION b/web/VERSION index 26d99a2..03f488b 100644 --- a/web/VERSION +++ b/web/VERSION @@ -1 +1 @@ -5.2.1 +5.3.0 diff --git a/web/lang/de.php b/web/lang/de.php index 4776088..d6e4159 100644 --- a/web/lang/de.php +++ b/web/lang/de.php @@ -358,6 +358,8 @@ 'execute_now_already_pending' => 'Sofortausführung bereits geplant – bitte warten bis der Job gestartet wurde.', 'live_running' => 'live', 'live_auto_refresh' => 'Job läuft – Seite aktualisiert sich in', + 'detail_auto_reload_toggle' => 'Automatischen Reload an/aus', + 'detail_manual_reload' => 'Jetzt neu laden', // ------------------------------------------------------------------------- // Ersteinrichtung diff --git a/web/lang/en.php b/web/lang/en.php index 12dd98b..6cb93ed 100644 --- a/web/lang/en.php +++ b/web/lang/en.php @@ -358,6 +358,8 @@ 'execute_now_already_pending' => 'Immediate execution already scheduled – please wait for the job to start.', 'live_running' => 'live', 'live_auto_refresh' => 'Job is running – page refreshes in', + 'detail_auto_reload_toggle' => 'Toggle auto-reload', + 'detail_manual_reload' => 'Reload now', // ------------------------------------------------------------------------- // Initial Setup page diff --git a/web/src/Controller/CronController.php b/web/src/Controller/CronController.php index 6ce87ac..83790ac 100644 --- a/web/src/Controller/CronController.php +++ b/web/src/Controller/CronController.php @@ -406,6 +406,54 @@ public function show(array $params): void $id = (string) ($params['id'] ?? ''); $agent = $this->agentClient(); + // JSON mode: AJAX history refresh from the detail page + if ($this->isJsonRequest()) { + $csrfToken = SessionManager::getCsrfToken(); + $isAdmin = SessionManager::hasRole('admin'); + SessionManager::writeClose(); + + try { + $results = $agent->getMultiple([ + 'job' => ['path' => '/crons/' . rawurlencode($id)], + 'history' => ['path' => '/history', 'query' => ['job_id' => $id, 'limit' => 20]], + ]); + $job = $results['job']; + $history = $results['history']['data'] ?? []; + } catch (\RuntimeException $e) { + $this->logger->error('CronController::show (json): agent request failed', [ + 'id' => $id, + 'message' => $e->getMessage(), + ]); + $this->jsonResponse(['error' => 'agent_unavailable'], 503); + return; + } + + $hasRunning = false; + foreach ($history as $_e) { + $ec = $_e['exit_code'] ?? null; + $fa = (string) ($_e['finished_at'] ?? ''); + if ($ec === null && $fa === '') { + $hasRunning = true; + break; + } + } + + $translator = $this->translator(); + $t = fn(string $k, array $r = []): string => $translator->t($k, $r); + $csrf_token = $csrfToken; + $jobId = $id; + + ob_start(); + include __DIR__ . '/../../templates/cron/_detail_history_rows.php'; + $rowsHtml = (string) ob_get_clean(); + + $this->jsonResponse([ + 'has_running' => $hasRunning, + 'rows_html' => $rowsHtml, + ]); + return; + } + try { // One parallel batch instead of two sequential roundtrips $results = $agent->getMultiple([ diff --git a/web/templates/cron/_detail_history_rows.php b/web/templates/cron/_detail_history_rows.php new file mode 100644 index 0000000..8524f83 --- /dev/null +++ b/web/templates/cron/_detail_history_rows.php @@ -0,0 +1,186 @@ + elements for the execution-history tbody in the job-detail + * view. Included by both the full-page render and the ?_json=1 AJAX endpoint + * so that the HTML is always generated by the same PHP code. + * + * Expected variables (set by the including template or controller): + * array $history – execution records (up to 20) + * array $job – job record (used for retry_count) + * bool $isAdmin – whether the current user has admin role + * callable $t – translator callable: $t('key', ['var' => val]) + * string $csrf_token – CSRF token for kill forms + * int|string $jobId – numeric job id (for kill-form return URL) + * + * @author Christian Schulz + * @license GNU General Public License version 3 or later + */ + +foreach ($history as $idx => $entry): + $startedAt = (string) ($entry['started_at'] ?? ''); + $finishedAt = (string) ($entry['finished_at'] ?? ''); + $exitCode = isset($entry['exit_code']) ? $entry['exit_code'] : null; + $executionId = (string) ($entry['execution_id'] ?? ''); + $isRunning = $exitCode === null && $finishedAt === ''; + $duration = isset($entry['duration_seconds']) + ? round((float) $entry['duration_seconds'], 1) . 's' + : '–'; + $entryTarget = (string) ($entry['target'] ?? ''); + $output = (string) ($entry['output'] ?? ''); + $outputId = 'hist-output-' . $idx; + $outputTrunc = mb_strlen($output) > 200 + ? mb_substr($output, 0, 200) . '…' + : $output; + $duringMaintenance = !empty($entry['during_maintenance']); + $retryAttempt = (int) ($entry['retry_attempt'] ?? 0); + $retryTotal = (int) ($job['retry_count'] ?? 0); + + // Exit code badge + if ($isRunning) { + $exitBadge = '' + . htmlspecialchars($t('status_running'), ENT_QUOTES, 'UTF-8') + . ''; + } elseif ($exitCode !== null && (int) $exitCode === -5) { + $exitBadge = '' + . htmlspecialchars($t('cron_interrupted_badge'), ENT_QUOTES, 'UTF-8') + . ''; + } elseif ($exitCode !== null && (int) $exitCode === -4) { + $exitBadge = '' + . htmlspecialchars($t('cron_maintenance_skipped_badge'), ENT_QUOTES, 'UTF-8') + . ''; + } elseif ($exitCode !== null && (int) $exitCode === 0) { + $exitBadge = '0'; + if ($duringMaintenance) { + $exitBadge .= ' ' + . htmlspecialchars($t('cron_during_maintenance_badge'), ENT_QUOTES, 'UTF-8') + . ''; + } + } elseif ($exitCode !== null && (int) $exitCode === -2) { + $exitBadge = '' + . htmlspecialchars($t('cron_kill_running'), ENT_QUOTES, 'UTF-8') + . ''; + } else { + $safeCode = htmlspecialchars((string) $exitCode, ENT_QUOTES, 'UTF-8'); + $exitBadge = '' . $safeCode . ''; + if ($duringMaintenance) { + $exitBadge .= ' ' + . htmlspecialchars($t('cron_during_maintenance_badge'), ENT_QUOTES, 'UTF-8') + . ''; + } + } + ?> + + + + + + + + + + + + + + + + + + + + + + + + 0): ?> + + $retryAttempt, 'total' => $retryTotal]), ENT_QUOTES, 'UTF-8') ?> + + + + + +
+ + +
+ + + + + + + + 200): ?> + + + + +
+ + +
+ + + + + + + +
+ + + +
+ + + + + + + diff --git a/web/templates/cron/detail.php b/web/templates/cron/detail.php index 8fc77b0..40987f0 100644 --- a/web/templates/cron/detail.php +++ b/web/templates/cron/detail.php @@ -413,12 +413,6 @@ class="hidden flex items-start gap-2 rounded-lg border border-red-200 bg-red-50 Execution history section ====================================================================== -->
-
-

- -

-
- - -
- - - 15s +
+

+ +

+
+ + + +
- +
@@ -469,171 +496,8 @@ class="hidden flex items-start gap-2 rounded-lg border border-red-200 bg-red-50 - - $entry): ?> - 200 - ? mb_substr($output, 0, 200) . '…' - : $output; - $duringMaintenance = !empty($entry['during_maintenance']); - $retryAttempt = (int) ($entry['retry_attempt'] ?? 0); - $retryTotal = (int) ($job['retry_count'] ?? 0); - - // Exit code badge - if ($isRunning) { - $exitBadge = '' - . htmlspecialchars($t('status_running'), ENT_QUOTES, 'UTF-8') - . ''; - } elseif ($exitCode !== null && (int) $exitCode === -5) { - $exitBadge = '' - . htmlspecialchars($t('cron_interrupted_badge'), ENT_QUOTES, 'UTF-8') - . ''; - } elseif ($exitCode !== null && (int) $exitCode === -4) { - $exitBadge = '' - . htmlspecialchars($t('cron_maintenance_skipped_badge'), ENT_QUOTES, 'UTF-8') - . ''; - } elseif ($exitCode !== null && (int) $exitCode === 0) { - $exitBadge = '0'; - if ($duringMaintenance) { - $exitBadge .= ' ' - . htmlspecialchars($t('cron_during_maintenance_badge'), ENT_QUOTES, 'UTF-8') - . ''; - } - } elseif ($exitCode !== null && (int) $exitCode === -2) { - $exitBadge = '' - . htmlspecialchars($t('cron_kill_running'), ENT_QUOTES, 'UTF-8') - . ''; - } else { - $safeCode = htmlspecialchars((string) $exitCode, ENT_QUOTES, 'UTF-8'); - $exitBadge = '' . $safeCode . ''; - if ($duringMaintenance) { - $exitBadge .= ' ' - . htmlspecialchars($t('cron_during_maintenance_badge'), ENT_QUOTES, 'UTF-8') - . ''; - } - } - ?> - - - - - - - - - - - - - - - - - - - - - - - - 0): ?> - - $retryAttempt, 'total' => $retryTotal]), ENT_QUOTES, 'UTF-8') ?> - - - - - -
- - -
- - - - - - - - 200): ?> - - - - -
- - -
- - - - - - - -
- - - -
- - - - - - - + +
@@ -895,16 +759,77 @@ function updateBanner() { })(); - -