SSR module: rebuilt on TYPO3 module layout, cards and core components

Ticket 0096-FE (aehm): the module now uses the core Module layout (DocHeader with reload and shortcut, module-body spacing), a Status card as description list with one badge style, an Actions card with core buttons and icons, help texts below the buttons and TYPO3 modal confirmations (ssr-module.js) instead of window.confirm, and three protocol cards. Purge protocol rows show a one-line time, the trigger as badge plus user, paths as count + first two + disclosure, result and duration right-aligned, with 30/100/500 row switches. Status shows the last health sample with relative age and the cache limit (new optional SSR_CACHE_LIMIT_MB in var/vitec-ssr-purge.env, SsrPurgeService::getCacheLimitMb). ssr-warm-progress.js now also disables the Warm button with a spinner while a run is active. backend-ssr.css holds only layout glue with core CSS variables (dark mode safe). Verified in the backend: layout, modal, running progress bar, protocol tables.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
orascheandClaude Fable 5.1 committed 2026-11-11 10:15:42 +01:00
1 parent 07f74e372b
commit 086a178341
6 files changed
+477 -192

No files matched your search

@@ -0,0 +1,55 @@
/* VITEC SSR module (web_vitecssr): layout glue on top of TYPO3's own
components - cards, badges, buttons and tables come from the core. */
.ssr-status {
margin: 0;
}
.ssr-status-row {
display: grid;
grid-template-columns: 9rem minmax(0, 1fr);
gap: 0 1rem;
padding: .75rem 0;
border-bottom: 1px solid var(--typo3-component-border-color, #dee2e6);
}
.ssr-status-row:first-child {
padding-top: 0;
}
.ssr-status-row:last-child {
border-bottom: 0;
padding-bottom: 0;
}
.ssr-status-row dt {
font-weight: normal;
color: var(--typo3-text-color-secondary, #6c757d);
}
.ssr-status-row dd {
margin: 0;
min-width: 0;
}
@media (max-width: 575.98px) {
.ssr-status-row {
grid-template-columns: minmax(0, 1fr);
gap: .25rem 0;
}
}
.ssr-action + .ssr-action {
margin-top: 1.5rem;
padding-top: 1.5rem;
border-top: 1px solid var(--typo3-component-border-color, #dee2e6);
}
.ssr-action-title {
font-size: 1rem;
font-weight: 600;
margin: 0 0 .5rem;
}
#ssr-warm-button .ssr-warm-busy {
display: none;
}
#ssr-warm-button.is-running .ssr-warm-idle {
display: none;
}
#ssr-warm-button.is-running .ssr-warm-busy {
display: inline;
}
@@ -0,0 +1,42 @@
/**
* VITEC SSR module: TYPO3 modal confirmation for the destructive / heavy
* actions (Purge everything, Warm cache) instead of window.confirm.
* A form opts in with data-confirm="text" (+ data-confirm-title,
* data-confirm-ok, data-confirm-severity="danger|primary").
*/
import Modal from '@typo3/backend/modal.js';
import Severity from '@typo3/backend/severity.js';
document.querySelectorAll('form[data-confirm]').forEach((form) => {
form.addEventListener('submit', (event) => {
if (form.dataset.confirmed === '1') {
return;
}
event.preventDefault();
const danger = form.dataset.confirmSeverity === 'danger';
Modal.confirm(
form.dataset.confirmTitle || 'Please confirm',
form.dataset.confirm,
danger ? Severity.error : Severity.warning,
[
{
text: 'Cancel',
btnClass: 'btn-default',
name: 'cancel',
trigger: (e, modal) => modal.hideModal(),
},
{
text: form.dataset.confirmOk || 'Continue',
btnClass: danger ? 'btn-danger' : 'btn-primary',
name: 'ok',
active: true,
trigger: (e, modal) => {
modal.hideModal();
form.dataset.confirmed = '1';
form.requestSubmit();
},
},
],
);
});
});
@@ -3,7 +3,8 @@
* 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.
* Also refreshes the "Cache: N pages / M MB" line and disables the
* "Warm cache" button while a run is active.
*/
import AjaxRequest from '@typo3/core/ajax/ajax-request.js';
@@ -13,6 +14,7 @@ if (cell) {
const bar = document.getElementById('ssr-warm-bar');
const text = document.getElementById('ssr-warm-text');
const cacheLine = document.getElementById('ssr-cache-line');
const warmButton = document.getElementById('ssr-warm-button');
let timer = null;
const num = (v) => (typeof v === 'number' ? v : 0);
@@ -33,11 +35,11 @@ if (cell) {
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>';
+ '<div class="form-text">Live - updates every 2 seconds.</div>';
} else if (w.startedAt) {
wrap.style.display = 'none';
const finished = w.finishedAt
? ' <small class="text-muted">finished ' + new Date(w.finishedAt).toLocaleTimeString() + '</small>'
? ' <span class="text-body-secondary">finished ' + new Date(w.finishedAt).toLocaleTimeString() + '</span>'
: '';
text.innerHTML = 'Last run: ' + num(w.done) + ' of ' + queued + ' pages rendered ('
+ num(w.skipped) + ' still fresh, ' + failed + ')'
@@ -45,11 +47,17 @@ if (cell) {
+ finished;
} else {
wrap.style.display = 'none';
text.innerHTML = '<span class="text-muted">no run since the server started</span>';
text.innerHTML = '<span class="text-body-secondary">no run since the server started</span>';
}
if (cacheLine && typeof h.cachedPages === 'number') {
cacheLine.textContent = 'Cache: ' + h.cachedPages + ' pages / ' + num(h.cacheMB) + ' MB';
const limit = parseInt(cacheLine.dataset.limit || '0', 10);
cacheLine.textContent = 'Cache: ' + h.cachedPages + ' pages · ' + num(h.cacheMB) + ' MB'
+ (limit > 0 ? ' of ' + limit + ' MB' : '');
}
if (warmButton) {
warmButton.disabled = !!w.running;
warmButton.classList.toggle('is-running', !!w.running);
}
return !!w.running;
};