'use strict'; // ── Helpers API ─────────────────────────────────────────────────────────────── async function api(method, path, body = null) { const isForm = body instanceof FormData; const res = await fetch(path, { method, credentials: 'include', headers: (!isForm && body) ? {'Content-Type': 'application/json'} : {}, body: body ? (isForm ? body : JSON.stringify(body)) : undefined, }); if (res.status === 401) { showPage('login'); return null; } if (!res.ok) { const err = await res.json().catch(() => ({ detail: res.statusText })); throw new Error(err.detail || 'Erreur serveur'); } return res.json(); } function esc(s) { return String(s) .replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } function fmtDate(iso) { try { return new Date(iso).toLocaleString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', }); } catch { return iso; } } // ── Pages ───────────────────────────────────────────────────────────────────── function showPage(name) { document.getElementById('page-login').classList.toggle('hidden', name !== 'login'); document.getElementById('page-app').classList.toggle('hidden', name !== 'app'); } // ── Auth ────────────────────────────────────────────────────────────────────── async function checkAuth() { const me = await api('GET', '/api/auth/me'); if (!me) return; document.getElementById('header-user').textContent = me.username; showPage('app'); loadAll(); } document.getElementById('form-login').addEventListener('submit', async e => { e.preventDefault(); const errEl = document.getElementById('login-error'); errEl.classList.add('hidden'); try { const res = await fetch('/api/auth/login', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: document.getElementById('username').value, password: document.getElementById('password').value, }), }); if (!res.ok) { const err = await res.json(); errEl.textContent = err.detail || 'Identifiants incorrects'; errEl.classList.remove('hidden'); return; } const data = await res.json(); document.getElementById('header-user').textContent = data.username; showPage('app'); loadAll(); } catch (err) { errEl.textContent = err.message; errEl.classList.remove('hidden'); } }); document.getElementById('btn-logout').addEventListener('click', async () => { await api('POST', '/api/auth/logout'); showPage('login'); }); // ── Init globale ────────────────────────────────────────────────────────────── async function loadAll() { await Promise.all([loadConfig(), loadHistory(), loadCacheStatus()]); } // ── Config ──────────────────────────────────────────────────────────────────── async function loadConfig() { const cfg = await api('GET', '/api/config'); if (!cfg) return; document.getElementById('cfg-ollama-url').textContent = cfg.ollama_url; document.getElementById('cfg-cluster-model').textContent = cfg.cluster_model; } // ── Upload ──────────────────────────────────────────────────────────────────── let selectedFiles = []; const dropZone = document.getElementById('drop-zone'); const fileInput = document.getElementById('file-input'); dropZone.addEventListener('click', () => fileInput.click()); dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('drag-over'); }); dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over')); dropZone.addEventListener('drop', e => { e.preventDefault(); dropZone.classList.remove('drag-over'); addFiles(Array.from(e.dataTransfer.files).filter(f => f.name.toLowerCase().endsWith('.pdf'))); }); fileInput.addEventListener('change', e => { addFiles(Array.from(e.target.files)); fileInput.value = ''; }); function addFiles(newFiles) { const existing = new Set(selectedFiles.map(f => f.name)); newFiles.forEach(f => { if (!existing.has(f.name)) selectedFiles.push(f); }); renderFileList(); } function renderFileList() { const listEl = document.getElementById('file-list'); const actionsEl = document.getElementById('upload-actions'); if (!selectedFiles.length) { listEl.classList.add('hidden'); actionsEl.classList.add('hidden'); return; } listEl.classList.remove('hidden'); actionsEl.classList.remove('hidden'); listEl.innerHTML = selectedFiles.map((f, i) => `
📄 ${esc(f.name)}
`).join(''); listEl.querySelectorAll('.file-remove').forEach(btn => btn.addEventListener('click', () => { selectedFiles.splice(+btn.dataset.i, 1); renderFileList(); }) ); } document.getElementById('btn-clear-files').addEventListener('click', () => { selectedFiles = []; renderFileList(); }); // ── Traitement ──────────────────────────────────────────────────────────────── let currentJobId = null; document.getElementById('btn-process').addEventListener('click', async () => { if (!selectedFiles.length) return; const btn = document.getElementById('btn-process'); const progSect = document.getElementById('section-progress'); const resSect = document.getElementById('section-results'); const fill = document.getElementById('progress-fill'); const log = document.getElementById('progress-log'); btn.disabled = true; resSect.classList.add('hidden'); progSect.classList.remove('hidden'); log.innerHTML = ''; fill.style.width = '3%'; fill.classList.add('running'); let steps = 0; // Upload const fd = new FormData(); selectedFiles.forEach(f => fd.append('files', f)); let jobId; try { const res = await fetch('/api/process', { method: 'POST', credentials: 'include', body: fd, }); if (!res.ok) throw new Error((await res.json()).detail); jobId = (await res.json()).job_id; currentJobId = jobId; } catch (err) { addLog('Erreur : ' + err.message, 'error'); fill.classList.remove('running'); btn.disabled = false; return; } // SSE const es = new EventSource(`/api/progress/${jobId}`); es.onmessage = e => { const msg = JSON.parse(e.data); if (msg.type === 'ping') return; if (msg.type === 'progress') { addLog(msg.message); steps++; fill.style.width = Math.min(5 + steps * 7, 90) + '%'; } if (msg.type === 'done') { fill.classList.remove('running'); fill.style.width = '100%'; addLog('Traitement termin\u00e9 avec succ\u00e8s', 'done'); es.close(); renderResults(jobId, msg.series); loadHistory(); loadCacheStatus(); btn.disabled = false; } if (msg.type === 'error') { fill.classList.remove('running'); addLog('Erreur : ' + msg.message, 'error'); es.close(); btn.disabled = false; } }; es.onerror = () => { es.close(); btn.disabled = false; }; }); function addLog(msg, cls = '') { const log = document.getElementById('progress-log'); const div = document.createElement('div'); div.className = 'log-line' + (cls ? ' ' + cls : ''); div.textContent = msg; log.appendChild(div); log.scrollTop = log.scrollHeight; } // ── R\u00e9sultats ────────────────────────────────────────────────────────────────── function renderResults(jobId, series) { const sect = document.getElementById('section-results'); const grid = document.getElementById('series-grid'); sect.classList.remove('hidden'); grid.innerHTML = series.map(s => `

${esc(s.name)}

${s.event_count} \u00e9v\u00e9nement${s.event_count > 1 ? 's' : ''} ⇓ T\u00e9l\u00e9charger ICS
`).join(''); // Bouton "tout t\u00e9l\u00e9charger" : t\u00e9l\u00e9charge l'un apr\u00e8s l'autre document.getElementById('btn-dl-all').onclick = () => { series.forEach((s, i) => { setTimeout(() => { const a = document.createElement('a'); a.href = `/api/download/${jobId}/${encodeURIComponent(s.filename)}`; a.download = s.filename; a.click(); }, i * 400); }); }; sect.scrollIntoView({ behavior: 'smooth', block: 'start' }); } // ── Historique ──────────────────────────────────────────────────────────────── async function loadHistory() { const jobs = await api('GET', '/api/jobs'); if (!jobs) return; const listEl = document.getElementById('history-list'); if (!jobs.length) { listEl.innerHTML = '

Aucun traitement pr\u00e9c\u00e9dent

'; return; } listEl.innerHTML = jobs.slice(0, 15).map(j => `
${j.series ? j.series.length : '?'} s\u00e9rie${j.series?.length > 1 ? 's' : ''} ${fmtDate(j.created_at)}
${esc((j.pdf_names || []).join(', '))}
`).join(''); listEl.querySelectorAll('.history-item').forEach(item => item.addEventListener('click', async () => { const job = await api('GET', `/api/jobs/${item.dataset.jobid}`); if (job?.series) renderResults(job.job_id, job.series); }) ); } // ── Cache ───────────────────────────────────────────────────────────────────── async function loadCacheStatus() { const status = await api('GET', '/api/cache/status'); if (!status) return; const set = (id, ok) => { const el = document.getElementById(id); el.textContent = ok ? '\u2713 En cache' : '\u2715 Non mis en cache'; el.className = 'setting-value ' + (ok ? 'ok' : 'nok'); }; set('cache-website', status.website_cached); set('cache-series', status.series_cached); } document.getElementById('btn-clear-cache').addEventListener('click', async () => { if (!confirm('Vider le cache ?\nLe prochain traitement re-scrapera le site et re-classifiera les s\u00e9ries avec l\'IA.')) return; const result = await api('DELETE', '/api/cache'); if (!result) return; alert(result.deleted.length ? 'Cache vid\u00e9 : ' + result.deleted.join(', ') : 'Cache d\u00e9j\u00e0 vide'); loadCacheStatus(); }); // ── D\u00e9marrage ────────────────────────────────────────────────────────────────── checkAuth();