redesign
This commit is contained in:
1 parent
ad620c976a
commit
0973129c17
26 files changed
+2255
-2245
No files matched your search
+81
-216
@@ -1,216 +1,81 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="dark">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Stats</title>
|
||||
<meta name="theme-color" content="#0f1115">
|
||||
<link rel="preload" href="{{ url_for('static', filename='css/main.css') }}" as="style">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<div class="brand">
|
||||
<svg aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
|
||||
<path d="M4 4h16v4H4zM4 10h10v4H4zM4 16h16v4H4z" fill="currentColor" />
|
||||
</svg>
|
||||
<span>Hosts Converter</span>
|
||||
</div>
|
||||
<nav class="actions">
|
||||
<button class="btn ghost" type="button" data-action="toggle-theme" aria-label="Toggle theme">🌓</button>
|
||||
<a class="btn ghost" href="/" rel="nofollow">Home</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="container container--wide">
|
||||
<!-- KPIs -->
|
||||
<section class="card kpi-card">
|
||||
<h2 class="section-title">Overview</h2>
|
||||
<div class="kpi-grid">
|
||||
<div class="kpi">
|
||||
<div class="kpi-label">Convert requests</div>
|
||||
<div class="kpi-value">{{ stats.get('stats:convert_requests', 0) }}</div>
|
||||
</div>
|
||||
<div class="kpi">
|
||||
<div class="kpi-label">Successful conversions</div>
|
||||
<div class="kpi-value">{{ stats.get('stats:conversions_success', 0) }}</div>
|
||||
</div>
|
||||
<div class="kpi">
|
||||
<div class="kpi-label">Errors 4xx</div>
|
||||
<div class="kpi-value">{{ stats.get('stats:errors_400', 0) }}</div>
|
||||
</div>
|
||||
<div class="kpi">
|
||||
<div class="kpi-label">Errors 5xx</div>
|
||||
<div class="kpi-value">{{ stats.get('stats:errors_500', 0) }}</div>
|
||||
</div>
|
||||
<div class="kpi">
|
||||
<div class="kpi-label">Avg processing (s)</div>
|
||||
<div class="kpi-value">{{ '%.3f' % detailed.processing_time_avg_sec }}</div>
|
||||
</div>
|
||||
<div class="kpi">
|
||||
<div class="kpi-label">Avg content (bytes)</div>
|
||||
<div class="kpi-value">{{ detailed.content_size_avg_bytes }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Recent converts -->
|
||||
<section class="card">
|
||||
<div class="section-head">
|
||||
<h2>Recent converts (latest {{ recent|length }})</h2>
|
||||
<div class="head-actions">
|
||||
<input class="table-filter" type="search" placeholder="Filter…" data-action="filter-table"
|
||||
data-target="#recent-table">
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table id="recent-table" class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>URL</th>
|
||||
<th>Target IP</th>
|
||||
<th>Client</th>
|
||||
<th>User agent</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in recent %}
|
||||
{% set q = row.get('url','') %}
|
||||
{% set parts = q.split('&ip=') %}
|
||||
{% set url = parts[0].replace('/convert?url=', '') | urlencode %}
|
||||
{% set ip = (parts[1] if parts|length > 1 else '') %}
|
||||
<tr>
|
||||
<td class="mono nowrap">{{ row.time|datetimeformat }}</td>
|
||||
<td class="mono ellipsis" title="{{ url|safe }}">
|
||||
{{ url|safe }}
|
||||
</td>
|
||||
<td class="mono">{{ ip }}</td>
|
||||
<td class="mono ellipsis" title="{{ row.hostname }} ({{ row.ip }})">
|
||||
{{ row.hostname }} ({{ row.ip }})
|
||||
</td>
|
||||
<td class="ellipsis" title="{{ row.user_agent }}">{{ row.user_agent }}</td>
|
||||
<td class="actions">
|
||||
<a class="btn tiny outline" href="{{ q }}" target="_blank" rel="noopener">Open</a>
|
||||
<button class="btn tiny" data-action="copy-text" data-text="{{ q }}">Copy</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Top tables -->
|
||||
<section class="card">
|
||||
<div class="section-head">
|
||||
<h2>Top sources</h2>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<div class="col-6">
|
||||
<h3 class="subhead">Source URLs</h3>
|
||||
<div class="table-wrap">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>URL</th>
|
||||
<th class="right">Hits</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for u, c in url_requests.items()|sort(attribute=1, reverse=True) %}
|
||||
<tr>
|
||||
<td class="mono ellipsis" title="{{ u }}">{{ u }}</td>
|
||||
<td class="right">{{ c }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-6">
|
||||
<h3 class="subhead">Target IPs</h3>
|
||||
<div class="table-wrap">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>IP</th>
|
||||
<th class="right">Hits</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for ip, c in target_ips.items()|sort(attribute=1, reverse=True) %}
|
||||
<tr>
|
||||
<td class="mono">{{ ip }}</td>
|
||||
<td class="right">{{ c }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-6">
|
||||
<h3 class="subhead">User agents</h3>
|
||||
<div class="table-wrap">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User agent</th>
|
||||
<th class="right">Hits</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for ua, c in user_agents.items()|sort(attribute=1, reverse=True) %}
|
||||
<tr>
|
||||
<td class="ellipsis" title="{{ ua }}">{{ ua }}</td>
|
||||
<td class="right">{{ c }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-6">
|
||||
<h3 class="subhead">Client IPs</h3>
|
||||
<div class="table-wrap">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Client IP</th>
|
||||
<th class="right">Hits</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for ip, c in client_ips.items()|sort(attribute=1, reverse=True) %}
|
||||
<tr>
|
||||
<td class="mono">{{ ip }}</td>
|
||||
<td class="right">{{ c }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div>© 2025 <a href="https://www.linuxiarz.pl" target="_blank" rel="noopener">linuxiarz.pl</a></div>
|
||||
<div class="meta">Your IP: <strong>{{ request.remote_addr }}</strong></div>
|
||||
</footer>
|
||||
|
||||
<div id="toast" role="status" aria-live="polite" aria-atomic="true"></div>
|
||||
<script defer src="{{ url_for('static', filename='js/main.js') }}"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/stats.js') }}"></script>
|
||||
<script>(function () { const t = localStorage.getItem('theme') || 'dark'; document.documentElement.setAttribute('data-theme', t); })();</script>
|
||||
</body>
|
||||
|
||||
</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 %}
|
||||
Reference in new issue
Block a user