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