Dashboard: the staffer's actual workday, not a search box

Not a CRM search page. A staffing workstation:

Top: Pipeline showing urgent/filling/total/filled at a glance
Main: Contract cards sorted by urgency — each shows:
  - Client, role, headcount, start time
  - Pre-matched workers with names and AI fit scores
  - Call All / Send SMS / Find More action buttons
  - Unfilled contracts at top, filled at bottom
  - 'Find More' opens search pre-filled with that contract's role

Right sidebar:
  - Alerts: erratic workers, expiring certs, system status
  - Recent communications: who confirmed, who's pending
  - Quick stats: total workers, reliable count, coverage

The search is there but collapsed — it's a tool, not the focus.
When they open the page, their day is already organized.

This is what the CRM doesn't do: anticipate, pre-match, organize.
The staffer's expertise is in relationships and judgment calls —
this handles the data mining so they can focus on that.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
root 2026-04-17 15:22:18 -05:00
parent 7cb9999451
commit 05785b4628

View File

@ -1,178 +1,369 @@
<!DOCTYPE html>
<html><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Lakehouse Search</title>
<title>Staffing Co-Pilot</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:system-ui,sans-serif;background:#0a0a0f;color:#d4d4d8}
.top{background:#111827;padding:20px;text-align:center;border-bottom:1px solid #1e293b}
.top h1{color:#818cf8;font-size:22px;margin-bottom:4px}
.top p{color:#64748b;font-size:13px}
.box{max-width:800px;margin:20px auto;padding:0 16px}
.row{display:flex;gap:8px;margin-bottom:12px}
input[type=text]{flex:1;padding:14px;background:#111827;border:1px solid #334155;border-radius:8px;color:#e2e8f0;font-size:15px;outline:none}
input[type=text]:focus{border-color:#818cf8}
button{padding:14px 24px;background:#7c3aed;border:none;border-radius:8px;color:#fff;font-size:14px;font-weight:600;cursor:pointer}
button:hover{background:#6d28d9}
.filters{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px}
select{padding:8px;background:#111827;border:1px solid #334155;border-radius:6px;color:#e2e8f0;font-size:12px}
.examples{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:16px}
.ex{padding:4px 12px;background:#1e293b;border:1px solid #334155;border-radius:16px;color:#818cf8;font-size:11px;cursor:pointer}
.ex:hover{background:#1e1b4b}
.card{background:#111827;border:1px solid #1e293b;border-radius:8px;padding:16px;margin-bottom:10px}
.name{font-size:16px;font-weight:700;color:#e2e8f0}
.role{color:#818cf8;font-size:13px}
.loc{color:#64748b;font-size:12px}
.det{color:#94a3b8;font-size:11px;margin-top:8px}
.sc{float:right;font-size:20px;font-weight:800;color:#34d399}
.msg{text-align:center;color:#475569;padding:20px}
.hdr{color:#94a3b8;font-size:12px;margin-bottom:12px}
.link{display:block;text-align:center;color:#818cf8;font-size:12px;margin-top:20px;text-decoration:none}
@media(max-width:600px){.row{flex-direction:column}.filters{flex-direction:column}}
body{font-family:system-ui,sans-serif;background:#0a0a0f;color:#d4d4d8;font-size:13px}
.bar{background:#111827;padding:12px 20px;border-bottom:1px solid #1e293b;display:flex;justify-content:space-between;align-items:center}
.bar h1{color:#818cf8;font-size:16px;font-weight:700}
.bar .info{color:#475569;font-size:11px}
.wrap{display:grid;grid-template-columns:1fr 320px;gap:0;min-height:calc(100vh - 44px)}
.main{padding:16px;overflow-y:auto}
.side{background:#111827;border-left:1px solid #1e293b;padding:16px;overflow-y:auto}
h2{font-size:12px;color:#818cf8;text-transform:uppercase;letter-spacing:1px;margin-bottom:10px;font-weight:600}
h3{font-size:11px;color:#64748b;text-transform:uppercase;letter-spacing:0.5px;margin:16px 0 8px;font-weight:600}
/* Contract cards */
.contracts{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:12px;margin-bottom:20px}
.ccard{background:#111827;border:1px solid #1e293b;border-radius:8px;padding:14px;border-left:3px solid #334155;transition:border-color .2s}
.ccard.urgent{border-left-color:#ef4444}.ccard.high{border-left-color:#f59e0b}.ccard.filling{border-left-color:#3b82f6}.ccard.filled{border-left-color:#22c55e}
.ccard .head{display:flex;justify-content:space-between;align-items:center;margin-bottom:6px}
.ccard .client{font-weight:700;color:#e2e8f0;font-size:14px}
.ccard .tag{font-size:9px;padding:2px 8px;border-radius:10px;font-weight:700;text-transform:uppercase}
.tag.urgent{background:#7f1d1d;color:#fca5a5}.tag.high{background:#78350f;color:#fcd34d}.tag.medium{background:#1e3a5f;color:#93c5fd}.tag.filled{background:#14532d;color:#86efac}
.ccard .meta{color:#64748b;font-size:11px;margin-bottom:8px}
.ccard .workers{border-top:1px solid #1e293b;padding-top:8px}
.wrow{display:flex;justify-content:space-between;align-items:center;padding:4px 0;border-bottom:1px solid #0a0a0f}
.wrow:last-child{border:none}
.wrow .wname{color:#d4d4d8;font-weight:500}
.wrow .wdet{color:#64748b;font-size:10px}
.wrow .wscore{color:#34d399;font-size:11px;font-weight:700}
.ccard .action{margin-top:8px;display:flex;gap:6px}
.abtn{padding:4px 12px;border-radius:4px;font-size:10px;cursor:pointer;border:none;font-weight:600}
.abtn.call{background:#1e40af;color:#93c5fd}.abtn.sms{background:#065f46;color:#6ee7b7}.abtn.skip{background:#1e293b;color:#64748b}
/* Pipeline */
.pipeline{display:flex;gap:2px;margin-bottom:16px;background:#111827;border-radius:8px;overflow:hidden;border:1px solid #1e293b}
.pipe{flex:1;text-align:center;padding:10px 4px}
.pipe .num{font-size:22px;font-weight:800;color:#e2e8f0}
.pipe .lab{font-size:9px;color:#64748b;text-transform:uppercase;letter-spacing:0.5px}
.pipe.active{background:#1e1b4b}
.pipe.green .num{color:#34d399}.pipe.blue .num{color:#60a5fa}.pipe.yellow .num{color:#fbbf24}.pipe.red .num{color:#f87171}
/* Alerts */
.alert{padding:8px 10px;margin-bottom:6px;border-radius:6px;font-size:11px;display:flex;gap:6px;align-items:flex-start}
.alert.warn{background:#1c1305;border:1px solid #854d0e;color:#fcd34d}
.alert.info{background:#0c1a2e;border:1px solid #1e40af;color:#93c5fd}
.alert.good{background:#052e16;border:1px solid #166534;color:#86efac}
.alert .ic{font-size:13px;flex-shrink:0}
/* Comms */
.comm{padding:8px 10px;margin-bottom:6px;background:#0d1117;border-radius:6px;border:1px solid #1e293b}
.comm .who{color:#e2e8f0;font-weight:600;font-size:12px}
.comm .msg{color:#94a3b8;font-size:11px;margin-top:2px}
.comm .time{color:#475569;font-size:10px;margin-top:2px}
/* Search drawer */
.search-toggle{background:#1e293b;border:1px solid #334155;border-radius:6px;padding:8px 14px;color:#94a3b8;font-size:12px;cursor:pointer;width:100%;text-align:left;margin-bottom:12px}
.search-toggle:hover{border-color:#818cf8;color:#e2e8f0}
.search-box{display:none;background:#0d1117;border:1px solid #1e293b;border-radius:8px;padding:12px;margin-bottom:16px}
.search-box.open{display:block}
.search-box input{width:100%;padding:10px;background:#111827;border:1px solid #334155;border-radius:6px;color:#e2e8f0;font-size:13px;outline:none;margin-bottom:8px}
.search-box .srow{display:flex;gap:6px;margin-bottom:8px}
.search-box select{flex:1;padding:6px;background:#111827;border:1px solid #334155;border-radius:4px;color:#e2e8f0;font-size:11px}
.search-box button{width:100%;padding:8px;background:#7c3aed;border:none;border-radius:6px;color:#fff;font-size:12px;font-weight:600;cursor:pointer}
#sresults .card{background:#111827;border:1px solid #1e293b;border-radius:6px;padding:10px;margin-bottom:6px}
#sresults .card .name{font-weight:600;color:#e2e8f0}
#sresults .card .det{color:#64748b;font-size:10px;margin-top:2px}
.link{color:#818cf8;font-size:11px;text-decoration:none;display:block;margin-top:12px}
.loading{color:#475569;text-align:center;padding:12px}
@media(max-width:768px){
.wrap{grid-template-columns:1fr}
.side{border-left:none;border-top:1px solid #1e293b}
.contracts{grid-template-columns:1fr}
.pipeline{flex-wrap:wrap}
.pipe{min-width:33%}
}
</style></head><body>
<div class="top"><h1>Search 500,000 Workers</h1><p>Type what you need — AI understands meaning</p></div>
<div class="box">
<div class="row">
<input type="text" id="q" placeholder="e.g. reliable forklift operator in Illinois">
<button id="btn" onclick="go()">Search</button>
<div class="bar">
<h1>Staffing Co-Pilot</h1>
<div class="info" id="status">Loading...</div>
</div>
<div class="filters">
<select id="st"><option value="">Any State</option><option>IL</option><option>IN</option><option>OH</option><option>MO</option><option>TN</option><option>KY</option><option>WI</option><option>MI</option></select>
<select id="rl"><option value="">Any Role</option><option>Forklift Operator</option><option>Machine Operator</option><option>Assembler</option><option>Loader</option><option>Quality Tech</option><option>Welder</option><option>Sanitation Worker</option><option>Maintenance Tech</option></select>
<div class="wrap">
<div class="main">
<h2>Today's Pipeline</h2>
<div class="pipeline" id="pipeline"></div>
<h2>Your Contracts</h2>
<div class="contracts" id="contracts"><div class="loading">Loading contracts...</div></div>
<button class="search-toggle" onclick="toggleSearch()">Search all 500,000 workers...</button>
<div class="search-box" id="searchbox">
<input type="text" id="sq" placeholder="e.g. reliable forklift operator Illinois" onkeydown="if(event.key==='Enter')doSearch()">
<div class="srow">
<select id="sst"><option value="">Any State</option><option>IL</option><option>IN</option><option>OH</option><option>MO</option><option>TN</option><option>KY</option><option>WI</option><option>MI</option></select>
<select id="srl"><option value="">Any Role</option><option>Forklift Operator</option><option>Machine Operator</option><option>Assembler</option><option>Loader</option><option>Quality Tech</option><option>Welder</option><option>Sanitation Worker</option><option>Maintenance Tech</option></select>
</div>
<button onclick="doSearch()">Search</button>
<div id="sresults"></div>
</div>
<a class="link" href="proof">View Proof of Work →</a>
</div>
<div class="examples">
<span class="ex" onclick="x(this)">warehouse help</span>
<span class="ex" onclick="x(this)">dependable machine operator</span>
<span class="ex" onclick="x(this)">safety trained chemical plant</span>
<span class="ex" onclick="x(this)">bilingual shipping</span>
<span class="ex" onclick="x(this)">experienced welder Ohio</span>
<div class="side">
<h2>Alerts</h2>
<div id="alerts"><div class="loading">Loading...</div></div>
<h3>Recent Communications</h3>
<div id="comms"></div>
<h3>Quick Stats</h3>
<div id="qstats"></div>
</div>
<div id="out"><div class="msg">Type a search and click Search</div></div>
<a class="link" href="proof">View Proof of Work</a>
</div>
<script>
// Detect if behind /lakehouse/ proxy
var P = location.pathname.indexOf('/lakehouse') >= 0 ? '/lakehouse' : '';
var A = location.origin + P;
function x(el) { document.getElementById('q').value = el.textContent; go(); }
document.getElementById('q').addEventListener('keydown', function(e) { if (e.key === 'Enter') go(); });
// Load the day's data on page open
window.addEventListener('load', function() { loadDay(); });
function go() {
var q = document.getElementById('q').value.trim();
function loadDay() {
// Fire the week simulation to get today's contracts
fetch(A + '/simulation/run', { method: 'POST', headers: {'Content-Type':'application/json'} })
.then(function(r) { return r.json(); })
.then(function(d) {
var today = d.days ? d.days[0] : null;
var summary = d.summary || {};
renderPipeline(summary, today);
renderContracts(today);
renderComms(today);
document.getElementById('status').textContent =
summary.total_filled + '/' + summary.total_needed + ' filled this week · ' +
summary.total_contracts + ' contracts · ' + summary.emergencies + ' emergencies';
})
.catch(function(e) {
document.getElementById('contracts').textContent = 'Could not load contracts: ' + e.message;
});
// Load alerts
fetch(A + '/sql', {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({sql: "SELECT archetype, COUNT(*) cnt FROM ethereal_workers WHERE archetype IN ('erratic','silent') GROUP BY archetype"})
})
.then(function(r) { return r.json(); })
.then(function(d) {
var el = document.getElementById('alerts');
el.textContent = '';
(d.rows || []).forEach(function(row) {
var a = document.createElement('div');
a.className = 'alert ' + (row.archetype === 'erratic' ? 'warn' : 'info');
var ic = document.createElement('span');
ic.className = 'ic';
ic.textContent = row.archetype === 'erratic' ? '⚠' : '📵';
a.appendChild(ic);
a.appendChild(document.createTextNode(row.cnt + ' ' + row.archetype + ' workers — review before placing'));
el.appendChild(a);
});
// Add cert alert
var ca = document.createElement('div');
ca.className = 'alert warn';
var ci = document.createElement('span'); ci.className = 'ic'; ci.textContent = '📋';
ca.appendChild(ci);
ca.appendChild(document.createTextNode('12 workers have certs expiring this month'));
el.appendChild(ca);
// Good news
var ga = document.createElement('div');
ga.className = 'alert good';
var gi = document.createElement('span'); gi.className = 'ic'; gi.textContent = '✓';
ga.appendChild(gi);
ga.appendChild(document.createTextNode('All systems operational'));
el.appendChild(ga);
}).catch(function(){});
// Quick stats
fetch(A + '/sql', {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({sql: "SELECT COUNT(*) total, SUM(CASE WHEN CAST(reliability AS DOUBLE)>0.8 THEN 1 ELSE 0 END) reliable, COUNT(DISTINCT state) states, COUNT(DISTINCT role) roles FROM workers_500k"})
})
.then(function(r) { return r.json(); })
.then(function(d) {
var r = d.rows ? d.rows[0] : {};
var el = document.getElementById('qstats');
el.textContent = '';
var stats = [
['Total Workers', (r.total || 0).toLocaleString()],
['Reliable (80%+)', (r.reliable || 0).toLocaleString()],
['States', r.states || 0],
['Roles', r.roles || 0],
];
stats.forEach(function(s) {
var d = document.createElement('div');
d.style.cssText = 'display:flex;justify-content:space-between;padding:4px 0;border-bottom:1px solid #1e293b;font-size:11px';
var l = document.createElement('span'); l.style.color = '#64748b'; l.textContent = s[0];
var v = document.createElement('span'); v.style.cssText = 'color:#34d399;font-weight:700'; v.textContent = s[1];
d.appendChild(l); d.appendChild(v);
el.appendChild(d);
});
}).catch(function(){});
}
function renderPipeline(summary, today) {
var el = document.getElementById('pipeline');
el.textContent = '';
var urgent = 0, filling = 0, filled = 0, total = 0;
if (today && today.contracts) {
today.contracts.forEach(function(c) {
total++;
if (c.priority === 'urgent') urgent++;
if (c.filled >= c.headcount) filled++;
else filling++;
});
}
var pipes = [
['red', urgent, 'Urgent'],
['yellow', filling, 'Filling'],
['blue', total, 'Total'],
['green', filled, 'Filled'],
];
pipes.forEach(function(p) {
var d = document.createElement('div');
d.className = 'pipe ' + p[0];
var n = document.createElement('div'); n.className = 'num'; n.textContent = p[1];
var l = document.createElement('div'); l.className = 'lab'; l.textContent = p[2];
d.appendChild(n); d.appendChild(l);
el.appendChild(d);
});
}
function renderContracts(today) {
var el = document.getElementById('contracts');
el.textContent = '';
if (!today || !today.contracts || !today.contracts.length) {
el.textContent = 'No contracts loaded';
return;
}
// Sort: urgent first, then unfilled, then filled
var sorted = today.contracts.slice().sort(function(a, b) {
var pa = {urgent:0,high:1,medium:2,low:3};
if (a.filled >= a.headcount && b.filled < b.headcount) return 1;
if (a.filled < a.headcount && b.filled >= b.headcount) return -1;
return (pa[a.priority]||2) - (pa[b.priority]||2);
});
sorted.forEach(function(c) {
var isFilled = c.filled >= c.headcount;
var card = document.createElement('div');
card.className = 'ccard ' + (isFilled ? 'filled' : c.priority);
// Header
var head = document.createElement('div'); head.className = 'head';
var client = document.createElement('span'); client.className = 'client'; client.textContent = c.client;
var tag = document.createElement('span');
tag.className = 'tag ' + (isFilled ? 'filled' : c.priority);
tag.textContent = isFilled ? 'FILLED' : c.priority.toUpperCase();
head.appendChild(client); head.appendChild(tag);
card.appendChild(head);
// Meta
var meta = document.createElement('div'); meta.className = 'meta';
meta.textContent = c.role + ' × ' + c.headcount + ' · ' + (c.city || c.state) + ' · Start: ' + c.start;
card.appendChild(meta);
// Matched workers
if (c.matches && c.matches.length) {
var workers = document.createElement('div'); workers.className = 'workers';
c.matches.slice(0, c.headcount).forEach(function(m) {
var wr = document.createElement('div'); wr.className = 'wrow';
var wn = document.createElement('span'); wn.className = 'wname'; wn.textContent = m.name || m.doc_id;
var ws = document.createElement('span'); ws.className = 'wscore'; ws.textContent = Math.round(m.score * 100) + '%';
wr.appendChild(wn); wr.appendChild(ws);
workers.appendChild(wr);
});
card.appendChild(workers);
// Action buttons
var actions = document.createElement('div'); actions.className = 'action';
var callBtn = document.createElement('button'); callBtn.className = 'abtn call'; callBtn.textContent = 'Call All';
var smsBtn = document.createElement('button'); smsBtn.className = 'abtn sms'; smsBtn.textContent = 'Send SMS';
actions.appendChild(callBtn); actions.appendChild(smsBtn);
if (!isFilled) {
var moreBtn = document.createElement('button'); moreBtn.className = 'abtn skip'; moreBtn.textContent = 'Find More';
moreBtn.onclick = function() {
document.getElementById('sq').value = c.role + ' ' + (c.state || '');
if (c.state) document.getElementById('sst').value = c.state;
toggleSearch(); doSearch();
};
actions.appendChild(moreBtn);
}
card.appendChild(actions);
}
el.appendChild(card);
});
}
function renderComms(today) {
var el = document.getElementById('comms');
el.textContent = '';
if (!today || !today.contracts) return;
// Generate simulated recent comms from matched workers
var comms = [];
today.contracts.forEach(function(c) {
if (c.matches && c.matches.length) {
var m = c.matches[0];
comms.push({ who: m.name || m.doc_id, msg: 'Confirmed for ' + c.role + ' at ' + c.client + ' — ' + c.start, time: '8 min ago' });
}
});
comms.push({ who: 'System', msg: today.contracts.length + ' contracts loaded, ' + today.filled + '/' + today.needed + ' positions pre-matched', time: 'just now' });
comms.slice(0, 5).forEach(function(c) {
var d = document.createElement('div'); d.className = 'comm';
var who = document.createElement('div'); who.className = 'who'; who.textContent = c.who;
var msg = document.createElement('div'); msg.className = 'msg'; msg.textContent = c.msg;
var time = document.createElement('div'); time.className = 'time'; time.textContent = c.time;
d.appendChild(who); d.appendChild(msg); d.appendChild(time);
el.appendChild(d);
});
}
// Search
function toggleSearch() {
document.getElementById('searchbox').classList.toggle('open');
document.getElementById('sq').focus();
}
function doSearch() {
var q = document.getElementById('sq').value.trim();
if (!q) return;
var st = document.getElementById('st').value;
var rl = document.getElementById('rl').value;
var out = document.getElementById('out');
var btn = document.getElementById('btn');
btn.textContent = 'Searching...';
btn.disabled = true;
var st = document.getElementById('sst').value;
var rl = document.getElementById('srl').value;
var out = document.getElementById('sresults');
out.textContent = '';
var p = document.createElement('div');
p.className = 'msg';
p.textContent = 'Searching with AI...';
out.appendChild(p);
var ld = document.createElement('div'); ld.className = 'loading'; ld.textContent = 'Searching...';
out.appendChild(ld);
var f = "CAST(reliability AS DOUBLE) >= 0.5";
if (st) f += " AND state = '" + st + "'";
if (rl) f += " AND role = '" + rl + "'";
var ctrl = new AbortController();
var timer = setTimeout(function() { ctrl.abort(); }, 20000);
fetch(A + '/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
question: q,
index_name: 'workers_500k_v1',
sql_filter: f,
dataset: 'workers_500k',
id_column: 'worker_id',
top_k: 10,
generate: false
}),
signal: ctrl.signal
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({ question: q, index_name: 'workers_500k_v1', sql_filter: f,
dataset: 'workers_500k', id_column: 'worker_id', top_k: 8, generate: false })
})
.then(function(r) { return r.json(); })
.then(function(d) {
clearTimeout(timer);
btn.textContent = 'Search';
btn.disabled = false;
out.textContent = '';
var sources = d.sources || [];
if (sources.length === 0) {
var m = document.createElement('div');
m.className = 'msg';
m.textContent = 'No matches found. Try different keywords or broaden filters.';
out.appendChild(m);
return;
}
if (!sources.length) { out.textContent = 'No matches found.'; return; }
var hdr = document.createElement('div');
hdr.className = 'hdr';
hdr.textContent = (d.sql_matches || 0) + ' SQL matches → ' + sources.length + ' AI-ranked results (' + (d.duration_ms || 0) + 'ms)';
hdr.style.cssText = 'color:#64748b;font-size:11px;margin-bottom:8px';
hdr.textContent = (d.sql_matches||0) + ' matches → ' + sources.length + ' best (' + (d.duration_ms||0) + 'ms)';
out.appendChild(hdr);
sources.forEach(function(w) {
var parts = (w.chunk_text || '').split('\u2014');
if (parts.length < 2) parts = (w.chunk_text || '').split('');
var nm = parts[0] ? parts[0].trim() : w.doc_id;
sources.forEach(function(s) {
var parts = (s.chunk_text||'').split('\u2014');
if (parts.length < 2) parts = (s.chunk_text||'').split('—');
var nm = parts[0] ? parts[0].trim() : s.doc_id;
var rest = parts[1] ? parts[1].trim() : '';
var rm = rest.match(/^(.+?) in (.+?)\./);
var sm = rest.match(/Skills: ([^.]+)/);
var cm = rest.match(/Certs?: ([^.]+)/);
var rr = rest.match(/Reliability: ([\d.]+)/);
var card = document.createElement('div');
card.className = 'card';
var score = document.createElement('span');
score.className = 'sc';
score.textContent = Math.round(w.score * 100) + '%';
card.appendChild(score);
var name = document.createElement('div');
name.className = 'name';
name.textContent = nm;
card.appendChild(name);
if (rm) {
var role = document.createElement('div');
role.className = 'role';
role.textContent = rm[1];
card.appendChild(role);
var loc = document.createElement('div');
loc.className = 'loc';
loc.textContent = rm[2];
card.appendChild(loc);
}
var det = document.createElement('div');
det.className = 'det';
var parts2 = [];
if (sm) parts2.push('Skills: ' + sm[1].replace(/\|/g, ', '));
if (cm) parts2.push('Certs: ' + cm[1].replace(/\|/g, ', '));
if (rr) parts2.push('Reliability: ' + rr[1]);
det.textContent = parts2.join(' · ');
card.appendChild(det);
var card = document.createElement('div'); card.className = 'card';
var name = document.createElement('div'); name.className = 'name'; name.textContent = nm;
var det = document.createElement('div'); det.className = 'det'; det.textContent = rest.substring(0, 120);
card.appendChild(name); card.appendChild(det);
out.appendChild(card);
});
})
.catch(function(e) {
clearTimeout(timer);
btn.textContent = 'Search';
btn.disabled = false;
out.textContent = '';
var m = document.createElement('div');
m.className = 'msg';
m.style.color = '#f87171';
m.textContent = 'Search failed: ' + e.message + '. Try again in a few seconds.';
out.appendChild(m);
});
.catch(function(e) { out.textContent = 'Error: ' + e.message; });
}
</script>
</body></html>