fix: site/ → src/ (stdlib conflict — site is built-in Python module)
Deploy contracts-flask / validate (push) Successful in 0s

This commit is contained in:
2026-07-15 11:16:42 +04:00
parent a7b53fde19
commit 81aba97304
42 changed files with 53 additions and 53 deletions
-315
View File
@@ -1,315 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DrHider — обфускация документов</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='4' fill='%23001C34'/%3E%3Ctext x='16' y='24' text-anchor='middle' font-size='22' font-weight='bold' fill='white' font-family='sans-serif'%3EN%3C/text%3E%3C/svg%3E" type="image/svg+xml">
<style>
:root {
--brand: #2563eb; --brand-dark: #1d4ed8;
--bg: #f8fafc; --card: #fff; --fg: #1e293b; --muted: #64748b;
--border: #e2e8f0; --red: #ef4444; --green: #22c55e;
--grey-light: #f3f4f6; --radius: 8px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font: 14px/1.6 -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--fg); }
.header { background: var(--card); border-bottom: 1px solid var(--border); padding: 12px 24px; display: flex; align-items: center; gap: 12px; position: sticky; top: 0; z-index: 100; }
.header img { height: 24px; }
.header .sep { color: var(--muted); }
.header a { color: var(--brand); text-decoration: none; font-size: 13px; }
.header a:hover { color: var(--brand-dark); }
main { max-width: 700px; margin: 0 auto; padding: 40px 20px 60px; }
h1 { font-size: 22px; font-weight: 600; margin-bottom: 4px; }
.sub { color: var(--muted); font-size: 14px; margin-bottom: 24px; }
.card { background: var(--card); border: 1px solid var(--border); border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,.04); }
.card-header { background: var(--grey-light); padding: 12px 16px; border-radius: 12px 12px 0 0; font-size: 14px; font-weight: 600; border-bottom: 1px solid var(--border); }
.card-body { padding: 16px; }
#dropZone {
border: 2px dashed var(--border); border-radius: 12px; padding: 36px 20px;
text-align: center; cursor: pointer; transition: all .2s;
background: var(--grey-light); margin-bottom: 16px;
}
#dropZone:hover, #dropZone.active { border-color: var(--brand); background: #eff6ff; }
#dropZone p { color: var(--muted); margin: 4px 0; font-size: 14px; }
#dropZone .icon { font-size: 32px; margin-bottom: 8px; }
#fileList { display: none; }
#fileList.show { display: block; }
.file-row { display: flex; align-items: center; gap: 10px; padding: 8px 12px; background: var(--grey-light); border-radius: 6px; margin-bottom: 4px; font-size: 13px; }
.file-row .name { flex: 1; }
.file-row .size { color: var(--muted); }
.file-row .remove { cursor: pointer; color: var(--red); font-weight: bold; font-size: 16px; }
#status { display: none; margin-top: 12px; padding: 12px 16px; border-radius: 8px; font-size: 13px; }
#status.show { display: block; }
#status.progress { background: #eff6ff; border: 1px solid var(--brand); color: var(--brand); }
#status.done { background: #f0fdf4; border: 1px solid var(--green); color: #16a34a; }
#status.error { background: #fef2f2; border: 1px solid var(--red); color: var(--red); }
.btn { display: inline-flex; align-items: center; justify-content: center; height: 36px; padding: 0 16px; border-radius: 6px; font-size: 14px; font-weight: 500; cursor: pointer; border: 1px solid var(--border); background: var(--card); transition: all .15s; }
.btn:hover { background: var(--grey-light); }
.btn-primary { background: var(--brand); color: #fff; border-color: var(--brand); width: 100%; margin-top: 8px; }
.btn-primary:hover { background: var(--brand-dark); }
.btn-primary:disabled { opacity: .5; cursor: not-allowed; }
/* ── Модальное окно ──────────────────────────────── */
.modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.4); z-index: 1000; justify-content: center; align-items: flex-start; padding-top: 60px; }
.modal-overlay.show { display: flex; }
.modal { background: var(--card); border-radius: 12px; max-width: 600px; width: 90%; max-height: 80vh; overflow-y: auto; box-shadow: 0 8px 32px rgba(0,0,0,.15); }
.modal-header { display: flex; align-items: center; justify-content: space-between; padding: 16px 20px; border-bottom: 1px solid var(--border); position: sticky; top: 0; background: var(--card); z-index: 1; }
.modal-header h2 { font-size: 16px; }
.modal-close { cursor: pointer; font-size: 20px; color: var(--muted); background: none; border: none; line-height: 1; }
.modal-close:hover { color: var(--fg); }
.modal-body { padding: 20px; font-size: 13px; line-height: 1.7; color: var(--fg); }
.modal-body h3 { font-size: 14px; margin: 16px 0 6px; }
.modal-body h3:first-child { margin-top: 0; }
.modal-body ul { margin: 4px 0 12px 20px; }
.modal-body li { margin-bottom: 4px; }
.modal-body .warn { background: #fef9e7; border-left: 3px solid #f59e0b; padding: 8px 12px; border-radius: 0 6px 6px 0; margin: 8px 0; font-size: 12px; }
.modal-body code { background: var(--grey-light); padding: 1px 4px; border-radius: 3px; font-size: 12px; }
/* ── Кнопка ? в шапке ───────────────────────────── */
.header .help-btn { margin-left: auto; width: 28px; height: 28px; border-radius: 50%; border: 1px solid var(--border); background: var(--card); cursor: pointer; font-size: 15px; font-weight: 600; color: var(--muted); display: flex; align-items: center; justify-content: center; transition: all .15s; }
.header .help-btn:hover { border-color: var(--brand); color: var(--brand); background: #eff6ff; }
</style>
</head>
<body>
<header class="header">
<svg width="82" height="18" viewBox="0 0 82 18" style="display:block"><rect width="82" height="18" rx="2" fill="#001C34"/><text x="41" y="14" text-anchor="middle" font-size="12" font-weight="bold" fill="white" font-family="sans-serif">NUBES</text></svg>
<span class="sep">|</span>
<a href="https://contractor.pythonk8s.services.ngcloud.ru/">Сверка договоров</a>
<span class="sep">|</span>
<strong>DrHider</strong>
<span style="font-size:11px;color:var(--muted);">v1.15</span>
<button class="help-btn" onclick="openModal()" title="О сервисе">?</button>
</header>
<!-- Модальное окно -->
<div class="modal-overlay" id="modalOverlay" onclick="if(event.target===this)closeModal()">
<div class="modal">
<div class="modal-header">
<h2>🛡️ DrHider — справка</h2>
<button class="modal-close" onclick="closeModal()"></button>
</div>
<div class="modal-body">
<h3>📌 Что делает</h3>
<p>Обезличивает документы перед загрузкой в сервис сверки договоров. Заменяет персональные данные, реквизиты компаний, телефоны, email и другие чувствительные данные на фиктивные, но реалистичные значения. <strong>Одна и та же сущность во всех файлах заменяется одинаково</strong> — можно сопоставлять документы между собой.</p>
<h3>⚙️ Как работает</h3>
<ul>
<li><strong>Два прохода:</strong> сначала сбор всех сущностей из всех файлов → построение единого словаря замен → применение замен.</li>
<li><strong>Всё в памяти:</strong> файлы не записываются на диск и не сохраняются в базе. После обработки данные удаляются.</li>
<li><strong>mapping.csv</strong> — внутри ZIP лежит таблица «тип → оригинал → замена» для аудита.</li>
</ul>
<h3>✅ Что распознаётся и заменяется</h3>
<ul>
<li>📞 Телефоны (+7, 8-800, ...)</li>
<li>📧 Email-адреса</li>
<li>🏢 Компании: ООО, ЗАО, АО, ПАО, ИП, ТОО (включая юникод-кавычки «» и "")</li>
<li>👤 ФИО (кириллица): Иванов А.П., Петрова Мария Сергеевна</li>
<li>🔢 Реквизиты: ИНН (10/12), ОГРН, КПП, БИК</li>
<li>💳 Банковские счета: р/с, кор/сч, корреспондентский счёт</li>
<li>📄 Паспортные данные: серия + номер</li>
</ul>
<h3>🧠 LLM-NER (нейросеть)</h3>
<p>Для поиска имён, названий компаний и адресов, которые не попадают под регулярные выражения, используется языковая модель. Работает не мгновенно — для больших документов добавляет 5–30 секунд.</p>
<h3>✅ Форматы файлов</h3>
<ul>
<li><strong>.docx</strong> — полная замена с сохранением структуры документа. ⚠️ Форматирование (жирный, курсив) заменённых фрагментов может сброситься.</li>
<li><strong>.pdf</strong> — текст и таблицы извлекаются в .docx, затем обфусцируется. Шрифты, цвета и PDF-вёрстка не сохраняются. При совпадении имён с существующим .docx добавляется суффикс <code>_из_pdf</code>.</li>
<li><strong>.doc</strong> — бинарный формат, <strong>не обфусцируется</strong>. Файл возвращается как есть. Конвертируйте в .docx перед загрузкой.</li>
<li><strong>.zip</strong> — автоматически распаковывается, все файлы внутри обрабатываются.</li>
</ul>
<h3>⚠️ Ограничения и возможные ошибки</h3>
<div class="warn">
<strong>Ложные пропуски:</strong> сущность может быть не найдена, если она записана в нестандартном формате (например, телефон через точку: <code>+7.495.123.44.55</code>).
</div>
<div class="warn">
<strong>Ложные срабатывания:</strong> короткие слова (например, фамилия «Иванов») могут быть заменены внутри более длинных слов («Ивановский» → ...ский). Минимизировано, но не исключено полностью.
</div>
<div class="warn">
<strong>.doc не обрабатывается:</strong> старые Word-файлы возвращаются без изменений.
</div>
<div class="warn">
<strong>PDF → DOCX:</strong> извлекаются только текст и таблицы. Шрифты, цвета и позиционирование теряются. Сканы (фотографии страниц) не распознаются.</div>
</div>
<div class="warn">
<strong>Сканы/изображения:</strong> текст на картинках не распознаётся (нет OCR).
</div>
<h3>🔒 Приватность</h3>
<ul>
<li>Файлы не покидают сервер Nubes.</li>
<li>Ничего не записывается в базу данных.</li>
<li>После отправки ZIP-архива все данные в памяти удаляются.</li>
</ul>
<h3>📊 mapping.csv</h3>
<p>В выходном ZIP всегда есть файл <code>mapping.csv</code> с колонками: <code>тип_данных, оригинал, замена</code>. Позволяет при необходимости восстановить исходные значения (храните этот файл отдельно и безопасно).</p>
</div>
</div>
</div>
<main>
<h1>🛡️ DrHider — обфускация документов</h1>
<p class="sub">Загрузите документы — получите ZIP с обезличенными копиями и таблицей соответствия. Без сохранения на сервере.</p>
<div class="card">
<div class="card-header">📁 Загрузка файлов</div>
<div class="card-body">
<div id="dropZone">
<div class="icon">📄</div>
<p>Выберите файлы или перетащите сюда</p>
<p style="font-size:12px;">.docx .pdf .doc .zip</p>
<input type="file" id="fileInput" multiple accept=".docx,.pdf,.doc,.zip" style="display:none">
</div>
<div id="fileList"></div>
<button class="btn btn-primary" id="btnGo" disabled>Обфусцировать</button>
<div id="status"></div>
</div>
</div>
<div class="card" style="margin-top:16px;">
<div class="card-header">️ Как это работает</div>
<div class="card-body" style="font-size:13px;color:var(--muted);">
<p><strong>⚡ Всё в памяти.</strong> Файлы не сохраняются на сервер. После обработки данные удаляются.</p>
<p><strong>📋 mapping.csv</strong> — таблица «оригинал → замена» внутри ZIP.</p>
<p><strong>🔒 Результат</strong> — обезличенные файлы можно загружать в основной сервис сверки.</p>
</div>
</div>
</main>
<script>
const DZ = document.getElementById('dropZone');
const INPUT = document.getElementById('fileInput');
const LIST = document.getElementById('fileList');
const BTN = document.getElementById('btnGo');
const STATUS = document.getElementById('status');
let files = [];
DZ.onclick = () => INPUT.click();
INPUT.onchange = () => { addFiles(INPUT.files); INPUT.value = ''; };
DZ.ondragover = e => { e.preventDefault(); DZ.classList.add('active'); };
DZ.ondragleave = () => DZ.classList.remove('active');
DZ.ondrop = e => { e.preventDefault(); DZ.classList.remove('active'); addFiles(e.dataTransfer.files); };
function addFiles(fl) { for (let f of fl) { if (files.find(x => x.name===f.name && x.size===f.size)) continue; files.push(f); } render(); }
function remove(i) { files.splice(i, 1); render(); }
function render() {
LIST.innerHTML = files.map((f,i) => {
let display = esc(f.name);
if (f.name.toLowerCase().endsWith('.pdf')) {
display += ' → <span style="color:var(--brand-primary)">*.docx</span>';
}
return `<div class="file-row"><span class="name">${display}</span><span class="size">${fmt(f.size)}</span><span class="remove" onclick="remove(${i})">×</span></div>`;
}).join('');
LIST.className = files.length ? 'show' : '';
BTN.disabled = !files.length;
}
BTN.onclick = () => {
if (!files.length) return;
// Проверить коллизии PDF→DOCX
const collisions = [];
const names = files.map(f => f.name);
files.forEach(f => {
if (f.name.toLowerCase().endsWith('.pdf')) {
const docxName = f.name.slice(0, -4) + '.docx';
if (names.includes(docxName)) {
collisions.push({pdf: f.name, docx: docxName});
}
}
});
if (collisions.length) {
showCollisionModal(collisions);
return;
}
doSubmit();
};
function showCollisionModal(collisions) {
const list = collisions.map(c => `<div style="margin-bottom:4px">📄 <b>${esc(c.pdf)}</b> → <b>${esc(c.docx)}</b> (уже есть)</div>`).join('');
document.getElementById('collisionList').innerHTML = list;
document.getElementById('collisionOverlay').classList.add('show');
window._collisions = collisions;
}
function closeCollision(skipAll) {
document.getElementById('collisionOverlay').classList.remove('show');
if (skipAll) {
// Удалить PDF-файлы из списка
const pdfNames = window._collisions.map(c => c.pdf);
files = files.filter(f => !pdfNames.includes(f.name));
render();
}
// В любом случае отправляем (бэкенд сам переименует)
setTimeout(doSubmit, 100);
}
function doSubmit() {
BTN.disabled = true;
setStatus('progress', '⏳ Отправка...');
const xhr = new XMLHttpRequest();
const fd = new FormData();
files.forEach(f => fd.append('files', f, f.name));
xhr.open('POST', 'https://contracts.kube5s.ru/api/drhider');
xhr.responseType = 'blob';
const startTime = Date.now();
const timer = setInterval(() => {
setStatus('progress', '⏳ Обработка... ' + Math.round((Date.now() - startTime) / 1000) + 'с');
}, 1000);
xhr.onload = () => {
clearInterval(timer);
if (xhr.status === 200) {
const a = document.createElement('a');
a.href = URL.createObjectURL(xhr.response);
a.download = 'drhider_output.zip';
a.click();
setStatus('done', '✅ Готово!');
} else {
setStatus('error', '❌ Ошибка ' + xhr.status);
}
BTN.disabled = false;
};
xhr.onerror = () => { clearInterval(timer); setStatus('error', '❌ Сеть'); BTN.disabled = false; };
xhr.ontimeout = () => { clearInterval(timer); setStatus('error', '❌ Таймаут'); BTN.disabled = false; };
xhr.timeout = 600000;
xhr.send(fd);
};
function setStatus(c,m) { STATUS.className = 'show '+c; STATUS.textContent = m; }
function esc(s) { return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
function fmt(n) { return n>1e6 ? (n/1e6).toFixed(1)+' MB' : n>1e3 ? (n/1e3).toFixed(0)+' KB' : n+' B'; }
// ── Модальное окно ─────────────────────────────────────────────
function openModal() { document.getElementById('modalOverlay').classList.add('show'); }
function closeModal() { document.getElementById('modalOverlay').classList.remove('show'); }
// ── Коллизия PDF→DOCX ──────────────────────────────────────────
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeCollision(true); });
</script>
<!-- Модальное окно: коллизия PDF→DOCX -->
<div class="modal-overlay" id="collisionOverlay">
<div class="modal" style="max-width:480px">
<div class="modal-header">⚠️ Конфликт имён</div>
<div class="modal-body">
<p style="margin-bottom:10px">PDF-файлы конвертируются в DOCX. Имена совпадают:</p>
<div id="collisionList" style="margin-bottom:12px"></div>
<p style="font-size:12px;color:var(--muted)">«Перезаписать» — PDF-файл заменит DOCX.<br>«Пропустить» — PDF будут удалены из списка.</p>
</div>
<div style="display:flex;gap:8px;padding:12px 16px;border-top:1px solid var(--brand-gray)">
<button class="btn" onclick="closeCollision(true)" style="flex:1">Пропустить PDF</button>
<button class="btn btn-primary" onclick="closeCollision(false)" style="flex:1">Перезаписать</button>
</div>
</div>
</div>
</body>
</html>