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>
306 lines
19 KiB
HTML
306 lines
19 KiB
HTML
<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 & 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 /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}&limit=30">30</a>
|
||
<a class="btn btn-default {f:if(condition: '{purgeLimit} == 100', then: 'active')}" href="{moduleUri}&limit=100">100</a>
|
||
<a class="btn btn-default {f:if(condition: '{purgeLimit} == 500', then: 'active')}" href="{moduleUri}&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>
|