Добавить статистику и очистку аудита; доработать 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
-115
View File
@@ -15,120 +15,6 @@
"css",
];
function tryParseJson(text) {
try {
JSON.parse(text);
return true;
} catch {
return false;
}
}
/**
* @returns {{ lang: string, confidence: number, suggestions: string[] }}
*/
function detectLanguage(text) {
const raw = (text || "").trim();
if (!raw) {
return { lang: "plaintext", confidence: 0, suggestions: LANGUAGES };
}
const scores = Object.fromEntries(LANGUAGES.map((l) => [l, 0]));
if (
(raw.startsWith("{") || raw.startsWith("[")) &&
tryParseJson(raw)
) {
scores.json += 10;
} else if (/^\s*[{[]/.test(raw) && /["']?\w+["']?\s*:/.test(raw)) {
scores.json += 4;
}
if (
/^---\s*$/m.test(raw) ||
(/^[\w.-]+\s*:\s+\S+/m.test(raw) &&
!tryParseJson(raw) &&
!raw.startsWith("<"))
) {
scores.yaml += 5;
}
if (/^\s*-\s+\w+/m.test(raw) && /:\s/.test(raw)) scores.yaml += 2;
if (
/\b(def|class|import|from|async def)\b/.test(raw) ||
/^\s*@\w+/m.test(raw)
) {
scores.python += 5;
}
if (/print\(|__name__|self\./.test(raw)) scores.python += 2;
if (
/\b(function|const|let|var|=>|console\.)\b/.test(raw) ||
/\brequire\s*\(/.test(raw)
) {
scores.javascript += 4;
}
if (/\b(interface|type)\s+\w+|:\s*\w+(\[\])?\s*[=;]/.test(raw)) {
scores.typescript += 5;
}
if (/\bimport\s+type\b|\bas\s+const\b/.test(raw)) scores.typescript += 2;
if (/\b(package|func|fmt\.|:=)\b/.test(raw)) scores.go += 5;
if (/\b(fn |let mut|impl |pub fn|cargo)\b/.test(raw)) scores.rust += 5;
if (
/\b(SELECT|INSERT|UPDATE|DELETE|CREATE TABLE|FROM|WHERE)\b/i.test(raw)
) {
scores.sql += 6;
}
if (/^\s*#!.*\b(bash|sh|zsh)\b/m.test(raw) || /^\s*(sudo |export |echo )/m.test(raw)) {
scores.bash += 4;
}
if (/<\/?[a-zA-Z][\w:-]*[\s>]/.test(raw) || raw.startsWith("<!DOCTYPE")) {
scores.html += 6;
}
if (/[{;]\s*$/m.test(raw) && /[.#]?[\w-]+\s*\{/.test(raw) && !tryParseJson(raw)) {
scores.css += 5;
}
if (/^#{1,6}\s+\S+/m.test(raw) || /\[[^\]]+\]\([^)]+\)/.test(raw)) {
scores.markdown += 4;
}
const ranked = Object.entries(scores)
.filter(([lang]) => lang !== "plaintext")
.sort((a, b) => b[1] - a[1]);
const best = ranked[0];
const second = ranked[1];
if (!best || best[1] < 3) {
return {
lang: "plaintext",
confidence: 0.2,
suggestions: ranked
.filter(([, s]) => s > 0)
.slice(0, 4)
.map(([l]) => l)
.concat(["plaintext"]),
};
}
const confidence =
best[1] >= 8
? 0.95
: best[1] >= 5
? 0.75
: second && best[1] - second[1] <= 1
? 0.45
: 0.6;
const suggestions = ranked
.filter(([, s]) => s > 0)
.slice(0, 5)
.map(([l]) => l);
if (!suggestions.includes("plaintext")) suggestions.push("plaintext");
return { lang: best[0], confidence, suggestions };
}
function hljsLang(lang) {
const map = {
plaintext: "plaintext",
@@ -182,7 +68,6 @@
}
window.WrappedHighlight = {
detectLanguage,
highlightElement,
syncThemeStylesheet,
languages,