Добавить статистику и очистку аудита; доработать UI расшифровки.

- Страница /admin/stats: MinIO, pending/consumed, загрузки, audit-метрики
- Кнопка «Очистить» в аудите с подтверждением
- Красивые callout’ы и ошибки пароля; пустой пароль не тратит попытку
- Убрать автоопределение языка; «Расшифруй» в RU UI
This commit is contained in:
Sergey Antropoff
2026-07-18 23:50:44 +03:00
parent 91719740b9
commit 8361b8e3f2
17 changed files with 1007 additions and 258 deletions
+73 -19
View File
@@ -1,5 +1,5 @@
(() => {
const t = (k) => window.WrappedI18n.t(k);
const t = (k, vars) => window.WrappedI18n.t(k, vars);
let settings = null;
const els = {
@@ -7,19 +7,60 @@
password: document.getElementById("unwrap-password"),
btn: document.getElementById("unwrap-btn"),
error: document.getElementById("form-error"),
errorTitle: document.getElementById("form-error-title"),
errorHint: document.getElementById("form-error-hint"),
errorAttempts: document.getElementById("form-error-attempts"),
form: document.getElementById("unwrap-form"),
result: document.getElementById("result-panel"),
items: document.getElementById("items"),
captchaSlot: document.getElementById("captcha-slot"),
};
function showError(msg) {
els.error.textContent = msg;
function showError(title, hint, attempts) {
if (!els.error) return;
if (els.errorTitle) els.errorTitle.textContent = title || "";
if (els.errorHint) {
if (hint) {
els.errorHint.textContent = hint;
els.errorHint.classList.remove("hidden");
} else {
els.errorHint.textContent = "";
els.errorHint.classList.add("hidden");
}
}
if (els.errorAttempts) {
if (attempts) {
els.errorAttempts.textContent = attempts;
els.errorAttempts.classList.remove("hidden");
} else {
els.errorAttempts.textContent = "";
els.errorAttempts.classList.add("hidden");
}
}
// Fallback if structured nodes missing
if (!els.errorTitle) els.error.textContent = [title, hint, attempts].filter(Boolean).join(" ");
els.error.classList.remove("hidden");
}
function clearError() {
if (!els.error) return;
els.error.classList.add("hidden");
if (els.errorTitle) els.errorTitle.textContent = "";
if (els.errorHint) {
els.errorHint.textContent = "";
els.errorHint.classList.add("hidden");
}
if (els.errorAttempts) {
els.errorAttempts.textContent = "";
els.errorAttempts.classList.add("hidden");
}
}
function attemptsLabel(detail) {
const n = Number(detail?.attempts_remaining);
const max = Number(detail?.attempts_max);
if (!Number.isFinite(n) || !Number.isFinite(max)) return "";
return t("unwrap.attemptsLeft", { n, max });
}
function loadCaptcha() {
@@ -151,24 +192,33 @@
const detail = err.detail;
if (detail && typeof detail === "object") {
if (detail.code === "password_locked") {
showError(t("unwrap.passwordLocked"));
showError(
t("unwrap.passwordLocked"),
t("unwrap.passwordLockedHint")
);
return;
}
if (detail.code === "password_required") {
showError(
t("unwrap.passwordRequired"),
t("unwrap.passwordRequiredHint"),
attemptsLabel(detail)
);
els.password?.focus();
return;
}
if (detail.code === "bad_password") {
const n = Number(detail.attempts_remaining);
const max = Number(detail.attempts_max);
if (Number.isFinite(n)) {
showError(
t("unwrap.badPasswordRemaining", {
n,
max: Number.isFinite(max) ? max : n,
})
);
return;
}
showError(
t("unwrap.badPassword"),
t("unwrap.badPasswordHint"),
attemptsLabel(detail)
);
els.password?.focus();
els.password?.select?.();
return;
}
}
showError(t("unwrap.badPassword"));
showError(t("unwrap.badPassword"), t("unwrap.badPasswordHint"));
return;
}
if (!resp.ok) {
@@ -185,9 +235,12 @@
els.password.value || null
);
} catch (err) {
if (err.message === "bad_password" || err.message === "password_required") {
// Should be rare now (server gates first); keep UX clear.
showError(t("unwrap.badPassword"));
if (err.message === "password_required") {
showError(t("unwrap.passwordRequired"), t("unwrap.passwordRequiredHint"));
return;
}
if (err.message === "bad_password") {
showError(t("unwrap.badPassword"), t("unwrap.badPasswordHint"));
return;
}
showError(t("common.error"));
@@ -218,3 +271,4 @@
init().catch(() => showError(t("common.error")));
})();