Files
2026-10-09 10:53:10 +02:00

82 lines
7.2 KiB
HTML

{% extends 'base.html' %}
{% set page = 'stats' %}
{% block title %}Statistics{% endblock %}
{% block content %}
<section class="page-intro stats-intro">
<div><div class="eyebrow"><i class="signal" aria-hidden="true"></i> REDIS COUNTERS / OBSERVABILITY</div>
<h1>Every request, <em>in focus.</em></h1>
<p class="lead">Conversion traffic, output sizes, client activity and recent requests. Metrics reflect traffic reaching this application; proxy cache hits do not execute the converter.</p></div>
<div class="intro-action"><a class="outline" href="{{ url_for('stats_json') }}" target="_blank" rel="noopener noreferrer">Open JSON API ↗</a></div>
</section>
{% set error4xx = (stats.get('stats:errors_400',0)|int + stats.get('stats:errors_403',0)|int + stats.get('stats:errors_404',0)|int + stats.get('stats:errors_413',0)|int + stats.get('stats:errors_415',0)|int + stats.get('stats:errors_422',0)|int + stats.get('stats:errors_429',0)|int) %}
{% set error5xx = (stats.get('stats:errors_500',0)|int + stats.get('stats:errors_502',0)|int) %}
<section class="stats-grid" aria-label="Overview metrics">
<article class="stat"><span>Convert requests</span><strong>{{ stats.get('stats:convert_requests',0) }}</strong><small>Reached backend</small></article>
<article class="stat"><span>Successful conversions</span><strong>{{ stats.get('stats:conversions_success',0) }}</strong><small>Validated responses</small></article>
<article class="stat"><span>4xx / 5xx errors</span><strong>{{ error4xx + error5xx }}</strong><small>{{ error4xx }} client / {{ error5xx }} server</small></article>
<article class="stat"><span>Avg. processing</span><strong>{{ '%.3f'|format(detailed.processing_time_avg_sec) }}<small class="unit">s</small></strong><small>Requests served by backend</small></article>
</section>
<section class="stats-secondary" aria-label="Performance metrics">
<div><span>AVERAGE OUTPUT</span><strong>{{ '{:,.0f}'.format(detailed.content_size_avg_bytes) }} B</strong></div>
<div><span>TOTAL OUTPUT</span><strong>{{ '{:,.0f}'.format(detailed.content_size_total_bytes) }} B</strong></div>
<div><span>FASTEST REQUEST</span><strong>{{ '%.3f'|format(detailed.processing_time_min_sec) }} s</strong></div>
<div><span>SLOWEST REQUEST</span><strong>{{ '%.3f'|format(detailed.processing_time_max_sec) }} s</strong></div>
</section>
<section class="card" id="activity">
<header class="card-heading card-heading-responsive"><div><span class="small-label">01 / TRAFFIC</span><h2>Recent conversion requests <span class="count">{{ recent|length }}</span></h2></div>
<div class="head-actions"><label class="sr-only" for="stats-filter">Filter requests</label><input id="stats-filter" type="search" data-filter="#activity-table" placeholder="Search traffic…"></div></header>
<div class="table-scroll"><table class="data-table" id="activity-table">
<thead><tr><th>Time</th><th>Source</th><th>Target</th><th>Client</th><th>User agent</th><th>Open</th></tr></thead>
<tbody>
{% for row in recent %}
<tr>
<td class="mono subdued nowrap">{{ row.get('time','')|datetimeformat }}</td>
<td class="source-cell"><span class="source-glyph" aria-hidden="true">↗</span><span class="source-ellipsis" title="{{ row.get('source_url','') }}">{{ row.get('source_url','') or 'Unknown' }}</span></td>
<td class="mono"><span class="ip-label">{{ row.get('target_ip','') }}</span></td>
<td class="mono" title="{{ row.get('hostname','') }} ({{ row.get('ip','') }})">{{ row.get('ip','') }}</td>
<td title="{{ row.get('user_agent','') }}"><span class="source-ellipsis">{{ row.get('user_agent','') }}</span></td>
<td>{% if row.get('source_url') %}<a class="quiet tiny" href="{{ url_for('convert',url=row.get('source_url'),ip=row.get('target_ip')) }}" target="_blank" rel="noopener noreferrer">Open ↗</a>{% else %}—{% endif %}</td>
</tr>
{% else %}
<tr class="empty-row"><td colspan="6" class="empty-table"><span class="empty-title">No requests recorded</span><span>Recent conversion requests will appear here.</span></td></tr>
{% endfor %}
</tbody>
</table></div>
<p class="fine no-results" data-empty-for="#activity-table" hidden>No matching records.</p>
</section>
<section class="table-card-grid" id="top-sources">
<article class="card"><header class="card-heading"><div><span class="small-label">02 / LEADERBOARD</span><h2>Top sources</h2></div><span class="pill">URL</span></header>
<div class="table-scroll"><table class="data-table"><thead><tr><th>Source address</th><th class="right">Requests</th></tr></thead><tbody>
{% for url, total in url_requests.items()|sort(attribute=1,reverse=True) %}
<tr><td class="source-cell"><span class="source-ellipsis" title="{{ url }}">{{ url }}</span></td><td class="right mono">{{ total }}</td></tr>
{% else %}<tr><td colspan="2" class="empty-table">No sources recorded.</td></tr>{% endfor %}
</tbody></table></div>
</article>
<article class="card"><header class="card-heading"><div><span class="small-label">03 / LEADERBOARD</span><h2>Target addresses</h2></div><span class="pill">IP</span></header>
<div class="table-scroll"><table class="data-table"><thead><tr><th>Target IP</th><th class="right">Requests</th></tr></thead><tbody>
{% for ip, total in target_ips.items()|sort(attribute=1,reverse=True) %}
<tr><td class="mono"><span class="ip-label">{{ ip }}</span></td><td class="right mono">{{ total }}</td></tr>
{% else %}<tr><td colspan="2" class="empty-table">No targets recorded.</td></tr>{% endfor %}
</tbody></table></div>
</article>
<article class="card"><header class="card-heading"><div><span class="small-label">04 / CLIENTS</span><h2>Top client IPs</h2></div></header>
<div class="table-scroll"><table class="data-table"><thead><tr><th>Client IP</th><th class="right">Requests</th></tr></thead><tbody>
{% for ip, total in client_ips.items()|sort(attribute=1,reverse=True) %}
<tr><td class="mono"><span class="source-ellipsis">{{ ip }}</span></td><td class="right mono">{{ total }}</td></tr>
{% else %}<tr><td colspan="2" class="empty-table">No clients recorded.</td></tr>{% endfor %}
</tbody></table></div>
</article>
<article class="card"><header class="card-heading"><div><span class="small-label">05 / CLIENTS</span><h2>User agents</h2></div></header>
<div class="table-scroll"><table class="data-table"><thead><tr><th>User agent</th><th class="right">Requests</th></tr></thead><tbody>
{% for ua, total in user_agents.items()|sort(attribute=1,reverse=True) %}
<tr><td><span class="source-ellipsis" title="{{ ua }}">{{ ua }}</span></td><td class="right mono">{{ total }}</td></tr>
{% else %}<tr><td colspan="2" class="empty-table">No user agents recorded.</td></tr>{% endfor %}
</tbody></table></div>
</article>
</section>
<details class="card json-details"><summary><span><span class="small-label">06 / INSPECTION</span><strong>Raw JSON snapshot</strong></span><span class="fine">Expand <span aria-hidden="true">↓</span></span></summary>
<div class="json-actions"><p class="fine">This is application-side activity only. Requests served from RustProxy or Varnish cache are not counted here.</p><button type="button" class="quiet tiny" data-action="copy" data-target="#raw-json">Copy JSON</button></div>
<pre class="raw-json" id="raw-json">{{ raw_json }}</pre>
</details>
{% endblock %}