diff --git a/web/app.js b/web/app.js index 635538a..92ec321 100644 --- a/web/app.js +++ b/web/app.js @@ -15,7 +15,8 @@ function renderSettings(){const ai=settings.ai||{};const sch=settings.schedules| async function load(){[prospects,runs,settings]=await Promise.all([api('/api/prospects').then(x=>x.items||[]),api('/api/runs').then(x=>x.items||[]),Promise.all([api('/api/settings'),api('/api/schedules'),api('/api/suppressions')]).then(([s,sch,sup])=>({...s,schedules:sch.items||[],suppressions:sup.items||[]}))]);} async function route(){const r=location.hash.slice(1)||'dashboard';document.querySelectorAll('[data-route]').forEach(a=>a.classList.toggle('active',a.dataset.route===r));try{await load();({dashboard:renderDashboard,discover:renderDiscover,prospects:renderProspects,runs:renderRuns,settings:renderSettings}[r]||renderDashboard)();$('updated').textContent='Updated '+new Date().toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'});}catch(e){$('view').innerHTML=empty(e.message)}} async function openMessage(id){selected=prospects.find(p=>String(p.id)===String(id));if(!selected)return;$('messageTitle').textContent='Draft for '+selected.name;$('offerInput').value='';$('subjectOutput').value=selected.ai_subject||'';$('bodyOutput').value=selected.ai_message||'';$('messageStatus').textContent='';$('messageDialog').showModal()} -document.addEventListener('click',async e=>{const open=e.target.closest('[data-open]');if(open){const p=prospects.find(x=>String(x.id)===open.dataset.open);if(p){selected=p;alert(`${p.name}\n\nScore: ${p.opportunity_score}/100\nWebsite: ${p.website_status}\nReasons: ${JSON.parse(p.score_reasons||'[]').join(', ')||'—'}\n\nEmails: ${p.emails||'None found'}\nEvidence: ${p.email_evidence||'[]'}`)}}const msg=e.target.closest('[data-message]');if(msg)openMessage(msg.dataset.message);if(e.target.id==='exportCsv')location.href='/api/export.csv';if(e.target.id==='refreshProspects')route();}); +function openDrawer(id){const p=prospects.find(x=>String(x.id)===String(id));if(!p)return;selected=p;$('drawerName').textContent=p.name;let reasons=[];try{reasons=JSON.parse(p.score_reasons||'[]')}catch{};$('drawerBody').innerHTML=`
${p.opportunity_score||0}
Opportunity scoreExplainable triage signal, not an AI verdict

Contact

${esc(p.emails||'No public email found')}

${esc(p.phone||'No phone listed')}

Website health

Status${esc((p.website_status||'unknown').replaceAll('_',' '))}HTTPS${p.website_https?'Yes':'No / unknown'}Latency${p.website_latency_ms?p.website_latency_ms+' ms':'—'}Checked${date(p.website_checked_at)}

Why this scored

${p.website?`Open website ↗`:''}
`;$('detailDrawer').classList.add('open');$('detailDrawer').setAttribute('aria-hidden','false')} +document.addEventListener('click',async e=>{const open=e.target.closest('[data-open]');if(open)openDrawer(open.dataset.open);const msg=e.target.closest('[data-message]');if(msg)openMessage(msg.dataset.message);if(e.target.id==='exportCsv')location.href='/api/export.csv';if(e.target.id==='refreshProspects')route();if(e.target.id==='closeDrawer'||!e.target.closest('#detailDrawer')&&$('detailDrawer').classList.contains('open')&&!e.target.closest('[data-open]')){$('detailDrawer').classList.remove('open');$('detailDrawer').setAttribute('aria-hidden','true')}}); document.addEventListener('change',async e=>{if(e.target.dataset.review){try{const p=await api(`/api/prospects/${e.target.dataset.review}/review`,{method:'PATCH',body:JSON.stringify({status:e.target.value})});const i=prospects.findIndex(x=>x.id===p.id);if(i>=0)prospects[i]=p}catch(err){alert(err.message)}}if(e.target.id==='prospectSearch'||e.target.id==='prospectStatus')$('prospectTable').innerHTML=prospectTable()}); document.addEventListener('submit',async e=>{if(e.target.id==='discoverForm'){e.preventDefault();const m=$('discoverMessage');$('discoverBtn').disabled=true;m.textContent='Queueing…';try{const r=await api('/api/discover',{method:'POST',body:JSON.stringify({keywords:$('keywords').value,category:$('category').value,location:$('location').value,max_results:Number($('max-results').value),extra_reviews:$('extra-reviews').checked,include_email:true})});m.textContent=`Run #${r.id} started. View it in Runs.`;setTimeout(route,500)}catch(x){m.textContent=x.message}finally{$('discoverBtn').disabled=false}}if(e.target.id==='settingsForm'){e.preventDefault();const m=$('settingsMessage');try{const ai={AI_BASE_URL:$('aiBase').value,AI_MODEL:$('aiModel').value};if($('aiKey').value)ai.AI_API_KEY=$('aiKey').value;await api('/api/settings',{method:'POST',body:JSON.stringify({ai})});m.textContent='Settings saved.';m.className='notice ok';}catch(x){m.textContent=x.message}}if(e.target.id==='scheduleForm'){e.preventDefault();try{await api('/api/schedules',{method:'POST',body:JSON.stringify({category:$('sCat').value,keywords:$('sKey').value,location:$('sLoc').value,interval_hours:Number($('sHours').value)})});route()}catch(x){alert(x.message)}}if(e.target.id==='suppressionForm'){e.preventDefault();try{await api('/api/suppressions',{method:'POST',body:JSON.stringify({kind:$('supKind').value,value:$('supValue').value,reason:$('supReason').value})});route()}catch(x){alert(x.message)}}}); document.addEventListener('click',async e=>{if(e.target.id==='generateMessage'){if(!selected)return;const s=$('messageStatus');s.textContent='Generating…';try{const d=await api(`/api/prospects/${selected.id}/message`,{method:'POST',body:JSON.stringify({offer:$('offerInput').value})});$('subjectOutput').value=d.subject;$('bodyOutput').value=d.message;s.textContent='Draft ready. Review and edit before sending.';s.className='notice ok'}catch(x){s.textContent=x.message;s.className='notice error'}}if(e.target.id==='copyMessage'){await navigator.clipboard.writeText(`Subject: ${$('subjectOutput').value}\n\n${$('bodyOutput').value}`);$('messageStatus').textContent='Copied to clipboard.'}}); diff --git a/web/index.html b/web/index.html index f74e148..6417fbe 100644 --- a/web/index.html +++ b/web/index.html @@ -1,3 +1,3 @@ ProspectOS -
Workspace/Dashboard
—

Tailor a message

Use the evidence and your offer to create an editable draft.

+
Workspace/Dashboard
—

Tailor a message

Use the evidence and your offer to create an editable draft.

diff --git a/web/style.css b/web/style.css index e0d5592..ee1c4b1 100644 --- a/web/style.css +++ b/web/style.css @@ -21,6 +21,9 @@ @media(max-width:900px){.sidebar{width:70px;padding:18px 10px}.brand div,.sidebar nav a:not(.active)::after,.sidebar-foot div{display:none}.brand{padding:0 8px 25px}.sidebar nav a{justify-content:center;padding:12px}.main{margin-left:70px;width:calc(100% - 70px);padding:0 20px}.metric-grid{grid-template-columns:repeat(2,1fr)}.settings-grid,.dashboard-grid,.two-col{grid-template-columns:1fr}.wide-panel{grid-column:auto}.toolbar{flex-wrap:wrap}.toolbar .search{flex-basis:100%}} @media(max-width:560px){.main{padding:0 14px}.page-head{display:block}.page-head h1{font-size:25px}.metric-grid{gap:8px}.metric{padding:14px}.metric b{font-size:25px}.form-panel form{grid-template-columns:1fr}.form-panel label:first-child{grid-column:auto}.form-row{grid-column:auto}.inline-form{display:grid}.topbar{height:58px}.toolbar select,.toolbar button{flex:1}.list>div{display:block}.list span{display:block;margin-top:4px}} +/* refined visual system */ +:root{--navy:#111d2e;--ink:#1d2939;--muted2:#7b8798;--lav:#635bff;--bg:#f7f8fc;--card:#fff}body{background:var(--bg);color:var(--ink);letter-spacing:-.01em}.sidebar{background:var(--navy);width:248px;padding:28px 18px}.main{margin-left:248px;width:calc(100% - 248px);max-width:none;padding:0 58px 42px}.topbar{height:76px}.topbar b{font-size:13px;color:var(--ink)}.page-head{padding:48px 0 32px}.page-head h1{font-size:36px;line-height:1.1;letter-spacing:-1.4px;font-weight:760}.page-head p{font-size:14px;line-height:1.6;color:var(--muted2)}.panel,.metric{border-color:#e8ecf3;box-shadow:0 8px 26px #2334520a;border-radius:16px}.metric{min-height:139px;padding:24px}.metric b{font-size:34px;font-weight:760}.metric small{letter-spacing:1.4px}.panel{padding:27px}.panel-title h2{font-size:20px;letter-spacing:-.35px}.panel-title p{font-size:13px}.primary{background:var(--lav);box-shadow:0 7px 16px #635bff33;transition:.18s}.primary:hover{transform:translateY(-1px);box-shadow:0 9px 20px #635bff44}.secondary,.icon-btn{transition:.18s}.secondary:hover,.icon-btn:hover{border-color:#b9b5ff;background:#f7f6ff;color:#4f47df}.spot{padding:16px 4px}.spot:hover{background:#fafaff}.toolbar{padding-bottom:4px}.table-wrap table{min-width:850px}.table-wrap tr[data-open]{cursor:pointer}.table-wrap tbody tr:hover{background:#fbfbff}.table-wrap td{padding:17px 12px}.badge{font-size:10px;text-transform:capitalize;margin-right:7px}.drawer-backdrop{display:none}.detail-drawer{position:fixed;z-index:20;right:-470px;top:0;bottom:0;width:min(470px,100%);background:#fff;box-shadow:-18px 0 45px #17233d2b;padding:30px;overflow:auto;transition:right .24s ease}.detail-drawer.open{right:0}.drawer-head{display:flex;justify-content:space-between;align-items:flex-start;padding-bottom:24px;border-bottom:1px solid #edf0f5}.drawer-head h2{font-size:24px;margin:7px 0 0;letter-spacing:-.5px}.drawer-score{display:flex;align-items:center;gap:14px;padding:22px 0;border-bottom:1px solid #edf0f5}.drawer-score .score{width:48px;height:42px;font-size:17px}.drawer-score small{display:block;color:var(--muted2);font-size:11px;margin-top:3px}.drawer-section{padding:20px 0;border-bottom:1px solid #edf0f5}.drawer-section h3{font-size:11px;text-transform:uppercase;letter-spacing:1.2px;color:#8a96a8;margin:0 0 12px}.drawer-section p{font-size:13px;margin:7px 0;color:#47566d}.detail-grid{display:grid;grid-template-columns:1fr 1fr;gap:15px}.detail-grid span{font-size:11px;color:var(--muted2)}.detail-grid b{display:block;color:var(--ink);font-size:13px;margin-top:4px;text-transform:capitalize}.drawer-section ul{padding-left:19px;margin:0}.drawer-section li{font-size:13px;color:#4d5b70;padding:4px 0}.drawer-actions{display:flex;gap:9px;padding-top:22px}.drawer-actions a{display:inline-flex;align-items:center}.message-dialog{border-radius:18px}.message-dialog form{background:#fff}.message-dialog h2{font-size:22px;letter-spacing:-.5px}.message-dialog textarea:focus,.message-dialog input:focus,.settings-grid input:focus,.form-panel input:focus{outline:3px solid #635bff22;border-color:#8b85ff}@media(max-width:900px){.main{margin-left:70px;width:calc(100% - 70px);padding:0 24px 35px}.sidebar{width:70px}}@media(max-width:560px){.main{padding:0 15px 30px}.page-head h1{font-size:30px}.panel{padding:20px}.detail-drawer{padding:22px;width:100%}} + * { box-sizing: border-box; } html { scroll-behavior: smooth; } body { margin: 0; background: var(--soft); }