-
Purge these paths
+
+
+
+
-
-
+
+
+
+
SSR delivery
+
+
+ ON pages are served from the VPS
+ OFF SPA as before, TYPO3 serves the shell
+
+ Toggle: Site Management › Settings › VITEC › SSR delivery . Switching on purges the node cache once.
+
+
+
+
SSR server
+
+
+
+ running
+ {health.responseMs} ms
+ Release {health.release.release}
+ commit {health.release.commit}
+ Cache: {health.cachedPages} pages · {health.cacheMB} MB of {cacheLimitMb} MB
+
+
+ not reachable
+ {healthError}
+ After {failoverAfter} failed checks in a row the cron switches delivery back to the SPA.
+
+
+
+
+
+
Cache warming
+
+
+
+
+
+ running
+ {health.warming.done} of {health.warming.queued} pages rendered
+ ({health.warming.skipped} still fresh, {health.warming.failed} failed)
+ Live — updates every 2 seconds.
+
+
+
+
+ Last run: {health.warming.done} of {health.warming.queued} pages rendered
+ ({health.warming.skipped} still fresh,
+
+ {health.warming.failed} failed
+ 0 failed
+ )
+ aborted by restart
+
+ no run since the server started
+
+
+
+
+
+
+
+
Health check
+
+
+
+
+ ok
+ down
+
+ {lastSampleAge} ago
+ · {lastSample.response_ms} ms · {lastSample.cached_pages} pages
+ Every 5 minutes via cron vitec:ssr-health; alert mail and automatic fallback to the SPA after repeated failures.
+
+ no samples yet
+
+
+
+
+
Purge webhook
+
+
+ configured endpoint and secret from var/vitec-ssr-purge.env
+ not configured var/vitec-ssr-purge.env is missing or incomplete
+
+
+
+
+
+
+
-
Warm cache
-
+
+
+
+
+
+
+
+ Purge everything
+
+ Purge everything
+ For menu or footer changes while auto-purge is off, and after a frontend deploy.
+
+
+
+ Warm cache
+
+ Warm cache
+ Warming …
+
+ Pre-renders every sitemap page so no visitor waits for a first render. Use it after “Purge everything” or a deploy; progress shows in the status card.
+
+
+
-
Purge protocol (last 30)
-
- Time Trigger Paths Result Duration
-
-
-
- @{row.crdate}
- {row.trigger_source}
- {row.paths}
-
-
- ok
- HTTP {row.http_code} {row.message}
-
-
- {row.duration_ms} ms
-
-
-
-
+
+
+
+
+
+
+ Time
+ Trigger
+ Paths
+ Result
+ Duration
+
+
+
+
+
+ @{row.crdate}
+
+ {row.trigger_label}
+ {row.trigger_who}
+
+
+
+ all (*)
+
+ {row.paths_count} paths
+ {p}
+
+
+ show all {row.paths_count}
+ {p}
+
+
+
+
+
+
+
+ ok
+ HTTP {row.http_code} {row.message}
+
+
+ {row.duration_ms} ms
+
+
+
+ No purges logged yet.
+
+
+
+
+
-
Health samples (last 10 — history via cron vitec:ssr-health, every 5 minutes)
-
- Time Status Release Cache Response time
-
-
-
- @{row.crdate}
-
-
- ok
- down
-
-
- {row.release_id}
- {row.cached_pages} pages / {row.cache_mb} MB
- {row.response_ms} ms
-
-
-
-
+
+
+
+
+ Browser-side failures reported by the React frontend via POST /api/vitec/client-error
+ (JS errors, unhandled promise rejections, element error boundaries and “chunk reload” after a deploy).
+ Limited to 30 reports per minute and IP, 8 KB per report; IPs are stored anonymized; kept for 30 days
+ (cron vitec:client-error-cleanup). A release older than the one running above means an open tab with an old bundle.
+
+
+
+
+
+ Time Release Message Page Browser IP
+
+
+
+
+ @{row.crdate}
+ {row.release}—
+
+
+ chunk reload
+ error
+
+ {row.message}
+
+ details
+
+ Reported @{row.crdate} (entry #{row.uid})
+ Release {row.release}not transmitted
+ URL {row.url} not transmitted
+ Message {row.message}
+ Stack
+
+ {row.stack}
+ no stack transmitted
+
+
+ Browser {row.user_agent}not transmitted
+ IP {row.ip} (anonymized)
+
+
+
+ {row.path}
+ {row.ua_short}
+ {row.ip}
+
+
+
+ No client errors reported in the last 24 hours.
+
+
+
+
+
-
Client errors (last 24 h — {clientErrorCount24h} reports , newest 50 shown)
-
- Browser-side failures reported by the React frontend via POST /api/vitec/client-error
- (JS errors, unhandled promise rejections, element error boundaries and "chunk reload" after a deploy).
- Limited to 30 reports per minute and IP, 8 KB per report; IPs are stored anonymized.
- Kept for 30 days (cron vitec:client-error-cleanup).
-
-
- Time Release Message Page Browser IP
-
-
-
- @{row.crdate}
- {row.release}
-
-
- chunk reload
- error
-
- {row.message}
-
- details
-
- Reported @{row.crdate} (entry #{row.uid})
- Release {row.release}not transmitted
- URL {row.url} not transmitted
- Message {row.message}
- Stack
-
- {row.stack}
- no stack transmitted
-
-
- Browser {row.user_agent}not transmitted
- IP {row.ip} (anonymized)
-
-
-
- {row.path}
- {row.ua_short}
- {row.ip}
-
-
-
- No client errors reported in the last 24 hours.
-
-
-
+
+
+
+
+
+ Time Status Release Cache Response time
+
+
+
+
+ @{row.crdate}
+
+
+ ok
+ down
+
+
+ {row.release_id}
+ {row.cached_pages} pages · {row.cache_mb} MB
+ {row.response_ms} ms
+
+
+
+ No samples yet.
+
+
+
+
+
+
diff --git a/packages/vitec/Resources/Public/Css/backend-ssr.css b/packages/vitec/Resources/Public/Css/backend-ssr.css
new file mode 100644
index 0000000..6752f91
--- /dev/null
+++ b/packages/vitec/Resources/Public/Css/backend-ssr.css
@@ -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;
+}
diff --git a/packages/vitec/Resources/Public/Javascript/ssr-module.js b/packages/vitec/Resources/Public/Javascript/ssr-module.js
new file mode 100644
index 0000000..8a5ce5c
--- /dev/null
+++ b/packages/vitec/Resources/Public/Javascript/ssr-module.js
@@ -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();
+ },
+ },
+ ],
+ );
+ });
+});
diff --git a/packages/vitec/Resources/Public/Javascript/ssr-warm-progress.js b/packages/vitec/Resources/Public/Javascript/ssr-warm-progress.js
index 98c873c..6caaea1 100644
--- a/packages/vitec/Resources/Public/Javascript/ssr-warm-progress.js
+++ b/packages/vitec/Resources/Public/Javascript/ssr-warm-progress.js
@@ -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 = '
running '
+ handled + ' of ' + queued + ' pages ('
+ num(w.done) + ' rendered, ' + num(w.skipped) + ' still fresh, ' + failed + ')'
- + '
Live - updates every 2 seconds. ';
+ + '
Live - updates every 2 seconds.
';
} else if (w.startedAt) {
wrap.style.display = 'none';
const finished = w.finishedAt
- ? '
finished ' + new Date(w.finishedAt).toLocaleTimeString() + ' '
+ ? '
finished ' + new Date(w.finishedAt).toLocaleTimeString() + ' '
: '';
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 = '
no run since the server started ';
+ text.innerHTML = '
no run since the server started ';
}
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;
};