diff --git a/frontend/index.html b/frontend/index.html
index adea8db..7e7f5c3 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -393,6 +393,7 @@ const state = {
currentUser: null, // {id, username, role}
authWarnings: {}, // source_slug → {source_slug, source_name}
metaUpdating: new Set(), // urls where meta refresh is in progress
+ validating: {}, // url → {checked, total} for in-progress validations
};
// ── Auth ─────────────────────────────────────
@@ -703,6 +704,31 @@ function handleEvent(msg) {
_updateMetaBtn(msg.url, msg.failed === -1 ? 'error' : 'done');
break;
+ case 'validate_started':
+ state.validating[msg.url] = {checked: 0, total: 0};
+ _updateValidateBtn(msg.url);
+ break;
+
+ case 'validate_progress':
+ if(state.validating[msg.url]) {
+ state.validating[msg.url].checked = msg.checked;
+ state.validating[msg.url].total = msg.total;
+ }
+ _updateValidateBtn(msg.url);
+ break;
+
+ case 'validate_done': {
+ delete state.validating[msg.url];
+ const result = msg.total_to_redownload > 0 || msg.new_chapters > 0 ? 'issues' : 'ok';
+ _updateValidateBtn(msg.url, result, msg);
+ break;
+ }
+
+ case 'validate_error':
+ delete state.validating[msg.url];
+ _updateValidateBtn(msg.url, 'error');
+ break;
+
case 'manga_meta_updated':
if(state.mangas[msg.url]) {
state.mangas[msg.url].title = msg.title;
@@ -1691,6 +1717,63 @@ async function refreshMetaModal(url) {
// Спиннер появится через WS meta_refresh_started, исчезнет через meta_refreshed
}
+function _updateValidateBtn(url, result, data) {
+ const modal = document.getElementById('modal');
+ if(!modal || modal.classList.contains('hidden') || modal.dataset.currentUrl !== url) return;
+ const btn = document.getElementById('modal-validate-btn');
+ if(!btn) return;
+ const v = state.validating[url];
+ if(v !== undefined) {
+ const prog = v.total > 0 ? ` ${v.checked}/${v.total}` : '...';
+ btn.innerHTML = ` Проверка${prog}`;
+ btn.disabled = true;
+ btn.style.color = '#94a3b8';
+ btn.style.borderColor = '#334155';
+ } else if(result === 'ok') {
+ btn.innerHTML = '✅ Всё в порядке';
+ btn.disabled = false;
+ btn.style.color = '#4ade80';
+ btn.style.borderColor = '#166534';
+ setTimeout(() => { btn.innerHTML = '🔍 Проверить целостность'; btn.style.color='#67e8f9'; btn.style.borderColor='#164e63'; btn.disabled=false; }, 3000);
+ } else if(result === 'issues') {
+ const n = data ? (data.total_to_redownload + data.new_chapters) : '?';
+ btn.innerHTML = `⚡ Найдено проблем: ${n} — поставлено в очередь`;
+ btn.disabled = true;
+ btn.style.color = '#fbbf24';
+ btn.style.borderColor = '#78350f';
+ setTimeout(() => { btn.innerHTML = '🔍 Проверить целостность'; btn.style.color='#67e8f9'; btn.style.borderColor='#164e63'; btn.disabled=false; }, 5000);
+ } else if(result === 'error') {
+ btn.innerHTML = '❌ Ошибка валидации';
+ btn.disabled = false;
+ btn.style.color = '#f87171';
+ btn.style.borderColor = '#7f1d1d';
+ setTimeout(() => { btn.innerHTML = '🔍 Проверить целостность'; btn.style.color='#67e8f9'; btn.style.borderColor='#164e63'; btn.disabled=false; }, 3000);
+ } else {
+ btn.innerHTML = '🔍 Проверить целостность';
+ btn.disabled = false;
+ btn.style.color = '#67e8f9';
+ btn.style.borderColor = '#164e63';
+ }
+}
+
+async function validateManga(url) {
+ const btn = document.getElementById('modal-validate-btn');
+ if(btn) {
+ btn.innerHTML = ' Запуск...';
+ btn.disabled = true;
+ }
+ const r = await fetch('/api/mangas/validate?url='+encodeURIComponent(url), {method:'POST'});
+ if(!r.ok) {
+ const err = await r.json().catch(() => ({}));
+ if(btn) {
+ btn.innerHTML = '❌ ' + (err.detail || 'Ошибка');
+ btn.style.color = '#f87171';
+ btn.style.borderColor = '#7f1d1d';
+ setTimeout(() => { btn.innerHTML = '🔍 Проверить целостность'; btn.style.color='#67e8f9'; btn.style.borderColor='#164e63'; btn.disabled=false; }, 3000);
+ }
+ }
+}
+
async function forceRedownload(url, closeModalAfter = false) {
if(!confirm('Скачать заново ВСЕ главы? Уже скачанные файлы будут перезаписаны.')) return;
const r = await fetch('/api/mangas/force_redownload?url='+encodeURIComponent(url), {method:'POST'});
@@ -2233,6 +2316,15 @@ function renderModalBody(data) {
📁 Переименовать папку
` : ''}
${data.status === 'done' && canManage(data) ? `
+ ` : ''}
+ ${data.status === 'done' && canManage(data) ? `