Files
VITEC-website/packages/vitec/Resources/Private/Templates/Ssr/Index.html
T
orascheandClaude Opus 5.5 67ded6e502 SSR module: dark VITEC cockpit with live charts
Live band with 24 h availability strip, gauges for hit rate, response time, cache fill and client errors, traffic chart 24 h / 7 days from the health log counter deltas drawn with the core Chart.js of EXT:dashboard, warm progress ring with live polling, purge activity timeline, client error cards, link to the site settings. ssr-cockpit.js replaces ssr-warm-progress.js, whose deletion went into a9923e1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 10:52:46 +01:00

304 lines
20 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">
<div class="vitec-cockpit" id="ssr-cockpit" data-json="{cockpitJson}">
<h1 class="vc-sr">VITEC SSR — Control &amp; Protocol</h1>
<div class="vc-grid">
<!-- Live band: node state, release, delivery switch, 24 h availability -->
<section class="vc-card vc-hero vc-s12" aria-label="Live status">
<div class="vc-hero-top">
<div class="vc-live">
<f:if condition="{health}">
<f:then>
<span class="vc-pulse" id="vc-pulse" aria-hidden="true"></span>
<div>
<h2 class="vc-live-title" id="vc-live-title">SSR <span class="vc-ok-text">online</span></h2>
<p class="vc-live-sub">
<f:if condition="{flagOn}">
<f:then>Pages are rendered on the VPS</f:then>
<f:else>Node is ready, but delivery is off: TYPO3 serves the SPA</f:else>
</f:if>
<f:if condition="{lastSample}"> · last check <span title="{f:format.date(date: '@{lastSample.crdate}', format: 'Y-m-d H:i:s')}">{lastSampleAge} ago</span></f:if>
</p>
</div>
</f:then>
<f:else>
<span class="vc-pulse is-down" id="vc-pulse" aria-hidden="true"></span>
<div>
<h2 class="vc-live-title" id="vc-live-title">SSR <span class="vc-bad-text">not reachable</span></h2>
<p class="vc-live-sub">{healthError} · after {failoverAfter} failed checks in a row the cron switches delivery back to the SPA</p>
</div>
</f:else>
</f:if>
</div>
<dl class="vc-facts">
<div class="vc-fact">
<dt>Delivery</dt>
<dd>
<f:if condition="{flagOn}">
<f:then><span class="vc-pill is-ok">ON</span></f:then>
<f:else><span class="vc-pill is-muted">OFF</span></f:else>
</f:if>
<f:if condition="{settingsUri}">
<f:then><a href="{settingsUri}" class="vc-link" title="Site Management › Settings › VITEC › SSR delivery. Switching on purges the node cache once.">Site settings</a></f:then>
<f:else><span class="vc-dim" title="Site Management › Settings › VITEC › SSR delivery">via Site settings</span></f:else>
</f:if>
</dd>
</div>
<f:if condition="{health}">
<div class="vc-fact"><dt>Release</dt><dd><code>{health.release.release}</code></dd></div>
<div class="vc-fact"><dt>Commit</dt><dd><code>{health.release.commit}</code></dd></div>
</f:if>
<div class="vc-fact">
<dt>Purge webhook</dt>
<dd>
<f:if condition="{configured}">
<f:then><span class="vc-pill is-ok" title="Endpoint and secret from var/vitec-ssr-purge.env">configured</span></f:then>
<f:else><span class="vc-pill is-warn" title="var/vitec-ssr-purge.env is missing or incomplete">not configured</span></f:else>
</f:if>
</dd>
</div>
<div class="vc-fact">
<dt>Auto failover</dt>
<dd><span class="vc-pill is-navy" title="Cron vitec:ssr-health every 5 minutes: alert mail and fallback to the SPA after {failoverAfter} failed checks in a row">after {failoverAfter} failed checks</span></dd>
</div>
</dl>
</div>
<div class="vc-uptime">
<div class="vc-uptime-h">
<span>Availability, last 24 hours · one bar per 15 minutes</span>
<span id="vc-uptime-sum">–</span>
</div>
<div class="vc-bars" id="vc-bars" role="img" aria-label="Availability of the SSR node over the last 24 hours"></div>
<div class="vc-uptime-f" id="vc-uptime-axis"></div>
</div>
</section>
<!-- Gauges -->
<section class="vc-card vc-s3" aria-label="Cache hit rate">
<div class="vc-tile">
<svg class="vc-ring" width="96" height="96" viewBox="0 0 96 96" aria-hidden="true"><circle class="vc-track" cx="48" cy="48" r="40"/><circle class="vc-val is-ok" id="vc-ring-hit" cx="48" cy="48" r="40"/></svg>
<div class="vc-tile-body">
<div class="vc-label">Hit rate · 24 h</div>
<div class="vc-big"><span id="vc-hit">–</span><small>%</small></div>
<div class="vc-sub" id="vc-hit-trend">served from cache or refreshed in the background</div>
</div>
</div>
</section>
<section class="vc-card vc-s3" aria-label="Response time">
<div class="vc-label">Response time</div>
<div class="vc-big"><span id="vc-ms"><f:if condition="{health}"><f:then>{health.responseMs}</f:then><f:else>–</f:else></f:if></span><small>ms</small></div>
<div class="vc-sub">median 24 h <b id="vc-ms-median">–</b> · p95 <b id="vc-ms-p95">–</b></div>
<svg class="vc-spark" id="vc-spark" viewBox="0 0 300 46" preserveAspectRatio="none" aria-hidden="true"></svg>
</section>
<section class="vc-card vc-s3" aria-label="Cache fill">
<div class="vc-tile">
<svg class="vc-ring" width="96" height="96" viewBox="0 0 96 96" aria-hidden="true"><circle class="vc-track" cx="48" cy="48" r="40"/><circle class="vc-val is-sky" id="vc-ring-cache" cx="48" cy="48" r="40"/></svg>
<div class="vc-tile-body">
<div class="vc-label">Cache fill</div>
<div class="vc-big"><span id="vc-cache-mb">{cockpit.cacheMb}</span><small id="vc-cache-limit"><f:if condition="{cacheLimitMb}"><f:then>/ {cacheLimitMb} MB</f:then><f:else>MB</f:else></f:if></small></div>
<div class="vc-sub"><b id="vc-cache-pages">{cockpit.cachedPages}</b> pages cached<span id="vc-cache-pct"></span></div>
</div>
</div>
</section>
<section class="vc-card vc-s3" aria-label="Client errors">
<div class="vc-label">Client errors · 24 h</div>
<div class="vc-big">{clientErrorCount24h}<small>report<f:if condition="{clientErrorCount24h} != 1">s</f:if></small></div>
<div class="vc-sub">{chunkCount24h} chunk reload<f:if condition="{chunkCount24h} != 1">s</f:if> · {jsErrorCount24h} JS error<f:if condition="{jsErrorCount24h} != 1">s</f:if></div>
<div class="vc-mini" id="vc-mini" role="img" aria-label="Client errors per hour, last 24 hours"></div>
</section>
<!-- Traffic chart -->
<section class="vc-card vc-s8 vc-chart-card" aria-label="Traffic and cache">
<div class="vc-card-h">
<h2 class="vc-card-t"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M3 3v18h18M7 15l4-5 4 3 5-7"/></svg>Traffic and cache</h2>
<div class="vc-seg" role="group" aria-label="Time range">
<button type="button" data-range="24h" aria-pressed="true">24 h</button>
<button type="button" data-range="7d" aria-pressed="false">7 days</button>
</div>
</div>
<div class="vc-legend">
<span><i class="is-navy"></i>Served from cache</span>
<span><i class="is-sky"></i>Stale, refreshed in background</span>
<span><i class="is-warn"></i>Rendered fresh</span>
<span><i class="is-orange is-round"></i>Response time</span>
<span><i class="is-bad is-tri"></i>Purge</span>
</div>
<div class="vc-chart"><canvas id="vc-traffic" aria-label="Requests by cache result, with response time and purges"></canvas></div>
<p class="vc-empty" id="vc-traffic-empty" hidden>No traffic counters in this range yet. The cron <code>vitec:ssr-health</code> collects them every 5 minutes.</p>
<dl class="vc-stats">
<div><dt>Requests</dt><dd id="vc-st-req">–</dd></div>
<div><dt>Hit rate</dt><dd id="vc-st-hit">–</dd></div>
<div><dt>Render errors</dt><dd id="vc-st-err">–</dd></div>
<div><dt>Purges</dt><dd id="vc-st-purge">–</dd></div>
</dl>
</section>
<!-- Actions -->
<section class="vc-card vc-s4" aria-label="Actions">
<div class="vc-card-h"><h2 class="vc-card-t"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M13 2 4 14h7l-1 8 9-12h-7z"/></svg>Actions</h2></div>
<div class="vc-actions">
<form action="{f:be.uri(route: 'web_vitecssr.warm')}" method="post" class="vc-act"
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">
<div class="vc-act-h"><h3 class="vc-act-t">Warm cache</h3><span class="vc-pill" id="vc-warm-pill" hidden></span></div>
<div class="vc-warm">
<div class="vc-warm-ring">
<svg width="92" height="92" viewBox="0 0 96 96" aria-hidden="true" class="vc-ring"><circle class="vc-track" cx="48" cy="48" r="40"/><circle class="vc-val is-orange is-fast" id="vc-ring-warm" cx="48" cy="48" r="40"/></svg>
<div class="vc-warm-pct" id="vc-warm-pct">–</div>
</div>
<div class="vc-warm-meta" id="vc-warm-meta">no run since the server started</div>
</div>
<div class="vc-btn-row">
<button type="submit" class="vc-btn is-primary" id="ssr-warm-button" {f:if(condition: health.warming.running, then: 'disabled="disabled"')}>
<core:icon identifier="actions-play" size="small" /><span id="vc-warm-label">{f:if(condition: health.warming.running, then: 'Warming …', else: 'Warm cache')}</span>
</button>
</div>
<p class="vc-help">Renders every sitemap page in advance, so no visitor waits for a first render. Run it after “Purge everything” or a deploy.</p>
</form>
<form action="{f:be.uri(route: 'web_vitecssr.purge')}" method="post" class="vc-act">
<h3 class="vc-act-t">Purge paths</h3>
<label class="vc-lbl" for="purge-paths">Paths, one per line</label>
<textarea class="vc-input" id="purge-paths" name="paths" rows="3" placeholder="/products/aligo&#10;/insights/news"></textarea>
<input type="hidden" name="mode" value="paths">
<div class="vc-btn-row"><button type="submit" class="vc-btn is-ghost"><core:icon identifier="actions-refresh" size="small" />Purge these paths</button></div>
<p class="vc-help">Drops those pages, all hosts and query variants, from the SSR cache. They render again on their next request.</p>
</form>
<form action="{f:be.uri(route: 'web_vitecssr.purge')}" method="post" class="vc-act"
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="vc-act-t">Purge everything</h3>
<p class="vc-help">For menu or footer changes while auto-purge is off, and after a frontend deploy.</p>
<input type="hidden" name="mode" value="all">
<div class="vc-btn-row"><button type="submit" class="vc-btn is-danger"><core:icon identifier="actions-delete" size="small" />Purge everything</button></div>
</form>
</div>
</section>
<!-- Purge activity -->
<section class="vc-card vc-s7" aria-label="Purge activity">
<div class="vc-card-h">
<h2 class="vc-card-t"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>Purge activity</h2>
<div class="vc-seg" role="group" aria-label="Entries to show">
<a href="{moduleUri}&amp;limit=30" aria-pressed="{f:if(condition: '{purgeLimit} == 30', then: 'true', else: 'false')}">30</a>
<a href="{moduleUri}&amp;limit=100" aria-pressed="{f:if(condition: '{purgeLimit} == 100', then: 'true', else: 'false')}">100</a>
<a href="{moduleUri}&amp;limit=500" aria-pressed="{f:if(condition: '{purgeLimit} == 500', then: 'true', else: 'false')}">500</a>
</div>
</div>
<div class="vc-legend vc-legend-tl">
<span><i class="is-ok is-round"></i>auto-purge</span>
<span><i class="is-orange is-round"></i>manual</span>
<span><i class="is-navy is-round"></i>context menu</span>
<span><i class="is-sky is-round"></i>cli / deploy</span>
<span><i class="is-warn is-round"></i>switch-on</span>
</div>
<f:if condition="{purgeLog}">
<f:then>
<ol class="vc-timeline">
<f:for each="{purgeLog}" as="row">
<f:if condition="{row.day_first}"><li class="vc-day">{row.day_label}</li></f:if>
<li class="vc-ev">
<time title="{f:format.date(date: '@{row.crdate}', format: 'Y-m-d H:i:s')}"><f:format.date format="H:i">@{row.crdate}</f:format.date></time>
<span class="vc-dot is-{row.trigger_tone}"></span>
<div class="vc-ev-main">
<div class="vc-ev-line">
<span class="vc-pill is-{row.trigger_tone}">{row.trigger_label}</span>
<f:if condition="{row.trigger_who}"><span class="vc-dim">{row.trigger_who}</span></f:if>
</div>
<div class="vc-paths">
<f:if condition="{row.paths_all}">
<f:then><span class="vc-pill is-warn">all pages (*)</span></f:then>
<f:else>
<f:for each="{row.paths_first}" as="p"><span class="vc-path" title="{p}">{p}</span></f:for>
<f:if condition="{row.paths_rest}">
<details class="vc-more">
<summary>+<f:count subject="{row.paths_rest}" /> more</summary>
<f:for each="{row.paths_rest}" as="p"><span class="vc-path" title="{p}">{p}</span></f:for>
</details>
</f:if>
</f:else>
</f:if>
</div>
</div>
<div class="vc-ev-r">
<f:if condition="{row.success}">
<f:then><span class="vc-pill is-ok">ok</span></f:then>
<f:else><span class="vc-pill is-bad" title="{row.message}">HTTP {row.http_code}</span></f:else>
</f:if>
<span>{row.duration_ms} ms</span>
</div>
</li>
</f:for>
</ol>
</f:then>
<f:else><p class="vc-empty">No purges logged yet. Saving a page, the context menu “Purge from SSR cache” or the buttons on the right create the first entry.</p></f:else>
</f:if>
</section>
<!-- Client errors -->
<section class="vc-card vc-s5" aria-label="Client errors">
<div class="vc-card-h">
<h2 class="vc-card-t"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M12 3 2 21h20zM12 10v4M12 17.5v.5"/></svg>Client errors</h2>
<span class="vc-pill is-sky" title="Reported by the React frontend via POST /api/vitec/client-error; max. 30 per minute and IP, IPs anonymized, cleaned up after 30 days by cron vitec:client-error-cleanup">kept 30 days</span>
</div>
<dl class="vc-err-sum">
<div><dt>24 h</dt><dd>{clientErrorCount24h}</dd></div>
<div><dt>Chunk reloads</dt><dd class="vc-warn-text">{chunkCount24h}</dd></div>
<div><dt>JS errors</dt><dd class="vc-bad-text">{jsErrorCount24h}</dd></div>
</dl>
<f:if condition="{clientErrors}">
<f:then>
<div class="vc-errs">
<f:for each="{clientErrors}" as="row">
<details class="vc-err">
<summary>
<f:if condition="{row.is_chunk_reload}">
<f:then><span class="vc-pill is-warn">chunk reload</span></f:then>
<f:else><span class="vc-pill is-bad">error</span></f:else>
</f:if>
<span class="vc-err-msg">{row.message}<small>{row.path}</small></span>
<span class="vc-err-t"><f:format.date format="H:i">@{row.crdate}</f:format.date></span>
</summary>
<div class="vc-err-body">
<dl class="vc-kv">
<dt>Reported</dt><dd><f:format.date format="Y-m-d H:i:s">@{row.crdate}</f:format.date> · entry #{row.uid}</dd>
<dt>Release</dt><dd><f:if condition="{row.release}"><f:then><code>{row.release}</code><f:if condition="{health.release.release}"><f:if condition="{row.release} != {health.release.release}"> <span class="vc-pill is-warn">old bundle</span></f:if></f:if></f:then><f:else><span class="vc-dim">not transmitted</span></f:else></f:if></dd>
<dt>URL</dt><dd><f:if condition="{row.url}"><f:then><a class="vc-link" href="{row.url}" target="_blank" rel="noopener">{row.url}</a></f:then><f:else><span class="vc-dim">not transmitted</span></f:else></f:if></dd>
<dt>Browser</dt><dd><f:if condition="{row.user_agent}"><f:then>{row.user_agent}</f:then><f:else><span class="vc-dim">not transmitted</span></f:else></f:if></dd>
<dt>IP</dt><dd><code>{row.ip}</code> <span class="vc-dim">anonymized</span></dd>
</dl>
<f:if condition="{row.stack}">
<f:then><pre class="vc-stack">{row.stack}</pre></f:then>
<f:else><p class="vc-dim">No stack transmitted.</p></f:else>
</f:if>
</div>
</details>
</f:for>
</div>
</f:then>
<f:else><p class="vc-empty">No client errors in the last 24 hours. The frontend reports JS errors and chunk reloads here as they happen.</p></f:else>
</f:if>
</section>
</div>
</div>
</f:section>
</html>