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 <noreply@anthropic.com>
This commit is contained in:
1 parent
f4f18e913a
commit
da2ca2690d
4 files changed
+136
-26
No files matched your search
@@ -19,6 +19,7 @@ use TYPO3\CMS\Core\Http\RequestFactory;
|
|||||||
use TYPO3\CMS\Core\Messaging\FlashMessage;
|
use TYPO3\CMS\Core\Messaging\FlashMessage;
|
||||||
use TYPO3\CMS\Core\Site\SiteFinder;
|
use TYPO3\CMS\Core\Site\SiteFinder;
|
||||||
use TYPO3\CMS\Core\Messaging\FlashMessageService;
|
use TYPO3\CMS\Core\Messaging\FlashMessageService;
|
||||||
|
use TYPO3\CMS\Core\Page\PageRenderer;
|
||||||
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
|
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
|
||||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
|
||||||
@@ -36,6 +37,7 @@ final class SsrController
|
|||||||
private readonly ConnectionPool $connectionPool,
|
private readonly ConnectionPool $connectionPool,
|
||||||
private readonly FlashMessageService $flashMessageService,
|
private readonly FlashMessageService $flashMessageService,
|
||||||
private readonly UriBuilder $uriBuilder,
|
private readonly UriBuilder $uriBuilder,
|
||||||
|
private readonly PageRenderer $pageRenderer,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function indexAction(ServerRequestInterface $request): ResponseInterface
|
public function indexAction(ServerRequestInterface $request): ResponseInterface
|
||||||
@@ -43,29 +45,8 @@ final class SsrController
|
|||||||
$view = $this->moduleTemplateFactory->create($request);
|
$view = $this->moduleTemplateFactory->create($request);
|
||||||
$view->setTitle('VITEC SSR');
|
$view->setTitle('VITEC SSR');
|
||||||
|
|
||||||
$health = null;
|
[$health, $healthError] = $this->fetchHealth();
|
||||||
$healthError = '';
|
$this->pageRenderer->loadJavaScriptModule('@evomedien/vitec/ssr-warm-progress.js');
|
||||||
$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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$view->assignMultiple([
|
$view->assignMultiple([
|
||||||
'flagOn' => file_exists(Environment::getVarPath() . '/vitec-ssr-on.flag'),
|
'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
|
private function rows(string $table, int $limit): array
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -33,4 +33,10 @@ return [
|
|||||||
'path' => '/vitec/ssr/purge-page',
|
'path' => '/vitec/ssr/purge-page',
|
||||||
'target' => \Evomedien\Vitec\Controller\Backend\SsrController::class . '::purgePage',
|
'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',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
<span class="badge badge-success">running</span> {health.responseMs} ms<br>
|
<span class="badge badge-success">running</span> {health.responseMs} ms<br>
|
||||||
Release <strong>{health.release.release}</strong>
|
Release <strong>{health.release.release}</strong>
|
||||||
(commit {health.release.commit})<br>
|
(commit {health.release.commit})<br>
|
||||||
Cache: {health.cachedPages} pages / {health.cacheMB} MB
|
<span id="ssr-cache-line">Cache: {health.cachedPages} pages / {health.cacheMB} MB</span>
|
||||||
</f:then>
|
</f:then>
|
||||||
<f:else>
|
<f:else>
|
||||||
<span class="badge badge-danger">not reachable</span> {healthError}
|
<span class="badge badge-danger">not reachable</span> {healthError}
|
||||||
@@ -34,13 +34,17 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Cache warming</th>
|
<th>Cache warming</th>
|
||||||
<td>
|
<td id="ssr-warm-status" data-running="{f:if(condition: health.warming.running, then: '1', else: '0')}">
|
||||||
|
<div class="progress" id="ssr-warm-progress" style="height:16px;margin-bottom:6px;{f:if(condition: health.warming.running, else: 'display:none')}">
|
||||||
|
<div class="progress-bar progress-bar-striped progress-bar-animated bg-info" id="ssr-warm-bar" role="progressbar" style="width:0%;min-width:3em">0 %</div>
|
||||||
|
</div>
|
||||||
|
<div id="ssr-warm-text">
|
||||||
<f:if condition="{health.warming.running}">
|
<f:if condition="{health.warming.running}">
|
||||||
<f:then>
|
<f:then>
|
||||||
<span class="badge badge-info">running</span>
|
<span class="badge badge-info">running</span>
|
||||||
{health.warming.done} of {health.warming.queued} pages rendered
|
{health.warming.done} of {health.warming.queued} pages rendered
|
||||||
({health.warming.skipped} still fresh, {health.warming.failed} failed)
|
({health.warming.skipped} still fresh, {health.warming.failed} failed)
|
||||||
<br><small>Reload this module to follow the progress.</small>
|
<br><small class="text-muted">Live — updates every 2 seconds.</small>
|
||||||
</f:then>
|
</f:then>
|
||||||
<f:else>
|
<f:else>
|
||||||
<f:if condition="{health.warming.startedAt}">
|
<f:if condition="{health.warming.startedAt}">
|
||||||
@@ -57,6 +61,7 @@
|
|||||||
</f:if>
|
</f:if>
|
||||||
</f:else>
|
</f:else>
|
||||||
</f:if>
|
</f:if>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
? '<span class="badge badge-danger">' + num(w.failed) + ' failed</span>'
|
||||||
|
: '0 failed';
|
||||||
|
|
||||||
|
if (w.running) {
|
||||||
|
wrap.style.display = '';
|
||||||
|
bar.style.width = pct + '%';
|
||||||
|
bar.textContent = pct + ' %';
|
||||||
|
text.innerHTML = '<span class="badge badge-info">running</span> '
|
||||||
|
+ handled + ' of ' + queued + ' pages ('
|
||||||
|
+ num(w.done) + ' rendered, ' + num(w.skipped) + ' still fresh, ' + failed + ')'
|
||||||
|
+ '<br><small class="text-muted">Live - updates every 2 seconds.</small>';
|
||||||
|
} else if (w.startedAt) {
|
||||||
|
wrap.style.display = 'none';
|
||||||
|
const finished = w.finishedAt
|
||||||
|
? ' <small class="text-muted">finished ' + new Date(w.finishedAt).toLocaleTimeString() + '</small>'
|
||||||
|
: '';
|
||||||
|
text.innerHTML = 'Last run: ' + num(w.done) + ' of ' + queued + ' pages rendered ('
|
||||||
|
+ num(w.skipped) + ' still fresh, ' + failed + ')'
|
||||||
|
+ (w.aborted ? ' <span class="badge badge-warning">aborted by restart</span>' : '')
|
||||||
|
+ finished;
|
||||||
|
} else {
|
||||||
|
wrap.style.display = 'none';
|
||||||
|
text.innerHTML = '<span class="text-muted">no run since the server started</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in new issue
Block a user