<!DOCTYPE html><html lang="uk"><head><base target="_top"><meta charset="utf-8"><style>
*{box-sizing:border-box}body{margin:0;padding:16px;font:13px/1.45 Arial,sans-serif;background:#edf3f8;color:#18344b}h1{font-size:21px;line-height:1.2;margin:0}h2{font-size:14px;margin:0 0 10px}.sub{font-size:11px;color:#587084;margin-top:5px}.card{padding:14px;background:white;border:1px solid #d9e3eb;border-radius:14px;margin-top:13px}.badge{display:inline-block;background:#dcecf9;color:#155585;border-radius:20px;padding:4px 9px;font-size:11px;margin:10px 0 0}.row{display:flex;gap:8px;flex-wrap:wrap}button{border:0;border-radius:8px;padding:10px 12px;background:#e8eff5;color:#18344b;cursor:pointer;font:inherit}button.primary{background:#12699b;color:white}button.danger{color:#a32929}button:disabled{opacity:.5;cursor:wait}select,input{width:100%;border:1px solid #cad8e3;border-radius:8px;padding:9px;margin:5px 0 10px;background:white;color:#18344b}label{font-size:12px}.bar{height:7px;background:#e8eff5;border-radius:8px;margin:9px 0;overflow:hidden}.fill{height:100%;background:#26a7be;width:0}.stat{display:flex;justify-content:space-between;gap:10px;padding:4px 0}.muted{color:#617788;font-size:12px}.error{background:#fff0ee;color:#992a23;padding:10px;border-radius:9px;white-space:pre-wrap;overflow-wrap:anywhere;margin-top:10px}.notice{background:#fff6dd;padding:9px;border-radius:8px;margin-top:9px}#last{overflow-wrap:anywhere}details summary{cursor:pointer;font-weight:bold}small{font-size:11px}#manualNote{margin:0 0 8px}
</style></head><body>
<h1>NewsTrace Flow</h1><div class="sub">WaybackAudit → відбір → NewsTrace · 2.1.0</div><div id="badge" class="badge">Завантаження…</div>
<div id="rpcError" class="error" hidden></div>
<div class="card"><h2>Керування</h2><label>Вибірка NewsTrace<select id="sample"><option value="12">Швидка — до 12 HTML URL</option><option value="36">Ширша — до 36 HTML URL</option></select></label><label>Історичні редиректи<select id="redirect"><option value="0">Увесь доступний період, вибірково</option><option value="2">Останні 2 роки, вибірково</option></select></label>
<div class="row"><button id="start" class="primary" onclick="action('startNewsTraceFlow',{sampleLimit:Number(el('sample').value),redirectYears:Number(el('redirect').value)})">Запустити</button><button id="pause" onclick="action('controlNewsTraceFlow','paused')">Пауза</button><button id="resume" onclick="action('controlNewsTraceFlow','running')">Продовжити</button></div>
<div class="muted" style="margin-top:9px">Ризик: 0–19 — низький, 20–59 — перевірити, 60–100 — високий. Бали не є ймовірністю. Мовні та інші застереження оцінюються окремо. Первинний аудит читає HTML. Ознак ризику не знайдено — висновок лише про перевірену вибірку.</div></div>
<div class="card"><h2>1. Первинна перевірка</h2><div class="stat"><span>Перевірено доменів</span><b id="audit">0 / 0</b></div><div class="bar"><div id="auditBar" class="fill"></div></div><div class="stat"><span>Передано в NewsTrace</span><b id="candidates">0</b></div><div class="stat"><span>Недопущені / невизначені</span><b id="review">0</b></div><div class="stat"><span>Пропущено за типом</span><b id="skipped">0</b></div><div class="stat"><span>Невдалі перевірки</span><b id="auditErrors">0</b></div><div id="auditEta" class="muted"></div></div>
<div class="card"><h2>2. Пошук новинної історії</h2><div class="stat"><span>Завершено кандидатів</span><b id="news">0 / 0</b></div><div class="bar"><div id="newsBar" class="fill"></div></div><div class="stat"><span>Кроків у черзі</span><b id="pending">0</b></div><div class="stat"><span>HTML повторно використано</span><b id="reused">0</b></div><div class="stat"><span>Непрочитаних кроків</span><b id="newsErrors">0</b></div><div id="retry" class="muted"></div></div>
<div class="card"><h2>Архівар · стан роботи</h2><div id="policy" class="notice" hidden></div><div id="last">Очікування</div><div id="wait" class="notice" hidden></div><div id="workerError" class="error" hidden></div><div class="stat"><span>Минуло часу</span><b id="elapsed">—</b></div><div class="stat"><span>HTTP-запитів</span><b id="requests">0</b></div><div class="stat"><span>Відповідей із кешу аудиту</span><b id="hits">0</b></div><div class="muted">Загальний час завершення стане зрозумілішим після формування черги. Нові карти сайту можуть додавати кроки.</div><div id="stale" class="notice" hidden>Давно немає оновлення. Перевірте виконання Apps Script; це не підтвердження активної роботи.</div></div>
<div class="card"><details><summary>Ручна передача та повтори</summary><p id="manualNote" class="muted">Передати домен із поточного запуску, навіть якщо автоматичний відбір його не допустив. Попередні застереження залишаться в результатах.</p><input id="domain" placeholder="example.com"><button onclick="manual()">Передати в NewsTrace</button><p><button onclick="action('recheckNewsTraceFlowPolicy')">Підготувати переоцінку 2.1.0</button></p><p><button onclick="retry()">Підготувати повтор помилок</button></p><small>Повтор переводить запуск на паузу. Для роботи натисніть «Продовжити». Очікування квот зберігається.</small></details></div>
<div class="card"><details><summary>Налаштування й очищення</summary><p><button onclick="action('setupNewsTraceFlow')">Підготувати вкладки</button></p><p><button onclick="action('controlNewsTraceFlow','stopped')">Зупинити</button></p><button class="danger" onclick="clearAll()">Очистити результати й архівні дані</button><p class="muted">Очищення видаляє створені результати обох етапів, черги та спільні архівні дані. Вхідні домени залишаються.</p></details></div>
<script>
const el=id=>document.getElementById(id);let busy=false,polling=false,epoch=0,lastState={},errors={poll:'',action:''};
function showErrors(){const value=[errors.action,errors.poll].filter(Boolean).join('\n');el('rpcError').textContent=value;el('rpcError').hidden=!value;}
function dur(sec){sec=Math.max(0,Math.floor(sec||0));return Math.floor(sec/3600)+' год '+Math.floor(sec%3600/60)+' хв';}
function render(s){lastState=s||{};const n=s.news||{},labels={ready:'Готово до запуску',running:'Працює у фоні',paused:'На паузі',stopped:'Зупинено',completed:'Завершено'};
let label=labels[s.status]||s.status;if(s.control&&s.control!==s.status&&s.status==='running')label=s.control==='paused'?'Завершую крок перед паузою':s.control==='stopped'?'Завершую крок перед зупинкою':label;if(s.status==='running'&&s.waitingSeconds>0)label='Очікування відновлення доступу';el('badge').textContent=label;
el('audit').textContent=(s.audited||0)+' / '+(s.total||0);el('auditBar').style.width=(s.total?100*s.audited/s.total:0)+'%';el('candidates').textContent=s.candidates||0;el('review').textContent=s.review||0;el('skipped').textContent=s.skipped||0;el('auditErrors').textContent=s.errors||0;
el('news').textContent=(n.done||0)+' / '+(n.total||0)+(s.revoked?' (відкликано '+s.revoked+')':'');el('newsBar').style.width=(n.total?100*n.done/n.total:0)+'%';el('pending').textContent=n.pending||0;el('reused').textContent=n.reused||0;el('newsErrors').textContent=n.errors||0;el('retry').textContent='Повтори: '+(n.retries||0)+' · готові '+(n.retryReady||0)+' · очікують '+(n.retryWaiting||0);
el('policy').hidden=!s.policyRemaining;el('policy').textContent='Переоцінка збережених результатів: залишилося '+(s.policyRemaining||0)+'. Нові правила 2.1.0';el('last').textContent=s.last||'';el('workerError').textContent=s.error||'';el('workerError').hidden=!s.error;el('wait').hidden=!(s.waitingSeconds>0);el('wait').textContent=s.waitingSeconds>0?'Наступна спроба не раніше '+new Date(s.blockedUntil).toLocaleString('uk-UA')+'. Це не гарантований час відновлення.':'';if(s.quota&&s.waitingSeconds>0)el('wait').textContent+=' '+s.quota.label+'. '+s.quota.phase+'. Вперше: '+new Date(s.quota.firstAt).toLocaleString('uk-UA')+'. Спроб: '+s.quota.attempts+'. Причина: '+s.quota.raw;
el('elapsed').textContent=dur(s.wallSeconds);el('requests').textContent=s.requestsTotal||0;el('hits').textContent=s.cacheHits||0;el('stale').hidden=!s.stale||s.waitingSeconds>0||s.control!=='running';el('auditEta').textContent=s.auditEtaSeconds?'Орієнтовно до кінця первинного етапу: '+dur(s.auditEtaSeconds):'Оцінка часу первинного етапу ще формується';
el('start').disabled=busy||['running','paused'].includes(s.status);el('pause').disabled=busy||s.status!=='running';el('resume').disabled=busy||!['paused','stopped'].includes(s.status);}
function poll(){if(busy||polling)return;polling=true;const token=epoch;google.script.run.withSuccessHandler(s=>{polling=false;if(token!==epoch)return;errors.poll='';showErrors();render(s);}).withFailureHandler(e=>{polling=false;if(token!==epoch)return;errors.poll=String(e.message||e);showErrors();}).getNewsTraceFlowStatus();}
function action(name,arg){if(busy)return;busy=true;epoch++;render(lastState);const runner=google.script.run.withSuccessHandler(s=>{busy=false;errors.action='';errors.poll='';showErrors();render(s);}).withFailureHandler(e=>{busy=false;errors.action=String(e.message||e);showErrors();render(lastState);});if(arg===undefined)runner[name]();else runner[name](arg);}
function manual(){const d=el('domain').value.trim();if(d&&confirm('Передати '+d+' у NewsTrace незалежно від первинного висновку?'))action('forceNewsTraceFlowDomain',d);}
function retry(){if(confirm('Підготувати повтор невдалих кроків і поставити запуск на паузу?'))action('retryNewsTraceFlowErrors');}
function clearAll(){if(confirm('Видалити результати ОБОХ перевірок, усі черги й архівні дані? Домени залишаться.'))action('clearNewsTraceFlow',true);}
poll();setInterval(poll,10000);
</script></body></html>
