replace ProspectOS with BrowserOnly build
This commit is contained in:
+10
@@ -0,0 +1,10 @@
|
||||
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>'});
|
||||
@@ -0,0 +1,9 @@
|
||||
<!doctype html>
|
||||
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>ProspectOS · Browser Discovery</title><link rel="stylesheet" href="/style.css"></head>
|
||||
<body><header class="top"><div><span class="eyebrow">PROSPECTOS</span><h1>Browser discovery</h1><p>Find businesses through Google, remove anything already catalogued, then review the remaining prospects.</p></div><div class="source-pill">● Google · Playwright</div></header>
|
||||
<main>
|
||||
<section class="grid stats"><article><span>Catalogued prospects</span><strong id="total">0</strong></article><article><span>Pending review</span><strong id="pending">0</strong></article><article><span>Approved</span><strong id="approved">0</strong></article><article><span>Last run</span><strong id="lastRun">—</strong></article></section>
|
||||
<section class="panel"><div class="section-head"><div><span class="eyebrow">DISCOVERY</span><h2>Find new prospects</h2><p>Search criteria are used only for this run. The system stores the business name, website, query, and discovery time.</p></div></div><form id="discoverForm" class="form-grid"><label>Category<input name="category" placeholder="Plumbers" required></label><label>Keywords<input name="keywords" placeholder="plumbing services" required></label><label>Location<input name="location" value="South Africa" required></label><label>Maximum results<select name="max_results"><option value="5">5</option><option value="10" selected>10</option><option value="25">25</option></select></label><div class="form-actions"><button class="primary" type="submit" id="discoverBtn">Run browser discovery</button><span id="discoverMessage" role="status"></span></div></form></section>
|
||||
<section class="panel"><div class="section-head row"><div><span class="eyebrow">CATALOGUE</span><h2>Prospect explorer</h2><p>Duplicates are filtered by root domain before they enter the catalogue.</p></div><div class="filters"><input id="search" placeholder="Search name or domain"><select id="status"><option value="">All review states</option><option value="pending">Pending</option><option value="approved">Approved</option><option value="rejected">Rejected</option><option value="contacted">Contacted</option></select><button class="secondary" id="refresh" type="button">Refresh</button></div></div><div id="explorer" class="explorer"><p class="muted">Loading prospects…</p></div></section>
|
||||
<section class="panel"><div class="section-head row"><div><span class="eyebrow">RUN HISTORY</span><h2>Discovery runs</h2></div><button class="secondary" id="runsRefresh" type="button">Refresh</button></div><div id="runs" class="runs"><p class="muted">Loading runs…</p></div></section>
|
||||
</main><script src="/app.js"></script></body></html>
|
||||
@@ -0,0 +1 @@
|
||||
:root{font-family:Inter,ui-sans-serif,system-ui,-apple-system,sans-serif;color:#172033;background:#f5f7fb;line-height:1.45}*{box-sizing:border-box}body{margin:0}.top{background:#151b31;color:#fff;padding:36px max(24px,calc((100% - 1120px)/2));display:flex;justify-content:space-between;gap:24px;align-items:flex-start}.top h1{font-size:32px;margin:4px 0 6px}.top p{margin:0;color:#bfc8e0}.eyebrow{font-size:11px;letter-spacing:.16em;font-weight:800;color:#685cff}.top .eyebrow{color:#9c93ff}.source-pill{border:1px solid #495273;border-radius:999px;padding:9px 14px;color:#d9dded;font-size:13px;white-space:nowrap}main{max-width:1120px;margin:26px auto;padding:0 24px}.grid{display:grid;gap:14px}.stats{grid-template-columns:repeat(4,1fr);margin-bottom:18px}.stats article,.panel{background:#fff;border:1px solid #e1e6f0;border-radius:14px;box-shadow:0 8px 24px #1c29400b}.stats article{padding:18px}.stats span{display:block;color:#69758c;font-size:13px}.stats strong{font-size:28px;display:block;margin-top:7px}.panel{padding:24px;margin-bottom:18px}.section-head{margin-bottom:18px}.section-head.row{display:flex;justify-content:space-between;align-items:flex-end;gap:20px}.section-head h2{margin:4px 0;font-size:22px}.section-head p{margin:0;color:#69758c;font-size:14px}.form-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:16px}label{font-weight:700;font-size:13px;color:#33405a}input,select{display:block;width:100%;margin-top:6px;border:1px solid #cdd5e5;border-radius:9px;padding:12px;background:#fff;color:#172033;font:inherit}button{border:0;border-radius:9px;padding:11px 16px;font-weight:800;cursor:pointer}.primary{background:#6257f5;color:#fff}.secondary{background:#eef1f8;color:#33405a}.form-actions{grid-column:1/-1;display:flex;align-items:center;gap:14px}.form-actions span{font-size:13px;color:#52705e}.filters{display:flex;gap:8px;align-items:center}.filters input{width:190px;margin:0}.filters select{width:155px;margin:0}.explorer{display:grid;gap:10px}.prospect{border:1px solid #e3e7ef;border-radius:10px;padding:15px;display:grid;grid-template-columns:1fr auto;gap:10px}.prospect h3{font-size:16px;margin:0}.prospect a{color:#4b46c6;font-size:13px;word-break:break-all}.meta{color:#738098;font-size:12px;margin-top:4px}.review{display:flex;align-items:center;gap:7px}.review select{margin:0;width:auto;padding:7px 9px;font-size:12px}.runs{display:grid;gap:8px}.run{display:flex;justify-content:space-between;border-bottom:1px solid #edf0f5;padding:10px 0;font-size:13px}.run:last-child{border:0}.ok{color:#22754b}.bad{color:#b83346}.muted{color:#7a8598}@media(max-width:720px){.top{display:block}.source-pill{display:inline-block;margin-top:18px}.stats{grid-template-columns:repeat(2,1fr)}.form-grid{grid-template-columns:1fr}.section-head.row{display:block}.filters{margin-top:14px;flex-wrap:wrap}.filters input,.filters select{width:100%}.prospect{grid-template-columns:1fr}.form-actions{align-items:flex-start;flex-direction:column}}
|
||||
Reference in New Issue
Block a user