Улучшить unwrap, UI и админку; обновить документацию.

- Неверный пароль не сжигает wrap сразу: Argon2-гейт до consume и лимит попыток (по умолчанию 3) в настройках
- Подсветка синтаксиса, автоопределение языка, спиннеры, размеры файлов, пагинация audit
- make push (git, Ctrl-D) и актуальный README
This commit is contained in:
2026-07-18 10:17:05 +03:00
parent 8f2d798e1a
commit 8c8dbd2348
21 changed files with 1170 additions and 178 deletions
+29 -18
View File
@@ -1,7 +1,7 @@
{% extends "admin_base.html" %}
{% block admin_content %}
<section class="audit-page">
<form class="audit-filters" method="get" action="/admin/audit">
<form class="audit-filters" id="audit-filter-form" method="get" action="/admin/audit">
<input type="hidden" name="page" value="1" />
<label class="audit-field">
<span data-i18n="admin.audit.eventType">Event type</span>
@@ -16,6 +16,14 @@
<span data-i18n="admin.audit.wrapId">Wrap ID</span>
<input name="wrap_id" value="{{ wrap_id }}" class="mono" placeholder="…" autocomplete="off" />
</label>
<label class="audit-field">
<span data-i18n="admin.audit.perPage">Per page</span>
<select name="per_page" id="audit-per-page">
{% for n in per_page_options %}
<option value="{{ n }}" {% if per_page == n %}selected{% endif %}>{{ n }}</option>
{% endfor %}
</select>
</label>
<button class="btn primary audit-filter-btn" type="submit" data-i18n="admin.audit.filter">Filter</button>
</form>
@@ -46,7 +54,7 @@
<tbody>
{% for e in events %}
<tr class="{% if not e.success %}audit-row-fail{% endif %}">
<td class="mono audit-time">{{ e.created_at }}</td>
<td class="mono audit-time" data-ts="{{ e.created_at.isoformat() if e.created_at else '' }}">{{ e.created_at }}</td>
<td><span class="audit-event-pill">{{ e.event_type }}</span></td>
<td>
{% if e.success %}
@@ -69,34 +77,39 @@
</table>
</div>
{% if pages > 1 %}
{% if show_pagination %}
<nav class="pagination" aria-label="Pagination">
{% if has_prev %}
<a class="btn" href="/admin/audit?page={{ page - 1 }}&amp;event_type={{ event_type|urlencode }}&amp;wrap_id={{ wrap_id|urlencode }}">
<a class="btn pagination-nav" href="/admin/audit?page={{ page - 1 }}&amp;per_page={{ per_page }}&amp;event_type={{ event_type|urlencode }}&amp;wrap_id={{ wrap_id|urlencode }}">
<i class="fa-solid fa-chevron-left" aria-hidden="true"></i>
<span data-i18n="admin.audit.prev">Prev</span>
</a>
{% else %}
<span class="btn" aria-disabled="true" disabled>
<span class="btn pagination-nav" aria-disabled="true">
<i class="fa-solid fa-chevron-left" aria-hidden="true"></i>
<span data-i18n="admin.audit.prev">Prev</span>
</span>
{% endif %}
<span class="pagination-status">
<span data-i18n="admin.audit.page">Page</span>
{{ page }}
<span data-i18n="admin.audit.of">of</span>
{{ pages }}
</span>
<div class="pagination-pages">
{% for p in page_window %}
{% if p is none %}
<span class="pagination-ellipsis" aria-hidden="true"></span>
{% elif p == page %}
<span class="pagination-page is-current" aria-current="page">{{ p }}</span>
{% else %}
<a class="pagination-page" href="/admin/audit?page={{ p }}&amp;per_page={{ per_page }}&amp;event_type={{ event_type|urlencode }}&amp;wrap_id={{ wrap_id|urlencode }}">{{ p }}</a>
{% endif %}
{% endfor %}
</div>
{% if has_next %}
<a class="btn" href="/admin/audit?page={{ page + 1 }}&amp;event_type={{ event_type|urlencode }}&amp;wrap_id={{ wrap_id|urlencode }}">
<a class="btn pagination-nav" href="/admin/audit?page={{ page + 1 }}&amp;per_page={{ per_page }}&amp;event_type={{ event_type|urlencode }}&amp;wrap_id={{ wrap_id|urlencode }}">
<span data-i18n="admin.audit.next">Next</span>
<i class="fa-solid fa-chevron-right" aria-hidden="true"></i>
</a>
{% else %}
<span class="btn" aria-disabled="true" disabled>
<span class="btn pagination-nav" aria-disabled="true">
<span data-i18n="admin.audit.next">Next</span>
<i class="fa-solid fa-chevron-right" aria-hidden="true"></i>
</span>
@@ -104,9 +117,7 @@
</nav>
{% endif %}
</section>
<script>
document.getElementById("audit-event-type")?.addEventListener("change", (e) => {
e.target.form?.requestSubmit();
});
</script>
{% endblock %}
{% block admin_scripts %}
<script src="/static/js/admin-audit.js"></script>
{% endblock %}
+1
View File
@@ -68,5 +68,6 @@
<script src="/static/js/i18n.js"></script>
<script src="/static/js/theme.js"></script>
<script src="/static/js/modal.js"></script>
{% block admin_scripts %}{% endblock %}
</body>
</html>
+14
View File
@@ -80,6 +80,20 @@
</span>
</label>
{% endfor %}
<label class="admin-field-spaced">
<span data-i18n="admin.password.maxAttempts">Wrong password attempts (unwrap)</span>
<input
name="password_max_attempts"
type="number"
min="1"
max="50"
step="1"
value="{{ settings.password_max_attempts or 3 }}"
/>
<small class="hint" data-i18n="admin.password.maxAttemptsHint">
After this many wrong passwords the wrap is destroyed. Default: 3.
</small>
</label>
</section>
<section class="admin-tabpanel" data-panel="captcha" role="tabpanel" hidden>
+5
View File
@@ -12,6 +12,8 @@
<link rel="icon" href="/static/favicon.ico" sizes="any" />
<link rel="apple-touch-icon" href="/static/apple-touch-icon.png" />
<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 />
{% block head %}{% endblock %}
</head>
<body>
@@ -68,8 +70,11 @@
· <a href="https://devops.org.ru" target="_blank" rel="noopener noreferrer">devops.org.ru</a>
</p>
</footer>
<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/highlight-ui.js"></script>
{% block scripts %}{% endblock %}
</body>
</html>
+30 -19
View File
@@ -2,27 +2,26 @@
{% block content %}
<section class="hero-panel create-panel">
<div class="composer">
<div class="field-row">
<label for="text-language" data-i18n="create.language">Language</label>
<select id="text-language">
<option value="plaintext" data-i18n="lang.plaintext">Обычный текст</option>
<option value="markdown" data-i18n="lang.markdown">Markdown</option>
<option value="json" data-i18n="lang.json">JSON</option>
<option value="yaml" data-i18n="lang.yaml">YAML</option>
<option value="python" data-i18n="lang.python">Python</option>
<option value="javascript" data-i18n="lang.javascript">JavaScript</option>
<option value="typescript" data-i18n="lang.typescript">TypeScript</option>
<option value="go" data-i18n="lang.go">Go</option>
<option value="rust" data-i18n="lang.rust">Rust</option>
<option value="sql" data-i18n="lang.sql">SQL</option>
<option value="bash" data-i18n="lang.bash">Bash</option>
<option value="html" data-i18n="lang.html">HTML</option>
<option value="css" data-i18n="lang.css">CSS</option>
</select>
<div class="lang-bar">
<div class="lang-current">
<span class="lang-label" data-i18n="create.language">Highlight language</span>
<button type="button" class="lang-chip" id="lang-chip" aria-haspopup="dialog">
<span id="lang-chip-text">Plain text</span>
<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>
<textarea id="payload-text" class="code-input" spellcheck="false" data-i18n-placeholder="create.textPlaceholder" placeholder="Paste secrets, configs, notes…"></textarea>
<div class="code-editor" id="code-editor">
<pre class="code-highlight" aria-hidden="true"><code id="code-highlight" class="hljs"></code></pre>
<textarea id="payload-text" class="code-input" spellcheck="false" data-i18n-placeholder="create.textPlaceholder" placeholder="Paste secrets, configs, notes…"></textarea>
</div>
<div id="dropzone" class="dropzone" tabindex="0">
<p data-i18n="create.drop">Drop files here, click to browse, or paste a screenshot (⌘V / Ctrl+V)</p>
@@ -71,10 +70,22 @@
<input id="share-token" class="mono" readonly />
<button type="button" class="btn" id="copy-token" data-i18n="common.copy">Copy</button>
</div>
<p class="meta" id="expires-meta"></p>
<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>
<div id="lang-modal" class="modal-root hidden" role="dialog" aria-modal="true" aria-labelledby="lang-modal-title">
<div class="modal-backdrop" data-lang-close></div>
<div class="modal-panel lang-modal-panel" role="document">
<h2 class="modal-title" id="lang-modal-title" data-i18n="create.languagePickTitle">Text type</h2>
<p class="modal-message" data-i18n="create.languagePickHint">Choose how the text should be highlighted.</p>
<div class="lang-grid" id="lang-grid"></div>
<div class="modal-actions">
<button type="button" class="btn" data-lang-close data-i18n="modal.confirm.cancel">Cancel</button>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="/static/js/crypto.js"></script>
+3 -1
View File
@@ -18,7 +18,9 @@
</div>
<div id="captcha-slot" class="captcha-slot"></div>
<p id="form-error" class="error hidden"></p>
<button type="button" id="unwrap-btn" class="btn primary" data-i18n="unwrap.submit">Unwrap</button>
<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">