ui: align obfuscator with Nubes branding

Update the interface palette, typography and layout to match Nubes while keeping the file workflow unchanged. Bundle Ubuntu locally and bump the app version.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Repinoid
2026-10-24 09:19:37 +03:00
co-authored by Copilot
parent bbb05e361a
commit baa7418cfd
3 changed files with 196 additions and 202 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ if _sys_path_root not in sys.path:
sys.path.insert(0, _sys_path_root) sys.path.insert(0, _sys_path_root)
# Версия приложения (меняется при изменениях) # Версия приложения (меняется при изменениях)
VERSION = "1.1.0" VERSION = "1.1.1"
def setup_logging(): def setup_logging():
Binary file not shown.
+195 -201
View File
@@ -10,16 +10,24 @@
<title>Obfuscator — обфускация документов</title> <title>Obfuscator — обфускация документов</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"> <link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
<style> <style>
@font-face {
font-family: 'Ubuntu';
src: url('/static/fonts/Ubuntu.woff2') format('woff2');
font-weight: 300 700;
font-display: swap;
}
:root { :root {
--brand-primary: #2563eb; --navy: #001c34;
--brand-gray: #d1d5db; --brand-primary: #0d6efd;
--brand-grey-light: #f3f4f6; --brand-gray: #dee2e6;
--bg: #f9fafb; --brand-grey-light: #f8f9fa;
--bg: #f8f9fa;
--card: #ffffff; --card: #ffffff;
--text: #1a1a1a; --text: #001c34;
--muted: #6b7280; --muted: #6c757d;
--red: #ef4444; --red: #dc3545;
--green: #22c55e; --green: #198754;
} }
* { * {
@@ -29,95 +37,103 @@
} }
body { body {
font: 14px system-ui, sans-serif; font: 14px/1.5 'Ubuntu', system-ui, sans-serif;
color: var(--text); color: var(--text);
background: var(--bg); background: var(--bg);
min-height: 100vh; min-height: 100vh;
} }
.topbar { .topbar {
background: var(--card); background: var(--navy);
border-bottom: 1px solid var(--brand-gray); color: #fff;
padding: 0 16px; padding: 0 24px;
height: 48px; height: 56px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 12px;
} }
.topbar img { .topbar img {
height: 18px; height: 20px;
filter: brightness(0) invert(1);
} }
.topbar .title { .topbar .title {
font-weight: 600; font-weight: 500;
font-size: 14px; font-size: 15px;
} }
.topbar .ver { .topbar .ver {
font-weight: 400; color: #a4acb3;
color: var(--muted);
font-size: 12px; font-size: 12px;
} }
.topbar .help { .topbar .help {
margin-left: auto; margin-left: auto;
cursor: pointer; cursor: pointer;
text-decoration: none;
user-select: none; user-select: none;
display: inline-flex; padding: 6px 16px;
align-items: center; border-radius: 8px;
justify-content: center; border: 1px solid rgba(255, 255, 255, .4);
padding: 5px 14px;
border-radius: 6px;
background: #1d4ed8;
color: #fff; color: #fff;
font-size: 13px; font-size: 13px;
font-weight: 700; font-weight: 500;
line-height: 1;
letter-spacing: .06em;
box-shadow: 0 1px 3px rgba(0, 0, 0, .25);
} }
.topbar .help:hover { .topbar .help:hover {
background: #1e40af; background: rgba(255, 255, 255, .1);
color: #fff;
} }
.content { .content {
max-width: 820px; max-width: 960px;
margin: 20px auto; margin: 40px auto;
padding: 0 12px; padding: 0 24px;
}
.page-title {
font-size: 28px;
font-weight: 500;
color: var(--navy);
line-height: 1.2;
}
.page-sub {
color: var(--muted);
margin: 8px 0 24px;
} }
.card { .card {
background: var(--card); background: var(--card);
border-radius: 12px; border-radius: 16px;
border: 1px solid var(--brand-gray); border: 1px solid var(--brand-gray);
box-shadow: 0 1px 2px rgba(0, 0, 0, .04);
}
.card-header {
background: var(--brand-grey-light);
padding: 10px 12px;
font-weight: 600;
font-size: 14px;
border-bottom: 1px solid var(--brand-gray);
} }
.card-body { .card-body {
padding: 12px; padding: 24px;
} }
.sub { .sub {
font-size: 12px; font-size: 13px;
color: var(--muted); color: var(--muted);
margin-bottom: 10px;
line-height: 1.5; line-height: 1.5;
} }
.limits {
background: var(--brand-grey-light);
border-radius: 8px;
padding: 12px 16px;
margin-bottom: 20px;
font-size: 13px;
color: var(--muted);
}
.limits ul {
padding-left: 18px;
}
/* FilePicker styling */ /* FilePicker styling */
.file-picker { #filePicker .file-picker,
#filePicker .fp-table {
font-family: inherit; font-family: inherit;
color: var(--text); color: var(--text);
} }
@@ -129,70 +145,77 @@
margin-bottom: 10px; margin-bottom: 10px;
} }
.file-picker .fp-toolbar button { #filePicker .fp-toolbar button {
height: 32px; height: 40px;
border-radius: 6px; border-radius: 8px;
padding: 0 12px; padding: 0 18px;
font-size: 13px; font-size: 14px;
font-family: inherit; font-family: inherit;
cursor: pointer; cursor: pointer;
border: 1px solid var(--brand-gray); border: 1px solid var(--brand-primary);
background: var(--card); background: var(--card);
color: var(--text); color: var(--brand-primary);
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 5px; gap: 6px;
font-weight: 500; font-weight: 500;
} }
.file-picker .fp-toolbar button#fp-files-btn { #filePicker .fp-toolbar button:hover {
background: #eff6ff; background: #e7f1ff;
border-color: var(--brand-primary);
color: var(--brand-primary);
} }
.file-picker .fp-toolbar button#fp-folder-btn { #filePicker .fp-toolbar button#fp-clear-btn {
background: var(--card);
}
.file-picker .fp-toolbar button#fp-clear-btn {
color: var(--muted); color: var(--muted);
border-color: transparent; border-color: transparent;
margin-left: auto;
} }
.file-picker .fp-toolbar button#fp-clear-btn:hover { #filePicker .fp-toolbar button#fp-clear-btn:hover {
background: transparent;
color: var(--red); color: var(--red);
} }
.file-picker .fp-table-wrap { #filePicker .fp-table-wrap {
border: 1px solid var(--brand-gray); border: 1px solid var(--brand-gray);
border-radius: 8px; border-radius: 12px;
overflow: hidden; overflow: hidden;
margin-bottom: 6px; margin-bottom: 6px;
} }
.file-picker .fp-table { #filePicker .fp-table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
font-size: 13px; font-size: 14px;
} }
.file-picker .fp-table th { #filePicker .fp-table th {
background: var(--brand-grey-light); background: var(--brand-grey-light);
text-transform: uppercase; padding: 10px 16px;
padding: 6px 8px;
border-right: 1px solid var(--brand-gray);
border-bottom: 1px solid var(--brand-gray); border-bottom: 1px solid var(--brand-gray);
text-align: left; text-align: left;
font-weight: 600; font-weight: 500;
font-size: 10px; font-size: 13px;
letter-spacing: 0;
text-transform: none;
color: var(--muted); color: var(--muted);
} }
.file-picker .fp-table td { #filePicker .fp-table td {
padding: 5px 8px; padding: 10px 16px;
border-right: 1px solid var(--brand-gray);
border-bottom: 1px solid var(--brand-gray); border-bottom: 1px solid var(--brand-gray);
color: var(--text);
}
#filePicker .fp-table tr:last-child td {
border-bottom: 0;
}
#filePicker .fp-table td[colspan] {
color: var(--muted);
text-align: center;
padding: 32px 16px;
font-size: 14px;
} }
.file-picker .tree-row:hover td { .file-picker .tree-row:hover td {
@@ -224,7 +247,7 @@
font-size: 9px; font-size: 9px;
} }
.file-picker .fp-status { #filePicker .fp-status {
font-size: 12px; font-size: 12px;
color: var(--muted); color: var(--muted);
margin: 4px 0; margin: 4px 0;
@@ -258,19 +281,21 @@
} }
.footer-bar { .footer-bar {
margin-top: 10px; margin-top: 16px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
font-size: 12px; gap: 8px;
font-size: 13px;
color: var(--muted); color: var(--muted);
} }
.btn { .btn {
height: 32px; height: 40px;
border-radius: 6px; border-radius: 8px;
padding: 0 12px; padding: 0 20px;
font-size: 13px; font-size: 14px;
font-weight: 500;
font-family: inherit; font-family: inherit;
cursor: pointer; cursor: pointer;
border: 1px solid var(--brand-gray); border: 1px solid var(--brand-gray);
@@ -278,7 +303,7 @@
color: var(--text); color: var(--text);
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 5px; gap: 6px;
} }
.btn-primary { .btn-primary {
@@ -288,148 +313,116 @@
} }
.btn-primary:hover { .btn-primary:hover {
opacity: .9; background: #0b5ed7;
} }
.btn:disabled { .btn:disabled {
opacity: .4; background: #e9ecef;
border-color: #e9ecef;
color: #a4acb3;
cursor: not-allowed; cursor: not-allowed;
} }
.status { .status {
margin-top: 10px; margin-top: 16px;
padding: 8px 12px; padding: 10px 16px;
border-radius: 8px; border-radius: 8px;
font-size: 13px; font-size: 14px;
display: none; display: none;
} }
.status.progress { .status.progress {
background: #eff6ff; background: #e7f1ff;
border: 1px solid var(--brand-primary); color: #0a58ca;
color: var(--brand-primary);
display: block; display: block;
} }
.status.done { .status.done {
background: #f0fdf4; background: #e8f5ee;
border: 1px solid var(--green); color: #146c43;
color: #16a34a;
display: block; display: block;
} }
.status.error { .status.error {
background: #fef2f2; background: #fdecee;
border: 1px solid var(--red); color: #b02a37;
color: #991b1b;
display: block; display: block;
} }
.dl-btns { .dl-btns {
display: none; display: none;
gap: 8px; gap: 8px;
margin-top: 8px; margin-top: 12px;
} }
.dl-btns.show { .dl-btns.show {
display: flex; display: flex;
} }
.stats-block { .stats-block,
.live-block {
display: none; display: none;
margin-top: 12px; margin-top: 16px;
padding: 16px; padding: 16px 20px;
background: linear-gradient(135deg, #f0fdf4, #ecfdf5); background: var(--brand-grey-light);
border: 2px solid var(--green); border: 1px solid var(--brand-gray);
border-radius: 12px; border-radius: 12px;
} }
.stats-block.show { .stats-block.show,
.live-block.show {
display: block; display: block;
} }
.stats-title { .stats-title,
font-size: 16px; .live-title {
font-weight: 700; font-size: 15px;
color: #14532d; font-weight: 500;
margin-bottom: 10px; margin-bottom: 8px;
} }
.stats-row { .stats-row {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 10px; gap: 12px;
} }
.stat-item { .stat-item {
flex: 1; flex: 1;
min-width: 130px; min-width: 130px;
background: #fff; background: #fff;
border: 1px solid #bbf7d0; border: 1px solid var(--brand-gray);
border-radius: 10px; border-radius: 8px;
padding: 10px 12px; padding: 10px 14px;
text-align: center;
} }
.stat-label { .stat-label {
font-size: 12px; font-size: 13px;
text-transform: uppercase;
letter-spacing: .04em;
color: var(--muted); color: var(--muted);
margin-bottom: 4px; margin-bottom: 2px;
} }
.stat-value { .stat-value {
font-size: 24px; font-size: 22px;
font-weight: 800; font-weight: 500;
color: #166534;
}
.live-block {
display: none;
margin-top: 12px;
padding: 16px;
background: linear-gradient(135deg, #eff6ff, #dbeafe);
border: 2px solid var(--brand-primary);
border-radius: 12px;
}
.live-block.show {
display: block;
}
.live-title {
font-size: 18px;
font-weight: 800;
color: #1e40af;
margin-bottom: 6px;
} }
.live-file { .live-file {
font-size: 15px; color: var(--muted);
color: #1e3a8a; margin-bottom: 8px;
margin-bottom: 10px;
word-break: break-word; word-break: break-word;
} }
.live-timer { .live-timer {
font-size: 40px; font-size: 32px;
font-weight: 800; font-weight: 500;
color: #1d4ed8;
line-height: 1.1; line-height: 1.1;
} }
.live-llm { .live-llm {
display: none; display: none;
margin-top: 8px; margin-top: 8px;
padding: 8px 12px; color: var(--muted);
border-radius: 8px;
background: #fef3c7;
border: 1px solid #f59e0b;
color: #92400e;
font-size: 18px;
font-weight: 700;
width: fit-content;
} }
.live-llm.show { .live-llm.show {
@@ -438,9 +431,7 @@
.live-eta { .live-eta {
margin-top: 8px; margin-top: 8px;
font-size: 14px; color: var(--muted);
color: #1d4ed8;
font-weight: 600;
} }
</style> </style>
</head> </head>
@@ -453,43 +444,41 @@
<span class="help" onclick="document.getElementById('helpModal').style.display='flex'">HELP</span> <span class="help" onclick="document.getElementById('helpModal').style.display='flex'">HELP</span>
</div> </div>
<div class="content"> <div class="content">
<h1 class="page-title">Обфускация документов</h1>
<p class="page-sub">Загрузите документы — получите ZIP с обезличенными копиями. CSV с таблицей замен скачивается отдельно.</p>
<div class="card"> <div class="card">
<div class="card-header">Обфускация документов</div>
<div class="card-body"> <div class="card-body">
<p class="sub"> <div class="limits">
Загрузите документы (.docx, .doc, .pdf, .txt, .md, .zip) — получите ZIP с обезличенными копиями.<br> <ul>
CSV с таблицей замен скачивается отдельно.<br> <li>Форматы: .docx, .doc, .pdf, .txt, .md, .zip. Не более 50 МБ на файл и 500 МБ суммарно — файлы сверх лимитов получают статус «пропущен».</li>
<span style="color:#c0392b;">Ограничения: один файл не более 50 МБ, суммарно не более 500 МБ. Файлы сверх <li>Пустые, повреждённые файлы и сканы без текстового слоя получают статус «не извлечён» и не попадают в результат.</li>
лимитов помечаются статусом «пропущен» и не участвуют в обфускации. Пустые, повреждённые файлы и сканы без <li>Время обработки каждого файла ориентировочное и обновляется по факту.</li>
текстового слоя получают статус «не извлечён» — они тоже не попадают в результат.</span><br> <li>Фильтр по маскам: положите в корень папки файл <code>obscure.lst</code>. Подробнее — кнопка HELP.</li>
<span style="color:#7d3c98;">⏱ Время обработки каждого файла — ориентировочное (обновляется по факту).</span> </ul>
</p> </div>
<div id="filePicker"></div> <div id="filePicker"></div>
<p class="sub" style="margin-top:6px;font-size:12px;">
Фильтр по маскам: положите в корень папки файл <code>obscure.lst</code>. Подробнее — кнопка HELP.
</p>
<div class="footer-bar"> <div class="footer-bar">
<span id="fileCount">0 файлов</span> <span id="fileCount">0 файлов</span>
<button class="btn" id="cancelBtn" style="display:none" onclick="confirmCancel()">⏹ Прервать</button> <button class="btn" id="cancelBtn" style="display:none" onclick="confirmCancel()">Прервать</button>
<button class="btn" id="newSessionBtn" style="display:none" onclick="resetAll()">🔄 Новая сессия</button> <button class="btn" id="newSessionBtn" style="display:none" onclick="resetAll()">Новая сессия</button>
<button class="btn btn-primary" id="uploadBtn" disabled onclick="uploadFiles()">🛡️ Обфусцировать</button> <button class="btn btn-primary" id="uploadBtn" disabled onclick="uploadFiles()">Обфусцировать</button>
</div> </div>
<div class="status" id="status"></div> <div class="status" id="status"></div>
<div class="dl-btns" id="dlBtns"> <div class="dl-btns" id="dlBtns">
<button class="btn btn-primary" id="dlZip" onclick="downloadZip()">📦 Скачать ZIP</button> <button class="btn btn-primary" id="dlZip" onclick="downloadZip()">Скачать ZIP</button>
<button class="btn" id="dlCsv" onclick="downloadCsv()">📋 Скачать CSV</button> <button class="btn" id="dlCsv" onclick="downloadCsv()">Скачать CSV</button>
</div> </div>
<div class="live-block" id="liveBlock"> <div class="live-block" id="liveBlock">
<div class="live-title">⏱ Обработка…</div> <div class="live-title">Обработка…</div>
<div class="live-file" id="liveFile">—</div> <div class="live-file" id="liveFile">—</div>
<div class="live-timer" id="liveTimer">0.0 с</div> <div class="live-timer" id="liveTimer">0.0 с</div>
<div class="live-llm" id="liveLlm">🤖 ИИ обрабатывает… <span id="liveLlmTime">0.0 с</span></div> <div class="live-llm" id="liveLlm">ИИ обрабатывает… <span id="liveLlmTime">0.0 с</span></div>
<div class="live-eta" id="liveEta"></div> <div class="live-eta" id="liveEta"></div>
</div> </div>
<div class="stats-block" id="statsBlock"> <div class="stats-block" id="statsBlock">
<div class="stats-title">📊 Итоги обработки</div> <div class="stats-title">Итоги обработки</div>
<div class="stats-row"> <div class="stats-row">
<div class="stat-item"> <div class="stat-item">
<div class="stat-label">Общее время</div> <div class="stat-label">Общее время</div>
@@ -533,8 +522,8 @@
mount: '#filePicker', mount: '#filePicker',
allowedExt: DOC_EXTS, allowedExt: DOC_EXTS,
labels: { labels: {
pickFiles: '📄 Выбрать файлы', pickFiles: 'Выбрать файлы',
pickFolder: '📁 Выбрать папку', pickFolder: 'Выбрать папку',
clear: 'Очистить', clear: 'Очистить',
empty: 'Нет выбранных файлов', empty: 'Нет выбранных файлов',
statusReady: 'готов', statusReady: 'готов',
@@ -574,7 +563,7 @@
} }
if (currentSid) { if (currentSid) {
fetch('/api/cancel/' + currentSid, { method: 'POST' }).catch(() => { }); fetch('/api/cancel/' + currentSid, { method: 'POST' }).catch(() => { });
st.textContent = '⏹ Остановка… завершаем текущий файл'; st.textContent = 'Остановка… завершаем текущий файл';
} }
} }
@@ -660,7 +649,7 @@
if (!res.ok) { if (!res.ok) {
if (res.aborted) { if (res.aborted) {
st.className = 'status'; st.className = 'status';
st.textContent = '⏹ Загрузка отменена пользователем.'; st.textContent = 'Загрузка отменена пользователем.';
} else { } else {
st.className = 'status error'; st.className = 'status error';
st.textContent = res.error || 'Ошибка загрузки'; st.textContent = res.error || 'Ошибка загрузки';
@@ -676,7 +665,7 @@
if (TEST_UPLOAD_ONLY) { if (TEST_UPLOAD_ONLY) {
st.className = 'status done'; st.className = 'status done';
st.textContent = '✅ Загружено ' + uploadedCount + ' файлов (тест: обработка пропущена). Сессия: ' + currentSid; st.textContent = 'Загружено ' + uploadedCount + ' файлов (тест: обработка пропущена). Сессия: ' + currentSid;
setBusy(false); setBusy(false);
cb.style.display = 'none'; cb.style.display = 'none';
ub.disabled = false; ub.disabled = false;
@@ -738,7 +727,7 @@
curRows.forEach(r => r.classList.remove('row-current')); curRows.forEach(r => r.classList.remove('row-current'));
curRows[idx].classList.add('row-current'); curRows[idx].classList.add('row-current');
const td = curRows[idx].querySelectorAll('td')[2]; const td = curRows[idx].querySelectorAll('td')[2];
if (td) td.innerHTML = '<span style="color:#2563eb">⚙️</span>'; if (td) td.innerHTML = '<span style="color:var(--brand-primary)">…</span>';
} }
currentLiveFile = 'Файл ' + (d.idx + 1) + '/' + total + ': ' + d.name; currentLiveFile = 'Файл ' + (d.idx + 1) + '/' + total + ': ' + d.name;
liveFileEl.textContent = currentLiveFile; liveFileEl.textContent = currentLiveFile;
@@ -757,7 +746,7 @@
curRows.forEach(r => r.classList.remove('row-current')); curRows.forEach(r => r.classList.remove('row-current'));
curRows[idx].classList.add('row-current'); curRows[idx].classList.add('row-current');
const td = curRows[idx].querySelectorAll('td')[2]; const td = curRows[idx].querySelectorAll('td')[2];
if (td) td.innerHTML = '<span style="color:#2563eb">⚙️ анализ…</span>'; if (td) td.innerHTML = '<span style="color:var(--brand-primary)">анализ…</span>';
} }
liveFileEl.textContent = 'Файл ' + (d.idx + 1) + '/' + total + ': ' + d.name; liveFileEl.textContent = 'Файл ' + (d.idx + 1) + '/' + total + ': ' + d.name;
}); });
@@ -773,7 +762,7 @@
if (curRows[idx]) { if (curRows[idx]) {
curRows[idx].classList.remove('row-current'); curRows[idx].classList.remove('row-current');
const td = curRows[idx].querySelectorAll('td')[2]; const td = curRows[idx].querySelectorAll('td')[2];
if (td) td.innerHTML = '✅ готов'; if (td) td.innerHTML = '<span style="color:var(--green)">готов</span>';
} }
}); });
activeES.addEventListener('llm', function (e) { activeES.addEventListener('llm', function (e) {
@@ -781,7 +770,7 @@
if (d.active) { if (d.active) {
liveLlmTime.textContent = d.elapsed + ' с'; liveLlmTime.textContent = d.elapsed + ' с';
liveLlm.classList.add('show'); liveLlm.classList.add('show');
liveFileEl.textContent = '🤖 ИИ анализирует все файлы…'; liveFileEl.textContent = 'ИИ анализирует все файлы…';
if (typeof d.eta_sec === 'number' && d.eta_sec >= 0) liveEta.textContent = '⏳ осталось ~' + fmtSec(d.eta_sec); if (typeof d.eta_sec === 'number' && d.eta_sec >= 0) liveEta.textContent = '⏳ осталось ~' + fmtSec(d.eta_sec);
else liveEta.textContent = ''; else liveEta.textContent = '';
} else { } else {
@@ -805,7 +794,7 @@
: ((performance.now() - (fileTimers[idx] || performance.now())) / 1000).toFixed(1); : ((performance.now() - (fileTimers[idx] || performance.now())) / 1000).toFixed(1);
if (curRows[idx]) { if (curRows[idx]) {
const td = curRows[idx].querySelectorAll('td')[2]; const td = curRows[idx].querySelectorAll('td')[2];
if (td) td.innerHTML = '✅ ' + sec + 'с'; if (td) td.innerHTML = '<span style="color:var(--green)">' + sec + 'с</span>';
} }
}); });
activeES.addEventListener('cancelled', function (e) { activeES.addEventListener('cancelled', function (e) {
@@ -813,7 +802,7 @@
finishProcUI(); finishProcUI();
const totalSec = ((performance.now() - t0) / 1000).toFixed(1); const totalSec = ((performance.now() - t0) / 1000).toFixed(1);
st.className = 'status done'; st.className = 'status done';
st.textContent = '⏹ Остановлено. Сохранено ' + d.saved + ' из ' + d.total + ' файлов + таблица замен (общее ' + totalSec + 'с)'; st.textContent = 'Остановлено. Сохранено ' + d.saved + ' из ' + d.total + ' файлов + таблица замен (общее ' + totalSec + 'с)';
const sb = document.getElementById('statsBlock'); const sb = document.getElementById('statsBlock');
document.getElementById('stTotalTime').textContent = totalSec + ' с'; document.getElementById('stTotalTime').textContent = totalSec + ' с';
if (d.llm_sec > 0) { if (d.llm_sec > 0) {
@@ -835,7 +824,7 @@
const d = JSON.parse(e.data); const d = JSON.parse(e.data);
const totalSec = ((performance.now() - t0) / 1000).toFixed(1); const totalSec = ((performance.now() - t0) / 1000).toFixed(1);
st.className = 'status done'; st.className = 'status done';
st.textContent = '✅ Обработано ' + d.total + ' файлов: общее ' + totalSec + 'с, из них ИИ ' + (d.llm_sec > 0 ? d.llm_sec + 'с' : '0с'); st.textContent = 'Обработано ' + d.total + ' файлов: общее ' + totalSec + 'с, из них ИИ ' + (d.llm_sec > 0 ? d.llm_sec + 'с' : '0с');
const sb = document.getElementById('statsBlock'); const sb = document.getElementById('statsBlock');
document.getElementById('stTotalTime').textContent = totalSec + ' с'; document.getElementById('stTotalTime').textContent = totalSec + ' с';
if (d.llm_sec > 0) { if (d.llm_sec > 0) {
@@ -957,7 +946,8 @@
</p> </p>
<ul style="font-size:13px;color:var(--muted);line-height:1.7;padding-left:20px;margin:8px 0"> <ul style="font-size:13px;color:var(--muted);line-height:1.7;padding-left:20px;margin:8px 0">
<li>Файл ищется только в корне папки; имя без учёта регистра: <code>obscure.lst</code>, <li>Файл ищется только в корне папки; имя без учёта регистра: <code>obscure.lst</code>,
<code>OBSCURE.LST</code>.</li> <code>OBSCURE.LST</code>.
</li>
<li>Если файла нет — обрабатываются все поддерживаемые документы (как раньше).</li> <li>Если файла нет — обрабатываются все поддерживаемые документы (как раньше).</li>
<li>Сам <code>obscure.lst</code> не загружается и не обфусцируется.</li> <li>Сам <code>obscure.lst</code> не загружается и не обфусцируется.</li>
<li>Другие <code>.lst</code> игнорируются; вложенный <code>subdir/obscure.lst</code> фильтр не включает.</li> <li>Другие <code>.lst</code> игнорируются; вложенный <code>subdir/obscure.lst</code> фильтр не включает.</li>
@@ -993,21 +983,25 @@
</tr> </tr>
<tr> <tr>
<td style="padding:4px 8px;border-bottom:1px solid var(--brand-gray);white-space:nowrap"><code>[a-z]</code>, <td style="padding:4px 8px;border-bottom:1px solid var(--brand-gray);white-space:nowrap"><code>[a-z]</code>,
<code>[abc]</code></td> <code>[abc]</code>
</td>
<td style="padding:4px 8px;border-bottom:1px solid var(--brand-gray);color:var(--muted)">класс или диапазон <td style="padding:4px 8px;border-bottom:1px solid var(--brand-gray);color:var(--muted)">класс или диапазон
символов</td> символов</td>
</tr> </tr>
<tr> <tr>
<td style="padding:4px 8px;border-bottom:1px solid var(--brand-gray);white-space:nowrap"><code>[!a]</code>, <td style="padding:4px 8px;border-bottom:1px solid var(--brand-gray);white-space:nowrap"><code>[!a]</code>,
<code>[^a]</code></td> <code>[^a]</code>
</td>
<td style="padding:4px 8px;border-bottom:1px solid var(--brand-gray);color:var(--muted)">любой символ, кроме <td style="padding:4px 8px;border-bottom:1px solid var(--brand-gray);color:var(--muted)">любой символ, кроме
указанных</td> указанных</td>
</tr> </tr>
<tr> <tr>
<td style="padding:4px 8px;border-bottom:1px solid var(--brand-gray);white-space:nowrap"> <td style="padding:4px 8px;border-bottom:1px solid var(--brand-gray);white-space:nowrap">
<code>reports/</code></td> <code>reports/</code>
</td>
<td style="padding:4px 8px;border-bottom:1px solid var(--brand-gray);color:var(--muted)">то же, что <td style="padding:4px 8px;border-bottom:1px solid var(--brand-gray);color:var(--muted)">то же, что
<code>reports/**</code> — всё поддерево</td> <code>reports/**</code> — всё поддерево
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -1043,7 +1037,7 @@ reports/name*.docx
<div <div
style="background:var(--card);border-radius:12px;padding:24px;max-width:440px;box-shadow:0 4px 24px rgba(0,0,0,.15)" style="background:var(--card);border-radius:12px;padding:24px;max-width:440px;box-shadow:0 4px 24px rgba(0,0,0,.15)"
onclick="event.stopPropagation()"> onclick="event.stopPropagation()">
<h3 style="margin-bottom:12px">⏹ Остановить обработку?</h3> <h3 style="margin-bottom:12px">Остановить обработку?</h3>
<p style="font-size:13px;color:var(--muted);line-height:1.6" id="cancelModalText"> <p style="font-size:13px;color:var(--muted);line-height:1.6" id="cancelModalText">
Будут сохранены: полностью обработанные файлы и таблица замен. Будут сохранены: полностью обработанные файлы и таблица замен.
Необработанные файлы в результат не попадут. Необработанные файлы в результат не попадут.