<!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">
  <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; }
    .topbar .help { margin-left: auto; font-size: 12px; color: var(--muted); cursor: pointer; text-decoration: underline; }
    .topbar .help:hover { color: var(--text); }
    .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; }
    .dl-btns { display: none; gap: 8px; margin-top: 8px; }
    .dl-btns.show { display: flex; }
  </style>
</head>
<body>
<div class="topbar">
  <img src="/static/logo.svg" alt="Nubes">
  <span class="title">DrHider</span>
  <span class="ver">v{{ version }}</span>
  <span class="help" onclick="document.getElementById('helpModal').style.display='flex'">?</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>
        CSV с таблицей замен скачивается отдельно.
      </p>
      <div class="file-input-wrap">
        <input type="file" id="fileInput" multiple accept=".docx,.pdf,.txt,.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 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>
      </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');
const db = document.getElementById('dlBtns');
let sf = [];
let currentSid = '';
let activeES = null;  // активный EventSource
let activeXHR = null; // активный XHR

function resetAll() {
  if (activeES) { activeES.close(); activeES = null; }
  if (activeXHR) { activeXHR.abort(); activeXHR = null; }
  currentSid = '';
  sf = [];
  fi.value = '';
  rr();
  st.className = '';
  st.textContent = '';
  db.classList.remove('show');
  ub.disabled = false;
}

// При F5 / закрытии вкладки — обрубить всё
window.addEventListener('beforeunload', () => resetAll());

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(); /* прогрев upstream-соединения */ fetch('/health').catch(() => {}); });
fi.addEventListener('change', () => {
  const incoming = Array.from(fi.files);
  // Добавляем к существующим, исключая дубликаты по имени+размеру
  const seen = new Set(sf.map(f => f.name + '|' + f.size));
  for (const f of incoming) {
    if (!seen.has(f.name + '|' + f.size)) sf.push(f);
  }
  // Обновить file input — все файлы из sf
  const d = new DataTransfer();
  sf.forEach(f => d.items.add(f));
  fi.files = d.files;
  rr();
});

function ss(idx, h) { const e = document.getElementById('st-' + idx); if (e) e.innerHTML = h; }

async function uploadFiles() {
  if (sf.length === 0) return;
  // Сохранить список до очистки
  const files = sf.slice();
  // Обрубить всё что могло остаться от предыдущего раза
  if (activeES) { activeES.close(); activeES = null; }
  if (activeXHR) { activeXHR.abort(); activeXHR = null; }
  currentSid = '';
  st.className = '';
  st.textContent = '';
  db.classList.remove('show');
  ub.disabled = true;
  const total = files.length;

  // Фаза 1: загрузка (XMLHttpRequest — прогресс в строке)
  for (let i = 0; i < total; i++) {
    const f = files[i], n = i + 1;
    st.className = 'status progress';
    st.textContent = 'Загрузка ' + n + '/' + total + ': ' + f.name;
    try {
      const t0 = performance.now();
      await new Promise((resolve, reject) => {
        const fd = new FormData();
        fd.append('files', f, f.name);
        if (currentSid) fd.append('session', currentSid);
        const url = '/api/upload?_=' + Date.now();
        const xhr = new XMLHttpRequest();
        xhr.open('POST', url);
        xhr.timeout = 30000;
        xhr.upload.onprogress = function(e) {
          if (e.lengthComputable) {
            const pct = Math.round(e.loaded / e.total * 100);
            ss(i, '<span style="color:#2563eb">⏳ ' + pct + '%</span>');
          }
        };
        xhr.onload = function() {
          if (xhr.status === 200) {
            const data = JSON.parse(xhr.responseText);
            if (data.ok) {
              const elapsed = (performance.now() - t0) / 1000;
              const speed = f.size / elapsed;
              ss(i, '<span style="color:#22c55e">✓ ' + fs(speed) + '/s</span>');
              currentSid = data.session;
              resolve();
            } else {
              reject(new Error(data.error));
            }
          } else {
            reject(new Error('HTTP ' + xhr.status));
          }
        };
        xhr.onerror = function() { reject(new Error('Сеть')); };
        xhr.ontimeout = function() { reject(new Error('Таймаут 30с')); };
        activeXHR = xhr;
        xhr.send(fd);
      });
    } catch (err) {
      ss(i, '<span style="color:#ef4444">✗</span>');
      st.className = 'status error';
      st.textContent = 'Ошибка загрузки: ' + err.message;
      ub.disabled = false;
      return;
    }
  }

  // Фаза 2: обработка (SSE — прогресс по каждому файлу)
  for (let i = 0; i < total; i++) ss(i, '<span style="color:#2563eb">⏳</span>');
  const fileTimers = {};  // idx -> performance.now()
  const fileIntervals = {};  // idx -> setInterval id
  st.className = 'status progress';
  st.textContent = 'Обработка...';
  const t0 = performance.now();
  const ptimer = setInterval(() => {
    const sec = Math.round((performance.now() - t0) / 1000);
    st.textContent = 'Обработка... ' + sec + 'с';
  }, 1000);

  try {
    await new Promise((resolve, reject) => {
      activeES = new EventSource('/api/process_stream/' + currentSid);
      activeES.addEventListener('start', function(e) {
        const d = JSON.parse(e.data);
        fileTimers[d.idx] = performance.now();
        fileIntervals[d.idx] = setInterval(function() {
          const elapsed = ((performance.now() - fileTimers[d.idx]) / 1000).toFixed(1);
          ss(d.idx, '<span style="color:#2563eb">⏳ ' + elapsed + 'с</span>');
        }, 200);
      });
      activeES.addEventListener('done', function(e) {
        const d = JSON.parse(e.data);
        clearInterval(fileIntervals[d.idx]);
        const elapsed = ((performance.now() - (fileTimers[d.idx] || performance.now())) / 1000).toFixed(1);
        ss(d.idx, '<span style="color:#22c55e">✓ ' + elapsed + 'с</span>');
      });
      activeES.addEventListener('complete', function(e) {
        activeES.close();
        activeES = null;
        clearInterval(ptimer);
        const d = JSON.parse(e.data);
        st.className = 'status done';
        st.textContent = '✅ Обработано ' + d.total + ' файлов за ' + ((performance.now() - t0) / 1000).toFixed(1) + 'с';
        db.classList.add('show');
        resolve();
      });
      activeES.onerror = function() {
        activeES.close();
        activeES = null;
        clearInterval(ptimer);
        reject(new Error('SSE connection failed'));
      };
    });
  } catch (err) {
    clearInterval(ptimer);
    st.className = 'status error';
    st.textContent = 'Ошибка: ' + err.message;
  }
  ub.disabled = false;
}

function downloadZip() {
  if (!currentSid) return;
  fetch('/api/download/' + currentSid)
    .then(r => {
      const disp = r.headers.get('Content-Disposition');
      const m = disp && disp.match(/filename="?(.+?)"?$/);
      return Promise.all([r.blob(), m ? m[1] : 'drhider.zip']);
    })
    .then(([blob, fname]) => {
      const a = document.createElement('a');
      a.href = URL.createObjectURL(blob);
      a.download = fname;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(a.href);
    });
}

function downloadCsv() {
  if (!currentSid) return;
  fetch('/api/csv/' + currentSid)
    .then(r => {
      const disp = r.headers.get('Content-Disposition');
      const m = disp && disp.match(/filename="?(.+?)"?$/);
      return Promise.all([r.blob(), m ? m[1] : 'mapping.csv']);
    })
    .then(([blob, fname]) => {
      const a = document.createElement('a');
      a.href = URL.createObjectURL(blob);
      a.download = fname;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(a.href);
    });
}
</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 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>
    <p style="font-size:13px;color:var(--muted);line-height:1.6">
      Загрузите документы (.docx, .pdf, .txt, .zip) — получите ZIP-архив с обезличенными копиями.
      Персональные данные, реквизиты компаний, телефоны, email и другие приватные сведения заменяются на обезличенные коды.
      CSV-файл с таблицей соответствия оригинал→замена скачивается отдельно.
    </p>
    <button class="btn" style="margin-top:12px;width:100%" onclick="document.getElementById('helpModal').style.display='none'">Закрыть</button>
  </div>
</div>
</body>
</html>
