216 lines
9.4 KiB
HTML
216 lines
9.4 KiB
HTML
<!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> |