Update upload interface and ignore temporary E2E artifacts
Deploy drhider / validate (push) Canceled after 0s

This commit is contained in:
Repinoid
2026-09-16 07:42:31 +03:00
parent 49bb4d70d8
commit 52da9a3923
3 changed files with 860 additions and 541 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
- НЕЛЬЗЯ менять ЧТО-ЛИБО при вопросе (?, почему, как, где, что) — только ответ словами - НЕЛЬЗЯ менять ЧТО-ЛИБО при вопросе (?, почему, как, где, что) — только ответ словами
- НЕЛЬЗЯ спешить — обдумать, проверить, потом отвечать - НЕЛЬЗЯ спешить — обдумать, проверить, потом отвечать
- НЕЛЬЗЯ лезть в /home/naeel/nubes/contracts/ (кроме loadtest) - НЕЛЬЗЯ лезть в /home/naeel/nubes/contracts/ (кроме loadtest)
- НЕЛЬЗЯ предполагать — сомневаешься = спроси - НИКОГДА не руководствоваться догадками. Если что-либо неизвестно точно или недоступно — немедленно остановиться и сообщить об этом; не продолжать действие и не выдавать предположение за факт.
- НЕЛЬЗЯ грузить внешние CDN/библиотеки на фронтенд — всё в коде - НЕЛЬЗЯ грузить внешние CDN/библиотеки на фронтенд — всё в коде
# ✅ ПОРЯДОК ПРАВКИ # ✅ ПОРЯДОК ПРАВКИ
+2
View File
@@ -26,6 +26,8 @@ dist/
# about1500.md # about1500.md
*.har *.har
chrome-net-export-log.json chrome-net-export-log.json
History/tests/2026-09-15-*.md
tests/e2e_mass_processing.py
TSTFILES/ TSTFILES/
files/ files/
upload-platform/ upload-platform/
+475 -158
View File
@@ -1,5 +1,6 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="ru"> <html lang="ru">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -20,142 +21,447 @@
--red: #ef4444; --red: #ef4444;
--green: #22c55e; --green: #22c55e;
} }
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font: 14px system-ui, sans-serif; color: var(--text); background: var(--bg); min-height: 100vh; } * {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font: 14px system-ui, sans-serif;
color: var(--text);
background: var(--bg);
min-height: 100vh;
}
.topbar { .topbar {
background: var(--card); border-bottom: 1px solid var(--brand-gray); background: var(--card);
padding: 0 16px; height: 48px; display: flex; align-items: center; gap: 10px; border-bottom: 1px solid var(--brand-gray);
padding: 0 16px;
height: 48px;
display: flex;
align-items: center;
gap: 10px;
} }
.topbar img { height: 18px; }
.topbar .title { font-weight: 600; font-size: 14px; } .topbar img {
.topbar .ver { font-weight: 400; color: var(--muted); font-size: 12px; } height: 18px;
}
.topbar .title {
font-weight: 600;
font-size: 14px;
}
.topbar .ver {
font-weight: 400;
color: var(--muted);
font-size: 12px;
}
.topbar .help { .topbar .help {
margin-left: auto; cursor: pointer; text-decoration: none; user-select: none; margin-left: auto;
display: inline-flex; align-items: center; justify-content: center; cursor: pointer;
padding: 5px 14px; border-radius: 6px; text-decoration: none;
background: #1d4ed8; color: #fff; font-size: 13px; font-weight: 700; line-height: 1; user-select: none;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 5px 14px;
border-radius: 6px;
background: #1d4ed8;
color: #fff;
font-size: 13px;
font-weight: 700;
line-height: 1;
letter-spacing: .06em; letter-spacing: .06em;
box-shadow: 0 1px 3px rgba(0,0,0,.25); box-shadow: 0 1px 3px rgba(0, 0, 0, .25);
} }
.topbar .help:hover { background: #1e40af; color: #fff; }
.content { max-width: 820px; margin: 20px auto; padding: 0 12px; } .topbar .help:hover {
background: #1e40af;
color: #fff;
}
.content {
max-width: 820px;
margin: 20px auto;
padding: 0 12px;
}
.card { .card {
background: var(--card); border-radius: 12px; background: var(--card);
border: 1px solid var(--brand-gray); box-shadow: 0 1px 2px rgba(0,0,0,.04); border-radius: 12px;
border: 1px solid var(--brand-gray);
box-shadow: 0 1px 2px rgba(0, 0, 0, .04);
} }
.card-header { .card-header {
background: var(--brand-grey-light); padding: 10px 12px; background: var(--brand-grey-light);
font-weight: 600; font-size: 14px; border-bottom: 1px solid var(--brand-gray); padding: 10px 12px;
font-weight: 600;
font-size: 14px;
border-bottom: 1px solid var(--brand-gray);
}
.card-body {
padding: 12px;
}
.sub {
font-size: 12px;
color: var(--muted);
margin-bottom: 10px;
line-height: 1.5;
} }
.card-body { padding: 12px; }
.sub { font-size: 12px; color: var(--muted); margin-bottom: 10px; line-height: 1.5; }
/* FilePicker styling */ /* FilePicker styling */
.file-picker { font-family: inherit; color: var(--text); } .file-picker {
.file-picker .fp-toolbar { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 10px; } font-family: inherit;
color: var(--text);
}
.file-picker .fp-toolbar {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 10px;
}
.file-picker .fp-toolbar button { .file-picker .fp-toolbar button {
height: 32px; border-radius: 6px; padding: 0 12px; font-size: 13px; height: 32px;
font-family: inherit; cursor: pointer; border: 1px solid var(--brand-gray); border-radius: 6px;
background: var(--card); color: var(--text); padding: 0 12px;
display: inline-flex; align-items: center; gap: 5px; font-weight: 500; font-size: 13px;
font-family: inherit;
cursor: pointer;
border: 1px solid var(--brand-gray);
background: var(--card);
color: var(--text);
display: inline-flex;
align-items: center;
gap: 5px;
font-weight: 500;
} }
.file-picker .fp-toolbar button#fp-files-btn { background: #eff6ff; border-color: var(--brand-primary); color: var(--brand-primary); }
.file-picker .fp-toolbar button#fp-folder-btn { background: var(--card); } .file-picker .fp-toolbar button#fp-files-btn {
.file-picker .fp-toolbar button#fp-clear-btn { color: var(--muted); border-color: transparent; } background: #eff6ff;
.file-picker .fp-toolbar button#fp-clear-btn:hover { color: var(--red); } border-color: var(--brand-primary);
.file-picker .fp-table-wrap { border: 1px solid var(--brand-gray); border-radius: 8px; overflow: hidden; margin-bottom: 6px; } color: var(--brand-primary);
.file-picker .fp-table { width: 100%; border-collapse: collapse; font-size: 13px; } }
.file-picker .fp-toolbar button#fp-folder-btn {
background: var(--card);
}
.file-picker .fp-toolbar button#fp-clear-btn {
color: var(--muted);
border-color: transparent;
}
.file-picker .fp-toolbar button#fp-clear-btn:hover {
color: var(--red);
}
.file-picker .fp-table-wrap {
border: 1px solid var(--brand-gray);
border-radius: 8px;
overflow: hidden;
margin-bottom: 6px;
}
.file-picker .fp-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.file-picker .fp-table th { .file-picker .fp-table th {
background: var(--brand-grey-light); text-transform: uppercase; padding: 6px 8px; background: var(--brand-grey-light);
border-right: 1px solid var(--brand-gray); border-bottom: 1px solid var(--brand-gray); text-transform: uppercase;
text-align: left; font-weight: 600; font-size: 10px; color: var(--muted); padding: 6px 8px;
border-right: 1px solid var(--brand-gray);
border-bottom: 1px solid var(--brand-gray);
text-align: left;
font-weight: 600;
font-size: 10px;
color: var(--muted);
} }
.file-picker .fp-table td { .file-picker .fp-table td {
padding: 5px 8px; border-right: 1px solid var(--brand-gray); border-bottom: 1px solid var(--brand-gray); padding: 5px 8px;
border-right: 1px solid var(--brand-gray);
border-bottom: 1px solid var(--brand-gray);
} }
.file-picker .tree-row:hover td { background: rgba(37,99,235,.03); }
.file-picker .tree-row.row-current td { background: rgba(37,99,235,.12) !important; color: #1d4ed8; font-weight: 600; } .file-picker .tree-row:hover td {
background: rgba(37, 99, 235, .03);
}
.file-picker .tree-row.row-current td {
background: rgba(37, 99, 235, .12) !important;
color: #1d4ed8;
font-weight: 600;
}
.file-picker .tree-name { .file-picker .tree-name {
display: inline-flex; align-items: center; gap: 6px; border: 0; background: transparent; display: inline-flex;
color: inherit; font: inherit; text-align: left; cursor: pointer; align-items: center;
gap: 6px;
border: 0;
background: transparent;
color: inherit;
font: inherit;
text-align: left;
cursor: pointer;
} }
.file-picker .tree-chevron { display: inline-block; width: 12px; color: var(--muted); font-size: 9px; }
.file-picker .fp-status { font-size: 12px; color: var(--muted); margin: 4px 0; } .file-picker .tree-chevron {
.file-picker .fp-count { display: none; } display: inline-block;
width: 12px;
color: var(--muted);
font-size: 9px;
}
.file-picker .fp-status {
font-size: 12px;
color: var(--muted);
margin: 4px 0;
}
.file-picker .fp-count {
display: none;
}
.file-picker .remove-btn { .file-picker .remove-btn {
cursor: pointer; color: #f87171; background: none; border: none; cursor: pointer;
padding: 2px 4px; font-size: 14px; line-height: 1; color: #f87171;
background: none;
border: none;
padding: 2px 4px;
font-size: 14px;
line-height: 1;
}
.file-picker .remove-btn:hover {
color: var(--red);
}
body.busy .file-picker button {
pointer-events: none;
opacity: 0.5;
}
body.busy .file-picker .remove-btn {
display: none;
} }
.file-picker .remove-btn:hover { color: var(--red); }
body.busy .file-picker button { pointer-events: none; opacity: 0.5; }
body.busy .file-picker .remove-btn { display: none; }
.footer-bar { .footer-bar {
margin-top: 10px; display: flex; justify-content: space-between; align-items: center; margin-top: 10px;
font-size: 12px; color: var(--muted); display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: var(--muted);
} }
.btn { .btn {
height: 32px; border-radius: 6px; padding: 0 12px; font-size: 13px; height: 32px;
font-family: inherit; cursor: pointer; border: 1px solid var(--brand-gray); border-radius: 6px;
background: var(--card); color: var(--text); padding: 0 12px;
display: inline-flex; align-items: center; gap: 5px; font-size: 13px;
font-family: inherit;
cursor: pointer;
border: 1px solid var(--brand-gray);
background: var(--card);
color: var(--text);
display: inline-flex;
align-items: center;
gap: 5px;
} }
.btn-primary { background: var(--brand-primary); border-color: var(--brand-primary); color: #fff; }
.btn-primary:hover { opacity: .9; } .btn-primary {
.btn:disabled { opacity: .4; cursor: not-allowed; } background: var(--brand-primary);
.status { margin-top: 10px; padding: 8px 12px; border-radius: 8px; font-size: 13px; display: none; } border-color: var(--brand-primary);
.status.progress { background: #eff6ff; border: 1px solid var(--brand-primary); color: var(--brand-primary); display: block; } color: #fff;
.status.done { background: #f0fdf4; border: 1px solid var(--green); color: #16a34a; display: block; } }
.status.error { background: #fef2f2; border: 1px solid var(--red); color: #991b1b; display: block; }
.dl-btns { display: none; gap: 8px; margin-top: 8px; } .btn-primary:hover {
.dl-btns.show { display: flex; } opacity: .9;
}
.btn:disabled {
opacity: .4;
cursor: not-allowed;
}
.status {
margin-top: 10px;
padding: 8px 12px;
border-radius: 8px;
font-size: 13px;
display: none;
}
.status.progress {
background: #eff6ff;
border: 1px solid var(--brand-primary);
color: var(--brand-primary);
display: block;
}
.status.done {
background: #f0fdf4;
border: 1px solid var(--green);
color: #16a34a;
display: block;
}
.status.error {
background: #fef2f2;
border: 1px solid var(--red);
color: #991b1b;
display: block;
}
.dl-btns {
display: none;
gap: 8px;
margin-top: 8px;
}
.dl-btns.show {
display: flex;
}
.stats-block { .stats-block {
display: none; margin-top: 12px; padding: 16px; display: none;
margin-top: 12px;
padding: 16px;
background: linear-gradient(135deg, #f0fdf4, #ecfdf5); background: linear-gradient(135deg, #f0fdf4, #ecfdf5);
border: 2px solid var(--green); border-radius: 12px; border: 2px solid var(--green);
border-radius: 12px;
} }
.stats-block.show { display: block; }
.stats-title { font-size: 16px; font-weight: 700; color: #14532d; margin-bottom: 10px; } .stats-block.show {
.stats-row { display: flex; flex-wrap: wrap; gap: 10px; } display: block;
}
.stats-title {
font-size: 16px;
font-weight: 700;
color: #14532d;
margin-bottom: 10px;
}
.stats-row {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.stat-item { .stat-item {
flex: 1; min-width: 130px; background: #fff; border: 1px solid #bbf7d0; flex: 1;
border-radius: 10px; padding: 10px 12px; text-align: center; min-width: 130px;
background: #fff;
border: 1px solid #bbf7d0;
border-radius: 10px;
padding: 10px 12px;
text-align: center;
} }
.stat-label { font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); margin-bottom: 4px; }
.stat-value { font-size: 24px; font-weight: 800; color: #166534; } .stat-label {
font-size: 12px;
text-transform: uppercase;
letter-spacing: .04em;
color: var(--muted);
margin-bottom: 4px;
}
.stat-value {
font-size: 24px;
font-weight: 800;
color: #166534;
}
.live-block { .live-block {
display: none; margin-top: 12px; padding: 16px; display: none;
margin-top: 12px;
padding: 16px;
background: linear-gradient(135deg, #eff6ff, #dbeafe); background: linear-gradient(135deg, #eff6ff, #dbeafe);
border: 2px solid var(--brand-primary); border-radius: 12px; 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-block.show {
.live-file { font-size: 15px; color: #1e3a8a; margin-bottom: 10px; word-break: break-word; } display: block;
.live-timer { font-size: 40px; font-weight: 800; color: #1d4ed8; line-height: 1.1; } }
.live-title {
font-size: 18px;
font-weight: 800;
color: #1e40af;
margin-bottom: 6px;
}
.live-file {
font-size: 15px;
color: #1e3a8a;
margin-bottom: 10px;
word-break: break-word;
}
.live-timer {
font-size: 40px;
font-weight: 800;
color: #1d4ed8;
line-height: 1.1;
}
.live-llm { .live-llm {
display: none; margin-top: 8px; padding: 8px 12px; border-radius: 8px; display: none;
background: #fef3c7; border: 1px solid #f59e0b; color: #92400e; margin-top: 8px;
font-size: 18px; font-weight: 700; width: fit-content; padding: 8px 12px;
border-radius: 8px;
background: #fef3c7;
border: 1px solid #f59e0b;
color: #92400e;
font-size: 18px;
font-weight: 700;
width: fit-content;
}
.live-llm.show {
display: inline-block;
}
.live-eta {
margin-top: 8px;
font-size: 14px;
color: #1d4ed8;
font-weight: 600;
} }
.live-llm.show { display: inline-block; }
.live-eta { margin-top: 8px; font-size: 14px; color: #1d4ed8; font-weight: 600; }
</style> </style>
</head> </head>
<body> <body>
<div class="topbar"> <div class="topbar">
<img src="/static/logo.svg" alt="Nubes"> <img src="/static/logo.svg" alt="Nubes">
<span class="title">DrHider</span> <span class="title">DrHider</span>
<span class="ver">v{{ version }}</span> <span class="ver">v{{ version }}</span>
<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">
<div class="card"> <div class="card">
<div class="card-header">Обфускация документов</div> <div class="card-header">Обфускация документов</div>
<div class="card-body"> <div class="card-body">
<p class="sub"> <p class="sub">
Загрузите документы (.docx, .doc, .pdf, .txt, .md, .zip) — получите ZIP с обезличенными копиями.<br> Загрузите документы (.docx, .doc, .pdf, .txt, .md, .zip) — получите ZIP с обезличенными копиями.<br>
CSV с таблицей замен скачивается отдельно.<br> CSV с таблицей замен скачивается отдельно.<br>
<span style="color:#c0392b;">Ограничения: один файл не более 50 МБ, суммарно не более 500 МБ. Файлы сверх лимитов помечаются статусом «пропущен» и не участвуют в обфускации. Пустые, повреждённые файлы и сканы без текстового слоя получают статус «не извлечён» — они тоже не попадают в результат.</span><br> <span style="color:#c0392b;">Ограничения: один файл не более 50 МБ, суммарно не более 500 МБ. Файлы сверх
лимитов помечаются статусом «пропущен» и не участвуют в обфускации. Пустые, повреждённые файлы и сканы без
текстового слоя получают статус «не извлечён» — они тоже не попадают в результат.</span><br>
<span style="color:#7d3c98;">⏱ Время обработки каждого файла — ориентировочное (обновляется по факту).</span> <span style="color:#7d3c98;">⏱ Время обработки каждого файла — ориентировочное (обновляется по факту).</span>
</p> </p>
@@ -198,29 +504,29 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<script src="/file-picker/file-picker.iife.js?v={{ version }}"></script> <script src="/file-picker/file-picker.iife.js?v={{ version }}"></script>
<script> <script>
const DOC_EXTS = ['.pdf', '.doc', '.docx', '.txt', '.md']; const DOC_EXTS = ['.pdf', '.doc', '.docx', '.txt', '.md'];
const ub = document.getElementById('uploadBtn'); const ub = document.getElementById('uploadBtn');
const cb = document.getElementById('cancelBtn'); const cb = document.getElementById('cancelBtn');
const nb = document.getElementById('newSessionBtn'); const nb = document.getElementById('newSessionBtn');
const fc = document.getElementById('fileCount'); const fc = document.getElementById('fileCount');
const st = document.getElementById('status'); const st = document.getElementById('status');
const db = document.getElementById('dlBtns'); const db = document.getElementById('dlBtns');
const MAX_FILE_BYTES = 50 * 1024 * 1024; // 50 МБ на один файл const MAX_FILE_BYTES = 50 * 1024 * 1024; // 50 МБ на один файл
const MAX_SESSION_BYTES = 500 * 1024 * 1024; // 500 МБ суммарно на сессию const MAX_SESSION_BYTES = 500 * 1024 * 1024; // 500 МБ суммарно на сессию
const VM_UPLOAD_URL = 'https://contracts.kube5s.ru/drhider-upload/'; // ВМ-буфер const VM_UPLOAD_URL = 'https://contracts.kube5s.ru/drhider-upload/'; // ВМ-буфер
const TEST_UPLOAD_ONLY = new URLSearchParams(location.search).get('upload-only') === '1'; const TEST_UPLOAD_ONLY = new URLSearchParams(location.search).get('upload-only') === '1';
let currentSid = ''; let currentSid = '';
let activeES = null; let activeES = null;
let abortCtrl = null; let abortCtrl = null;
let busy = false; let busy = false;
let sessionDone = false; let sessionDone = false;
let ptimer = null, liveRefresh = null; let ptimer = null, liveRefresh = null;
const picker = FilePicker.initFilePicker({ const picker = FilePicker.initFilePicker({
mount: '#filePicker', mount: '#filePicker',
allowedExt: DOC_EXTS, allowedExt: DOC_EXTS,
labels: { labels: {
@@ -242,43 +548,43 @@ const picker = FilePicker.initFilePicker({
ub.disabled = files.length === 0 || busy; ub.disabled = files.length === 0 || busy;
fc.textContent = files.length ? `${files.length} файл(ов)` : '0 файлов'; fc.textContent = files.length ? `${files.length} файл(ов)` : '0 файлов';
} }
}); });
function setBusy(b) { function setBusy(b) {
busy = b; busy = b;
document.body.classList.toggle('busy', b); document.body.classList.toggle('busy', b);
} }
function fmtSec(s) { function fmtSec(s) {
s = Math.max(0, Math.round(s)); s = Math.max(0, Math.round(s));
return s >= 60 ? Math.floor(s / 60) + 'м ' + (s % 60) + 'с' : s + 'с'; return s >= 60 ? Math.floor(s / 60) + 'м ' + (s % 60) + 'с' : s + 'с';
} }
function confirmCancel() { function confirmCancel() {
document.getElementById('cancelModal').style.display = 'flex'; document.getElementById('cancelModal').style.display = 'flex';
} }
function doCancel() { function doCancel() {
document.getElementById('cancelModal').style.display = 'none'; document.getElementById('cancelModal').style.display = 'none';
if (abortCtrl) { if (abortCtrl) {
abortCtrl.abort(); abortCtrl.abort();
} }
if (currentSid) { if (currentSid) {
fetch('/api/cancel/' + currentSid, { method: 'POST' }).catch(() => {}); fetch('/api/cancel/' + currentSid, { method: 'POST' }).catch(() => { });
st.textContent = '⏹ Остановка… завершаем текущий файл'; st.textContent = '⏹ Остановка… завершаем текущий файл';
} }
} }
function finishProcUI() { function finishProcUI() {
if (activeES) { activeES.close(); activeES = null; } if (activeES) { activeES.close(); activeES = null; }
if (ptimer) { clearInterval(ptimer); ptimer = null; } if (ptimer) { clearInterval(ptimer); ptimer = null; }
if (liveRefresh) { clearInterval(liveRefresh); liveRefresh = null; } if (liveRefresh) { clearInterval(liveRefresh); liveRefresh = null; }
document.getElementById('liveBlock').classList.remove('show'); document.getElementById('liveBlock').classList.remove('show');
cb.style.display = 'none'; cb.style.display = 'none';
setBusy(false); setBusy(false);
} }
function resetAll() { function resetAll() {
if (activeES) { activeES.close(); activeES = null; } if (activeES) { activeES.close(); activeES = null; }
if (abortCtrl) { abortCtrl.abort(); abortCtrl = null; } if (abortCtrl) { abortCtrl.abort(); abortCtrl = null; }
if (ptimer) { clearInterval(ptimer); ptimer = null; } if (ptimer) { clearInterval(ptimer); ptimer = null; }
@@ -295,11 +601,11 @@ function resetAll() {
document.getElementById('statsBlock').classList.remove('show'); document.getElementById('statsBlock').classList.remove('show');
document.getElementById('liveBlock').classList.remove('show'); document.getElementById('liveBlock').classList.remove('show');
ub.disabled = true; ub.disabled = true;
} }
window.addEventListener('beforeunload', () => resetAll()); window.addEventListener('beforeunload', () => resetAll());
async function uploadFiles() { async function uploadFiles() {
if (busy) return; if (busy) return;
const files = picker.getFiles(); const files = picker.getFiles();
if (!files || files.length === 0) return; if (!files || files.length === 0) return;
@@ -376,9 +682,9 @@ async function uploadFiles() {
} }
await startProcessingPhase(total, files); await startProcessingPhase(total, files);
} }
async function startProcessingPhase(total, files) { async function startProcessingPhase(total, files) {
st.className = 'status progress'; st.className = 'status progress';
st.textContent = 'Обработка (этап 2/2)…'; st.textContent = 'Обработка (этап 2/2)…';
cb.style.display = 'inline-block'; cb.style.display = 'inline-block';
@@ -421,7 +727,7 @@ async function startProcessingPhase(total, files) {
try { try {
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
activeES = new EventSource('/api/process_stream/' + currentSid); activeES = new EventSource('/api/process_stream/' + currentSid);
activeES.addEventListener('start', function(e) { activeES.addEventListener('start', function (e) {
const d = JSON.parse(e.data); const d = JSON.parse(e.data);
const idx = d.idx; const idx = d.idx;
const curRows = document.querySelectorAll('.tree-row.tree-file'); const curRows = document.querySelectorAll('.tree-row.tree-file');
@@ -434,12 +740,12 @@ async function startProcessingPhase(total, files) {
currentLiveFile = 'Файл ' + (d.idx + 1) + '/' + total + ': ' + d.name; currentLiveFile = 'Файл ' + (d.idx + 1) + '/' + total + ': ' + d.name;
liveFileEl.textContent = currentLiveFile; liveFileEl.textContent = currentLiveFile;
}); });
activeES.addEventListener('extract_done', function(e) { activeES.addEventListener('extract_done', function (e) {
procExtractDone = true; procExtractDone = true;
const curRows = document.querySelectorAll('.tree-row.tree-file'); const curRows = document.querySelectorAll('.tree-row.tree-file');
curRows.forEach(r => r.classList.remove('row-current')); curRows.forEach(r => r.classList.remove('row-current'));
}); });
activeES.addEventListener('file_start', function(e) { activeES.addEventListener('file_start', function (e) {
const d = JSON.parse(e.data); const d = JSON.parse(e.data);
const idx = d.idx; const idx = d.idx;
fileTimers[idx] = performance.now(); fileTimers[idx] = performance.now();
@@ -452,12 +758,12 @@ async function startProcessingPhase(total, files) {
} }
liveFileEl.textContent = 'Файл ' + (d.idx + 1) + '/' + total + ': ' + d.name; liveFileEl.textContent = 'Файл ' + (d.idx + 1) + '/' + total + ': ' + d.name;
}); });
activeES.addEventListener('file_chunk', function(e) { activeES.addEventListener('file_chunk', function (e) {
const d = JSON.parse(e.data); const d = JSON.parse(e.data);
liveFileEl.textContent = 'Файл ' + (d.idx + 1) + '/' + total + ': ' + d.name + liveFileEl.textContent = 'Файл ' + (d.idx + 1) + '/' + total + ': ' + d.name +
(typeof d.eta_sec === 'number' ? ' — осталось ~' + fmtSec(d.eta_sec) : ''); (typeof d.eta_sec === 'number' ? ' — осталось ~' + fmtSec(d.eta_sec) : '');
}); });
activeES.addEventListener('file_done', function(e) { activeES.addEventListener('file_done', function (e) {
const d = JSON.parse(e.data); const d = JSON.parse(e.data);
const idx = d.idx; const idx = d.idx;
const curRows = document.querySelectorAll('.tree-row.tree-file'); const curRows = document.querySelectorAll('.tree-row.tree-file');
@@ -467,7 +773,7 @@ async function startProcessingPhase(total, files) {
if (td) td.innerHTML = '✅ готов'; if (td) td.innerHTML = '✅ готов';
} }
}); });
activeES.addEventListener('llm', function(e) { activeES.addEventListener('llm', function (e) {
const d = JSON.parse(e.data); const d = JSON.parse(e.data);
if (d.active) { if (d.active) {
liveLlmTime.textContent = d.elapsed + ' с'; liveLlmTime.textContent = d.elapsed + ' с';
@@ -480,7 +786,7 @@ async function startProcessingPhase(total, files) {
if (currentLiveFile) liveFileEl.textContent = currentLiveFile; if (currentLiveFile) liveFileEl.textContent = currentLiveFile;
} }
}); });
activeES.addEventListener('done', function(e) { activeES.addEventListener('done', function (e) {
const d = JSON.parse(e.data); const d = JSON.parse(e.data);
const idx = d.idx; const idx = d.idx;
const curRows = document.querySelectorAll('.tree-row.tree-file'); const curRows = document.querySelectorAll('.tree-row.tree-file');
@@ -499,7 +805,7 @@ async function startProcessingPhase(total, files) {
if (td) td.innerHTML = '✅ ' + sec + 'с'; if (td) td.innerHTML = '✅ ' + sec + 'с';
} }
}); });
activeES.addEventListener('cancelled', function(e) { activeES.addEventListener('cancelled', function (e) {
const d = JSON.parse(e.data); const d = JSON.parse(e.data);
finishProcUI(); finishProcUI();
const totalSec = ((performance.now() - t0) / 1000).toFixed(1); const totalSec = ((performance.now() - t0) / 1000).toFixed(1);
@@ -521,7 +827,7 @@ async function startProcessingPhase(total, files) {
nb.style.display = 'inline-block'; nb.style.display = 'inline-block';
resolve(); resolve();
}); });
activeES.addEventListener('complete', function(e) { activeES.addEventListener('complete', function (e) {
finishProcUI(); finishProcUI();
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);
@@ -543,7 +849,7 @@ async function startProcessingPhase(total, files) {
nb.style.display = 'inline-block'; nb.style.display = 'inline-block';
resolve(); resolve();
}); });
activeES.addEventListener('proc_error', function(e) { activeES.addEventListener('proc_error', function (e) {
const d = JSON.parse(e.data); const d = JSON.parse(e.data);
finishProcUI(); finishProcUI();
setBusy(false); setBusy(false);
@@ -551,7 +857,7 @@ async function startProcessingPhase(total, files) {
st.textContent = 'Ошибка обработки: ' + (d.error || 'неизвестная ошибка'); st.textContent = 'Ошибка обработки: ' + (d.error || 'неизвестная ошибка');
resolve(); resolve();
}); });
activeES.onerror = function() { activeES.onerror = function () {
finishProcUI(); finishProcUI();
reject(new Error('SSE connection failed')); reject(new Error('SSE connection failed'));
}; };
@@ -563,9 +869,9 @@ async function startProcessingPhase(total, files) {
st.textContent = 'Ошибка: ' + err.message; st.textContent = 'Ошибка: ' + err.message;
} }
if (!sessionDone) ub.disabled = false; if (!sessionDone) ub.disabled = false;
} }
function downloadZip() { function downloadZip() {
if (!currentSid) return; if (!currentSid) return;
fetch('/api/download/' + currentSid) fetch('/api/download/' + currentSid)
.then(r => { .then(r => {
@@ -582,9 +888,9 @@ function downloadZip() {
document.body.removeChild(a); document.body.removeChild(a);
URL.revokeObjectURL(a.href); URL.revokeObjectURL(a.href);
}); });
} }
function downloadCsv() { function downloadCsv() {
if (!currentSid) return; if (!currentSid) return;
fetch('/api/csv/' + currentSid) fetch('/api/csv/' + currentSid)
.then(r => { .then(r => {
@@ -601,30 +907,40 @@ function downloadCsv() {
document.body.removeChild(a); document.body.removeChild(a);
URL.revokeObjectURL(a.href); URL.revokeObjectURL(a.href);
}); });
} }
window.uploadFiles = uploadFiles; window.uploadFiles = uploadFiles;
window.resetAll = resetAll; window.resetAll = resetAll;
window.confirmCancel = confirmCancel; window.confirmCancel = confirmCancel;
window.doCancel = doCancel; window.doCancel = doCancel;
window.downloadZip = downloadZip; window.downloadZip = downloadZip;
window.downloadCsv = downloadCsv; window.downloadCsv = downloadCsv;
fetch('/health').catch(() => {}); fetch('/health').catch(() => { });
</script> </script>
<div id="helpModal" style="display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.5);z-index:999;justify-content:center;align-items:center" onclick="this.style.display='none'"> <div id="helpModal"
<div style="background:var(--card);border-radius:12px;padding:24px;max-width:420px;box-shadow:0 4px 24px rgba(0,0,0,.15)" onclick="event.stopPropagation()"> style="display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.5);z-index:999;justify-content:center;align-items:center"
onclick="this.style.display='none'">
<div
style="background:var(--card);border-radius:12px;padding:24px;max-width:420px;box-shadow:0 4px 24px rgba(0,0,0,.15)"
onclick="event.stopPropagation()">
<h3 style="margin-bottom:12px">DrHider — обфускация документов</h3> <h3 style="margin-bottom:12px">DrHider — обфускация документов</h3>
<p style="font-size:13px;color:var(--muted);line-height:1.6"> <p style="font-size:13px;color:var(--muted);line-height:1.6">
Загрузите документы (.docx, .doc, .pdf, .txt, .md, .zip) — получите ZIP-архив с обезличенными копиями. Загрузите документы (.docx, .doc, .pdf, .txt, .md, .zip) — получите ZIP-архив с обезличенными копиями.
Персональные данные, реквизиты компаний, телефоны, email и другие приватные сведения заменяются на обезличенные коды. Персональные данные, реквизиты компаний, телефоны, email и другие приватные сведения заменяются на обезличенные
коды.
CSV-файл с таблицей соответствия оригинал→замена скачивается отдельно. CSV-файл с таблицей соответствия оригинал→замена скачивается отдельно.
</p> </p>
<button class="btn" style="margin-top:12px;width:100%" onclick="document.getElementById('helpModal').style.display='none'">Закрыть</button> <button class="btn" style="margin-top:12px;width:100%"
onclick="document.getElementById('helpModal').style.display='none'">Закрыть</button>
</div> </div>
</div> </div>
<div id="cancelModal" style="display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.5);z-index:999;justify-content:center;align-items:center" onclick="this.style.display='none'"> <div id="cancelModal"
<div 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()"> style="display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.5);z-index:999;justify-content:center;align-items:center"
onclick="this.style.display='none'">
<div
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()">
<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">
Будут сохранены: полностью обработанные файлы и таблица замен. Будут сохранены: полностью обработанные файлы и таблица замен.
@@ -636,6 +952,7 @@ fetch('/health').catch(() => {});
<button class="btn btn-primary" onclick="doCancel()">Остановить</button> <button class="btn btn-primary" onclick="doCancel()">Остановить</button>
</div> </div>
</div> </div>
</div> </div>
</body> </body>
</html> </html>