c1eef4c6aa
Screen-capture lead tool: capture agent, Claude vision extractor with pluggable site recipes (LinkedIn), canonical funnel model, SQLite storage, FastAPI backend, dashboard, and setup/run scripts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
122 lines
6.5 KiB
HTML
122 lines
6.5 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>Screen Leads</title>
|
|
<style>
|
|
:root {
|
|
--bg: #0f1115; --panel: #171a21; --border: #262b36; --fg: #e6e9ef;
|
|
--muted: #8b93a3; --accent: #4f8cff; --ok: #34c759; --warn: #ffb020; --danger: #ff5c5c;
|
|
}
|
|
@media (prefers-color-scheme: light) {
|
|
:root { --bg:#f5f6f8; --panel:#fff; --border:#e2e5ea; --fg:#1b1f27; --muted:#6b7280; }
|
|
}
|
|
* { box-sizing: border-box; }
|
|
body { margin:0; font:14px/1.5 -apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;
|
|
background:var(--bg); color:var(--fg); }
|
|
header { display:flex; align-items:center; gap:12px; padding:14px 20px;
|
|
border-bottom:1px solid var(--border); background:var(--panel); position:sticky; top:0; }
|
|
header h1 { font-size:16px; margin:0; font-weight:600; }
|
|
header a { color:var(--accent); text-decoration:none; font-size:13px; font-weight:600; }
|
|
header a:hover { text-decoration:underline; }
|
|
.pill { padding:2px 10px; border-radius:999px; font-size:12px; font-weight:600; border:1px solid var(--border); }
|
|
.s-IDLE{color:var(--muted)} .s-RUNNING{color:var(--ok);border-color:var(--ok)}
|
|
.s-PAUSED{color:var(--warn);border-color:var(--warn)} .s-STOPPED{color:var(--danger);border-color:var(--danger)}
|
|
main { padding:20px; max-width:1200px; margin:0 auto; }
|
|
.controls { display:flex; gap:8px; flex-wrap:wrap; margin-left:auto; }
|
|
button { font:inherit; font-weight:600; padding:7px 14px; border-radius:8px; cursor:pointer;
|
|
border:1px solid var(--border); background:var(--panel); color:var(--fg); }
|
|
button:hover { border-color:var(--accent); }
|
|
button:disabled { opacity:.4; cursor:not-allowed; }
|
|
button.primary { background:var(--accent); border-color:var(--accent); color:#fff; }
|
|
.event { color:var(--muted); font-size:12px; padding:8px 20px; border-bottom:1px solid var(--border);
|
|
background:var(--panel); }
|
|
.stages { display:flex; gap:10px; flex-wrap:wrap; margin-bottom:16px; }
|
|
.stage { background:var(--panel); border:1px solid var(--border); border-radius:10px; padding:10px 14px; min-width:120px; }
|
|
.stage .n { font-size:22px; font-weight:700; }
|
|
.stage .l { font-size:11px; color:var(--muted); letter-spacing:.04em; }
|
|
table { width:100%; border-collapse:collapse; background:var(--panel);
|
|
border:1px solid var(--border); border-radius:10px; overflow:hidden; }
|
|
th,td { text-align:left; padding:9px 12px; border-bottom:1px solid var(--border); vertical-align:top; }
|
|
th { font-size:11px; letter-spacing:.05em; color:var(--muted); text-transform:uppercase; }
|
|
tr:last-child td { border-bottom:0; }
|
|
.tag { font-size:11px; padding:1px 8px; border-radius:6px; border:1px solid var(--border); white-space:nowrap; }
|
|
.tag-NEW{color:var(--muted)} .tag-ENRICHED{color:var(--accent);border-color:var(--accent)}
|
|
.tag-CONTACT_FOUND{color:var(--ok);border-color:var(--ok)} .tag-EXPORTED{color:var(--warn);border-color:var(--warn)}
|
|
a { color:var(--accent); text-decoration:none; }
|
|
.muted { color:var(--muted); }
|
|
.empty { text-align:center; color:var(--muted); padding:40px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<h1>Screen Leads</h1>
|
|
<span id="state" class="pill s-IDLE">IDLE</span>
|
|
<div class="controls">
|
|
<button id="btn-start" class="primary" onclick="act('start')">Start</button>
|
|
<button id="btn-pause" onclick="act('pause')">Pause</button>
|
|
<button id="btn-resume" onclick="act('resume')">Resume</button>
|
|
<button id="btn-stop" onclick="act('stop')">Stop</button>
|
|
<a href="/support">Help</a>
|
|
</div>
|
|
</header>
|
|
<div id="event" class="event">—</div>
|
|
<main>
|
|
<div id="stages" class="stages"></div>
|
|
<table>
|
|
<thead><tr>
|
|
<th>Name</th><th>Title / Company</th><th>Location</th><th>Contact</th>
|
|
<th>Stage</th><th>Conf.</th><th>Captured</th><th>Source</th>
|
|
</tr></thead>
|
|
<tbody id="rows"><tr><td colspan="8" class="empty">No leads yet.</td></tr></tbody>
|
|
</table>
|
|
</main>
|
|
<script>
|
|
const STAGES = ["NEW","ENRICHED","CONTACT_FOUND","EXPORTED"];
|
|
async function api(path, method="GET"){ const r=await fetch(path,{method}); return r.json(); }
|
|
async function act(name){ await api("/api/"+name,"POST"); refresh(); }
|
|
function esc(s){ return (s||"").replace(/[&<>]/g,c=>({"&":"&","<":"<",">":">"}[c])); }
|
|
function fmtDate(ts){ if(!ts) return '<span class="muted">—</span>';
|
|
const d=new Date(ts*1000);
|
|
return d.toLocaleDateString(undefined,{year:'numeric',month:'short',day:'numeric'})
|
|
+'<br><span class="muted">'+d.toLocaleTimeString(undefined,{hour:'2-digit',minute:'2-digit'})+'</span>'; }
|
|
|
|
async function refresh(){
|
|
const st = await api("/api/status");
|
|
const el = document.getElementById("state");
|
|
el.textContent = st.state; el.className = "pill s-"+st.state;
|
|
document.getElementById("event").textContent =
|
|
(st.last_event||"—") + (st.reason && st.state==="STOPPED" ? " · "+st.reason : "");
|
|
document.getElementById("btn-start").disabled = st.state==="RUNNING"||st.state==="PAUSED";
|
|
document.getElementById("btn-pause").disabled = st.state!=="RUNNING";
|
|
document.getElementById("btn-resume").disabled = st.state!=="PAUSED";
|
|
document.getElementById("btn-stop").disabled = st.state!=="RUNNING"&&st.state!=="PAUSED";
|
|
|
|
const counts = st.stage_counts||{};
|
|
document.getElementById("stages").innerHTML = STAGES.map(s=>
|
|
`<div class="stage"><div class="n">${counts[s]||0}</div><div class="l">${s.replace("_"," ")}</div></div>`).join("");
|
|
|
|
const leads = await api("/api/leads");
|
|
const rows = document.getElementById("rows");
|
|
if(!leads.length){ rows.innerHTML='<tr><td colspan="7" class="empty">No leads yet.</td></tr>'; return; }
|
|
rows.innerHTML = leads.map(l=>{
|
|
const contact=[l.email,l.phone,l.website].filter(Boolean).map(esc).join("<br>")||'<span class="muted">—</span>';
|
|
const src = l.source_url?`<a href="${esc(l.source_url)}" target="_blank" rel="noopener">${esc(l.source_site||"link")}</a>`:esc(l.source_site);
|
|
return `<tr>
|
|
<td><strong>${esc(l.full_name)||'<span class="muted">?</span>'}</strong><br><span class="muted">${esc(l.headline)}</span></td>
|
|
<td>${esc(l.title)}${l.company?' · '+esc(l.company):''}</td>
|
|
<td>${esc(l.location)}</td>
|
|
<td>${contact}</td>
|
|
<td><span class="tag tag-${l.stage}">${l.stage.replace("_"," ")}</span></td>
|
|
<td>${l.confidence?Math.round(l.confidence*100)+'%':'—'}</td>
|
|
<td>${fmtDate(l.captured_at)}</td>
|
|
<td>${src}</td>
|
|
</tr>`;
|
|
}).join("");
|
|
}
|
|
refresh(); setInterval(refresh, 2000);
|
|
</script>
|
|
</body>
|
|
</html>
|