Files
VITEC-website/packages/vitec/Resources/Private/Templates/Ssr/Index.html
T
orascheandClaude Fable 5.1 086a178341 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>
2026-11-11 10:15:42 +01:00

306 lines
19 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
xmlns:core="http://typo3.org/ns/TYPO3/CMS/Core/ViewHelpers"
data-namespace-typo3-fluid="true">
<f:layout name="Module" />
<f:section name="Content">
<h1>VITEC SSR — Control &amp; Protocol</h1>
<div class="row g-4">
<div class="col-xl-7 d-flex">
<div class="card w-100 mb-0">
<div class="card-header">
<h2 class="card-title">Status</h2>
</div>
<div class="card-body">
<dl class="ssr-status">
<div class="ssr-status-row">
<dt>SSR delivery</dt>
<dd>
<f:if condition="{flagOn}">
<f:then><span class="badge badge-success">ON</span> pages are served from the VPS</f:then>
<f:else><span class="badge badge-secondary">OFF</span> SPA as before, TYPO3 serves the shell</f:else>
</f:if>
<div class="form-text">Toggle: Site Management › Settings › VITEC › <em>SSR delivery</em>. Switching on purges the node cache once.</div>
</dd>
</div>
<div class="ssr-status-row">
<dt>SSR server</dt>
<dd>
<f:if condition="{health}">
<f:then>
<span class="badge badge-success">running</span>
<span class="text-body-secondary">{health.responseMs} ms</span>
<div class="mt-1">Release <code>{health.release.release}</code>
<span class="text-body-secondary">commit <code>{health.release.commit}</code></span></div>
<div id="ssr-cache-line" data-limit="{cacheLimitMb}">Cache: {health.cachedPages} pages · {health.cacheMB} MB<f:if condition="{cacheLimitMb}"> of {cacheLimitMb} MB</f:if></div>
</f:then>
<f:else>
<span class="badge badge-danger">not reachable</span>
<span class="text-body-secondary">{healthError}</span>
<div class="form-text">After {failoverAfter} failed checks in a row the cron switches delivery back to the SPA.</div>
</f:else>
</f:if>
</dd>
</div>
<div class="ssr-status-row">
<dt>Cache warming</dt>
<dd 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:then>
<span class="badge badge-info">running</span>
{health.warming.done} of {health.warming.queued} pages rendered
({health.warming.skipped} still fresh, {health.warming.failed} failed)
<div class="form-text">Live — updates every 2 seconds.</div>
</f:then>
<f:else>
<f:if condition="{health.warming.startedAt}">
<f:then>
Last run: {health.warming.done} of {health.warming.queued} pages rendered
({health.warming.skipped} still fresh,
<f:if condition="{health.warming.failed}">
<f:then><span class="badge badge-danger">{health.warming.failed} failed</span></f:then>
<f:else>0 failed</f:else>
</f:if>)
<f:if condition="{health.warming.aborted}"> <span class="badge badge-warning">aborted by restart</span></f:if>
</f:then>
<f:else><span class="text-body-secondary">no run since the server started</span></f:else>
</f:if>
</f:else>
</f:if>
</div>
</dd>
</div>
<div class="ssr-status-row">
<dt>Health check</dt>
<dd>
<f:if condition="{lastSample}">
<f:then>
<f:if condition="{lastSample.ok}">
<f:then><span class="badge badge-success">ok</span></f:then>
<f:else><span class="badge badge-danger">down</span></f:else>
</f:if>
<span title="{f:format.date(date: '@{lastSample.crdate}', format: 'Y-m-d H:i:s')}">{lastSampleAge} ago</span>
<span class="text-body-secondary">· {lastSample.response_ms} ms · {lastSample.cached_pages} pages</span>
<div class="form-text">Every 5 minutes via cron <code>vitec:ssr-health</code>; alert mail and automatic fallback to the SPA after repeated failures.</div>
</f:then>
<f:else><span class="text-body-secondary">no samples yet</span></f:else>
</f:if>
</dd>
</div>
<div class="ssr-status-row">
<dt>Purge webhook</dt>
<dd>
<f:if condition="{configured}">
<f:then><span class="badge badge-success">configured</span> <span class="text-body-secondary">endpoint and secret from <code>var/vitec-ssr-purge.env</code></span></f:then>
<f:else><span class="badge badge-warning">not configured</span> <span class="text-body-secondary"><code>var/vitec-ssr-purge.env</code> is missing or incomplete</span></f:else>
</f:if>
</dd>
</div>
</dl>
</div>
</div>
</div>
<div class="col-xl-5 d-flex">
<div class="card w-100 mb-0">
<div class="card-header">
<h2 class="card-title">Actions</h2>
</div>
<div class="card-body">
<form action="{f:be.uri(route: 'web_vitecssr.purge')}" method="post" class="ssr-action">
<h3 class="ssr-action-title">Purge paths</h3>
<label class="form-label" for="purge-paths">Paths</label>
<textarea class="form-control font-monospace" id="purge-paths" name="paths" rows="4"
placeholder="/products/aligo&#10;/insights/news"></textarea>
<div class="form-text">One path per line, e.g. <code>/products/aligo</code>. Drops those pages (all host and query variants) from the SSR cache; they are rendered again on their next request.</div>
<input type="hidden" name="mode" value="paths">
<button type="submit" class="btn btn-default mt-2"><core:icon identifier="actions-refresh" size="small" /> Purge these paths</button>
</form>
<form action="{f:be.uri(route: 'web_vitecssr.purge')}" method="post" class="ssr-action"
data-confirm-title="Purge the entire SSR cache?"
data-confirm="Every cached page is dropped and rendered again on its next request. Run “Warm cache” afterwards so visitors do not wait for the first render."
data-confirm-ok="Purge everything" data-confirm-severity="danger">
<h3 class="ssr-action-title">Purge everything</h3>
<input type="hidden" name="mode" value="all">
<button type="submit" class="btn btn-danger"><core:icon identifier="actions-delete" size="small" /> Purge everything</button>
<div class="form-text">For menu or footer changes while auto-purge is off, and after a frontend deploy.</div>
</form>
<form action="{f:be.uri(route: 'web_vitecssr.warm')}" method="post" class="ssr-action"
data-confirm-title="Warm the whole cache?"
data-confirm="Renders every sitemap page (about 850) in the background and sends a few thousand requests to TYPO3. Takes about one minute with a warm TYPO3 cache, up to ten minutes with a cold one."
data-confirm-ok="Start warming" data-confirm-severity="primary">
<h3 class="ssr-action-title">Warm cache</h3>
<button type="submit" class="btn btn-primary {f:if(condition: health.warming.running, then: 'is-running')}" id="ssr-warm-button" {f:if(condition: health.warming.running, then: 'disabled="disabled"')}>
<span class="ssr-warm-idle"><core:icon identifier="actions-play" size="small" /> Warm cache</span>
<span class="ssr-warm-busy"><core:icon identifier="spinner-circle" size="small" /> Warming …</span>
</button>
<div class="form-text">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.</div>
</form>
</div>
</div>
</div>
</div>
<div class="card mt-4">
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
<h2 class="card-title mb-0">Purge protocol</h2>
<div class="btn-group btn-group-sm" role="group" aria-label="Rows to show">
<a class="btn btn-default {f:if(condition: '{purgeLimit} == 30', then: 'active')}" href="{moduleUri}&amp;limit=30">30</a>
<a class="btn btn-default {f:if(condition: '{purgeLimit} == 100', then: 'active')}" href="{moduleUri}&amp;limit=100">100</a>
<a class="btn btn-default {f:if(condition: '{purgeLimit} == 500', then: 'active')}" href="{moduleUri}&amp;limit=500">500</a>
</div>
</div>
<div class="table-fit mb-0">
<table class="table table-striped table-hover mb-0">
<thead>
<tr>
<th class="col-nowrap">Time</th>
<th>Trigger</th>
<th>Paths</th>
<th class="text-end">Result</th>
<th class="text-end">Duration</th>
</tr>
</thead>
<tbody>
<f:for each="{purgeLog}" as="row">
<tr>
<td class="col-nowrap"><f:format.date format="d.m. H:i:s">@{row.crdate}</f:format.date></td>
<td class="col-nowrap">
<span class="badge {row.trigger_badge}">{row.trigger_label}</span>
<f:if condition="{row.trigger_who}"> <small class="text-body-secondary">{row.trigger_who}</small></f:if>
</td>
<td>
<f:if condition="{row.paths_all}">
<f:then><span class="badge badge-warning">all (*)</span></f:then>
<f:else>
<span class="text-body-secondary small">{row.paths_count} path<f:if condition="{row.paths_count} != 1">s</f:if></span>
<f:for each="{row.paths_first}" as="p"><code class="d-block">{p}</code></f:for>
<f:if condition="{row.paths_rest}">
<details class="small">
<summary class="text-body-secondary" style="cursor:pointer">show all {row.paths_count}</summary>
<f:for each="{row.paths_rest}" as="p"><code class="d-block">{p}</code></f:for>
</details>
</f:if>
</f:else>
</f:if>
</td>
<td class="text-end col-nowrap">
<f:if condition="{row.success}">
<f:then><span class="badge badge-success">ok</span></f:then>
<f:else><span class="badge badge-danger">HTTP {row.http_code}</span> <small>{row.message}</small></f:else>
</f:if>
</td>
<td class="text-end col-nowrap">{row.duration_ms} ms</td>
</tr>
</f:for>
<f:if condition="!{purgeLog}">
<tr><td colspan="5" class="text-body-secondary">No purges logged yet.</td></tr>
</f:if>
</tbody>
</table>
</div>
</div>
<div class="card mt-4">
<div class="card-header">
<h2 class="card-title mb-0">Client errors <small class="text-body-secondary fw-normal">last 24 h — {clientErrorCount24h} report<f:if condition="{clientErrorCount24h} != 1">s</f:if>, newest 50 shown</small></h2>
</div>
<div class="card-body pb-0">
<p class="form-text mt-0">
Browser-side failures reported by the React frontend via <code>POST /api/vitec/client-error</code>
(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 <code>vitec:client-error-cleanup</code>). A <code>release</code> older than the one running above means an open tab with an old bundle.
</p>
</div>
<div class="table-fit mb-0">
<table class="table table-striped table-hover mb-0">
<thead>
<tr><th class="col-nowrap">Time</th><th>Release</th><th>Message</th><th>Page</th><th>Browser</th><th>IP</th></tr>
</thead>
<tbody>
<f:for each="{clientErrors}" as="row">
<tr>
<td class="col-nowrap"><f:format.date format="d.m. H:i:s">@{row.crdate}</f:format.date></td>
<td><code>{row.release}</code><f:if condition="!{row.release}"><span class="text-body-secondary">—</span></f:if></td>
<td>
<f:if condition="{row.is_chunk_reload}">
<f:then><span class="badge badge-warning">chunk reload</span> </f:then>
<f:else><span class="badge badge-danger">error</span> </f:else>
</f:if>
{row.message}
<details class="small mt-1">
<summary class="text-body-secondary" style="cursor:pointer">details</summary>
<table class="table table-sm mt-2 mb-0">
<tr><th style="width:90px">Reported</th><td><f:format.date format="Y-m-d H:i:s">@{row.crdate}</f:format.date> (entry #{row.uid})</td></tr>
<tr><th>Release</th><td><code>{row.release}</code><f:if condition="!{row.release}"><span class="text-body-secondary">not transmitted</span></f:if></td></tr>
<tr><th>URL</th><td style="word-break:break-all"><a href="{row.url}" target="_blank" rel="noopener">{row.url}</a><f:if condition="!{row.url}"><span class="text-body-secondary">not transmitted</span></f:if></td></tr>
<tr><th>Message</th><td style="white-space:pre-wrap;word-break:break-word">{row.message}</td></tr>
<tr><th>Stack</th><td>
<f:if condition="{row.stack}">
<f:then><pre class="mb-0" style="white-space:pre-wrap;max-height:320px;overflow:auto">{row.stack}</pre></f:then>
<f:else><span class="text-body-secondary">no stack transmitted</span></f:else>
</f:if>
</td></tr>
<tr><th>Browser</th><td style="word-break:break-all">{row.user_agent}<f:if condition="!{row.user_agent}"><span class="text-body-secondary">not transmitted</span></f:if></td></tr>
<tr><th>IP</th><td>{row.ip} <span class="text-body-secondary">(anonymized)</span></td></tr>
</table>
</details>
</td>
<td><a href="{row.url}" target="_blank" rel="noopener" title="{row.url}">{row.path}</a></td>
<td title="{row.user_agent}"><small>{row.ua_short}</small></td>
<td><small>{row.ip}</small></td>
</tr>
</f:for>
<f:if condition="!{clientErrors}">
<tr><td colspan="6" class="text-body-secondary">No client errors reported in the last 24 hours.</td></tr>
</f:if>
</tbody>
</table>
</div>
</div>
<div class="card mt-4">
<div class="card-header">
<h2 class="card-title mb-0">Health samples <small class="text-body-secondary fw-normal">last 10 — cron <code>vitec:ssr-health</code>, every 5 minutes</small></h2>
</div>
<div class="table-fit mb-0">
<table class="table table-striped table-hover mb-0">
<thead>
<tr><th class="col-nowrap">Time</th><th>Status</th><th>Release</th><th>Cache</th><th class="text-end">Response time</th></tr>
</thead>
<tbody>
<f:for each="{healthLog}" as="row">
<tr>
<td class="col-nowrap"><f:format.date format="d.m. H:i:s">@{row.crdate}</f:format.date></td>
<td>
<f:if condition="{row.ok}">
<f:then><span class="badge badge-success">ok</span></f:then>
<f:else><span class="badge badge-danger">down</span></f:else>
</f:if>
</td>
<td><code>{row.release_id}</code></td>
<td>{row.cached_pages} pages · {row.cache_mb} MB</td>
<td class="text-end col-nowrap">{row.response_ms} ms</td>
</tr>
</f:for>
<f:if condition="!{healthLog}">
<tr><td colspan="5" class="text-body-secondary">No samples yet.</td></tr>
</f:if>
</tbody>
</table>
</div>
</div>
</f:section>
</html>