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>
304 lines
20 KiB
HTML
304 lines
20 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">
|
||
|
||
<div class="vitec-cockpit" id="ssr-cockpit" data-json="{cockpitJson}">
|
||
|
||
<h1 class="vc-sr">VITEC SSR — Control & 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 /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}&limit=30" aria-pressed="{f:if(condition: '{purgeLimit} == 30', then: 'true', else: 'false')}">30</a>
|
||
<a href="{moduleUri}&limit=100" aria-pressed="{f:if(condition: '{purgeLimit} == 100', then: 'true', else: 'false')}">100</a>
|
||
<a href="{moduleUri}&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>
|