Добавить статистику и очистку аудита; доработать UI расшифровки.
- Страница /admin/stats: MinIO, pending/consumed, загрузки, audit-метрики - Кнопка «Очистить» в аудите с подтверждением - Красивые callout’ы и ошибки пароля; пустой пароль не тратит попытку - Убрать автоопределение языка; «Расшифруй» в RU UI
This commit is contained in:
@@ -1,5 +1,13 @@
|
||||
{% extends "admin_base.html" %}
|
||||
{% block admin_content %}
|
||||
{% if request.query_params.get('cleared') is not none %}
|
||||
<p class="ok">
|
||||
<span data-i18n="admin.audit.clearedPrefix">Cleared</span>
|
||||
<span class="audit-count">{{ request.query_params.get('cleared') }}</span>
|
||||
<span data-i18n="admin.audit.clearedSuffix">audit event(s).</span>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<section class="audit-page">
|
||||
<form class="audit-filters" id="audit-filter-form" method="get" action="/admin/audit">
|
||||
<input type="hidden" name="page" value="1" />
|
||||
@@ -27,15 +35,28 @@
|
||||
<button class="btn primary audit-filter-btn" type="submit" data-i18n="admin.audit.filter">Filter</button>
|
||||
</form>
|
||||
|
||||
<div class="audit-meta">
|
||||
{% if total %}
|
||||
<span class="audit-count">{{ from_idx }}–{{ to_idx }}</span>
|
||||
<span data-i18n="admin.audit.of">of</span>
|
||||
<span class="audit-count">{{ total }}</span>
|
||||
{% else %}
|
||||
<span class="audit-count">0</span>
|
||||
{% endif %}
|
||||
<span data-i18n="admin.audit.events">events</span>
|
||||
<div class="audit-meta-row">
|
||||
<div class="audit-meta">
|
||||
{% if total %}
|
||||
<span class="audit-count">{{ from_idx }}–{{ to_idx }}</span>
|
||||
<span data-i18n="admin.audit.of">of</span>
|
||||
<span class="audit-count">{{ total }}</span>
|
||||
{% else %}
|
||||
<span class="audit-count">0</span>
|
||||
{% endif %}
|
||||
<span data-i18n="admin.audit.events">events</span>
|
||||
</div>
|
||||
<form method="post" action="/admin/audit/clear" id="audit-clear-form" class="audit-clear-form">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn danger tiny"
|
||||
id="audit-clear-btn"
|
||||
{% if not total %}disabled{% endif %}
|
||||
data-i18n="admin.audit.clear"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="audit-table-wrap">
|
||||
|
||||
@@ -27,6 +27,10 @@
|
||||
</a>
|
||||
<div class="admin-top-right">
|
||||
<nav class="admin-nav" aria-label="Admin">
|
||||
<a href="/admin/stats" class="admin-nav-link {% if active == 'stats' %}active{% endif %}">
|
||||
<i class="fa-solid fa-chart-simple" aria-hidden="true"></i>
|
||||
<span data-i18n="admin.nav.stats">Stats</span>
|
||||
</a>
|
||||
<a href="/admin/settings" class="admin-nav-link {% if active == 'settings' %}active{% endif %}">
|
||||
<i class="fa-solid fa-sliders" aria-hidden="true"></i>
|
||||
<span data-i18n="admin.nav.settings">Settings</span>
|
||||
@@ -62,7 +66,7 @@
|
||||
</div>
|
||||
</header>
|
||||
<main class="admin-shell">
|
||||
<h1 data-i18n="{% if active == 'audit' %}admin.audit.title{% elif active == 'danger' %}admin.danger.title{% else %}admin.settings.title{% endif %}">{{ title }}</h1>
|
||||
<h1 data-i18n="{% if active == 'stats' %}admin.stats.title{% elif active == 'audit' %}admin.audit.title{% elif active == 'danger' %}admin.danger.title{% else %}admin.settings.title{% endif %}">{{ title }}</h1>
|
||||
{% block admin_content %}{% endblock %}
|
||||
</main>
|
||||
<script src="/static/js/i18n.js"></script>
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
{% extends "admin_base.html" %}
|
||||
{% block admin_content %}
|
||||
<section class="stats-page">
|
||||
<p class="stats-updated hint">
|
||||
<span data-i18n="admin.stats.updated">Updated</span>
|
||||
<time id="stats-generated" data-ts="{{ stats.generated_at.isoformat() }}">{{ stats.generated_at }}</time>
|
||||
</p>
|
||||
|
||||
<div class="stats-grid">
|
||||
<article class="stat-card accent">
|
||||
<div class="stat-card-head">
|
||||
<i class="fa-solid fa-database" aria-hidden="true"></i>
|
||||
<h2 data-i18n="admin.stats.minioTitle">MinIO (pending ciphertext)</h2>
|
||||
</div>
|
||||
<p class="stat-value">{{ stats.minio_human }}</p>
|
||||
<p class="stat-sub">
|
||||
<span class="stat-num">{{ stats.minio_objects }}</span>
|
||||
<span data-i18n="admin.stats.objects">object(s)</span>
|
||||
·
|
||||
<span data-i18n="admin.stats.dbPending">DB pending</span>
|
||||
{{ stats.size_pending_human }}
|
||||
</p>
|
||||
{% if stats.minio_error %}
|
||||
<p class="error stat-error">{{ stats.minio_error }}</p>
|
||||
{% endif %}
|
||||
</article>
|
||||
|
||||
<article class="stat-card">
|
||||
<div class="stat-card-head">
|
||||
<i class="fa-solid fa-lock-open" aria-hidden="true"></i>
|
||||
<h2 data-i18n="admin.stats.unwrappedTitle">Unwrapped (all time)</h2>
|
||||
</div>
|
||||
<p class="stat-value">{{ stats.wraps_consumed }}</p>
|
||||
<p class="stat-sub">
|
||||
<span data-i18n="admin.stats.auditOk">audit ok</span>
|
||||
<span class="stat-num">{{ stats.audit_unwraps_ok }}</span>
|
||||
·
|
||||
<span data-i18n="admin.stats.last24h">24h</span>
|
||||
<span class="stat-num">{{ stats.consumed_24h }}</span>
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article class="stat-card">
|
||||
<div class="stat-card-head">
|
||||
<i class="fa-solid fa-hourglass-half" aria-hidden="true"></i>
|
||||
<h2 data-i18n="admin.stats.pendingTitle">Not unwrapped</h2>
|
||||
</div>
|
||||
<p class="stat-value">{{ stats.wraps_pending }}</p>
|
||||
<p class="stat-sub">
|
||||
{{ stats.size_pending_human }}
|
||||
·
|
||||
<span class="stat-num">{{ stats.items_pending }}</span>
|
||||
<span data-i18n="admin.stats.items">item(s)</span>
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article class="stat-card">
|
||||
<div class="stat-card-head">
|
||||
<i class="fa-solid fa-cloud-arrow-up" aria-hidden="true"></i>
|
||||
<h2 data-i18n="admin.stats.uploadedTitle">Uploaded (all time)</h2>
|
||||
</div>
|
||||
<p class="stat-value">{{ stats.wraps_total }}</p>
|
||||
<p class="stat-sub">
|
||||
<span class="stat-num">{{ stats.items_total }}</span>
|
||||
<span data-i18n="admin.stats.items">item(s)</span>
|
||||
·
|
||||
{{ stats.size_total_human }}
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="stats-sections">
|
||||
<section class="stats-section">
|
||||
<h3 data-i18n="admin.stats.wrapsSection">Wraps by status</h3>
|
||||
<div class="stats-table-wrap">
|
||||
<table class="stats-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-i18n="admin.stats.col.status">Status</th>
|
||||
<th data-i18n="admin.stats.col.count">Count</th>
|
||||
<th data-i18n="admin.stats.col.size">Size</th>
|
||||
<th data-i18n="admin.stats.col.items">Items</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td data-i18n="admin.stats.status.pending">pending</td>
|
||||
<td class="mono">{{ stats.wraps_pending }}</td>
|
||||
<td class="mono">{{ stats.size_pending_human }}</td>
|
||||
<td class="mono">{{ stats.items_pending }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td data-i18n="admin.stats.status.consumed">consumed</td>
|
||||
<td class="mono">{{ stats.wraps_consumed }}</td>
|
||||
<td class="mono">{{ stats.size_consumed_human }}</td>
|
||||
<td class="mono">—</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td data-i18n="admin.stats.status.expired">expired</td>
|
||||
<td class="mono">{{ stats.wraps_expired }}</td>
|
||||
<td class="mono">—</td>
|
||||
<td class="mono">—</td>
|
||||
</tr>
|
||||
<tr class="stats-total-row">
|
||||
<td data-i18n="admin.stats.total">Total</td>
|
||||
<td class="mono">{{ stats.wraps_total }}</td>
|
||||
<td class="mono">{{ stats.size_total_human }}</td>
|
||||
<td class="mono">{{ stats.items_total }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="stats-section">
|
||||
<h3 data-i18n="admin.stats.extraSection">More</h3>
|
||||
<ul class="stats-list">
|
||||
<li>
|
||||
<span data-i18n="admin.stats.withPassword">With password</span>
|
||||
<strong class="mono">{{ stats.wraps_with_password }}</strong>
|
||||
</li>
|
||||
<li>
|
||||
<span data-i18n="admin.stats.created24h">Created (24h)</span>
|
||||
<strong class="mono">{{ stats.created_24h }}</strong>
|
||||
</li>
|
||||
<li>
|
||||
<span data-i18n="admin.stats.created7d">Created (7d)</span>
|
||||
<strong class="mono">{{ stats.created_7d }}</strong>
|
||||
</li>
|
||||
<li>
|
||||
<span data-i18n="admin.stats.createsOk">Creates ok (audit)</span>
|
||||
<strong class="mono">{{ stats.audit_creates_ok }}</strong>
|
||||
</li>
|
||||
<li>
|
||||
<span data-i18n="admin.stats.createsFail">Creates fail (audit)</span>
|
||||
<strong class="mono">{{ stats.audit_creates_fail }}</strong>
|
||||
</li>
|
||||
<li>
|
||||
<span data-i18n="admin.stats.unwrapsFail">Unwrap fails (audit)</span>
|
||||
<strong class="mono">{{ stats.audit_unwraps_fail }}</strong>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
{% block admin_scripts %}
|
||||
<script>
|
||||
(() => {
|
||||
const el = document.getElementById("stats-generated");
|
||||
if (!el) return;
|
||||
const raw = el.getAttribute("data-ts");
|
||||
if (!raw) return;
|
||||
const d = new Date(raw);
|
||||
if (Number.isNaN(d.getTime())) return;
|
||||
const locale = window.WrappedI18n?.locale?.() || undefined;
|
||||
el.textContent = new Intl.DateTimeFormat(locale, {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
}).format(d);
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -10,12 +10,8 @@
|
||||
<i class="fa-solid fa-chevron-down" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="btn tiny ghost" id="lang-change" data-i18n="create.languageChange">
|
||||
Change type
|
||||
</button>
|
||||
<input type="hidden" id="text-language" value="plaintext" />
|
||||
</div>
|
||||
<p id="lang-hint" class="lang-hint hidden"></p>
|
||||
|
||||
<label class="sr-only" for="payload-text" data-i18n="create.text">Text</label>
|
||||
<div class="code-editor" id="code-editor">
|
||||
@@ -59,7 +55,15 @@
|
||||
|
||||
<div id="success-panel" class="success-panel hidden">
|
||||
<h2 data-i18n="create.successTitle">Token issued</h2>
|
||||
<p class="warn" data-i18n="create.successWarn">One-time unwrap. After someone opens it, ciphertext is destroyed on the server.</p>
|
||||
<div class="success-callout" role="status">
|
||||
<span class="success-callout-icon" aria-hidden="true">
|
||||
<i class="fa-solid fa-fire"></i>
|
||||
</span>
|
||||
<span class="success-callout-body">
|
||||
<strong data-i18n="create.successWarnTitle">One-time unwrap</strong>
|
||||
<span data-i18n="create.successWarnHint">After opening, ciphertext is deleted on the server.</span>
|
||||
</span>
|
||||
</div>
|
||||
<label data-i18n="create.shareLink">Share link</label>
|
||||
<div class="copy-row">
|
||||
<input id="share-link" readonly />
|
||||
|
||||
@@ -17,14 +17,31 @@
|
||||
<input id="unwrap-password" type="password" autocomplete="current-password" />
|
||||
</div>
|
||||
<div id="captcha-slot" class="captcha-slot"></div>
|
||||
<p id="form-error" class="error hidden"></p>
|
||||
<div id="form-error" class="form-alert danger hidden" role="alert">
|
||||
<span class="form-alert-icon" aria-hidden="true">
|
||||
<i class="fa-solid fa-key"></i>
|
||||
</span>
|
||||
<span class="form-alert-body">
|
||||
<strong id="form-error-title"></strong>
|
||||
<span id="form-error-hint" class="hidden"></span>
|
||||
<span id="form-error-attempts" class="form-alert-attempts hidden"></span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="actions actions-center">
|
||||
<button type="button" id="unwrap-btn" class="btn primary" data-i18n="unwrap.submit">Unwrap</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="result-panel" class="result-panel hidden">
|
||||
<p class="warn" data-i18n="unwrap.destroyed">Server copy destroyed. Preview lives only in this browser session.</p>
|
||||
<div class="success-callout" role="status">
|
||||
<span class="success-callout-icon" aria-hidden="true">
|
||||
<i class="fa-solid fa-fire"></i>
|
||||
</span>
|
||||
<span class="success-callout-body">
|
||||
<strong data-i18n="unwrap.destroyedTitle">Server copy destroyed</strong>
|
||||
<span data-i18n="unwrap.destroyedHint">Preview lives only in this browser session.</span>
|
||||
</span>
|
||||
</div>
|
||||
<div id="items" class="items"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user