Выпущен 0.1.3: UX convenience pack, unwrap/admin polish и cache-bust статики.
devops-tools/wrapped/wrapped-build/pipeline/head There was a failure building this commit

Добавлены Share/QR PNG, zip all, size meter, PWA-manifest, системная тема и haptic на copy; улучшены success/unwrap и статистика админки (EN/RU).
This commit is contained in:
2026-07-29 11:33:21 +03:00
parent 49df683281
commit 806f2bdb1b
26 changed files with 1317 additions and 136 deletions
+1 -1
View File
@@ -140,5 +140,5 @@
</section>
{% endblock %}
{% block admin_scripts %}
<script src="/static/js/admin-audit.js"></script>
<script src="{{ static_url('js/admin-audit.js') }}"></script>
{% endblock %}
+51 -37
View File
@@ -7,15 +7,17 @@
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/static/css/app.css" />
<link rel="icon" href="/static/favicon.svg" type="image/svg+xml" />
<link rel="icon" href="/static/favicon.ico" sizes="any" />
<link rel="apple-touch-icon" href="/static/apple-touch-icon.png" />
<link rel="stylesheet" href="{{ static_url('css/app.css') }}" />
<link rel="icon" href="{{ static_url('favicon.svg') }}" type="image/svg+xml" />
<link rel="icon" href="{{ static_url('favicon.ico') }}" sizes="any" />
<link rel="apple-touch-icon" href="{{ static_url('apple-touch-icon.png') }}" />
<link rel="manifest" href="{{ static_url('manifest.webmanifest') }}" />
<meta name="theme-color" content="#0b1020" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer" />
</head>
<body class="admin-body">
<div class="bg-grid" aria-hidden="true"></div>
<header class="admin-top">
<header class="admin-top" id="admin-top">
<a class="brand" href="/admin">
<span class="brand-mark" aria-hidden="true">
<i class="fa-solid fa-shield-halved"></i>
@@ -25,35 +27,46 @@
<span class="brand-tagline" data-i18n="admin.brand.tagline">Admin console</span>
</span>
</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>
</a>
<a href="/admin/audit" class="admin-nav-link {% if active == 'audit' %}active{% endif %}">
<i class="fa-solid fa-list" aria-hidden="true"></i>
<span data-i18n="admin.nav.audit">Audit</span>
</a>
<a href="/admin/danger" class="admin-nav-link admin-nav-danger {% if active == 'danger' %}active{% endif %}">
<i class="fa-solid fa-triangle-exclamation" aria-hidden="true"></i>
<span data-i18n="admin.nav.danger">Danger</span>
</a>
<a href="/" class="admin-nav-link" target="_blank" rel="noopener noreferrer">
<i class="fa-solid fa-arrow-up-right-from-square" aria-hidden="true"></i>
<span data-i18n="admin.nav.site">Site</span>
</a>
<form method="post" action="/admin/logout" class="inline">
<button class="admin-nav-link admin-nav-logout" type="submit">
<i class="fa-solid fa-right-from-bracket" aria-hidden="true"></i>
<span data-i18n="admin.nav.logout">Logout</span>
</button>
</form>
</nav>
<nav class="admin-nav" id="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>
</a>
<a href="/admin/audit" class="admin-nav-link {% if active == 'audit' %}active{% endif %}">
<i class="fa-solid fa-list" aria-hidden="true"></i>
<span data-i18n="admin.nav.audit">Audit</span>
</a>
<a href="/admin/danger" class="admin-nav-link admin-nav-danger {% if active == 'danger' %}active{% endif %}">
<i class="fa-solid fa-triangle-exclamation" aria-hidden="true"></i>
<span data-i18n="admin.nav.danger">Danger</span>
</a>
<a href="/" class="admin-nav-link" target="_blank" rel="noopener noreferrer">
<i class="fa-solid fa-arrow-up-right-from-square" aria-hidden="true"></i>
<span data-i18n="admin.nav.site">Site</span>
</a>
<form method="post" action="/admin/logout" class="inline admin-nav-logout-form">
<button class="admin-nav-link admin-nav-logout" type="submit">
<i class="fa-solid fa-right-from-bracket" aria-hidden="true"></i>
<span data-i18n="admin.nav.logout">Logout</span>
</button>
</form>
</nav>
<div class="admin-top-tools">
<button
type="button"
class="icon-btn admin-nav-toggle"
id="admin-nav-toggle"
aria-controls="admin-nav"
aria-expanded="false"
title="Menu"
aria-label="Menu"
>
<i class="fa-solid fa-bars" aria-hidden="true"></i>
</button>
<div class="top-actions">
<button type="button" class="icon-btn" id="lang-toggle" title="Language" aria-label="Language">
<span id="lang-label">RU</span>
@@ -69,9 +82,10 @@
<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>
<script src="/static/js/theme.js"></script>
<script src="/static/js/modal.js"></script>
<script src="{{ static_url('js/i18n.js') }}"></script>
<script src="{{ static_url('js/theme.js') }}"></script>
<script src="{{ static_url('js/modal.js') }}"></script>
<script src="{{ static_url('js/admin-nav.js') }}"></script>
{% block admin_scripts %}{% endblock %}
</body>
</html>
+8 -6
View File
@@ -7,10 +7,12 @@
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/static/css/app.css" />
<link rel="icon" href="/static/favicon.svg" type="image/svg+xml" />
<link rel="icon" href="/static/favicon.ico" sizes="any" />
<link rel="apple-touch-icon" href="/static/apple-touch-icon.png" />
<link rel="stylesheet" href="{{ static_url('css/app.css') }}" />
<link rel="icon" href="{{ static_url('favicon.svg') }}" type="image/svg+xml" />
<link rel="icon" href="{{ static_url('favicon.ico') }}" sizes="any" />
<link rel="apple-touch-icon" href="{{ static_url('apple-touch-icon.png') }}" />
<link rel="manifest" href="{{ static_url('manifest.webmanifest') }}" />
<meta name="theme-color" content="#0b1020" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer" />
</head>
<body class="admin-body">
@@ -47,7 +49,7 @@
<button class="btn primary" type="submit" data-i18n="admin.login.submit">Sign in</button>
</form>
</main>
<script src="/static/js/i18n.js"></script>
<script src="/static/js/theme.js"></script>
<script src="{{ static_url('js/i18n.js') }}"></script>
<script src="{{ static_url('js/theme.js') }}"></script>
</body>
</html>
+1 -1
View File
@@ -150,5 +150,5 @@
</form>
</div>
<script src="/static/js/admin-tabs.js"></script>
<script src="{{ static_url('js/admin-tabs.js') }}"></script>
{% endblock %}
+76 -7
View File
@@ -28,18 +28,29 @@
<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>
<h2 data-i18n="admin.stats.unwrappedTitle">Successful unwraps</h2>
</div>
<p class="stat-value">{{ stats.wraps_consumed }}</p>
<p class="stat-value">{{ stats.unwraps_success }}</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.dbConsumed">DB consumed</span>
<span class="stat-num">{{ stats.wraps_consumed }}</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-shield-halved" aria-hidden="true"></i>
<h2 data-i18n="admin.stats.passwordBurnsTitle">Burned by password</h2>
</div>
<p class="stat-value">{{ stats.password_burns }}</p>
<p class="stat-sub" data-i18n="admin.stats.passwordBurnsHint">
Destroyed after too many wrong passwords
</p>
</article>
<article class="stat-card">
<div class="stat-card-head">
<i class="fa-solid fa-hourglass-half" aria-hidden="true"></i>
@@ -93,13 +104,13 @@
<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>
<td class="mono">{{ stats.items_consumed }}</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>
<td class="mono">{{ stats.size_expired_human }}</td>
<td class="mono">{{ stats.items_expired }}</td>
</tr>
<tr class="stats-total-row">
<td data-i18n="admin.stats.total">Total</td>
@@ -142,6 +153,64 @@
</ul>
</section>
</div>
<div class="stats-sections">
<section class="stats-section">
<h3 data-i18n="admin.stats.failReasonsSection">Unwrap fail reasons (all time)</h3>
<div class="stats-table-wrap">
<table class="stats-table">
<thead>
<tr>
<th data-i18n="admin.stats.col.reason">Reason</th>
<th data-i18n="admin.stats.col.count">Count</th>
</tr>
</thead>
<tbody>
{% if stats.unwrap_fail_reasons %}
{% for row in stats.unwrap_fail_reasons %}
<tr>
<td class="mono">{{ row.reason }}</td>
<td class="mono">{{ row.count }}</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="2" class="hint" data-i18n="admin.stats.noFailReasons">No failed unwraps yet.</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</section>
<section class="stats-section">
<h3 data-i18n="admin.stats.failReasons24hSection">Unwrap fail reasons (24h)</h3>
<div class="stats-table-wrap">
<table class="stats-table">
<thead>
<tr>
<th data-i18n="admin.stats.col.reason">Reason</th>
<th data-i18n="admin.stats.col.count">Count</th>
</tr>
</thead>
<tbody>
{% if stats.unwrap_fail_reasons_24h %}
{% for row in stats.unwrap_fail_reasons_24h %}
<tr>
<td class="mono">{{ row.reason }}</td>
<td class="mono">{{ row.count }}</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="2" class="hint" data-i18n="admin.stats.noFailReasons">No failed unwraps yet.</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</section>
</div>
</section>
{% endblock %}
{% block admin_scripts %}
+11 -9
View File
@@ -7,10 +7,12 @@
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/static/css/app.css" />
<link rel="icon" href="/static/favicon.svg" type="image/svg+xml" />
<link rel="icon" href="/static/favicon.ico" sizes="any" />
<link rel="apple-touch-icon" href="/static/apple-touch-icon.png" />
<link rel="stylesheet" href="{{ static_url('css/app.css') }}" />
<link rel="icon" href="{{ static_url('favicon.svg') }}" type="image/svg+xml" />
<link rel="icon" href="{{ static_url('favicon.ico') }}" sizes="any" />
<link rel="apple-touch-icon" href="{{ static_url('apple-touch-icon.png') }}" />
<link rel="manifest" href="{{ static_url('manifest.webmanifest') }}" />
<meta name="theme-color" content="#0b1020" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer" />
<link id="hljs-theme-dark" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github-dark.min.css" />
<link id="hljs-theme-light" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github.min.css" disabled />
@@ -71,11 +73,11 @@
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="/static/js/i18n.js"></script>
<script src="/static/js/theme.js"></script>
<script src="/static/js/ui.js"></script>
<script src="/static/js/about.js"></script>
<script src="/static/js/highlight-ui.js"></script>
<script src="{{ static_url('js/i18n.js') }}"></script>
<script src="{{ static_url('js/theme.js') }}"></script>
<script src="{{ static_url('js/ui.js') }}"></script>
<script src="{{ static_url('js/about.js') }}"></script>
<script src="{{ static_url('js/highlight-ui.js') }}"></script>
{% block scripts %}{% endblock %}
</body>
</html>
+56 -12
View File
@@ -24,6 +24,7 @@
<input id="file-input" type="file" multiple hidden />
</div>
<ul id="file-list" class="file-list"></ul>
<p id="size-meter" class="size-meter hint" aria-live="polite"></p>
<div class="grid-2">
<div class="field">
@@ -64,17 +65,59 @@
<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 />
<button type="button" class="btn" id="copy-link" data-i18n="common.copy">Copy</button>
<ul class="success-checklist" id="success-checklist">
<li data-i18n="create.checkLink">Copy the share link</li>
<li id="check-password-item" class="hidden" data-i18n="create.checkPassword">Send the password in a separate message</li>
<li data-i18n="create.checkExpires">Note the expiry time</li>
</ul>
<p class="hint success-trust" data-i18n="create.trustKey">
The encryption key lives only in the link #fragment — the server never sees it.
</p>
<div class="success-layout">
<div class="success-main">
<div id="success-password-block" class="success-password-block hidden">
<div class="success-password-badge" role="status">
<i class="fa-solid fa-lock" aria-hidden="true"></i>
<span data-i18n="create.passwordProtected">Password protected</span>
</div>
<label for="share-password" data-i18n="create.sharePassword">Password</label>
<div class="copy-row">
<input id="share-password" class="mono" readonly autocomplete="off" />
<button type="button" class="btn" id="copy-password" data-i18n="common.copy">Copy</button>
</div>
<p class="hint success-password-hint" data-i18n="create.passwordSeparateHint">
Do not send the password in the same chat as the link.
</p>
</div>
<label for="share-link" data-i18n="create.shareLink">Share link</label>
<div class="copy-row">
<input id="share-link" readonly />
<button type="button" class="btn" id="copy-link" data-i18n="common.copy">Copy</button>
</div>
<label for="share-token" data-i18n="create.token">Wrapped token</label>
<div class="copy-row">
<input id="share-token" class="mono" readonly />
<button type="button" class="btn" id="copy-token" data-i18n="common.copy">Copy</button>
</div>
<div class="success-actions">
<button type="button" class="btn hidden" id="share-native" data-i18n="create.share">Share</button>
<button type="button" class="btn" id="download-qr" data-i18n="create.downloadQr">Download QR</button>
</div>
<p class="expires-meta" id="expires-meta"></p>
</div>
<div class="success-qr" aria-label="QR code">
<div id="share-qr" class="share-qr"></div>
<p class="hint success-qr-hint" data-i18n="create.qrHint">Scan to open the share link</p>
</div>
</div>
<label data-i18n="create.token">Wrapped token</label>
<div class="copy-row">
<input id="share-token" class="mono" readonly />
<button type="button" class="btn" id="copy-token" data-i18n="common.copy">Copy</button>
</div>
<p class="expires-meta" id="expires-meta"></p>
<button type="button" class="btn ghost" id="create-another" data-i18n="create.another">Create another</button>
</div>
</section>
@@ -92,6 +135,7 @@
</div>
{% endblock %}
{% block scripts %}
<script src="/static/js/crypto.js"></script>
<script src="/static/js/create.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="{{ static_url('js/crypto.js') }}"></script>
<script src="{{ static_url('js/create.js') }}"></script>
{% endblock %}
+27 -3
View File
@@ -1,7 +1,7 @@
{% extends "base.html" %}
{% block content %}
<section class="hero-panel unwrap-panel">
<div class="panel-head">
<div class="panel-head" id="unwrap-head">
<p class="eyebrow" data-i18n="unwrap.eyebrow">Unwrap</p>
<h1 data-i18n="unwrap.title">Reveal once</h1>
<p class="lede" data-i18n="unwrap.lede">Paste a wrapped token or open a share link. Ciphertext is fetched once and deleted from the server.</p>
@@ -32,6 +32,15 @@
</div>
</div>
<div id="unwrap-state" class="unwrap-state hidden" role="status">
<div class="unwrap-state-icon" aria-hidden="true">
<i class="fa-solid fa-link-slash" id="unwrap-state-fa"></i>
</div>
<h2 id="unwrap-state-title"></h2>
<p class="lede" id="unwrap-state-hint"></p>
<a class="btn primary" href="/" data-i18n="unwrap.createOwn">Create your own wrap</a>
</div>
<div id="result-panel" class="result-panel hidden">
<div class="success-callout" role="status">
<span class="success-callout-icon" aria-hidden="true">
@@ -42,11 +51,26 @@
<span data-i18n="unwrap.destroyedHint">Preview lives only in this browser session.</span>
</span>
</div>
<p class="hint success-trust" data-i18n="unwrap.trustKey">
The key was only in the link #fragment and was never sent to the server.
</p>
<div class="result-actions">
<button type="button" class="btn hidden" id="download-all" data-i18n="unwrap.downloadAll">Download all</button>
</div>
<div id="items" class="items"></div>
</div>
</section>
<div id="image-lightbox" class="image-lightbox hidden" role="dialog" aria-modal="true" aria-label="Image preview">
<button type="button" class="image-lightbox-close" id="lightbox-close" aria-label="Close">
<i class="fa-solid fa-xmark" aria-hidden="true"></i>
</button>
<img id="lightbox-img" alt="" />
<p class="image-lightbox-caption" id="lightbox-caption"></p>
</div>
{% endblock %}
{% block scripts %}
<script src="/static/js/crypto.js"></script>
<script src="/static/js/unwrap.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="{{ static_url('js/crypto.js') }}"></script>
<script src="{{ static_url('js/unwrap.js') }}"></script>
{% endblock %}