From d9921e136a0c0b543bbcd0f6d937d99a267beb37 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 25 Aug 2026 21:23:08 +0200 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20v5.2.0=20=E2=80=93=20L=C3=A4uft-Bad?= =?UTF-8?q?ge,=20Einzel-Benutzer-Modus,=20Export-HMAC-Fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Job-Liste zeigt "Läuft"-Badge wenn finished_at IS NULL (is_running via CronListEndpoint) - User-spezifische UI-Elemente (Filter, Spalten, "Jobs nach Benutzer") werden ausgeblendet wenn nur ein Linux-User existiert (/crons, /timeline, /export, Dashboard) - ExportController::download(): HMAC-Signatur um userId/username-Felder ergänzt (fehlende Erweiterung aus v4.3.0 führte zu HTTP 401 auf jedem Export-Download) - VERSION auf 5.2.0 (agent + web) --- CHANGELOG.md | 8 +++++++- agent/VERSION | 2 +- agent/src/Endpoints/CronListEndpoint.php | 3 +++ web/VERSION | 2 +- web/src/Controller/CronController.php | 1 + web/src/Controller/DashboardController.php | 1 + web/src/Controller/ExportController.php | 13 ++++++++++--- web/src/Controller/TimelineController.php | 1 + web/templates/cron/list.php | 17 ++++++++++++++--- web/templates/dashboard.php | 9 ++++++++- web/templates/export.php | 7 +++++-- web/templates/timeline.php | 9 ++++++++- 12 files changed, 60 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf31b6f..501ed14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). --- -## [Unreleased] – branch: `fix/execute-now-cleanup-retry` +## [5.2.0] – branch: `feature/minor-improvements` ### Fixed @@ -15,6 +15,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - **CrontabManager – `getOnceEntrySchedule()`:** Neue Methode liest den Zeitplan-String (`{min} {hour} {dom} {month} *`) aus dem Crontab-Eintrag, der auf den once-Marker des angegebenen Jobs folgt. Wird von `ExecuteNowEndpoint::isScheduleInPast()` genutzt um veraltete Einträge zu erkennen. - **Tests – `tearDown()`-Guard:** `CronBulkDeleteEndpointTest` und `ExecuteNowEndpointTest` initialisierten `$crontabDir` nicht mit einem Default-Wert; wenn `parent::setUp()` wegen fehlender Test-DB via `markTestSkipped()` abbrach, führte `tearDown()` zu einem Typed-Property-Initialisierungsfehler. Beide Properties werden jetzt mit `= ''` initialisiert und `rmdirRecursive()` nur aufgerufen wenn der Wert nicht leer ist. - **ExecuteNowEndpointTest – Szenario 4:** Neuer Integrationstest deckt den Auto-Clean-Pfad ab: staler once-Eintrag mit Zeitplan 1 Stunde in der Vergangenheit + abgeschlossener `execution_log`-Row (exit code 126) → Endpoint gibt 200 zurück, schreibt einen neuen once-Eintrag, entfernt den alten. +- **ExportController – HMAC-Signatur für `/export/download`:** Der Download-Pfad baut die Guzzle-Anfrage manuell (Streaming). Die Signatur verwendete das alte Format ohne `"\0" + userId + "\0" + username`-Felder, die seit v4.3.0 Pflicht sind. Alle Anfragen wurden deshalb mit HTTP 401 abgewiesen und führten zu einem 503 im Browser. Signatur entspricht jetzt dem in `HostAgentClient::sign()` dokumentierten Format. + +### Changed + +- **Job-Liste (`/crons`) – „Läuft"-Badge:** Wenn ein Job aktuell ausgeführt wird (`finished_at IS NULL` auf dem letzten `execution_log`-Eintrag), zeigt die Spalte „Exit-Code" jetzt einen blauen „Läuft"-Badge anstelle des letzten Exit-Codes. `CronListEndpoint` liefert dafür das neue Feld `is_running` (abgeleitet aus `el_last.finished_at`). Der bisher angezeigte Wert war der Exit-Code der letzten *abgeschlossenen* Ausführung — irreführend wenn der Job gerade läuft. +- **Einzel-Benutzer-Modus (Docker):** Alle User-spezifischen UI-Elemente werden ausgeblendet, wenn der Agent nur einen Linux-User kennt (`count(unique linux_users) <= 1`). Betrifft: User-Filter-Dropdown auf `/crons`, `/timeline` und `/export`, `linux_user`-Spalte in den Tabellen auf `/crons` und `/timeline`, `linux_user`-Spalte in der Fehlertabelle auf dem Dashboard, gesamter „Jobs nach Benutzer"-Block auf dem Dashboard. Die Entscheidung trifft der jeweilige Controller anhand der bereits vorhandenen User-Liste; kein neues API-Feld oder Konfigurationsschalter erforderlich. Fallback `true` in den Templates stellt sicher, dass die Elemente bei fehlendem Wert sichtbar bleiben. --- diff --git a/agent/VERSION b/agent/VERSION index 831446c..7cbea07 100644 --- a/agent/VERSION +++ b/agent/VERSION @@ -1 +1 @@ -5.1.0 +5.2.0 \ No newline at end of file diff --git a/agent/src/Endpoints/CronListEndpoint.php b/agent/src/Endpoints/CronListEndpoint.php index ccd66c3..b04a488 100644 --- a/agent/src/Endpoints/CronListEndpoint.php +++ b/agent/src/Endpoints/CronListEndpoint.php @@ -258,6 +258,7 @@ private function fetchJobs(?string $userFilter, ?string $tagFilter, ?string $tar GROUP_CONCAT(DISTINCT t.name ORDER BY t.name SEPARATOR ',') AS tags, GROUP_CONCAT(DISTINCT jt.target ORDER BY jt.target SEPARATOR ',') AS targets, el_last.started_at AS last_run, + el_last.finished_at AS last_finished_at, el_last_fin.exit_code AS last_exit_code FROM cronjobs j LEFT JOIN cronjob_tags ct ON ct.cronjob_id = j.id @@ -370,6 +371,8 @@ private function normaliseRow(array $row): array : null, 'last_run' => isset($row['last_run']) && $row['last_run'] !== null ? (string) $row['last_run'] : null, 'last_exit_code' => isset($row['last_exit_code']) && $row['last_exit_code'] !== null ? (int) $row['last_exit_code'] : null, + 'is_running' => isset($row['last_run']) && $row['last_run'] !== null + && (!isset($row['last_finished_at']) || $row['last_finished_at'] === null), ]; } } diff --git a/web/VERSION b/web/VERSION index 831446c..7cbea07 100644 --- a/web/VERSION +++ b/web/VERSION @@ -1 +1 @@ -5.1.0 +5.2.0 \ No newline at end of file diff --git a/web/src/Controller/CronController.php b/web/src/Controller/CronController.php index 902eff1..6ce87ac 100644 --- a/web/src/Controller/CronController.php +++ b/web/src/Controller/CronController.php @@ -244,6 +244,7 @@ public function index(array $params): void 'filterResult' => $filterResult, 'filterActive' => $filterActive, 'users' => $users, + 'multiUser' => count($users) > 1, 'allTargets' => $allTargets, 'isAdmin' => SessionManager::hasRole('admin'), 'pageSize' => $pageSize, diff --git a/web/src/Controller/DashboardController.php b/web/src/Controller/DashboardController.php index bd3cd8d..eb2930e 100644 --- a/web/src/Controller/DashboardController.php +++ b/web/src/Controller/DashboardController.php @@ -150,6 +150,7 @@ public function index(array $params): void 'recentFailures' => $recentFailures, 'tags' => $tags, 'stats' => $stats, + 'multiUser' => count($byUser) > 1, ], '/dashboard'); } } diff --git a/web/src/Controller/ExportController.php b/web/src/Controller/ExportController.php index 7b608d7..4e0c3ba 100644 --- a/web/src/Controller/ExportController.php +++ b/web/src/Controller/ExportController.php @@ -79,8 +79,9 @@ public function index(array $params): void )); $this->render('export.php', $this->translator()->t('export_title'), [ - 'tags' => $tags, - 'users' => $users, + 'tags' => $tags, + 'users' => $users, + 'multiUser' => count($users) > 1, ], '/export'); } @@ -128,7 +129,13 @@ public function download(array $params): void $secret = (string) ($agent['hmac_secret'] ?? ''); $path = '/export'; $queryString = '?' . http_build_query($query); - $signature = hash_hmac('sha256', 'GET' . $path . '', $secret); + $userId = \Cronmanager\Web\Session\SessionManager::getUserId() ?? 0; + $userName = \Cronmanager\Web\Session\SessionManager::getUsername() ?? ''; + $signature = hash_hmac( + 'sha256', + 'GET' . $path . '' . "\0" . $userId . "\0" . $userName, + $secret + ); $sslVerify = (bool) ($agent['ssl_verify'] ?? true); $caBundle = (string) ($agent['ssl_ca_bundle'] ?? ''); diff --git a/web/src/Controller/TimelineController.php b/web/src/Controller/TimelineController.php index 5e168ef..a6e06ef 100644 --- a/web/src/Controller/TimelineController.php +++ b/web/src/Controller/TimelineController.php @@ -179,6 +179,7 @@ public function index(array $params): void 'history' => $history, 'tags' => $tags, 'users' => $users, + 'multiUser' => count($users) > 1, 'allTargets' => $allTargets, 'total' => $total, 'limit' => $limit, diff --git a/web/templates/cron/list.php b/web/templates/cron/list.php index 5e488f6..10c2df7 100644 --- a/web/templates/cron/list.php +++ b/web/templates/cron/list.php @@ -32,6 +32,7 @@ $filterSearch = isset($filterSearch) ? (string) $filterSearch : ''; $filterResult = isset($filterResult) ? (string) $filterResult : ''; $filterActive = isset($filterActive) ? (string) $filterActive : ''; +$multiUser = isset($multiUser) ? (bool) $multiUser : true; $isAdmin = isset($isAdmin) && (bool) $isAdmin; $targetsInMaintenance = isset($targetsInMaintenance) && is_array($targetsInMaintenance) ? $targetsInMaintenance : []; @@ -181,7 +182,8 @@ class="w-full border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-2 t - + +
+ 1): ?> @@ -440,9 +443,11 @@ class="rounded border-gray-300 dark:border-gray-600 text-blue-600 + + @@ -476,6 +481,7 @@ class="rounded border-gray-300 dark:border-gray-600 text-blue-600 $crontabOk = !isset($job['crontab_ok']) || (bool) $job['crontab_ok']; $lastRun = (string) ($job['last_run'] ?? ''); $exitCode = isset($job['last_exit_code']) ? (int) $job['last_exit_code'] : null; + $isRunning = !empty($job['is_running']); $limitSeconds = isset($job['execution_limit_seconds']) && $job['execution_limit_seconds'] !== null ? (int) $job['execution_limit_seconds'] : null; @@ -483,7 +489,10 @@ class="rounded border-gray-300 dark:border-gray-600 text-blue-600 $runInMaint = !empty($job['run_in_maintenance']); // Exit code badge style - if ($exitCode === null) { + if ($isRunning) { + $exitBadge = '' + . htmlspecialchars($t('status_running'), ENT_QUOTES, 'UTF-8') . ''; + } elseif ($exitCode === null) { $exitBadge = ''; } elseif ($exitCode === -4) { $exitBadge = '' @@ -533,10 +542,12 @@ class="text-blue-600 hover:underline font-medium"> - + + + diff --git a/web/templates/dashboard.php b/web/templates/dashboard.php index a094db5..20f21a1 100644 --- a/web/templates/dashboard.php +++ b/web/templates/dashboard.php @@ -35,6 +35,7 @@ $tagsCount = (int) ($stats['tagsCount'] ?? 0); $failedLast24h = (int) ($stats['failedLast24h'] ?? 0); $byUser = (array) ($stats['byUser'] ?? []); +$multiUser = isset($multiUser) ? (bool) $multiUser : true; ?> + +

@@ -285,6 +291,7 @@ class="text-blue-600 hover:underline">

+ diff --git a/web/templates/export.php b/web/templates/export.php index 853b226..a50e9c0 100644 --- a/web/templates/export.php +++ b/web/templates/export.php @@ -19,8 +19,9 @@ /** @var \Cronmanager\Web\I18n\Translator $translator */ $t = fn(string $k, array $r = []): string => $translator->t($k, $r); -$tags = isset($tags) && is_array($tags) ? $tags : []; -$users = isset($users) && is_array($users) ? $users : []; +$tags = isset($tags) && is_array($tags) ? $tags : []; +$users = isset($users) && is_array($users) ? $users : []; +$multiUser = isset($multiUser) ? (bool) $multiUser : true; ?>
+
diff --git a/web/templates/timeline.php b/web/templates/timeline.php index de7941e..cd782f1 100644 --- a/web/templates/timeline.php +++ b/web/templates/timeline.php @@ -28,6 +28,7 @@ $tags = isset($tags) && is_array($tags) ? $tags : []; $users = isset($users) && is_array($users) ? $users : []; $allTargets = isset($allTargets) && is_array($allTargets) ? $allTargets : []; +$multiUser = isset($multiUser) ? (bool) $multiUser : true; $total = isset($total) ? (int) $total : 0; $limit = isset($limit) ? max(1, (int) $limit) : 50; $offset = isset($offset) ? max(0, (int) $offset) : 0; @@ -160,7 +161,8 @@ class="w-full border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-2 t
- + +
+ 1): ?> @@ -317,9 +320,11 @@ class="text-sm text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:t Job + + @@ -396,9 +401,11 @@ class="text-blue-600 hover:underline font-medium"> + +
From 30766a24783108d30c9e180c2c643eaa9951c610 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 25 Aug 2026 22:16:28 +0200 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20Dashboard=20=E2=80=93=20Statistik-W?= =?UTF-8?q?idget,=20Ausgabe-Vorschau,=20Layout=203+1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Neuer Agent-Endpoint GET /stats: ausgeführte/fehlerhafte Ausführungen heute und letzte 24h via idx_el_started_at (INDEX-Range-Scan, kein FTS) - Dashboard-Grid: lg:grid-cols-4, Fehler-Tabelle lg:col-span-3, Stats-Widget lg:col-span-1 (unter 4. Kachel) - Ausgabe-Vorschau in Fehler-Tabelle: letzte 120 Zeichen, font-mono, Tooltip mit vollem Text; per $showOutputPreview abschaltbar - Jobs-nach-Benutzer in separate 3. Zeile (Multi-User-Modus) - Fehlerhafte Zähler im Widget rot hervorgehoben - Beide Features per Schalter sofort deaktivierbar: $showOutputPreview in dashboard.php SHOW_EXECUTION_STATS in DashboardController --- CHANGELOG.md | 3 + agent/agent.php | 5 + agent/src/Endpoints/StatsEndpoint.php | 94 ++++++++++++++++ web/lang/de.php | 6 + web/lang/en.php | 6 + web/src/Controller/DashboardController.php | 29 +++-- web/templates/dashboard.php | 125 ++++++++++++++++++--- 7 files changed, 244 insertions(+), 24 deletions(-) create mode 100644 agent/src/Endpoints/StatsEndpoint.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 501ed14..71cd970 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - **Job-Liste (`/crons`) – „Läuft"-Badge:** Wenn ein Job aktuell ausgeführt wird (`finished_at IS NULL` auf dem letzten `execution_log`-Eintrag), zeigt die Spalte „Exit-Code" jetzt einen blauen „Läuft"-Badge anstelle des letzten Exit-Codes. `CronListEndpoint` liefert dafür das neue Feld `is_running` (abgeleitet aus `el_last.finished_at`). Der bisher angezeigte Wert war der Exit-Code der letzten *abgeschlossenen* Ausführung — irreführend wenn der Job gerade läuft. - **Einzel-Benutzer-Modus (Docker):** Alle User-spezifischen UI-Elemente werden ausgeblendet, wenn der Agent nur einen Linux-User kennt (`count(unique linux_users) <= 1`). Betrifft: User-Filter-Dropdown auf `/crons`, `/timeline` und `/export`, `linux_user`-Spalte in den Tabellen auf `/crons` und `/timeline`, `linux_user`-Spalte in der Fehlertabelle auf dem Dashboard, gesamter „Jobs nach Benutzer"-Block auf dem Dashboard. Die Entscheidung trifft der jeweilige Controller anhand der bereits vorhandenen User-Liste; kein neues API-Feld oder Konfigurationsschalter erforderlich. Fallback `true` in den Templates stellt sicher, dass die Elemente bei fehlendem Wert sichtbar bleiben. +- **Dashboard – Layout-Überarbeitung:** Die zweite Zeile nutzt jetzt `lg:grid-cols-4`; die Fehler-Tabelle belegt 3/4 der Breite (`lg:col-span-3`), das neue Statistik-Widget 1/4 (`lg:col-span-1`). „Jobs nach Benutzer" (Multi-User-Modus) rutscht in eine separate dritte Zeile. +- **Dashboard – Ausgabe-Vorschau in der Fehler-Tabelle:** Die letzte Spalte zeigt die letzten 120 Zeichen des Job-Outputs als `font-mono`-Vorschau mit `title`-Tooltip (vollständiger Text). Lässt sich per `$showOutputPreview = false` am Anfang von `dashboard.php` sofort ausblenden. +- **Dashboard – Ausführungsstatistik-Widget:** Neues `GET /stats`-Endpoint auf dem Agent liefert via zwei INDEX-Range-Scans (`idx_el_started_at`) vier Zähler: ausgeführte und fehlerhafte Ausführungen jeweils für „heute" (seit Mitternacht) und „letzte 24 Stunden". Der Widget-Call wird in denselben `getMultiple()`-Batch wie die übrigen Dashboard-Requests eingebettet (kein Extra-Round-Trip). Lässt sich per `SHOW_EXECUTION_STATS = false` in `DashboardController` ausschalten — deaktiviert sowohl den API-Call als auch die Anzeige. --- diff --git a/agent/agent.php b/agent/agent.php index 70a2244..296e7e5 100644 --- a/agent/agent.php +++ b/agent/agent.php @@ -332,6 +332,11 @@ function jsonResponse(int $statusCode, array $data): void $history = new \Cronmanager\Agent\Endpoints\HistoryEndpoint($pdo, $logger); $router->addRoute('GET', '/history', [$history, 'handle']); + // -- Stats (dashboard widget) --------------------------------------------- + + $stats = new \Cronmanager\Agent\Endpoints\StatsEndpoint($pdo, $logger); + $router->addRoute('GET', '/stats', [$stats, 'handle']); + // -- Audit log ------------------------------------------------------------ $auditLog = new \Cronmanager\Agent\Endpoints\AuditLogEndpoint($pdo, $logger); diff --git a/agent/src/Endpoints/StatsEndpoint.php b/agent/src/Endpoints/StatsEndpoint.php new file mode 100644 index 0000000..4f4b0f5 --- /dev/null +++ b/agent/src/Endpoints/StatsEndpoint.php @@ -0,0 +1,94 @@ + + * @license GNU General Public License version 3 or later + */ + +namespace Cronmanager\Agent\Endpoints; + +use Monolog\Logger; +use PDO; + +/** + * Class StatsEndpoint + * + * Provides aggregate execution statistics for the dashboard widget. + * Two queries, both resolved via idx_el_started_at (index range scan). + */ +final class StatsEndpoint +{ + public function __construct( + private readonly PDO $pdo, + private readonly Logger $logger, + ) {} + + /** + * Handle GET /stats. + * + * @param array $params Unused path parameters. + * + * @return void + */ + public function handle(array $params): void + { + $this->logger->debug('StatsEndpoint: handling GET /stats'); + + try { + // Today: started_at >= CURDATE() uses idx_el_started_at (range scan) + $stmtToday = $this->pdo->query( + "SELECT + COUNT(*) AS total, + COALESCE(SUM(CASE WHEN exit_code NOT IN (0, -4) THEN 1 END), 0) AS failed + FROM execution_log + WHERE started_at >= CURDATE()" + ); + /** @var array{total:string,failed:string}|false $today */ + $today = $stmtToday !== false ? $stmtToday->fetch(PDO::FETCH_ASSOC) : false; + + // Last 24 h: different cut-off, same index + $stmt24h = $this->pdo->query( + "SELECT + COUNT(*) AS total, + COALESCE(SUM(CASE WHEN exit_code NOT IN (0, -4) THEN 1 END), 0) AS failed + FROM execution_log + WHERE started_at >= NOW() - INTERVAL 24 HOUR" + ); + /** @var array{total:string,failed:string}|false $last24h */ + $last24h = $stmt24h !== false ? $stmt24h->fetch(PDO::FETCH_ASSOC) : false; + } catch (\Throwable $e) { + $this->logger->error('StatsEndpoint: query failed', ['message' => $e->getMessage()]); + jsonResponse(500, ['error' => 'Internal Server Error', 'message' => 'Stats query failed.', 'code' => 500]); + return; + } + + jsonResponse(200, [ + 'executed_today' => $today !== false ? (int) $today['total'] : 0, + 'failed_today' => $today !== false ? (int) $today['failed'] : 0, + 'executed_24h' => $last24h !== false ? (int) $last24h['total'] : 0, + 'failed_24h' => $last24h !== false ? (int) $last24h['failed'] : 0, + ]); + } +} diff --git a/web/lang/de.php b/web/lang/de.php index 966de8d..4776088 100644 --- a/web/lang/de.php +++ b/web/lang/de.php @@ -152,6 +152,12 @@ 'dashboard_inactive' => 'Inaktiv', 'dashboard_recent_failures' => 'Aktuelle Fehler', 'dashboard_jobs_by_user' => 'Jobs nach Benutzer', + 'dashboard_exec_stats' => 'Ausführungsstatistik', + 'dashboard_exec_today' => 'Heute', + 'dashboard_exec_last_24h' => 'Letzte 24 Stunden', + 'dashboard_exec_executed' => 'Ausgeführt', + 'dashboard_exec_failed' => 'Fehlerhaft', + 'dashboard_output_preview' => 'Ausgabe', // ------------------------------------------------------------------------- // Cron-Jobs diff --git a/web/lang/en.php b/web/lang/en.php index 34050bb..12dd98b 100644 --- a/web/lang/en.php +++ b/web/lang/en.php @@ -152,6 +152,12 @@ 'dashboard_inactive' => 'Inactive', 'dashboard_recent_failures' => 'Recent Failures', 'dashboard_jobs_by_user' => 'Jobs by User', + 'dashboard_exec_stats' => 'Execution Stats', + 'dashboard_exec_today' => 'Today', + 'dashboard_exec_last_24h' => 'Last 24 Hours', + 'dashboard_exec_executed' => 'Executed', + 'dashboard_exec_failed' => 'Failed', + 'dashboard_output_preview' => 'Output', // ------------------------------------------------------------------------- // Cron jobs diff --git a/web/src/Controller/DashboardController.php b/web/src/Controller/DashboardController.php index eb2930e..72d44d9 100644 --- a/web/src/Controller/DashboardController.php +++ b/web/src/Controller/DashboardController.php @@ -29,6 +29,12 @@ */ class DashboardController extends BaseController { + /** + * Set to false to disable the execution-statistics widget and skip the + * GET /stats agent call entirely (instant rollback if performance issues arise). + */ + private const SHOW_EXECUTION_STATS = true; + // ------------------------------------------------------------------------- // Actions // ------------------------------------------------------------------------- @@ -66,18 +72,23 @@ public function index(array $params): void // Fetch data from the host agent (three requests in parallel) // ------------------------------------------------------------------ try { - // Dispatch all three GET requests concurrently via Guzzle promises, + // Dispatch all GET requests concurrently via Guzzle promises, // reducing wall-clock time from ~sum(latencies) to ~max(latency). - $results = $agent->getMultiple([ + $batch = [ 'crons' => ['path' => '/crons'], // Fetch more than needed so that filtering maintenance skips // still leaves enough entries after the -4 exit-code filter. 'history' => ['path' => '/history', 'query' => ['limit' => 50, 'status' => 'failed']], 'tags' => ['path' => '/tags'], - ]); + ]; + if (self::SHOW_EXECUTION_STATS) { + $batch['execstats'] = ['path' => '/stats']; + } + $results = $agent->getMultiple($batch); $jobs = $results['crons']['data'] ?? []; $recentFailures = $results['history']['data'] ?? []; $tags = $results['tags']['data'] ?? []; + $executionStats = self::SHOW_EXECUTION_STATS ? ($results['execstats'] ?? []) : []; } catch (\RuntimeException $e) { $this->logger->error('DashboardController: agent request failed', [ 'message' => $e->getMessage(), @@ -146,11 +157,13 @@ public function index(array $params): void // Render // ------------------------------------------------------------------ $this->render('dashboard.php', $this->translator()->t('dashboard_title'), [ - 'jobs' => $jobs, - 'recentFailures' => $recentFailures, - 'tags' => $tags, - 'stats' => $stats, - 'multiUser' => count($byUser) > 1, + 'jobs' => $jobs, + 'recentFailures' => $recentFailures, + 'tags' => $tags, + 'stats' => $stats, + 'multiUser' => count($byUser) > 1, + 'executionStats' => $executionStats, + 'showExecutionStats' => self::SHOW_EXECUTION_STATS, ], '/dashboard'); } } diff --git a/web/templates/dashboard.php b/web/templates/dashboard.php index 20f21a1..65d1b44 100644 --- a/web/templates/dashboard.php +++ b/web/templates/dashboard.php @@ -24,10 +24,16 @@ /** @var \Cronmanager\Web\I18n\Translator $translator */ $t = fn(string $k, array $r = []): string => $translator->t($k, $r); -$jobs = isset($jobs) && is_array($jobs) ? $jobs : []; -$recentFailures = isset($recentFailures) && is_array($recentFailures) ? $recentFailures : []; -$tags = isset($tags) && is_array($tags) ? $tags : []; -$stats = isset($stats) && is_array($stats) ? $stats : []; +// ── Feature toggles ────────────────────────────────────────────────────────── +// Set to false to instantly hide a feature without touching the controller. +$showOutputPreview = true; // Ausgabe-Vorschau in der Fehler-Tabelle + +$jobs = isset($jobs) && is_array($jobs) ? $jobs : []; +$recentFailures = isset($recentFailures) && is_array($recentFailures) ? $recentFailures : []; +$tags = isset($tags) && is_array($tags) ? $tags : []; +$stats = isset($stats) && is_array($stats) ? $stats : []; +$executionStats = isset($executionStats) && is_array($executionStats) ? $executionStats : []; +$showExecutionStats = isset($showExecutionStats) ? (bool) $showExecutionStats : false; $total = (int) ($stats['total'] ?? 0); $active = (int) ($stats['active'] ?? 0); @@ -128,12 +134,12 @@
-
+
- -
+ +

@@ -172,6 +178,11 @@ + + + + + @@ -187,6 +198,14 @@ $duration = isset($entry['duration_seconds']) ? round((float) $entry['duration_seconds'], 1) . 's' : '–'; + // Truncate output to last 120 chars for the preview column + $outputRaw = isset($entry['output']) ? trim((string) $entry['output']) : ''; + $outputPreview = ''; + if ($outputRaw !== '') { + $outputPreview = mb_strlen($outputRaw) > 120 + ? '…' . mb_substr($outputRaw, -120) + : $outputRaw; + } // Deep-link to Timeline pre-filtered for this specific job/target/status. // _direct=1 prevents saved date-range cookies from hiding the entry. $timelineParams = array_filter([ @@ -228,12 +247,23 @@ class="text-blue-600 hover:underline font-medium"> - + - + + + + + + + + + + + + @@ -242,9 +272,74 @@ class="text-blue-600 hover:underline font-medium">

- - -
+ + +
+
+

+ +

+
+
+ + +
+

+ +

+
+ + + + +
+
+ + + + + + +
+
+
+ +
+

+ +

+
+ + + + +
+
+ + + + + + +
+
+
+
+ + +
+ + + +
+

@@ -270,7 +365,6 @@ class="text-blue-600 hover:underline font-medium"> $count): ?> @@ -291,9 +385,8 @@ class="text-blue-600 hover:underline">

- -
+