deploy Maps v10 update

This commit is contained in:
Marco0300
2026-09-05 17:35:15 +02:00
parent 6ea6884274
commit 6075460d16
3 changed files with 6 additions and 2 deletions
+2 -1
View File
@@ -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=`<div class="drawer-score"><span class="score ${scoreClass(Number(p.opportunity_score||0))}">${p.opportunity_score||0}</span><div><b>Opportunity score</b><small>Explainable triage signal, not an AI verdict</small></div></div><div class="drawer-section"><h3>Contact</h3><p>${esc(p.emails||'No public email found')}</p><p>${esc(p.phone||'No phone listed')}</p></div><div class="drawer-section"><h3>Website health</h3><div class="detail-grid"><span>Status<b>${esc((p.website_status||'unknown').replaceAll('_',' '))}</b></span><span>HTTPS<b>${p.website_https?'Yes':'No / unknown'}</b></span><span>Latency<b>${p.website_latency_ms?p.website_latency_ms+' ms':'—'}</b></span><span>Checked<b>${date(p.website_checked_at)}</b></span></div></div><div class="drawer-section"><h3>Why this scored</h3><ul>${reasons.map(r=>`<li>${esc(r)}</li>`).join('')||'<li>No score reasons recorded</li>'}</ul></div><div class="drawer-actions"><button class="primary" data-message="${p.id}">Draft AI email</button>${p.website?`<a class="secondary" href="${esc(p.website)}" target="_blank" rel="noopener">Open website ↗</a>`:''}</div>`;$('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.'}});
+1 -1
View File
@@ -1,3 +1,3 @@
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="theme-color" content="#0d1726"><title>ProspectOS</title><link rel="stylesheet" href="/style.css"></head>
<body><div class="app-shell"><aside class="sidebar"><div class="brand"><span class="brand-mark">P</span><div><b>ProspectOS</b><small>Growth workspace</small></div></div><nav id="nav"><a href="#dashboard" data-route="dashboard"><span>⌂</span>Dashboard</a><a href="#discover" data-route="discover"><span>⌁</span>Discover</a><a href="#prospects" data-route="prospects"><span>▦</span>Prospects</a><a href="#runs" data-route="runs"><span>◷</span>Runs</a><a href="#settings" data-route="settings"><span>⚙</span>Settings</a></nav><div class="sidebar-foot"><span class="status-dot" id="sourceDot"></span><div><b id="sourceLabel">Checking worker</b><small id="sourceSub">Connecting…</small></div></div></aside><main class="main"><header class="topbar"><div><span class="crumb">Workspace</span><span class="slash">/</span><b id="pageName">Dashboard</b></div><div class="top-actions"><span id="updated">—</span><button class="icon-btn" id="refreshAll" title="Refresh">↻</button></div></header><div id="view"></div><footer>ProspectOS · public data only · manual review before contact</footer></main></div><dialog id="messageDialog"><form method="dialog" id="messageForm"><div class="dialog-head"><div><label class="eyebrow">AI ASSISTED DRAFT</label><h2 id="messageTitle">Tailor a message</h2><p>Use the evidence and your offer to create an editable draft.</p></div><button class="icon-btn" value="cancel">×</button></div><label for="offerInput">Your offer or instruction</label><textarea id="offerInput" rows="4" placeholder="Move hosting to Gold Plus for R119/month and rebuild the website free."></textarea><div id="messageStatus" class="notice"></div><button class="primary" id="generateMessage" type="button">Generate subject and email</button><label for="subjectOutput">Subject</label><input id="subjectOutput"><label for="bodyOutput">Email draft</label><textarea id="bodyOutput" rows="12"></textarea><div class="dialog-actions"><button class="secondary" id="copyMessage" type="button">Copy draft</button><button class="secondary" value="cancel">Close</button></div></form></dialog><script src="/app.js?v=ui-v9"></script></body></html>
<body><div class="app-shell"><aside class="sidebar"><div class="brand"><span class="brand-mark">P</span><div><b>ProspectOS</b><small>Growth workspace</small></div></div><nav id="nav"><a href="#dashboard" data-route="dashboard"><span>⌂</span>Dashboard</a><a href="#discover" data-route="discover"><span>⌁</span>Discover</a><a href="#prospects" data-route="prospects"><span>▦</span>Prospects</a><a href="#runs" data-route="runs"><span>◷</span>Runs</a><a href="#settings" data-route="settings"><span>⚙</span>Settings</a></nav><div class="sidebar-foot"><span class="status-dot" id="sourceDot"></span><div><b id="sourceLabel">Checking worker</b><small id="sourceSub">Connecting…</small></div></div></aside><main class="main"><header class="topbar"><div><span class="crumb">Workspace</span><span class="slash">/</span><b id="pageName">Dashboard</b></div><div class="top-actions"><span id="updated">—</span><button class="icon-btn" id="refreshAll" title="Refresh">↻</button></div></header><div id="view"></div><footer>ProspectOS · public data only · manual review before contact</footer></main></div><aside id="detailDrawer" class="detail-drawer" aria-hidden="true"><div class="drawer-head"><div><span class="eyebrow">PROSPECT DETAIL</span><h2 id="drawerName">Prospect</h2></div><button class="icon-btn" id="closeDrawer">×</button></div><div id="drawerBody"></div></aside><dialog id="messageDialog"><form method="dialog" id="messageForm"><div class="dialog-head"><div><label class="eyebrow">AI ASSISTED DRAFT</label><h2 id="messageTitle">Tailor a message</h2><p>Use the evidence and your offer to create an editable draft.</p></div><button class="icon-btn" value="cancel">×</button></div><label for="offerInput">Your offer or instruction</label><textarea id="offerInput" rows="4" placeholder="Move hosting to Gold Plus for R119/month and rebuild the website free."></textarea><div id="messageStatus" class="notice"></div><button class="primary" id="generateMessage" type="button">Generate subject and email</button><label for="subjectOutput">Subject</label><input id="subjectOutput"><label for="bodyOutput">Email draft</label><textarea id="bodyOutput" rows="12"></textarea><div class="dialog-actions"><button class="secondary" id="copyMessage" type="button">Copy draft</button><button class="secondary" value="cancel">Close</button></div></form></dialog><script src="/app.js?v=ui-v10"></script></body></html>
+3
View File
@@ -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); }