Files
drhider/site/templates/index.html
T

229 lines
9.9 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>DrHider — обфускация документов</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
<script src="/static/zip.js"></script>
<style>
:root {
--brand-primary: #2563eb;
--brand-gray: #d1d5db;
--brand-grey-light: #f3f4f6;
--bg: #f9fafb;
--card: #ffffff;
--text: #1a1a1a;
--muted: #6b7280;
--red: #ef4444;
--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; }
.topbar {
background: var(--card); 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 .ver { font-weight: 400; color: var(--muted); font-size: 12px; margin-left: auto; }
.content { max-width: 820px; margin: 20px auto; padding: 0 12px; }
.card {
background: var(--card); border-radius: 12px;
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 { padding: 12px; }
.sub { font-size: 12px; color: var(--muted); margin-bottom: 10px; line-height: 1.5; }
.file-input-wrap { margin-bottom: 10px; }
.file-input-wrap input[type="file"] { width: 100%; font-size: 13px; }
.table-wrap { border: 1px solid var(--brand-gray); border-radius: 8px; overflow: hidden; }
table { width: 100%; border-collapse: collapse; font-size: 13px; }
th {
background: var(--brand-grey-light); text-transform: uppercase; padding: 6px 8px;
border-right: 1px solid var(--brand-gray); text-align: left;
font-weight: 600; font-size: 10px; color: var(--muted);
}
td {
padding: 5px 8px; border-right: 1px solid var(--brand-gray);
border-bottom: 1px solid var(--brand-gray);
}
tr:hover td { background: rgba(37,99,235,.03); }
.name-cell { max-width: 340px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.num-cell { text-align: right; white-space: nowrap; }
.empty-row td { color: var(--muted); text-align: center; padding: 20px; }
.remove-btn {
cursor: pointer; color: #f87171; background: none; border: none;
padding: 2px 4px; font-size: 14px; line-height: 1;
}
.remove-btn:hover { color: var(--red); }
.footer-bar {
margin-top: 10px; display: flex; justify-content: space-between; align-items: center;
font-size: 12px; color: var(--muted);
}
.btn {
height: 32px; border-radius: 6px; padding: 0 12px; 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: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; }
.status.pack { background: #fefce8; border: 1px solid #eab308; color: #a16207; display: block; }
</style>
</head>
<body>
<div class="topbar">
<img src="/static/logo.svg" alt="Nubes">
<span class="title">DrHider</span>
<span class="ver">v{{ version }}</span>
</div>
<div class="content">
<div class="card">
<div class="card-header">Обфускация документов</div>
<div class="card-body">
<p class="sub">
Загрузите документы (.docx, .pdf, .txt, .zip) — получите ZIP с обезличенными копиями.<br>
Персональные данные, реквизиты, телефоны, email заменяются. CSV с таблицей замен сохраняется отдельно.
</p>
<div class="file-input-wrap">
<input type="file" id="fileInput" multiple accept=".docx,.pdf,.txt,.doc,.zip">
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Имя</th>
<th style="width:80px;">Размер</th>
<th style="width:80px;">Статус</th>
<th style="width:32px;"></th>
</tr>
</thead>
<tbody id="fileList">
<tr class="empty-row"><td colspan="4">Нет выбранных файлов</td></tr>
</tbody>
</table>
</div>
<div class="footer-bar">
<span id="fileCount">0 файлов</span>
<button class="btn btn-primary" id="uploadBtn" disabled onclick="uploadFiles()">🛡️ Обфусцировать</button>
</div>
<div class="status" id="status"></div>
</div>
</div>
</div>
<script>
const fi = document.getElementById('fileInput');
const fl = document.getElementById('fileList');
const fc = document.getElementById('fileCount');
const ub = document.getElementById('uploadBtn');
const st = document.getElementById('status');
let sf = [];
function fs(b) { return b < 1024 ? b + ' B' : b < 1048576 ? (b / 1024).toFixed(1) + ' KB' : (b / 1048576).toFixed(1) + ' MB'; }
function rr() {
if (sf.length === 0) { fl.innerHTML = '<tr class="empty-row"><td colspan="4">Нет выбранных файлов</td></tr>'; }
else { fl.innerHTML = sf.map((f, i) => '<tr id="row-' + i + '"><td class="name-cell">' + f.name + '</td><td class="num-cell">' + fs(f.size) + '</td><td class="num-cell" id="st-' + i + '" style="font-size:12px;">—</td><td><button class="remove-btn" onclick="rm(' + i + ')">✕</button></td></tr>').join(''); }
fc.textContent = sf.length + ' файлов';
ub.disabled = sf.length === 0;
}
function rm(i) { sf.splice(i, 1); const d = new DataTransfer(); sf.forEach(f => d.items.add(f)); fi.files = d.files; rr(); }
window.addEventListener('load', () => { sf = []; fi.value = ''; rr(); });
fi.addEventListener('change', () => { sf = Array.from(fi.files); rr(); });
function ts() {
const d = new Date();
return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0') + '_' + String(d.getHours()).padStart(2, '0') + '-' + String(d.getMinutes()).padStart(2, '0') + '-' + String(d.getSeconds()).padStart(2, '0');
}
function ss(idx, h) { const e = document.getElementById('st-' + idx); if (e) e.innerHTML = h; }
async function uploadFiles() {
if (sf.length === 0) return;
ub.disabled = true;
const total = sf.length;
const tstamp = ts();
const maps = [];
const zEntries = [];
let ok = 0, fail = 0;
for (let i = 0; i < total; i++) {
const f = sf[i], n = i + 1;
const t0 = performance.now();
// Бегущий секундомер в статусе
const timer = setInterval(() => {
const sec = Math.round((performance.now() - t0) / 1000);
ss(i, '<span style="color:#2563eb">⏳ ' + sec + 'с</span>');
}, 200);
st.className = 'status progress';
st.textContent = 'Файл ' + n + '/' + total + ': ' + f.name;
const fd = new FormData();
fd.append('files', f, f.name);
try {
const blob = await new Promise((resolve, reject) => {
const x = new XMLHttpRequest();
x.open('POST', '/api/drhider');
x.responseType = 'blob';
x.timeout = 600000;
x.onload = () => x.status === 200 ? resolve(x.response) : reject(new Error('Сервер ' + x.status));
x.onerror = () => reject(new Error('Сеть'));
x.ontimeout = () => reject(new Error('Таймаут'));
x.send(fd);
});
clearInterval(timer);
const el = ((performance.now() - t0) / 1000).toFixed(1);
const files = await unzip(blob);
for (const [name, data] of Object.entries(files)) {
if (name === 'mapping.csv') {
const t = await data.text();
const ls = t.split('\n');
maps.push(maps.length === 0 ? ls.join('\n') : ls.slice(1).join('\n'));
} else { zEntries.push({ name, data }); }
}
ss(i, '<span style="color:#22c55e">✓ ' + el + 'с</span>');
ok++;
} catch (err) {
clearInterval(timer);
ss(i, '<span style="color:#ef4444">✗</span>');
fail++;
}
if (i < total - 1) await new Promise(r => setTimeout(r, 300));
}
if (ok > 0) {
st.className = 'status pack'; st.textContent = 'Упаковка...';
const zb = await buildZip(zEntries);
const u = URL.createObjectURL(zb);
const a = document.createElement('a'); a.href = u; a.download = 'drhider_' + tstamp + '.zip'; a.click();
URL.revokeObjectURL(u);
}
if (maps.length > 0) {
const cb = new Blob(['\ufeff' + maps.join('\n')], { type: 'text/csv;charset=utf-8' });
const u = URL.createObjectURL(cb);
const a = document.createElement('a'); a.href = u; a.download = 'drhider_' + tstamp + '.csv'; a.click();
URL.revokeObjectURL(u);
}
st.className = fail === 0 ? 'status done' : 'status error';
st.textContent = fail === 0 ? '✅ Готово! ' + ok + '/' + total : 'Готово: ' + ok + ' OK, ' + fail + ' ошибок из ' + total;
ub.disabled = false;
}
</script>
</body>
</html>