From da2ca2690df9e574291c3e5fb7d97f72642da996 Mon Sep 17 00:00:00 2001 From: Oliver Rasche Date: Tue, 10 Nov 2026 19:40:46 +0100 Subject: [PATCH] SSR module: live progress bar for cache warming The Cache warming row of the VITEC SSR module now shows a Bootstrap progress bar with a running counter instead of asking for a reload. New ajax route vitec_ssr_health (SsrController::healthJson) returns the node /__health JSON; ssr-warm-progress.js polls it every 2 s while a warm run is active and every 15 s otherwise (pauses in background tabs) and also refreshes the Cache: N pages / M MB line. The health fetch moved into SsrController::fetchHealth, shared by indexAction and the ajax route; its error text is English now. Verified live in the backend: bar, counter and cache line advance during a run. Co-Authored-By: Claude Fable 5.1 --- .../Controller/Backend/SsrController.php | 65 +++++++++------ .../Configuration/Backend/AjaxRoutes.php | 6 ++ .../Private/Templates/Ssr/Index.html | 11 ++- .../Public/Javascript/ssr-warm-progress.js | 80 +++++++++++++++++++ 4 files changed, 136 insertions(+), 26 deletions(-) create mode 100644 packages/vitec/Resources/Public/Javascript/ssr-warm-progress.js diff --git a/packages/vitec/Classes/Controller/Backend/SsrController.php b/packages/vitec/Classes/Controller/Backend/SsrController.php index 3c8ff6c..9ddf537 100644 --- a/packages/vitec/Classes/Controller/Backend/SsrController.php +++ b/packages/vitec/Classes/Controller/Backend/SsrController.php @@ -19,6 +19,7 @@ use TYPO3\CMS\Core\Http\RequestFactory; use TYPO3\CMS\Core\Messaging\FlashMessage; use TYPO3\CMS\Core\Site\SiteFinder; use TYPO3\CMS\Core\Messaging\FlashMessageService; +use TYPO3\CMS\Core\Page\PageRenderer; use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -36,6 +37,7 @@ final class SsrController private readonly ConnectionPool $connectionPool, private readonly FlashMessageService $flashMessageService, private readonly UriBuilder $uriBuilder, + private readonly PageRenderer $pageRenderer, ) {} public function indexAction(ServerRequestInterface $request): ResponseInterface @@ -43,29 +45,8 @@ final class SsrController $view = $this->moduleTemplateFactory->create($request); $view->setTitle('VITEC SSR'); - $health = null; - $healthError = ''; - $endpoint = $this->purgeService->getHealthEndpoint(); - if ($endpoint === '') { - $healthError = 'var/vitec-ssr-purge.env fehlt oder ist unvollständig.'; - } else { - $start = microtime(true); - try { - $response = $this->requestFactory->request($endpoint, 'GET', [ - 'timeout' => 3, 'connect_timeout' => 2, 'http_errors' => false, - ]); - if ($response->getStatusCode() === 200) { - $health = json_decode((string)$response->getBody(), true) ?: null; - if ($health !== null) { - $health['responseMs'] = (int)round((microtime(true) - $start) * 1000); - } - } else { - $healthError = 'HTTP ' . $response->getStatusCode(); - } - } catch (\Throwable $e) { - $healthError = $e->getMessage(); - } - } + [$health, $healthError] = $this->fetchHealth(); + $this->pageRenderer->loadJavaScriptModule('@evomedien/vitec/ssr-warm-progress.js'); $view->assignMultiple([ 'flagOn' => file_exists(Environment::getVarPath() . '/vitec-ssr-on.flag'), @@ -186,6 +167,44 @@ final class SsrController ]); } + /** + * GET /__health of the SSR node. Returns [health array|null, error string]. + */ + private function fetchHealth(): array + { + $endpoint = $this->purgeService->getHealthEndpoint(); + if ($endpoint === '') { + return [null, 'var/vitec-ssr-purge.env missing or incomplete.']; + } + $start = microtime(true); + try { + $response = $this->requestFactory->request($endpoint, 'GET', [ + 'timeout' => 3, 'connect_timeout' => 2, 'http_errors' => false, + ]); + if ($response->getStatusCode() !== 200) { + return [null, 'HTTP ' . $response->getStatusCode()]; + } + $health = json_decode((string)$response->getBody(), true) ?: null; + if ($health === null) { + return [null, 'invalid JSON from /__health']; + } + $health['responseMs'] = (int)round((microtime(true) - $start) * 1000); + return [$health, '']; + } catch (\Throwable $e) { + return [null, $e->getMessage()]; + } + } + + /** + * Ajax route vitec_ssr_health: live status for the module's + * "Cache warming" progress bar (ssr-warm-progress.js polls it). + */ + public function healthJson(ServerRequestInterface $request): ResponseInterface + { + [$health, $error] = $this->fetchHealth(); + return new JsonResponse(['health' => $health, 'error' => $error]); + } + private function rows(string $table, int $limit): array { try { diff --git a/packages/vitec/Configuration/Backend/AjaxRoutes.php b/packages/vitec/Configuration/Backend/AjaxRoutes.php index d62acd4..1b544d5 100755 --- a/packages/vitec/Configuration/Backend/AjaxRoutes.php +++ b/packages/vitec/Configuration/Backend/AjaxRoutes.php @@ -33,4 +33,10 @@ return [ 'path' => '/vitec/ssr/purge-page', 'target' => \Evomedien\Vitec\Controller\Backend\SsrController::class . '::purgePage', ], + // Live "Cache warming" progress bar in the VITEC SSR module + // (Resources/Public/Javascript/ssr-warm-progress.js polls it). + 'vitec_ssr_health' => [ + 'path' => '/vitec/ssr/health', + 'target' => \Evomedien\Vitec\Controller\Backend\SsrController::class . '::healthJson', + ], ]; diff --git a/packages/vitec/Resources/Private/Templates/Ssr/Index.html b/packages/vitec/Resources/Private/Templates/Ssr/Index.html index 6eb6222..de889d9 100644 --- a/packages/vitec/Resources/Private/Templates/Ssr/Index.html +++ b/packages/vitec/Resources/Private/Templates/Ssr/Index.html @@ -24,7 +24,7 @@ running {health.responseMs} ms
Release {health.release.release} (commit {health.release.commit})
- Cache: {health.cachedPages} pages / {health.cacheMB} MB + Cache: {health.cachedPages} pages / {health.cacheMB} MB not reachable {healthError} @@ -34,13 +34,17 @@ Cache warming - + +
+
0 %
+
+
running {health.warming.done} of {health.warming.queued} pages rendered ({health.warming.skipped} still fresh, {health.warming.failed} failed) -
Reload this module to follow the progress. +
Live — updates every 2 seconds.
@@ -57,6 +61,7 @@
+
diff --git a/packages/vitec/Resources/Public/Javascript/ssr-warm-progress.js b/packages/vitec/Resources/Public/Javascript/ssr-warm-progress.js new file mode 100644 index 0000000..98c873c --- /dev/null +++ b/packages/vitec/Resources/Public/Javascript/ssr-warm-progress.js @@ -0,0 +1,80 @@ +/** + * Live progress for the "Cache warming" row of the VITEC SSR module. + * Polls the vitec_ssr_health ajax route (GET /__health of the node): + * every 2 s while a warm run is active, every 15 s otherwise so runs + * started from the CLI or the context menu show up without a reload. + * Also refreshes the "Cache: N pages / M MB" line. + */ +import AjaxRequest from '@typo3/core/ajax/ajax-request.js'; + +const cell = document.getElementById('ssr-warm-status'); +if (cell) { + const wrap = document.getElementById('ssr-warm-progress'); + const bar = document.getElementById('ssr-warm-bar'); + const text = document.getElementById('ssr-warm-text'); + const cacheLine = document.getElementById('ssr-cache-line'); + let timer = null; + + const num = (v) => (typeof v === 'number' ? v : 0); + + const render = (h) => { + const w = h.warming || {}; + const queued = num(w.queued); + const handled = num(w.done) + num(w.skipped) + num(w.failed); + const pct = queued > 0 ? Math.min(100, Math.round((handled / queued) * 100)) : 0; + const failed = num(w.failed) + ? '' + num(w.failed) + ' failed' + : '0 failed'; + + if (w.running) { + wrap.style.display = ''; + bar.style.width = pct + '%'; + bar.textContent = pct + ' %'; + text.innerHTML = 'running ' + + handled + ' of ' + queued + ' pages (' + + num(w.done) + ' rendered, ' + num(w.skipped) + ' still fresh, ' + failed + ')' + + '
Live - updates every 2 seconds.'; + } else if (w.startedAt) { + wrap.style.display = 'none'; + const finished = w.finishedAt + ? ' finished ' + new Date(w.finishedAt).toLocaleTimeString() + '' + : ''; + text.innerHTML = 'Last run: ' + num(w.done) + ' of ' + queued + ' pages rendered (' + + num(w.skipped) + ' still fresh, ' + failed + ')' + + (w.aborted ? ' aborted by restart' : '') + + finished; + } else { + wrap.style.display = 'none'; + text.innerHTML = 'no run since the server started'; + } + + if (cacheLine && typeof h.cachedPages === 'number') { + cacheLine.textContent = 'Cache: ' + h.cachedPages + ' pages / ' + num(h.cacheMB) + ' MB'; + } + return !!w.running; + }; + + const schedule = (ms) => { + clearTimeout(timer); + timer = setTimeout(tick, ms); + }; + + const tick = () => { + new AjaxRequest(TYPO3.settings.ajaxUrls.vitec_ssr_health).get() + .then((response) => response.resolve()) + .then((data) => { + const running = data && data.health ? render(data.health) : false; + schedule(running ? 2000 : 15000); + }) + .catch(() => schedule(15000)); + }; + + schedule(cell.dataset.running === '1' ? 1000 : 15000); + document.addEventListener('visibilitychange', () => { + if (document.hidden) { + clearTimeout(timer); + } else { + tick(); + } + }); +}