11 lines
3.5 KiB
JavaScript
11 lines
3.5 KiB
JavaScript
const $=id=>document.getElementById(id);let prospects=[];
|
|
async function api(path,options={}){const r=await fetch(path,{...options,headers:{'Content-Type':'application/json',...(options.headers||{})}});const b=await r.json().catch(()=>({}));if(!r.ok)throw Error(b.error||'Request failed');return b}
|
|
function renderStats(){ $('total').textContent=prospects.length; $('pending').textContent=prospects.filter(x=>x.review_status==='pending').length; $('approved').textContent=prospects.filter(x=>x.review_status==='approved').length; }
|
|
function renderExplorer(){const q=$('search').value.trim().toLowerCase(),s=$('status').value;const rows=prospects.filter(x=>(!q||(x.name+' '+x.domain).toLowerCase().includes(q))&&(!s||x.review_status===s));$('explorer').innerHTML=rows.length?rows.map(x=>`<article class="prospect"><div><h3>${esc(x.name)}</h3><a href="${esc(x.website)}" target="_blank" rel="noopener">${esc(x.domain)}</a><div class="meta">Found ${esc(x.discovered_at)} · Query: ${esc(x.query)}</div></div><div class="review"><select data-review="${x.id}"><option value="pending" ${x.review_status==='pending'?'selected':''}>Pending</option><option value="approved" ${x.review_status==='approved'?'selected':''}>Approved</option><option value="rejected" ${x.review_status==='rejected'?'selected':''}>Rejected</option><option value="contacted" ${x.review_status==='contacted'?'selected':''}>Contacted</option></select></div></article>`).join(''):'<p class="muted">No prospects match this filter.</p>';renderStats();}
|
|
function esc(v){return String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]))}
|
|
async function loadProspects(){prospects=(await api('/api/prospects')).items||[];renderExplorer()}
|
|
async function loadStats(){const s=await api('/api/stats');$('total').textContent=s.total;$('pending').textContent=s.pending;$('approved').textContent=s.approved}
|
|
async function loadRuns(){const rows=(await api('/api/runs')).items||[];$('runs').innerHTML=rows.length?rows.map(x=>`<div class="run"><span>#${x.id} · ${esc(x.query)}</span><span class="${x.status==='succeeded'?'ok':'bad'}">${x.status} · ${x.added} added${x.error?' · '+esc(x.error):''}</span></div>`).join(''):'<p class="muted">No discovery runs yet.</p>';if(rows[0])$('lastRun').textContent='#'+rows[0].id}
|
|
$('discoverForm').addEventListener('submit',async e=>{e.preventDefault();const btn=$('discoverBtn'),msg=$('discoverMessage');btn.disabled=true;msg.textContent='Opening headless browser…';try{const result=await api('/api/discover',{method:'POST',body:JSON.stringify(Object.fromEntries(new FormData(e.target)))});msg.textContent=`Found ${result.found}; added ${result.added} new prospect${result.added===1?'':'s'}.`;await Promise.all([loadProspects(),loadRuns(),loadStats()])}catch(err){msg.textContent='Discovery failed: '+err.message;msg.className='bad'}finally{btn.disabled=false}});
|
|
$('explorer').addEventListener('change',async e=>{const id=e.target.dataset.review;if(!id)return;try{await api('/api/prospects/'+id+'/review',{method:'PATCH',body:JSON.stringify({status:e.target.value})});await Promise.all([loadProspects(),loadStats()])}catch(err){alert(err.message)}});$('search').addEventListener('input',renderExplorer);$('status').addEventListener('change',renderExplorer);$('refresh').addEventListener('click',loadProspects);$('runsRefresh').addEventListener('click',loadRuns);Promise.all([loadProspects(),loadRuns(),loadStats()]).catch(err=>{$('explorer').innerHTML='<p class="bad">Unable to load the local catalogue: '+esc(err.message)+'</p>'});
|