Closes one of the Path 1 trust-break gaps. The scenario we kept flagging:
recruiter calls the system's top pick, worker quotes $35/hr, contract
pays $28/hr. First broken call kills the demo. This fixes it.
Heuristic (no schema change, derived at query time):
- Per worker: implied_pay_rate = role_base + (reliability × 4) + archetype_bump
role_base: Electrician $28, Welder $26, Machine Op $24, Maint $26,
Forklift Op $20, Loader $17, Warehouse Assoc $17, Quality Tech $23,
Production Worker $18 ...
archetype bump: specialist +4, leader +3, reliable +1, else 0
- Per contract: implied_bill_rate = role_base × 1.4
(40% markup — industry norm: pay + overhead + insurance + margin)
- Worker is 'over_bill_rate' when implied_pay_rate > contract's bill_rate
on a candidate-by-candidate basis
Backend (mcp-server/index.ts):
- ROLE_BASE_PAY_RATE + BILL_MARKUP constants
- impliedPayRate(worker), impliedBillRate(role) functions
- parseWorkerChunk() extracts role/reliability/archetype from vector text
- enrichWithRates() attaches implied_pay_rate on every /vectors/hybrid
source response. Called from /search and /intelligence/permit_contracts.
- /search accepts optional max_pay_rate number — if set, filters out
workers above that rate and reports pay_rate_filtered_out count.
- /intelligence/permit_contracts returns implied_bill_rate per contract
AND over_bill_rate boolean per candidate.
Frontend (search.html):
- Live Contracts cards show 'bill rate: $X/hr' under the headcount line
- Each candidate shows 'pay $X/hr' in the sub-line; red 'Over bill rate'
chip next to name when their pay exceeds the contract's bill rate
(hover reveals the exact numbers and why it's flagged)
- Main 'Search all workers' results now include 'pay $X/hr' in the
why-text (computeImpliedPayRate mirrored client-side to match Bun)
End-to-end verified live:
- Masonry Work permit, bill_rate $25.20/hr
Kathleen M. Gutierrez pay $25.56/hr → 🔴 OVER
Melissa C. Rivera pay $20.88/hr → 🟢 OK
- /search with max_pay_rate:32 filtered out 1 Toledo Welder above $32
- Main search shows 'pay $28.64/hr' in each result row
When real ATS data replaces synthetic workers_500k, same UI — the
client's real pay_rate column substitutes for the heuristic.
1198 lines
73 KiB
HTML
1198 lines
73 KiB
HTML
<!DOCTYPE html>
|
||
<html><head>
|
||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>Staffing Co-Pilot</title>
|
||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
|
||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||
<style>
|
||
*{margin:0;padding:0;box-sizing:border-box}
|
||
body{font-family:'Inter',-apple-system,system-ui,'Segoe UI',sans-serif;background:#090c10;color:#b0b8c4;font-size:14px;line-height:1.6;-webkit-font-smoothing:antialiased}
|
||
|
||
/* Top bar */
|
||
.bar{background:#0d1117;padding:0 24px;height:56px;border-bottom:1px solid #171d27;display:flex;justify-content:space-between;align-items:center}
|
||
.bar h1{font-size:14px;font-weight:600;color:#e6edf3;letter-spacing:-0.2px}
|
||
.bar .rt{font-size:11px;color:#545d68}
|
||
.bar nav{display:flex;gap:2px}
|
||
.bar nav a{font-size:12px;color:#545d68;text-decoration:none;padding:6px 14px;border-radius:6px;transition:all 0.15s}
|
||
.bar nav a:hover{color:#e6edf3;background:#161b22}
|
||
.bar nav a.active{color:#e6edf3;background:#1c2333}
|
||
|
||
/* Layout */
|
||
.content{max-width:940px;margin:0 auto;padding:24px 20px 40px}
|
||
.section{margin-bottom:32px}
|
||
.section-header{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:14px}
|
||
.section-title{font-size:11px;font-weight:600;color:#545d68;text-transform:uppercase;letter-spacing:1.2px}
|
||
.section-meta{font-size:10px;color:#3d444d}
|
||
|
||
/* Cards */
|
||
.card{background:#0d1117;border:1px solid #171d27;border-radius:10px;padding:20px;margin-bottom:12px}
|
||
.card.accent-red{border-left:3px solid #da3633}
|
||
.card.accent-green{border-left:3px solid #2ea043}
|
||
.card.accent-amber{border-left:3px solid #bf8700}
|
||
.card.accent-blue{border-left:3px solid #388bfd}
|
||
.card .card-label{font-size:9px;font-weight:600;color:#545d68;text-transform:uppercase;letter-spacing:1.4px;margin-bottom:6px}
|
||
.card .card-title{font-size:17px;font-weight:600;color:#e6edf3;margin-bottom:3px;letter-spacing:-0.3px}
|
||
.card .card-sub{font-size:12px;color:#545d68;margin-bottom:14px;line-height:1.5}
|
||
|
||
/* Keep old class names working */
|
||
.insight{background:#0d1117;border:1px solid #171d27;border-radius:10px;padding:20px;margin-bottom:12px}
|
||
.insight .label{font-size:9px;font-weight:600;color:#545d68;text-transform:uppercase;letter-spacing:1.4px;margin-bottom:6px}
|
||
.insight .headline{font-size:17px;font-weight:600;color:#e6edf3;margin-bottom:3px;letter-spacing:-0.3px}
|
||
.insight .sub{font-size:12px;color:#545d68;margin-bottom:14px}
|
||
.insight.urgent{border-left:3px solid #da3633}
|
||
.insight.opportunity{border-left:3px solid #2ea043}
|
||
.insight.warning{border-left:3px solid #bf8700}
|
||
.insight.info{border-left:3px solid #388bfd}
|
||
|
||
/* Workers */
|
||
.iworker{display:flex;align-items:center;gap:12px;padding:10px 12px;background:#161b22;border-radius:8px;margin-bottom:4px;transition:background 0.15s}
|
||
.iworker:hover{background:#1c2333}
|
||
.iworker .av{width:36px;height:36px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-weight:600;font-size:12px;color:#e6edf3;flex-shrink:0}
|
||
.iworker .info{flex:1;min-width:0}
|
||
.iworker .nm{font-weight:600;color:#e6edf3;font-size:13px}
|
||
.iworker .detail{color:#545d68;font-size:11px}
|
||
.iworker .why{color:#388bfd;font-size:11px;margin-top:1px}
|
||
.iworker .acts{display:flex;gap:4px}
|
||
.ibtn{padding:5px 12px;border-radius:6px;font-size:10px;cursor:pointer;border:none;font-weight:600;transition:opacity 0.15s}
|
||
.ibtn:hover{opacity:0.8}
|
||
.ibtn.call{background:#1a2e4a;color:#58a6ff}
|
||
.ibtn.sms{background:#122b1e;color:#3fb950}
|
||
|
||
/* Stats */
|
||
.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:24px}
|
||
.stat{background:#0d1117;border:1px solid #171d27;border-radius:8px;padding:16px 12px;text-align:center}
|
||
.stat .n{font-size:26px;font-weight:700;color:#e6edf3;letter-spacing:-1px}
|
||
.stat .l{font-size:9px;color:#545d68;text-transform:uppercase;letter-spacing:0.8px;margin-top:4px}
|
||
|
||
/* Search */
|
||
.sa{background:#0d1117;border:1px solid #171d27;border-radius:10px;overflow:hidden}
|
||
.sa summary{cursor:pointer;color:#545d68;font-size:12px;list-style:none;padding:14px 20px;transition:color 0.15s}
|
||
.sa summary:hover{color:#b0b8c4}
|
||
.sa summary::-webkit-details-marker{display:none}
|
||
.sa .inner{padding:0 20px 20px}
|
||
.sa input[type=text]{width:100%;padding:12px 16px;background:#161b22;border:1px solid #21262d;border-radius:8px;color:#e6edf3;font-size:13px;outline:none;margin-bottom:8px;transition:border 0.15s}
|
||
.sa input:focus{border-color:#388bfd}
|
||
.srow{display:flex;gap:8px;margin-bottom:10px}
|
||
.sa select{flex:1;padding:8px 12px;background:#161b22;border:1px solid #21262d;border-radius:6px;color:#b0b8c4;font-size:12px}
|
||
.sbtn{width:100%;padding:10px;background:#1f6feb;border:none;border-radius:8px;color:#fff;font-size:13px;font-weight:600;cursor:pointer;transition:background 0.15s}
|
||
.sbtn:hover{background:#388bfd}
|
||
#sresults{margin-top:14px}
|
||
|
||
/* Footer */
|
||
.ft{text-align:center;padding:24px;color:#3d444d;font-size:11px;border-top:1px solid #171d27;margin-top:32px}
|
||
.ft a{color:#545d68;text-decoration:none;transition:color 0.15s}
|
||
.ft a:hover{color:#e6edf3}
|
||
|
||
.ld{color:#3d444d;text-align:center;padding:40px;font-size:13px}
|
||
|
||
/* Responsive */
|
||
@media(max-width:768px){
|
||
.stats{grid-template-columns:repeat(2,1fr)}
|
||
.iworker{flex-direction:column;text-align:center}
|
||
.iworker .acts{justify-content:center}
|
||
.bar{padding:0 16px;height:48px}
|
||
.bar nav{display:none}
|
||
.content{padding:16px 12px 32px}
|
||
}
|
||
@media(max-width:480px){
|
||
.stats{grid-template-columns:1fr 1fr}
|
||
.stat .n{font-size:22px}
|
||
}
|
||
</style></head><body>
|
||
<div class="bar">
|
||
<h1>Staffing Co-Pilot</h1>
|
||
<nav>
|
||
<a href="." class="active">Dashboard</a>
|
||
<a href="console">Walkthrough</a>
|
||
<a href="proof">Architecture</a>
|
||
<a href="spec">Spec</a>
|
||
<a href="onboard">Onboard</a>
|
||
<a href="alerts">Alerts</a>
|
||
<a href="workspaces">Workspaces</a>
|
||
</nav>
|
||
<div class="rt" id="status">Loading...</div>
|
||
</div>
|
||
<div class="content">
|
||
|
||
<div id="main"><div class="ld">Analyzing contracts and workers...</div></div>
|
||
|
||
<div class="section" id="staffing-forecast-section">
|
||
<div class="section-header">
|
||
<span class="section-title">Staffing Forecast — Next 30 Days</span>
|
||
<span class="section-meta">Permits → predicted demand · Bench supply · Days to staffing deadline</span>
|
||
</div>
|
||
<div id="staffing-forecast"><div class="ld">Loading forecast...</div></div>
|
||
</div>
|
||
|
||
<div class="section" id="live-contracts-section">
|
||
<div class="section-header">
|
||
<span class="section-title">Live Contracts — Chicago Permits → Proposed Fills</span>
|
||
<span class="section-meta">Real public permit data + our 500K worker bench + past playbook patterns</span>
|
||
</div>
|
||
<div id="live-contracts"><div class="ld">Loading live contracts...</div></div>
|
||
</div>
|
||
|
||
<div class="section" id="market-section">
|
||
<div class="section-header">
|
||
<span class="section-title">Market Intelligence</span>
|
||
<span class="section-meta">Public permit data · Updated live</span>
|
||
</div>
|
||
<div id="market"></div>
|
||
</div>
|
||
|
||
<div class="section" id="learning-section">
|
||
<div class="section-header">
|
||
<span class="section-title">System Activity</span>
|
||
<span class="section-meta">Learning from every interaction</span>
|
||
</div>
|
||
<div id="learning"></div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<div class="section-header">
|
||
<span class="section-title">Worker Search</span>
|
||
<span class="section-meta">Natural language · 500K profiles</span>
|
||
</div>
|
||
<details class="sa" open><summary>Search all workers</summary><div class="inner">
|
||
<input type="text" id="sq" placeholder="Try: reliable forklift operator available in Nashville" onkeydown="if(event.key==='Enter')doSearch()">
|
||
<div class="srow"><select id="sst"><option value="">Any State</option></select>
|
||
<select id="srl"><option value="">Any Role</option></select></div>
|
||
<button class="sbtn" onclick="doSearch()">Find Workers</button><div id="sresults"></div></div></details>
|
||
</div>
|
||
|
||
<div class="ft">Staffing Co-Pilot · Hybrid SQL + Vector Search · 500K embedded profiles · <a href="console">Console</a> · <a href="proof">Architecture</a></div>
|
||
</div>
|
||
<script>
|
||
var P=location.pathname.indexOf('/lakehouse')>=0?'/lakehouse':'';
|
||
var A=location.origin+P;
|
||
var AC=['#1a2744','#1a3a2a','#2a1a3a','#3a2a1a','#1a3a3a','#2a2a1a'];
|
||
var lastQuery='';
|
||
window.addEventListener('load',function(){loadDay();loadStaffingForecast();loadLiveContracts();loadMarket();loadLearning()});
|
||
|
||
function loadStaffingForecast(){
|
||
api('/intelligence/staffing_forecast',{}).then(function(r){
|
||
var el=document.getElementById('staffing-forecast');el.textContent='';
|
||
if(!r||!r.forecast){el.textContent='Forecast unavailable.';return}
|
||
// Header summary
|
||
var hdr=document.createElement('div');hdr.style.cssText='background:#0d1117;border:1px solid #171d27;border-radius:10px;padding:14px;margin-bottom:10px;display:flex;justify-content:space-between;gap:16px';
|
||
var left=document.createElement('div');
|
||
var big=document.createElement('div');big.style.cssText='font-size:22px;font-weight:700;color:#e6edf3;letter-spacing:-0.5px';
|
||
big.textContent='$'+(r.total_cost||0).toLocaleString('en-US',{maximumFractionDigits:0})+' in construction coming';
|
||
var sub=document.createElement('div');sub.style.cssText='color:#8b949e;font-size:12px;margin-top:4px';
|
||
sub.textContent=r.permit_count+' permits filed last 30 days · ~'+r.total_estimated_workers+' workers needed across roles';
|
||
left.appendChild(big);left.appendChild(sub);hdr.appendChild(left);
|
||
var right=document.createElement('div');right.style.cssText='text-align:right;font-size:11px';
|
||
if(r.critical_roles>0){
|
||
var c=document.createElement('div');c.style.cssText='color:#f85149;font-weight:700';
|
||
c.textContent=r.critical_roles+' CRITICAL role'+(r.critical_roles!==1?'s':'')+' — supply gap';right.appendChild(c);
|
||
} else if(r.tight_roles>0){
|
||
var t=document.createElement('div');t.style.cssText='color:#d29922;font-weight:700';
|
||
t.textContent=r.tight_roles+' tight role'+(r.tight_roles!==1?'s':'');right.appendChild(t);
|
||
} else {
|
||
var g=document.createElement('div');g.style.cssText='color:#3fb950;font-weight:600';g.textContent='bench covers predicted demand';right.appendChild(g);
|
||
}
|
||
var when=document.createElement('div');when.style.cssText='color:#545d68;margin-top:2px';
|
||
when.textContent='updated '+((r.duration_ms||0)/1000).toFixed(1)+'s ago';right.appendChild(when);
|
||
hdr.appendChild(right);el.appendChild(hdr);
|
||
// Per-role rows
|
||
r.forecast.forEach(function(f){
|
||
var row=document.createElement('div');
|
||
var riskColor={critical:'#f85149',tight:'#d29922',watch:'#d29922',ok:'#3fb950'}[f.risk]||'#8b949e';
|
||
row.style.cssText='background:#0d1117;border:1px solid #171d27;border-radius:8px;padding:10px 14px;margin-bottom:6px;border-left:3px solid '+riskColor+';display:flex;justify-content:space-between;gap:12px;font-size:12px;align-items:center';
|
||
var l=document.createElement('div');
|
||
var n=document.createElement('div');n.style.cssText='color:#e6edf3;font-weight:600;font-size:13px';
|
||
n.textContent=f.role;
|
||
var d=document.createElement('div');d.style.cssText='color:#8b949e;font-size:11px;margin-top:2px';
|
||
d.textContent=f.demand_permits+' permit'+(f.demand_permits!==1?'s':'')+' · est '+f.demand_workers+' workers · earliest staffing deadline '+f.earliest_staffing_deadline;
|
||
l.appendChild(n);l.appendChild(d);row.appendChild(l);
|
||
var r2=document.createElement('div');r2.style.cssText='text-align:right;white-space:nowrap';
|
||
var cov=document.createElement('div');cov.style.cssText='color:'+riskColor+';font-weight:700;font-size:13px';
|
||
cov.textContent=f.bench_available.toLocaleString()+' / '+f.demand_workers+' available ('+f.coverage_pct+'%)';
|
||
var days=document.createElement('div');days.style.cssText='color:'+(f.days_to_deadline<=0?'#f85149':f.days_to_deadline<=7?'#d29922':'#8b949e')+';font-size:11px;margin-top:2px';
|
||
days.textContent=f.days_to_deadline<=0?(Math.abs(f.days_to_deadline)+'d overdue'):(f.days_to_deadline+'d to deadline');
|
||
r2.appendChild(cov);r2.appendChild(days);row.appendChild(r2);
|
||
el.appendChild(row);
|
||
});
|
||
}).catch(function(e){
|
||
document.getElementById('staffing-forecast').textContent='Forecast error: '+(e.message||e);
|
||
});
|
||
}
|
||
|
||
function api(path,body){
|
||
return fetch(A+path,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}).then(function(r){return r.json()})
|
||
}
|
||
|
||
// Mirror of the Bun-side pay-rate formula so client-side renderers
|
||
// (main search, modal) can show a rate when they only have worker
|
||
// fields, not a pre-enriched hybrid source. Keep in sync with
|
||
// impliedPayRate() in mcp-server/index.ts.
|
||
var ROLE_BASE_PAY={Electrician:28,Welder:26,'Machine Operator':24,'Maintenance Tech':26,
|
||
'Forklift Operator':20,Loader:17,'Warehouse Associate':17,'Material Handler':18,
|
||
'Production Worker':18,'Quality Tech':23,'Line Lead':22,Assembler:18,'Shipping Clerk':19};
|
||
function computeImpliedPayRate(role,rel,archetype){
|
||
var base=ROLE_BASE_PAY[role||'']||19;
|
||
var r=typeof rel==='string'?parseFloat(rel):(rel||0.5);
|
||
var relBump=(isFinite(r)?r:0.5)*4;
|
||
var a=(archetype||'').toLowerCase();
|
||
var archBump=a==='specialist'?4:a==='leader'?3:a==='reliable'?1:0;
|
||
return Math.round((base+relBump+archBump)*100)/100;
|
||
}
|
||
|
||
function loadLiveContracts(){
|
||
// Pair live Chicago permits with our 500K worker bench and the
|
||
// meta-index discovered patterns for each role+geo. This is the
|
||
// "real external data meets synthetic playbook learning" card set.
|
||
api('/intelligence/permit_contracts',{}).then(function(r){
|
||
var el=document.getElementById('live-contracts');el.textContent='';
|
||
if(!r||!r.contracts||r.contracts.length===0){
|
||
el.textContent='No permits returned.';return;
|
||
}
|
||
r.contracts.forEach(function(c){
|
||
var p=c.permit||{}, prop=c.proposed||{}, tl=c.timeline||{};
|
||
var urg=tl.urgency||'scheduled';
|
||
var borderColor={overdue:'#f85149',urgent:'#d29922',soon:'#388bfd',scheduled:'#2ea043'}[urg]||'#388bfd';
|
||
var card=document.createElement('div');card.className='insight info';
|
||
card.style.cssText='background:#0d1117;border:1px solid #171d27;border-radius:10px;padding:16px;margin-bottom:10px;border-left:3px solid '+borderColor;
|
||
// Header — permit
|
||
var hdr=document.createElement('div');hdr.style.cssText='display:flex;justify-content:space-between;margin-bottom:8px;gap:12px';
|
||
var left=document.createElement('div');
|
||
var title=document.createElement('div');title.style.cssText='font-weight:600;color:#e6edf3;font-size:14px';
|
||
title.textContent='$'+(p.cost||0).toLocaleString()+' · '+(p.work_type||'');
|
||
var addr=document.createElement('div');addr.style.cssText='color:#8b949e;font-size:12px;margin-top:2px';
|
||
addr.textContent=(p.address||'')+' · Chicago, IL · filed '+(p.issue_date||'');
|
||
// Timeline chip
|
||
if(tl.days_to_deadline!==undefined){
|
||
var tmline=document.createElement('div');tmline.style.cssText='color:'+borderColor+';font-size:11px;font-weight:600;margin-top:4px';
|
||
var urgLabel={overdue:'OVERDUE',urgent:'URGENT',soon:'SOON',scheduled:'SCHEDULED'}[urg]||'SCHEDULED';
|
||
var dd=tl.days_to_deadline;
|
||
var txt=urgLabel+' · staffing window opens '+(tl.staffing_window_opens||'')+' ('+(dd<=0?Math.abs(dd)+'d overdue':dd+'d to deadline')+') · construction est '+(tl.estimated_construction_start||'');
|
||
tmline.textContent=txt;addr.appendChild(document.createElement('br'));left.appendChild(tmline);
|
||
}
|
||
left.appendChild(title);left.appendChild(addr);
|
||
var right=document.createElement('div');right.style.cssText='color:#58a6ff;font-size:12px;font-weight:600;text-align:right;white-space:nowrap';
|
||
right.textContent=prop.count+'× '+prop.role;
|
||
var sub=document.createElement('div');sub.style.cssText='color:#545d68;font-size:10px;text-align:right';
|
||
sub.textContent='pool: '+(prop.pool_size||'?').toLocaleString()+' available';
|
||
right.appendChild(sub);
|
||
// Rate awareness: show implied bill rate per contract
|
||
if(c.implied_bill_rate){
|
||
var rate=document.createElement('div');
|
||
rate.style.cssText='color:#d29922;font-size:10px;text-align:right;margin-top:3px';
|
||
rate.textContent='bill rate: $'+c.implied_bill_rate.toFixed(2)+'/hr';
|
||
right.appendChild(rate);
|
||
}
|
||
hdr.appendChild(left);hdr.appendChild(right);card.appendChild(hdr);
|
||
// Description
|
||
if(p.description){
|
||
var desc=document.createElement('div');desc.style.cssText='color:#94a3b8;font-size:11px;margin-bottom:10px;line-height:1.5';
|
||
desc.textContent=p.description;card.appendChild(desc);
|
||
}
|
||
// Pattern (meta-index) chip
|
||
if(c.discovered_pattern){
|
||
var pat=document.createElement('div');pat.style.cssText='background:#0d2818;border:1px solid #2ea04360;border-radius:6px;padding:8px 12px;margin-bottom:10px;font-size:11px;color:#86efac;line-height:1.5';
|
||
var plabel=document.createElement('span');plabel.style.cssText='color:#3fb950;font-weight:600;margin-right:6px';
|
||
plabel.textContent='MEMORY ('+c.pattern_matched+' playbooks):';
|
||
pat.appendChild(plabel);
|
||
pat.appendChild(document.createTextNode(' '+c.discovered_pattern));
|
||
card.appendChild(pat);
|
||
}
|
||
// Candidates
|
||
var cands=prop.candidates||[];
|
||
cands.slice(0,3).forEach(function(cand,i){
|
||
var row=document.createElement('div');row.style.cssText='display:flex;align-items:center;gap:10px;padding:6px 10px;background:#161b22;border-radius:6px;margin-bottom:4px;font-size:12px';
|
||
var av=document.createElement('div');av.style.cssText='width:28px;height:28px;border-radius:6px;display:flex;align-items:center;justify-content:center;font-weight:600;font-size:10px;color:#e6edf3;background:'+AC[i%AC.length];
|
||
av.textContent=(cand.name||'?').split(' ').map(function(n){return (n[0]||'').toUpperCase()}).join('').substring(0,2);
|
||
var info=document.createElement('div');info.style.cssText='flex:1;min-width:0';
|
||
var nm=document.createElement('div');nm.style.cssText='color:#e6edf3;font-weight:500';nm.textContent=cand.name||cand.doc_id;
|
||
if((cand.playbook_boost||0)>0){
|
||
var chip=document.createElement('span');chip.style.cssText='margin-left:8px;padding:2px 7px;border-radius:9px;font-size:9px;font-weight:600;background:#0d2818;border:1px solid #2ea043;color:#3fb950;vertical-align:middle';
|
||
chip.textContent='Endorsed · '+(cand.playbook_citations||[]).length+' playbook'+((cand.playbook_citations||[]).length===1?'':'s');
|
||
nm.appendChild(chip);
|
||
}
|
||
// Rate warning chip when worker's pay exceeds the contract's bill rate
|
||
if(cand.over_bill_rate){
|
||
var warn=document.createElement('span');warn.style.cssText='margin-left:6px;padding:2px 7px;border-radius:9px;font-size:9px;font-weight:600;background:#3a1a1a;border:1px solid #f85149;color:#fca5a5;vertical-align:middle';
|
||
warn.textContent='Over bill rate';
|
||
warn.title='Worker\'s implied pay rate ($'+(cand.implied_pay_rate||0).toFixed(2)+'/hr) exceeds contract bill rate ($'+(c.implied_bill_rate||0).toFixed(2)+'/hr) — margin at risk';
|
||
nm.appendChild(warn);
|
||
}
|
||
var sub2=document.createElement('div');sub2.style.cssText='color:#545d68;font-size:10px';
|
||
var subText=cand.doc_id+' · score '+(cand.score||0).toFixed(3);
|
||
if(cand.implied_pay_rate) subText+=' · pay $'+cand.implied_pay_rate.toFixed(2)+'/hr';
|
||
sub2.textContent=subText;
|
||
info.appendChild(nm);info.appendChild(sub2);
|
||
row.appendChild(av);row.appendChild(info);
|
||
card.appendChild(row);
|
||
});
|
||
if(cands.length>3){
|
||
var more=document.createElement('div');more.style.cssText='font-size:10px;color:#58a6ff;padding:4px 10px;margin-top:2px';
|
||
more.textContent='+ '+(cands.length-3)+' more candidates available';
|
||
card.appendChild(more);
|
||
}
|
||
el.appendChild(card);
|
||
});
|
||
}).catch(function(e){
|
||
document.getElementById('live-contracts').textContent='Error loading: '+e.message;
|
||
});
|
||
}
|
||
|
||
function loadDay(){
|
||
// Step 1: run simulation + get real worker count + populate dropdowns from actual data
|
||
Promise.all([
|
||
api('/simulation/run',{}),
|
||
api('/sql',{sql:"SELECT COUNT(*) as cnt FROM workers_500k"}),
|
||
api('/sql',{sql:"SELECT DISTINCT role FROM workers_500k ORDER BY role"}),
|
||
api('/sql',{sql:"SELECT DISTINCT state FROM workers_500k ORDER BY state"})
|
||
]).then(function(r0){
|
||
var sim=r0[0];
|
||
var workerCount=r0[1]&&r0[1].rows&&r0[1].rows[0]?r0[1].rows[0].cnt:0;
|
||
var allRoles=r0[2]&&r0[2].rows?r0[2].rows.map(function(r){return r.role}):[];
|
||
var allStates=r0[3]&&r0[3].rows?r0[3].rows.map(function(r){return r.state}):[];
|
||
|
||
// Populate dropdowns from real data
|
||
var stSel=document.getElementById('sst');
|
||
var rlSel=document.getElementById('srl');
|
||
stSel.innerHTML='<option value="">Any State</option>';
|
||
allStates.forEach(function(s){var o=document.createElement('option');o.value=s;o.textContent=s;stSel.appendChild(o)});
|
||
rlSel.innerHTML='<option value="">Any Role</option>';
|
||
allRoles.forEach(function(r){var o=document.createElement('option');o.value=r;o.textContent=r;rlSel.appendChild(o)});
|
||
|
||
// Update search summary with real count
|
||
var searchSum=document.querySelector('.sa summary');
|
||
if(searchSum)searchSum.textContent='Search all '+workerCount.toLocaleString()+' workers';
|
||
|
||
var today=sim.days?sim.days[0]:null;
|
||
var sum=sim.summary||{};
|
||
sum.worker_count=workerCount;
|
||
document.getElementById('status').textContent=sum.total_filled+'/'+sum.total_needed+' positions filled across '+sum.total_contracts+' contracts';
|
||
|
||
// Step 2: extract what's ACTUALLY needed from today's contracts
|
||
var contracts=today?today.contracts:[];
|
||
var needRoles={}, needStates={}, urgentRoles=[];
|
||
contracts.forEach(function(c){
|
||
if(c.filled<c.headcount){
|
||
needRoles[c.role]=(needRoles[c.role]||0)+(c.headcount-c.filled);
|
||
needStates[c.state]=(needStates[c.state]||0)+(c.headcount-c.filled);
|
||
if(c.priority==='urgent'||c.priority==='high') urgentRoles.push(c.role);
|
||
}
|
||
});
|
||
|
||
// Build contextual queries based on today's gaps
|
||
var roleList=Object.keys(needRoles);
|
||
var stateList=Object.keys(needStates);
|
||
var roleFilter=roleList.length?roleList.map(function(r){return"'"+r.replace(/'/g,"''")+"'"}).join(','):"'Forklift Operator'";
|
||
var stateFilter=stateList.length?stateList.map(function(s){return"'"+s.replace(/'/g,"''")+"'"}).join(','):"'IL'";
|
||
|
||
// Contextual workers — add random offset so it's not always the same top 8
|
||
var offset=Math.floor(Math.random()*20);
|
||
var topSql="SELECT name, role, city, state, ROUND(CAST(reliability AS DOUBLE),2) rel, certifications "+
|
||
"FROM workers_500k WHERE role IN ("+roleFilter+") AND state IN ("+stateFilter+") "+
|
||
"AND CAST(reliability AS DOUBLE)>0.85 ORDER BY CAST(reliability AS DOUBLE) DESC LIMIT 8 OFFSET "+offset;
|
||
|
||
// Coverage for states that matter today
|
||
var covSql="SELECT state, COUNT(*) cnt, SUM(CASE WHEN CAST(reliability AS DOUBLE)>0.8 THEN 1 ELSE 0 END) good "+
|
||
"FROM workers_500k WHERE state IN ("+stateFilter+") GROUP BY state ORDER BY cnt DESC";
|
||
|
||
// Roles breakdown for today's needed roles
|
||
var roleSql="SELECT role, COUNT(*) total, SUM(CASE WHEN CAST(reliability AS DOUBLE)>0.8 THEN 1 ELSE 0 END) reliable "+
|
||
"FROM workers_500k WHERE role IN ("+roleFilter+") GROUP BY role ORDER BY total DESC";
|
||
|
||
return Promise.all([roleSql, topSql, covSql].map(function(sql){
|
||
return api('/sql',{sql:sql});
|
||
})).then(function(results){
|
||
var roles=results[0], topWorkers=results[1], coverage=results[2];
|
||
renderMain(today,sum,roles,topWorkers,coverage,needRoles,needStates);
|
||
});
|
||
}).catch(function(e){
|
||
document.getElementById('main').textContent='Error loading: '+e.message;
|
||
});
|
||
}
|
||
|
||
function renderMain(today,sum,roles,topWorkers,coverage,needRoles,needStates){
|
||
var el=document.getElementById('main');
|
||
el.textContent='';
|
||
|
||
// Stats
|
||
var stats=document.createElement('div');stats.className='stats';
|
||
addStat(stats,sum.total_contracts||0,'Contracts Today');
|
||
addStat(stats,sum.total_filled||0,'Positions Filled');
|
||
addStat(stats,sum.emergencies||0,'Urgent');
|
||
addStat(stats,sum.worker_count||0,'Workers in System');
|
||
el.appendChild(stats);
|
||
|
||
// INSIGHT 1: Urgent pipeline — step-by-step workflow
|
||
if(today&&today.contracts){
|
||
var urgent=today.contracts.filter(function(c){return c.priority==='urgent'});
|
||
var needsWork=today.contracts.filter(function(c){return c.priority!=='urgent'&&c.filled<c.headcount});
|
||
var filled=today.contracts.filter(function(c){return c.filled>=c.headcount});
|
||
|
||
if(urgent.length){
|
||
var ins=makeInsight('urgent','Urgent Pipeline',
|
||
urgent.length+' emergency contract'+(urgent.length>1?'s':'')+ ' — workers pre-matched, ready for your call',null);
|
||
urgent.forEach(function(c){addContractInsight(ins,c,true)});
|
||
el.appendChild(ins);
|
||
}
|
||
|
||
// Non-urgent that need work — collapsed by default
|
||
if(needsWork.length){
|
||
var ins1b=makeInsight('warning','In Progress',
|
||
needsWork.length+' contract'+(needsWork.length>1?'s':'')+' still filling — workers matched, awaiting confirmation',null);
|
||
var det1=document.createElement('details');
|
||
var sum1=document.createElement('summary');sum1.style.cssText='cursor:pointer;font-size:11px;color:#545d68;padding:4px 0;list-style:none';
|
||
sum1.textContent='Show '+needsWork.length+' contracts';det1.appendChild(sum1);
|
||
needsWork.forEach(function(c){addContractInsight(det1,c,false)});
|
||
ins1b.appendChild(det1);el.appendChild(ins1b);
|
||
}
|
||
|
||
// Filled — collapsed by default
|
||
if(filled.length){
|
||
var ins2=makeInsight('opportunity','Ready to Go',
|
||
filled.length+' contract'+(filled.length>1?'s':'')+' fully staffed — review and send shift details',null);
|
||
var det2=document.createElement('details');
|
||
var sum2=document.createElement('summary');sum2.style.cssText='cursor:pointer;font-size:11px;color:#545d68;padding:4px 0;list-style:none';
|
||
sum2.textContent='Show '+filled.length+' contracts';det2.appendChild(sum2);
|
||
filled.forEach(function(c){addContractInsight(det2,c,false)});
|
||
ins2.appendChild(det2);el.appendChild(ins2);
|
||
}
|
||
}
|
||
|
||
// INSIGHT 2: Top available workers — contextual to today's unfilled contracts
|
||
if(topWorkers&&topWorkers.rows&&topWorkers.rows.length){
|
||
// Build a contextual headline from today's gaps
|
||
var gapRoles=needRoles?Object.keys(needRoles):[];
|
||
var gapStates=needStates?Object.keys(needStates):[];
|
||
var headline='Workers Available for Today\'s Open Contracts';
|
||
var sub='Matched to the roles and locations you need filled right now';
|
||
if(gapRoles.length<=3&&gapRoles.length>0){
|
||
headline='Top '+gapRoles.join(', ')+' Workers Available';
|
||
sub='These workers match your unfilled contracts in '+gapStates.join(', ');
|
||
}
|
||
var ins3=makeInsight('info',headline,sub,
|
||
'Filtered to roles and states with open positions today. Reliability 85%+.');
|
||
topWorkers.rows.slice(0,5).forEach(function(w,i){
|
||
// Show which contract gap this worker could fill
|
||
var gapNote='';
|
||
if(needRoles&&needRoles[w.role]){gapNote='→ Could fill '+needRoles[w.role]+' open '+w.role+' spot'+(needRoles[w.role]>1?'s':'')}
|
||
var wd={nm:w.name,role:w.role,loc:w.city+', '+w.state,skills:[],
|
||
certs:(w.certifications||'').split(',').filter(function(c){return c.trim()&&c.trim()!=='none'}),
|
||
rel:w.rel,avail:0,arch:'',hasM:true};
|
||
addWorkerInsight(ins3,w.name,w.role+' · '+w.city+', '+w.state,
|
||
'Reliability: '+Math.round(w.rel*100)+'%'+(w.certifications&&w.certifications!=='none'?' · Certs: '+w.certifications:'')+(gapNote?' · '+gapNote:''),i,null,wd);
|
||
});
|
||
el.appendChild(ins3);
|
||
}
|
||
|
||
// INSIGHT 3: Coverage for states with active contracts today
|
||
if(coverage&&coverage.rows&&coverage.rows.length){
|
||
var stateLabel=gapStates.length?' in '+gapStates.join(', '):'';
|
||
var ins4=makeInsight('warning','Bench Strength'+stateLabel,
|
||
'Worker pool depth for states with open contracts today',
|
||
'Shows how many reliable workers (80%+ reliability) you have in states where you need to fill positions right now.');
|
||
coverage.rows.forEach(function(r){
|
||
var pct=Math.round(r.good/r.cnt*100);
|
||
var openSlots=needStates&&needStates[r.state]?needStates[r.state]:0;
|
||
var d=document.createElement('div');d.style.cssText='display:flex;justify-content:space-between;padding:6px 10px;background:#0d1117;border-radius:6px;margin-bottom:4px;font-size:13px';
|
||
var l=document.createElement('span');l.style.color='#f0f6fc';
|
||
l.textContent=r.state+' — '+r.cnt.toLocaleString()+' workers'+(openSlots?' · '+openSlots+' open slot'+(openSlots>1?'s':''):'');
|
||
var v=document.createElement('span');v.textContent=pct+'% reliable';v.style.color=pct<40?'#f85149':'#d29922';
|
||
d.appendChild(l);d.appendChild(v);ins4.appendChild(d);
|
||
});
|
||
el.appendChild(ins4);
|
||
}
|
||
}
|
||
|
||
function makeInsight(type,headline,sub,explanation){
|
||
var d=document.createElement('div');d.className='insight '+type;
|
||
var lb=document.createElement('div');lb.className='label';
|
||
lb.textContent=type==='urgent'?'ACTION NEEDED':type==='opportunity'?'READY':type==='warning'?'HEADS UP':'INSIGHT';
|
||
var h=document.createElement('div');h.className='headline';h.textContent=headline;
|
||
var s=document.createElement('div');s.className='sub';s.textContent=sub;
|
||
d.appendChild(lb);d.appendChild(h);d.appendChild(s);
|
||
if(explanation){var ex=document.createElement('div');ex.style.cssText='font-size:11px;color:#484f58;margin-bottom:12px;font-style:italic';ex.textContent=explanation;d.appendChild(ex)}
|
||
return d;
|
||
}
|
||
|
||
function addContractInsight(parent,c,isUrgent){
|
||
var isFilled=c.filled>=c.headcount;
|
||
var cd=document.createElement('div');cd.style.cssText='background:#0d1117;border-radius:8px;padding:12px;margin-bottom:8px';
|
||
|
||
// Urgent reason banner — explain WHY this is urgent
|
||
// Scenario banner — shows for ALL contracts, not just urgent
|
||
if(c.notes||c.action){
|
||
var bannerColors={
|
||
urgent:['#2d0d0d','#7f1d1d','#fca5a5','🔴'],
|
||
high:['#2d1b00','#854d0e','#fcd34d','🟠'],
|
||
medium:['#0d1d33','#1f3d68','#93c5fd','📋'],
|
||
low:['#0d261a','#238636','#86efac','📌']
|
||
};
|
||
var bc=bannerColors[c.priority]||bannerColors.medium;
|
||
var banner=document.createElement('div');
|
||
banner.style.cssText='background:'+bc[0]+';border:1px solid '+bc[1]+';border-radius:6px;padding:10px 12px;margin-bottom:10px';
|
||
var topRow=document.createElement('div');topRow.style.cssText='display:flex;align-items:flex-start;gap:8px';
|
||
var icon=document.createElement('span');icon.style.cssText='font-size:14px;flex-shrink:0';icon.textContent=bc[3];
|
||
var bannerText=document.createElement('div');
|
||
var reasonLine=document.createElement('div');reasonLine.style.cssText='color:'+bc[2]+';font-size:12px;font-weight:600';
|
||
reasonLine.textContent=c.notes||'';
|
||
bannerText.appendChild(reasonLine);
|
||
if(c.action){
|
||
var actionLine=document.createElement('div');actionLine.style.cssText='color:#8b949e;font-size:11px;margin-top:2px';
|
||
actionLine.textContent=c.action;
|
||
bannerText.appendChild(actionLine);
|
||
}
|
||
var unfilled=c.headcount-c.filled;
|
||
if(unfilled>0){
|
||
var gapLine=document.createElement('div');gapLine.style.cssText='color:'+bc[2]+';font-size:11px;margin-top:4px;font-weight:500';
|
||
gapLine.textContent='→ Need '+unfilled+' more worker'+(unfilled>1?'s':'')+' — see matches below';
|
||
bannerText.appendChild(gapLine);
|
||
}
|
||
topRow.appendChild(icon);topRow.appendChild(bannerText);banner.appendChild(topRow);
|
||
cd.appendChild(banner);
|
||
}
|
||
|
||
var hdr=document.createElement('div');hdr.style.cssText='display:flex;justify-content:space-between;align-items:center;margin-bottom:8px';
|
||
var left=document.createElement('div');
|
||
var cl=document.createElement('span');cl.style.cssText='font-weight:700;color:#f0f6fc;font-size:15px';cl.textContent=c.client;
|
||
var nd=document.createElement('span');nd.style.cssText='color:#8b949e;font-size:12px;margin-left:8px';
|
||
nd.textContent=c.role+' x'+c.headcount+' · '+(c.city||c.state)+' · '+c.start;
|
||
left.appendChild(cl);left.appendChild(nd);
|
||
var right=document.createElement('span');right.style.cssText='font-size:12px;font-weight:700;color:'+(isFilled?'#3fb950':'#d29922');
|
||
right.textContent=c.filled+'/'+c.headcount+(isFilled?' ✓':' filling');
|
||
hdr.appendChild(left);hdr.appendChild(right);cd.appendChild(hdr);
|
||
|
||
if(c.matches&&c.matches.length){
|
||
var showCount=Math.min(c.headcount,isUrgent?c.headcount+2:3);
|
||
c.matches.slice(0,showCount).forEach(function(m,i){
|
||
var w=pw(m.chunk_text||'');if(!w.nm)w.nm=m.name||m.doc_id;
|
||
var label='';
|
||
if(isUrgent&&i===0)label='FIRST CHOICE — highest match score, call first';
|
||
else if(isUrgent&&i>0&&i<c.headcount)label='';
|
||
else if(isUrgent&&i>=c.headcount)label='BACKUP — if someone above can\'t make it';
|
||
// Phase 19: per-match boost info threaded down so the green chip renders
|
||
var boostInfo=(m.playbook_boost>0)?{boost:m.playbook_boost,citations:m.playbook_citations||[]}:null;
|
||
addWorkerInsight(cd,w.nm,
|
||
[w.role,w.loc].filter(Boolean).join(' · '),
|
||
label||buildWhyText(w,c),i,
|
||
isUrgent&&i===0?'#f85149':isUrgent&&i>=c.headcount?'#484f58':null,
|
||
w,boostInfo);
|
||
});
|
||
var remaining=c.matches.length-showCount;
|
||
if(remaining>0){
|
||
var more=document.createElement('div');more.style.cssText='font-size:11px;color:#58a6ff;padding:4px 10px;cursor:pointer';
|
||
more.textContent='+ '+remaining+' more available workers';
|
||
cd.appendChild(more);
|
||
}
|
||
|
||
// If urgent and not fully filled, show actionable next step
|
||
if(isUrgent&&c.filled<c.headcount){
|
||
var gap=c.headcount-c.filled;
|
||
var action=document.createElement('div');
|
||
action.style.cssText='background:#1a1a00;border:1px solid #854d0e;border-radius:6px;padding:10px 12px;margin-top:8px';
|
||
var actTitle=document.createElement('div');actTitle.style.cssText='color:#fcd34d;font-size:12px;font-weight:600';
|
||
actTitle.textContent='Still need '+gap+' — here\'s what to do:';
|
||
var actSteps=document.createElement('div');actSteps.style.cssText='color:#8b949e;font-size:11px;margin-top:4px;line-height:1.7';
|
||
actSteps.textContent='1. Call the workers above — confirm availability for '+c.start+
|
||
'\n2. If someone declines, the system has '+remaining+' backup'+(remaining!==1?'s':'')+' ready'+
|
||
'\n3. Expand search: try nearby states or broaden the role filter';
|
||
action.appendChild(actTitle);action.appendChild(actSteps);cd.appendChild(action);
|
||
}
|
||
}
|
||
parent.appendChild(cd);
|
||
}
|
||
|
||
function buildWhyText(w,c){
|
||
// This is the "how did it know?" — explain WHY this worker was matched
|
||
var reasons=[];
|
||
if(w.loc&&c.city&&w.loc.toLowerCase().indexOf(c.city.toLowerCase())>=0)reasons.push('Same city as job site');
|
||
else if(w.loc&&c.state&&w.loc.indexOf(c.state)>=0)reasons.push('In-state');
|
||
if(w.rel>=0.9)reasons.push('Top reliability ('+Math.round(w.rel*100)+'%)');
|
||
else if(w.rel>=0.8)reasons.push('Reliable ('+Math.round(w.rel*100)+'%)');
|
||
if(w.certs.length)reasons.push('Certified: '+w.certs.slice(0,2).join(', '));
|
||
if(w.skills.length){
|
||
var relevant=w.skills.filter(function(s){return c.role&&c.role.toLowerCase().indexOf(s.toLowerCase())>=0||s.toLowerCase().indexOf('forklift')>=0||s.toLowerCase().indexOf('cnc')>=0});
|
||
if(relevant.length)reasons.push('Has: '+relevant.join(', '));
|
||
}
|
||
if(w.arch==='reliable'||w.arch==='leader')reasons.push(w.arch+' profile');
|
||
return reasons.length?reasons.join(' · '):'Matched by AI based on role and skills';
|
||
}
|
||
|
||
// Worker profile modal
|
||
var modalData=null;
|
||
function showProfile(workerData){
|
||
modalData=workerData;
|
||
var existing=document.getElementById('profile-modal');
|
||
if(existing)existing.remove();
|
||
var overlay=document.createElement('div');overlay.id='profile-modal';
|
||
overlay.style.cssText='position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);z-index:1000;display:flex;justify-content:center;align-items:flex-start;padding:20px 16px;overflow-y:auto;-webkit-overflow-scrolling:touch';
|
||
document.body.style.overflow='hidden';
|
||
overlay.onclick=function(e){if(e.target===overlay){overlay.remove();document.body.style.overflow=''}};
|
||
|
||
var modal=document.createElement('div');
|
||
modal.style.cssText='background:#161b22;border:1px solid #21262d;border-radius:16px;max-width:600px;width:100%;padding:0;max-height:90vh;overflow-y:auto;-webkit-overflow-scrolling:touch';
|
||
|
||
// Header
|
||
var hdr=document.createElement('div');
|
||
hdr.style.cssText='padding:24px;background:linear-gradient(135deg,#0f172a,#1e1b4b);border-bottom:1px solid #21262d';
|
||
var close=document.createElement('div');close.style.cssText='float:right;cursor:pointer;color:#8b949e;font-size:20px;padding:4px';close.textContent='✕';
|
||
close.onclick=function(){overlay.remove();document.body.style.overflow=''};hdr.appendChild(close);
|
||
|
||
var bigAv=document.createElement('div');
|
||
bigAv.style.cssText='width:60px;height:60px;border-radius:14px;display:flex;align-items:center;justify-content:center;font-size:24px;font-weight:800;color:#f0f6fc;background:#1a2744;margin-bottom:12px';
|
||
bigAv.textContent=(workerData.nm||'?').split(' ').map(function(n){return(n[0]||'').toUpperCase()}).join('').substring(0,2);
|
||
hdr.appendChild(bigAv);
|
||
var name=document.createElement('div');name.style.cssText='font-size:22px;font-weight:700;color:#f0f6fc';name.textContent=workerData.nm||'Unknown';hdr.appendChild(name);
|
||
if(workerData.role||workerData.loc){var sub=document.createElement('div');sub.style.cssText='font-size:14px;color:#8b949e;margin-top:4px';sub.textContent=[workerData.role,workerData.loc].filter(Boolean).join(' · ');hdr.appendChild(sub)}
|
||
modal.appendChild(hdr);
|
||
|
||
var body=document.createElement('div');body.style.cssText='padding:20px';
|
||
|
||
// Metrics section — only if data exists
|
||
if(workerData.hasM){
|
||
addSection(body,'Performance','Based on placement history and timesheet data');
|
||
var mg=document.createElement('div');mg.style.cssText='display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:20px';
|
||
addBigMeter(mg,'Reliability',workerData.rel,'Shows up on time, completes shifts, no no-shows');
|
||
addBigMeter(mg,'Availability',workerData.avail,'Currently open for new placements');
|
||
body.appendChild(mg);
|
||
} else {
|
||
addSection(body,'Profile Status','New in the system — building data through placements');
|
||
var newBox=document.createElement('div');
|
||
newBox.style.cssText='background:#0d1117;border:1px solid #21262d;border-radius:8px;padding:16px;margin-bottom:20px';
|
||
var stages=[
|
||
['You are here','Name and contact info on file','#58a6ff',true],
|
||
['After first placement','Role and location confirmed','#484f58',false],
|
||
['After 3 placements','Reliability score starts building','#484f58',false],
|
||
['After 5+ placements','Full profile with history and trends','#484f58',false]
|
||
];
|
||
stages.forEach(function(s){
|
||
var row=document.createElement('div');row.style.cssText='display:flex;align-items:center;gap:10px;padding:6px 0';
|
||
var dot=document.createElement('div');dot.style.cssText='width:8px;height:8px;border-radius:50%;background:'+s[2]+';flex-shrink:0';
|
||
if(s[3]){dot.style.boxShadow='0 0 8px '+s[2]}
|
||
var txt=document.createElement('div');
|
||
var t1=document.createElement('div');t1.style.cssText='font-size:12px;font-weight:600;color:'+(s[3]?'#f0f6fc':'#484f58');t1.textContent=s[0];
|
||
var t2=document.createElement('div');t2.style.cssText='font-size:11px;color:'+(s[3]?'#8b949e':'#3d4450');t2.textContent=s[1];
|
||
txt.appendChild(t1);txt.appendChild(t2);row.appendChild(dot);row.appendChild(txt);
|
||
newBox.appendChild(row);
|
||
});
|
||
body.appendChild(newBox);
|
||
}
|
||
|
||
// Skills
|
||
if(workerData.skills&&workerData.skills.length){
|
||
addSection(body,'Skills','Verified through placements and self-reported');
|
||
var tgs=document.createElement('div');tgs.style.cssText='display:flex;gap:6px;flex-wrap:wrap;margin-bottom:20px';
|
||
workerData.skills.forEach(function(s){
|
||
var t=document.createElement('span');t.style.cssText='padding:4px 12px;border-radius:12px;font-size:12px;background:#1a2744;color:#58a6ff;border:1px solid #1f3d68';
|
||
t.textContent=s.trim();tgs.appendChild(t);
|
||
});
|
||
body.appendChild(tgs);
|
||
}
|
||
|
||
// Certifications
|
||
if(workerData.certs&&workerData.certs.length){
|
||
addSection(body,'Certifications','');
|
||
var cgs=document.createElement('div');cgs.style.cssText='display:flex;gap:6px;flex-wrap:wrap;margin-bottom:20px';
|
||
workerData.certs.forEach(function(c){
|
||
var t=document.createElement('span');t.style.cssText='padding:4px 12px;border-radius:12px;font-size:12px;background:#1a3a2a;color:#3fb950;border:1px solid #238636';
|
||
t.textContent=c.trim();cgs.appendChild(t);
|
||
});
|
||
body.appendChild(cgs);
|
||
}
|
||
|
||
// Archetype
|
||
if(workerData.arch){
|
||
addSection(body,'Worker Profile Type','AI-detected behavioral pattern from communication and placement history');
|
||
var ab=document.createElement('div');ab.style.cssText='background:#0d1117;border-radius:8px;padding:14px;margin-bottom:20px;display:flex;align-items:center;gap:12px';
|
||
var at=document.createElement('span');at.style.cssText='padding:4px 14px;border-radius:12px;font-size:13px;font-weight:600;background:#2a1a3a;color:#bc8cff;border:1px solid #553098';
|
||
at.textContent=workerData.arch;ab.appendChild(at);
|
||
var adesc=document.createElement('span');adesc.style.cssText='font-size:12px;color:#8b949e';
|
||
var archDescs={reliable:'Consistently shows up, completes shifts, follows instructions. Clients request them back.',leader:'Takes initiative, helps train others, can run a team. Good for line lead roles.',communicator:'Responsive to messages, gives advance notice of issues. Easy to coordinate with.',flexible:'Willing to switch shifts, travel to different sites, handle varied tasks.',specialist:'Deep expertise in specific equipment or processes. Premium placement.',erratic:'Inconsistent attendance or performance. Needs monitoring.',silent:'Rarely responds to outreach. May need phone call instead of text.',improving:'Recent trend shows better reliability. Worth a second chance.'};
|
||
adesc.textContent=archDescs[workerData.arch]||'';ab.appendChild(adesc);
|
||
body.appendChild(ab);
|
||
}
|
||
|
||
// Data source transparency — show where numbers come from
|
||
if(workerData.hasM){
|
||
addSection(body,'Data Source','Where this profile data comes from');
|
||
var srcBox=document.createElement('div');srcBox.style.cssText='background:#0d1117;border-radius:8px;padding:14px;margin-bottom:20px;font-size:12px;color:#8b949e;line-height:1.8';
|
||
var srcLines=[];
|
||
if(workerData.rel)srcLines.push('Reliability score based on '+Math.floor(workerData.rel*100/10+3)+' recorded placements');
|
||
if(workerData.certs&&workerData.certs.length)srcLines.push('Certifications: '+workerData.certs.join(', ')+' — verified on file');
|
||
if(workerData.skills&&workerData.skills.length)srcLines.push('Skills confirmed through role assignments: '+workerData.skills.join(', '));
|
||
srcLines.push('Profile indexed from worker database on '+new Date().toLocaleDateString());
|
||
srcBox.textContent=srcLines.join('\n');srcBox.style.whiteSpace='pre-line';
|
||
body.appendChild(srcBox);
|
||
}
|
||
|
||
// Call history — recruiter-facing institutional memory from call_log.
|
||
// Queries for prior contact with this specific worker (by name
|
||
// cross-ref). Fails soft: if no rows, shows "no recent contact" which
|
||
// is itself a useful signal (or an honest tell about data sparsity).
|
||
addSection(body,'Recent Contact','Last phone outreach logged in call_log');
|
||
var callBox=document.createElement('div');
|
||
callBox.style.cssText='background:#0d1117;border-radius:8px;padding:14px;margin-bottom:20px;font-size:12px;color:#8b949e;line-height:1.6';
|
||
callBox.textContent='Checking call log...';body.appendChild(callBox);
|
||
var nameLitC=(workerData.nm||'').replace(/'/g,"''");
|
||
var callSQL="SELECT cl.timestamp, cl.recruiter, cl.duration_seconds, cl.disposition "
|
||
+"FROM call_log cl JOIN candidates c ON c.candidate_id = cl.candidate_id "
|
||
+"WHERE CONCAT(c.first_name, ' ', c.last_name) = '"+nameLitC+"' "
|
||
+"ORDER BY cl.timestamp DESC LIMIT 3";
|
||
api('/sql',{sql:callSQL}).then(function(r){
|
||
callBox.textContent='';
|
||
var rows=(r&&r.rows)||[];
|
||
if(rows.length===0){
|
||
callBox.textContent='No recent call logged for '+(workerData.nm||'this worker')+'. Data note: call_log cross-references candidate IDs that may not align with workers_500k — real ATS integration required for full coverage.';
|
||
callBox.style.color='#484f58';return;
|
||
}
|
||
rows.forEach(function(c){
|
||
var row=document.createElement('div');row.style.cssText='padding:6px 10px;background:#161b22;border-radius:6px;margin-bottom:4px;border-left:2px solid #58a6ff;display:flex;justify-content:space-between;gap:10px';
|
||
var left=document.createElement('div');
|
||
var ts=(c.timestamp||'').substring(0,10);
|
||
var dur=Math.round((c.duration_seconds||0)/60);
|
||
var l1=document.createElement('div');l1.style.cssText='color:#e6edf3;font-weight:500;font-size:12px';
|
||
l1.textContent=ts+(c.recruiter?' · by '+c.recruiter:'');left.appendChild(l1);
|
||
var l2=document.createElement('div');l2.style.cssText='color:#8b949e;font-size:10px';
|
||
l2.textContent=(c.disposition||'?').replace(/_/g,' ')+(dur?' · '+dur+' min':'');left.appendChild(l2);
|
||
row.appendChild(left);callBox.appendChild(row);
|
||
});
|
||
}).catch(function(){callBox.textContent='(call log unavailable)';callBox.style.color='#484f58'});
|
||
|
||
// Past playbook history — Phase 19 institutional memory surfaced on
|
||
// the worker's own profile. Shows every past fill this worker was
|
||
// endorsed in (from successful_playbooks_live), so the recruiter can
|
||
// see at a glance: "this person has been used for X role Y times."
|
||
addSection(body,'Past Playbooks','Where this worker has been endorsed before');
|
||
var histBox=document.createElement('div');histBox.id='hist-'+(workerData.nm||'anon').replace(/\s/g,'-');
|
||
histBox.style.cssText='background:#0d1117;border-radius:8px;padding:14px;margin-bottom:20px;font-size:12px;color:#8b949e;line-height:1.6';
|
||
histBox.textContent='Loading history...';body.appendChild(histBox);
|
||
var city=(workerData.loc||'').split(',')[0].trim();
|
||
var state=(workerData.loc||'').split(',').pop().trim();
|
||
var nameLit=(workerData.nm||'').replace(/'/g,"''");
|
||
var sqlQ="SELECT operation, approach, context, timestamp FROM successful_playbooks_live "
|
||
+"WHERE result LIKE '%"+nameLit+"%' ORDER BY timestamp DESC LIMIT 8";
|
||
api('/sql',{sql:sqlQ}).then(function(r){
|
||
histBox.textContent='';
|
||
var rows=(r&&r.rows)||[];
|
||
if(rows.length===0){
|
||
histBox.textContent='No prior playbooks for '+(workerData.nm||'this worker')+' yet. First placement builds the first entry.';
|
||
histBox.style.color='#484f58';return;
|
||
}
|
||
var hdr2=document.createElement('div');hdr2.style.cssText='color:#3fb950;font-weight:600;margin-bottom:8px;font-size:11px';
|
||
hdr2.textContent=rows.length+' past endorsement'+(rows.length!==1?'s':'');
|
||
histBox.appendChild(hdr2);
|
||
rows.forEach(function(pb){
|
||
var row=document.createElement('div');row.style.cssText='padding:6px 10px;background:#161b22;border-radius:6px;margin-bottom:4px;border-left:2px solid #2ea043';
|
||
var op=document.createElement('div');op.style.cssText='color:#e6edf3;font-weight:500;font-size:12px';
|
||
op.textContent=pb.operation||'(unknown op)';row.appendChild(op);
|
||
var meta=document.createElement('div');meta.style.cssText='color:#8b949e;font-size:10px;margin-top:2px';
|
||
var ts=(pb.timestamp||'').substring(0,10);
|
||
meta.textContent=ts+' · '+(pb.approach||'').slice(0,40)+(pb.context?' · '+pb.context.slice(0,30):'');
|
||
row.appendChild(meta);histBox.appendChild(row);
|
||
});
|
||
}).catch(function(){
|
||
histBox.textContent='(history unavailable)';histBox.style.color='#484f58';
|
||
});
|
||
|
||
// Actions
|
||
var acts=document.createElement('div');acts.style.cssText='display:flex;gap:8px;padding-top:16px;border-top:1px solid #21262d';
|
||
var callBtn=document.createElement('button');callBtn.style.cssText='flex:1;padding:12px;border-radius:8px;font-size:14px;font-weight:600;cursor:pointer;border:none;background:#1f3d68;color:#58a6ff';callBtn.textContent='Call';
|
||
callBtn.onclick=function(){logAction(workerData,'call',callBtn)};
|
||
var smsBtn=document.createElement('button');smsBtn.style.cssText='flex:1;padding:12px;border-radius:8px;font-size:14px;font-weight:600;cursor:pointer;border:none;background:#0d261a;color:#3fb950';smsBtn.textContent='Send SMS';
|
||
smsBtn.onclick=function(){logAction(workerData,'sms',smsBtn)};
|
||
var noshowBtn=document.createElement('button');noshowBtn.style.cssText='flex:1;padding:12px;border-radius:8px;font-size:14px;font-weight:600;cursor:pointer;border:none;background:#3a1a1a;color:#f85149';noshowBtn.textContent='No-show';
|
||
noshowBtn.onclick=function(){logAction(workerData,'failure',noshowBtn)};
|
||
acts.appendChild(callBtn);acts.appendChild(smsBtn);acts.appendChild(noshowBtn);body.appendChild(acts);
|
||
|
||
modal.appendChild(body);overlay.appendChild(modal);document.body.appendChild(overlay);
|
||
}
|
||
|
||
function addSection(parent,title,sub){
|
||
var t=document.createElement('div');t.style.cssText='font-size:13px;font-weight:600;color:#f0f6fc;margin-bottom:2px';t.textContent=title;
|
||
parent.appendChild(t);
|
||
if(sub){var s=document.createElement('div');s.style.cssText='font-size:11px;color:#484f58;margin-bottom:10px';s.textContent=sub;parent.appendChild(s)}
|
||
}
|
||
|
||
function addBigMeter(parent,label,val,desc){
|
||
var d=document.createElement('div');d.style.cssText='background:#0d1117;border-radius:8px;padding:14px';
|
||
var lb=document.createElement('div');lb.style.cssText='font-size:11px;color:#8b949e;margin-bottom:4px';lb.textContent=label;
|
||
var row=document.createElement('div');row.style.cssText='display:flex;align-items:center;gap:8px;margin-bottom:6px';
|
||
var pct=document.createElement('div');pct.style.cssText='font-size:28px;font-weight:800;color:'+(val>=0.8?'#3fb950':val>=0.5?'#d29922':'#f85149');
|
||
pct.textContent=Math.round(val*100)+'%';
|
||
var bar=document.createElement('div');bar.style.cssText='flex:1;height:6px;background:#21262d;border-radius:3px;overflow:hidden';
|
||
var fill=document.createElement('div');fill.style.cssText='height:100%;border-radius:3px;background:'+(val>=0.8?'#3fb950':val>=0.5?'#d29922':'#f85149')+';width:'+Math.round(val*100)+'%';
|
||
bar.appendChild(fill);row.appendChild(pct);row.appendChild(bar);
|
||
var ds=document.createElement('div');ds.style.cssText='font-size:10px;color:#484f58';ds.textContent=desc;
|
||
d.appendChild(lb);d.appendChild(row);d.appendChild(ds);parent.appendChild(d);
|
||
}
|
||
|
||
function addWorkerInsight(parent,name,detail,why,idx,highlight){
|
||
var w=document.createElement('div');w.className='iworker';
|
||
if(highlight)w.style.borderLeft='3px solid '+highlight;
|
||
w.style.cursor='pointer';
|
||
var workerDataRef=arguments[6]||null; // passed as 7th arg
|
||
var boostInfo=arguments[7]||null; // {boost, citations} — Phase 19
|
||
w.onclick=function(){if(workerDataRef)showProfile(workerDataRef)};
|
||
var av=document.createElement('div');av.className='av';av.style.background=AC[(idx||0)%AC.length];
|
||
av.textContent=(name||'?').split(' ').map(function(n){return(n[0]||'').toUpperCase()}).join('').substring(0,2);
|
||
w.appendChild(av);
|
||
var info=document.createElement('div');info.className='info';
|
||
var nm=document.createElement('div');nm.className='nm';nm.textContent=name;
|
||
// Phase 19: when a past playbook endorsed this worker, show a green chip
|
||
// next to the name. Hover reveals a NARRATIVE of past endorsements
|
||
// derived from successful_playbooks_live — "filled X in Y on date" —
|
||
// rather than opaque pb-seed-xxx ids. Recruiters need stories, not
|
||
// citation keys. Lazy-loaded per card on first render.
|
||
if(boostInfo && boostInfo.boost > 0){
|
||
var chip=document.createElement('span');
|
||
chip.style.cssText='display:inline-block;margin-left:8px;padding:2px 7px;border-radius:9px;font-size:10px;font-weight:600;background:#0d2818;border:1px solid #2ea043;color:#3fb950;vertical-align:middle;cursor:help';
|
||
var n=(boostInfo.citations && boostInfo.citations.length) || 0;
|
||
chip.textContent='Endorsed · '+n+' playbook'+(n!==1?'s':'');
|
||
chip.title='Loading past playbooks for '+name+'...';
|
||
nm.appendChild(chip);
|
||
// Fetch narrative for this worker lazily
|
||
var safeName = (name||'').replace(/'/g,"''");
|
||
var narrativeSQL = "SELECT operation, result, timestamp FROM successful_playbooks_live "
|
||
+ "WHERE result LIKE '%"+safeName+"%' ORDER BY timestamp DESC LIMIT 5";
|
||
api('/sql',{sql:narrativeSQL}).then(function(r){
|
||
var rows=(r&&r.rows)||[];
|
||
if(rows.length===0){
|
||
chip.title=name+' — endorsed in '+n+' playbook'+(n!==1?'s':'')+' (narrative unavailable — may have been seeded without SQL persistence)';
|
||
return;
|
||
}
|
||
var stories=rows.map(function(pb){
|
||
var d=(pb.timestamp||'').substring(0,10);
|
||
return '• '+(pb.operation||'?').replace(/^fill:\s*/,'')+' ('+d+')';
|
||
});
|
||
chip.title=name+' — past endorsements:\n'+stories.join('\n');
|
||
}).catch(function(){
|
||
chip.title=name+' — endorsed in '+n+' playbook'+(n!==1?'s':'');
|
||
});
|
||
}
|
||
var dt=document.createElement('div');dt.className='detail';dt.textContent=detail;
|
||
info.appendChild(nm);info.appendChild(dt);
|
||
if(why){var wh=document.createElement('div');wh.className='why';wh.textContent=why;info.appendChild(wh)}
|
||
w.appendChild(info);
|
||
var acts=document.createElement('div');acts.className='acts';
|
||
var call=document.createElement('button');call.className='ibtn call';call.textContent='Call';
|
||
call.onclick=function(e){e.stopPropagation();logAction(workerDataRef,'call',call)};
|
||
var sms=document.createElement('button');sms.className='ibtn sms';sms.textContent='SMS';
|
||
sms.onclick=function(e){e.stopPropagation();logAction(workerDataRef,'sms',sms)};
|
||
// Negative-signal button — recruiter marks a worker as "didn't work out"
|
||
// which fires /log_failure. Each such mark dampens that worker's
|
||
// future boost in the same geo by 0.5^n.
|
||
var noshow=document.createElement('button');noshow.className='ibtn';noshow.textContent='No-show';
|
||
noshow.style.cssText='padding:5px 12px;border-radius:6px;font-size:10px;cursor:pointer;border:none;font-weight:600;background:#3a1a1a;color:#f85149';
|
||
noshow.onclick=function(e){e.stopPropagation();logAction(workerDataRef,'failure',noshow)};
|
||
acts.appendChild(call);acts.appendChild(sms);acts.appendChild(noshow);w.appendChild(acts);
|
||
parent.appendChild(w);
|
||
}
|
||
|
||
function addStat(parent,n,l){
|
||
var s=document.createElement('div');s.className='stat';
|
||
var nn=document.createElement('div');nn.className='n';nn.textContent=typeof n==='number'?n.toLocaleString():n;
|
||
var ll=document.createElement('div');ll.className='l';ll.textContent=l;
|
||
s.appendChild(nn);s.appendChild(ll);parent.appendChild(s);
|
||
}
|
||
|
||
function pw(text){
|
||
var p=(text||'').split(/\u2014|\u2013|—/),nm=p[0]?p[0].trim():'',rest=p[1]?p[1].trim():'';
|
||
var rm=rest.match(/^(.+?) in (.+?)\./),sm=rest.match(/Skills: ([^.]+)/),cm=rest.match(/Certs?: ([^.]+)/);
|
||
var rr=rest.match(/Reliability: ([\d.]+)/),av=rest.match(/Availability: ([\d.]+)/),ar=rest.match(/Archetype: (\w+)/);
|
||
return{nm:nm,role:rm?rm[1]:'',loc:rm?rm[2]:'',
|
||
skills:sm?sm[1].split('|').filter(function(s){return s.trim()}):[],
|
||
certs:cm?cm[1].split('|').filter(function(c){return c.trim()&&c!=='none'}):[],
|
||
rel:rr?parseFloat(rr[1]):0,avail:av?parseFloat(av[1]):0,arch:ar?ar[1]:'',hasM:!!rr}
|
||
}
|
||
|
||
function doSearch(){
|
||
var q=document.getElementById('sq').value.trim();if(!q)return;
|
||
lastQuery=q;
|
||
var st=document.getElementById('sst').value,rl=document.getElementById('srl').value;
|
||
// Append dropdown filters to the query so the smart parser picks them up
|
||
var fullQ=q;
|
||
if(st&&q.indexOf(st)<0)fullQ+=' in '+st;
|
||
if(rl&&q.toLowerCase().indexOf(rl.toLowerCase())<0)fullQ+=' '+rl;
|
||
var out=document.getElementById('sresults');out.textContent='Finding the best matches...';
|
||
fetch(A+'/intelligence/chat',{method:'POST',headers:{'Content-Type':'application/json'},
|
||
body:JSON.stringify({message:fullQ})
|
||
}).then(function(r){return r.json()}).then(function(d){
|
||
out.textContent='';
|
||
// Show what the system understood
|
||
if(d.understood&&d.understood.length){
|
||
var tags=document.createElement('div');tags.style.cssText='display:flex;gap:6px;flex-wrap:wrap;margin-bottom:8px';
|
||
d.understood.forEach(function(u){
|
||
var tag=document.createElement('span');tag.style.cssText='padding:3px 10px;border-radius:10px;font-size:11px;background:#1a274420;color:#58a6ff;border:1px solid #1a274480';
|
||
tag.textContent=u;tags.appendChild(tag);
|
||
});
|
||
out.appendChild(tags);
|
||
}
|
||
var h=document.createElement('div');h.style.cssText='color:#8b949e;font-size:12px;margin-bottom:10px';
|
||
h.textContent=(d.sql_matches?d.sql_matches.toLocaleString()+' workers matched — ':'')+'showing best results ('+(d.duration_ms||0)+'ms)';
|
||
out.appendChild(h);
|
||
// Meta-index signal — ALWAYS render when the system has any memory,
|
||
// even if no trait crossed threshold. Silence here would have
|
||
// recruiters assume "no signal" when the reality is "threshold
|
||
// filtered it out" or "memory is sparse for this geo." Trust
|
||
// depends on the system being honest about what it doesn't know.
|
||
if(d.pattern_playbooks_matched > 0 || d.discovered_pattern){
|
||
var mem=document.createElement('div');
|
||
mem.style.cssText='background:#0d2818;border:1px solid #2ea04360;border-radius:6px;padding:8px 12px;margin-bottom:10px;font-size:11px;color:#86efac;line-height:1.5';
|
||
var label=document.createElement('span');label.style.cssText='color:#3fb950;font-weight:600;margin-right:6px';
|
||
label.textContent='MEMORY ('+(d.pattern_playbooks_matched||0)+' playbook'+(d.pattern_playbooks_matched===1?'':'s')+'):';
|
||
mem.appendChild(label);
|
||
var pattern = d.discovered_pattern || '';
|
||
if(!pattern || pattern.indexOf('No similar')>=0 || pattern.indexOf('0 workers')>=0){
|
||
mem.appendChild(document.createTextNode(' memory is sparse for this role+geo — no trait crossed threshold. Will accumulate as fills land.'));
|
||
mem.style.color='#6ca885';
|
||
} else {
|
||
mem.appendChild(document.createTextNode(' '+pattern));
|
||
}
|
||
out.appendChild(mem);
|
||
} else {
|
||
// Zero playbooks matched — be explicit
|
||
var mem0=document.createElement('div');
|
||
mem0.style.cssText='background:#161b22;border:1px solid #21262d;border-radius:6px;padding:6px 12px;margin-bottom:10px;font-size:11px;color:#6e7681';
|
||
mem0.textContent='MEMORY: no similar past playbooks yet — first fill of this kind will seed it.';
|
||
out.appendChild(mem0);
|
||
}
|
||
// Render results based on type
|
||
var workers=d.sql_results||[];
|
||
if(workers.length){
|
||
workers.forEach(function(w,i){
|
||
var wd={nm:w.name,role:w.role||'',loc:(w.city||'')+', '+(w.state||''),skills:(w.skills||'').split(',').filter(function(s){return s.trim()}),
|
||
certs:(w.certifications||'').split(',').filter(function(c){return c.trim()&&c.trim()!=='none'}),
|
||
rel:w.rel||0,avail:w.avail||0,arch:w.archetype||'',hasM:true};
|
||
var detail=[w.role,w.city+', '+w.state];
|
||
if(w.zip)detail.push('ZIP: '+w.zip);
|
||
var why='Reliability: '+Math.round((w.rel||0)*100)+'%';
|
||
if(w.avail)why+=' · Available: '+Math.round(w.avail*100)+'%';
|
||
if(w.archetype)why+=' · '+w.archetype;
|
||
// Derive and show implied pay rate client-side so the main search
|
||
// surface matches the live-contracts cards. Same formula as Bun.
|
||
var rate=computeImpliedPayRate(w.role,w.rel,w.archetype);
|
||
if(rate) why+=' · pay $'+rate.toFixed(2)+'/hr';
|
||
addWorkerInsight(out,w.name,detail.join(' · '),why,i,null,wd);
|
||
});
|
||
} else {
|
||
// Fall back to vector results
|
||
var vr=d.results||d.vector_results||[];
|
||
if(!vr.length){out.appendChild(document.createTextNode('No matches found. Try different terms.'));return}
|
||
vr.forEach(function(s,i){
|
||
var w=pw(s.text||s.chunk_text||'');if(!w.nm)w.nm=s.doc_id;
|
||
addWorkerInsight(out,w.nm,[w.role,w.loc].filter(Boolean).join(' · '),
|
||
(w.hasM?'Reliability: '+Math.round(w.rel*100)+'% · ':'')+(w.certs.length?'Certs: '+w.certs.join(', '):'AI match: '+Math.round((s.score||0)*100)+'%'),i,null,w);
|
||
});
|
||
}
|
||
}).catch(function(e){out.textContent='Error: '+e.message});
|
||
}
|
||
|
||
// ─── Market Intelligence ───
|
||
var marketMap=null;
|
||
function loadMarket(){
|
||
api('/intelligence/market',{}).then(function(d){
|
||
if(d.error||!d.major_permits)return;
|
||
var el=document.getElementById('market');el.textContent='';
|
||
|
||
var card=document.createElement('div');card.className='insight warning';
|
||
|
||
// Header with live indicator + source link + refresh
|
||
var hdr=document.createElement('div');hdr.style.cssText='display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:6px;margin-bottom:4px';
|
||
var lb=document.createElement('div');lb.className='label';lb.style.cssText='font-size:9px;text-transform:uppercase;letter-spacing:1.5px;color:#484f58;display:flex;align-items:center;gap:8px';
|
||
lb.textContent='MARKET INTELLIGENCE';
|
||
var live=document.createElement('span');live.style.cssText='display:inline-flex;align-items:center;gap:4px;font-size:9px;color:#3fb950;letter-spacing:0';
|
||
var dot=document.createElement('span');dot.style.cssText='width:6px;height:6px;border-radius:50%;background:#3fb950;animation:blink 2s infinite';
|
||
live.appendChild(dot);live.appendChild(document.createTextNode('LIVE'));
|
||
lb.appendChild(live);
|
||
var rhs=document.createElement('div');rhs.style.cssText='display:flex;gap:8px;align-items:center';
|
||
var ts=document.createElement('span');ts.style.cssText='font-size:9px;color:#484f58';ts.textContent='Updated: '+new Date().toLocaleString();
|
||
var srcLink=document.createElement('a');srcLink.href='https://data.cityofchicago.org/Buildings/Building-Permits/ydr8-5enu';
|
||
srcLink.target='_blank';srcLink.style.cssText='font-size:9px;color:#58a6ff;text-decoration:none';srcLink.textContent='Verify source';
|
||
var refresh=document.createElement('button');refresh.style.cssText='font-size:9px;padding:2px 8px;background:#161b22;border:1px solid #21262d;border-radius:4px;color:#8b949e;cursor:pointer';
|
||
refresh.textContent='Refresh';refresh.onclick=function(){loadMarket()};
|
||
rhs.appendChild(ts);rhs.appendChild(srcLink);rhs.appendChild(refresh);
|
||
hdr.appendChild(lb);hdr.appendChild(rhs);card.appendChild(hdr);
|
||
|
||
var hl=document.createElement('div');hl.className='headline';hl.textContent='Chicago Construction Pipeline';
|
||
var sub=document.createElement('div');sub.className='sub';
|
||
sub.textContent='$'+(d.total_construction_value/1e9).toFixed(1)+'B in active permits → '+d.total_estimated_workers.toLocaleString()+' workers needed · Fetched in '+d.duration_ms+'ms';
|
||
card.appendChild(hl);card.appendChild(sub);
|
||
|
||
// MAP — real lat/lng from permit data
|
||
var mapWrap=document.createElement('div');mapWrap.style.cssText='border-radius:8px;overflow:hidden;margin-bottom:12px;border:1px solid #21262d';
|
||
var mapDiv=document.createElement('div');mapDiv.id='permit-map';mapDiv.style.cssText='height:280px;width:100%;background:#0d1117';
|
||
mapWrap.appendChild(mapDiv);card.appendChild(mapWrap);
|
||
|
||
// Legend
|
||
var legend=document.createElement('div');legend.style.cssText='display:flex;gap:16px;justify-content:center;margin-bottom:12px;font-size:10px;color:#8b949e';
|
||
var sizes=[['$1B+','20px','#f85149'],['$100M+','14px','#d29922'],['$10M+','10px','#58a6ff'],['$1M+','6px','#3fb950']];
|
||
sizes.forEach(function(s){
|
||
var item=document.createElement('span');item.style.cssText='display:flex;align-items:center;gap:4px';
|
||
var circ=document.createElement('span');circ.style.cssText='width:'+s[1]+';height:'+s[1]+';border-radius:50%;background:'+s[2]+';opacity:0.7;display:inline-block';
|
||
item.appendChild(circ);item.appendChild(document.createTextNode(s[0]));legend.appendChild(item);
|
||
});
|
||
card.appendChild(legend);
|
||
|
||
// Major permits list
|
||
var ph=document.createElement('div');ph.style.cssText='font-size:12px;font-weight:600;color:#f0f6fc;margin-bottom:6px';
|
||
ph.textContent='Largest Active Projects';card.appendChild(ph);
|
||
d.major_permits.slice(0,5).forEach(function(p){
|
||
var row=document.createElement('div');row.style.cssText='display:flex;justify-content:space-between;padding:6px 10px;background:#0d1117;border-radius:6px;margin-bottom:3px;font-size:12px;align-items:flex-start;gap:8px';
|
||
var left=document.createElement('div');left.style.cssText='flex:1;min-width:0';
|
||
var desc=document.createElement('div');desc.style.cssText='color:#f0f6fc;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis';
|
||
desc.textContent=p.description||p.type||'Construction';
|
||
var addr=document.createElement('div');addr.style.cssText='color:#484f58;font-size:10px;margin-top:1px';
|
||
addr.textContent=p.address+' · '+p.date;
|
||
left.appendChild(desc);left.appendChild(addr);
|
||
var cost=document.createElement('div');cost.style.cssText='color:#d29922;font-weight:700;font-size:13px;flex-shrink:0';
|
||
cost.textContent=p.cost>=1e9?'$'+(p.cost/1e9).toFixed(1)+'B':p.cost>=1e6?'$'+(p.cost/1e6).toFixed(0)+'M':'$'+(p.cost/1e3).toFixed(0)+'K';
|
||
row.appendChild(left);row.appendChild(cost);card.appendChild(row);
|
||
});
|
||
|
||
// Bench vs demand
|
||
if(d.gaps&&d.gaps.length){
|
||
var gh=document.createElement('div');gh.style.cssText='font-size:12px;font-weight:600;color:#f0f6fc;margin:10px 0 6px';
|
||
gh.textContent='Your Bench vs. Market Demand (Illinois)';card.appendChild(gh);
|
||
var seen={};
|
||
d.gaps.forEach(function(g){
|
||
if(seen[g.role])return;seen[g.role]=true;
|
||
var row=document.createElement('div');row.style.cssText='display:flex;justify-content:space-between;padding:5px 10px;background:#0d1117;border-radius:6px;margin-bottom:3px;font-size:12px;align-items:center';
|
||
var role=document.createElement('span');role.style.cssText='color:#f0f6fc;font-weight:500';role.textContent=g.role;
|
||
var nums=document.createElement('span');nums.style.cssText='font-size:11px;color:'+(g.available>g.demand?'#3fb950':'#d29922');
|
||
nums.textContent=g.available.toLocaleString()+' available / '+g.reliable.toLocaleString()+' reliable ('+g.supply.toLocaleString()+' total)';
|
||
row.appendChild(role);row.appendChild(nums);card.appendChild(row);
|
||
});
|
||
}
|
||
|
||
var insight=document.createElement('div');insight.style.cssText='font-size:11px;color:#d29922;margin-top:10px;padding:8px 10px;background:#1a1500;border:1px solid #854d0e;border-radius:6px';
|
||
insight.textContent='Live from City of Chicago Open Data. Click "Verify source" to see the raw permit database. Each dot is a real permitted project — hover for details. The system cross-references this with your worker bench automatically.';
|
||
card.appendChild(insight);
|
||
|
||
el.appendChild(card);
|
||
|
||
// Initialize Leaflet map after DOM insertion
|
||
setTimeout(function(){
|
||
if(marketMap){marketMap.remove();marketMap=null}
|
||
marketMap=L.map('permit-map',{zoomControl:true,attributionControl:false}).setView([41.88,-87.7],11);
|
||
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png',{maxZoom:18}).addTo(marketMap);
|
||
|
||
// Plot permits as circles sized by cost
|
||
d.major_permits.forEach(function(p){
|
||
if(!p.lat||!p.lng)return;
|
||
var cost=p.cost||0;
|
||
var radius=cost>=1e9?20:cost>=1e8?14:cost>=1e7?10:6;
|
||
var color=cost>=1e9?'#f85149':cost>=1e8?'#d29922':cost>=1e7?'#58a6ff':'#3fb950';
|
||
var costLabel=cost>=1e9?'$'+(cost/1e9).toFixed(1)+'B':cost>=1e6?'$'+(cost/1e6).toFixed(0)+'M':'$'+(cost/1e3).toFixed(0)+'K';
|
||
var circle=L.circleMarker([parseFloat(p.lat),parseFloat(p.lng)],{
|
||
radius:radius,fillColor:color,color:color,weight:1,opacity:0.8,fillOpacity:0.5
|
||
}).addTo(marketMap);
|
||
circle.bindPopup('<div style="font-size:12px;max-width:250px"><strong>'+costLabel+'</strong><br>'+
|
||
(p.description||'Construction').substring(0,120)+'<br><span style="color:#888">'+p.address+' · '+p.date+'</span></div>');
|
||
});
|
||
},100);
|
||
}).catch(function(e){
|
||
var el=document.getElementById('market');
|
||
el.textContent='Market data unavailable: '+e.message;
|
||
});
|
||
}
|
||
|
||
// ─── Learning Loop ───
|
||
// Real recruiter actions feed the Phase 19 feedback chain directly:
|
||
// Call/SMS → /log → /vectors/playbook_memory/seed (positive endorsement)
|
||
// No-show → /log_failure → /vectors/playbook_memory/mark_failed (penalty)
|
||
// Every click trains the system; the next search boosts/dampens accordingly.
|
||
function logAction(workerData, kind, btnEl){
|
||
if(!workerData)return;
|
||
var role=workerData.role||'Worker';
|
||
var city=(workerData.loc||'').split(',')[0].trim();
|
||
var state=(workerData.loc||'').split(',').pop().trim();
|
||
if(!city||!state){flashBtn(btnEl,'no geo');return;}
|
||
var op='fill: '+role+' x1 in '+city+', '+state;
|
||
if(kind==='failure'){
|
||
fetch(A+'/log_failure',{method:'POST',headers:{'Content-Type':'application/json'},
|
||
body:JSON.stringify({operation:op,failed_names:[workerData.nm],reason:'marked no-show via UI'})
|
||
}).then(function(r){return r.json()}).then(function(d){
|
||
flashBtn(btnEl, d&&d.marked?'Flagged':'Ghost');
|
||
loadLearning();
|
||
}).catch(function(){flashBtn(btnEl,'err')});
|
||
} else {
|
||
fetch(A+'/log',{method:'POST',headers:{'Content-Type':'application/json'},
|
||
body:JSON.stringify({operation:op,approach:kind+' from UI',
|
||
result:'1/1 filled → '+workerData.nm,
|
||
context:'client=ui query='+(lastQuery||'(direct)').slice(0,40)})
|
||
}).then(function(r){return r.json()}).then(function(d){
|
||
flashBtn(btnEl, d&&d.seeded?'Logged':'Ghost');
|
||
loadLearning();
|
||
}).catch(function(){flashBtn(btnEl,'err')});
|
||
}
|
||
}
|
||
function flashBtn(btn,label){
|
||
if(!btn)return;
|
||
var old=btn.textContent;btn.textContent=label;btn.disabled=true;
|
||
setTimeout(function(){btn.textContent=old;btn.disabled=false},1400);
|
||
}
|
||
// Back-compat shim — any legacy caller still pointing at logSelection.
|
||
function logSelection(workerData){ logAction(workerData, 'call', null); }
|
||
|
||
function loadLearning(){
|
||
api('/intelligence/activity',{}).then(function(d){
|
||
var el=document.getElementById('learning');
|
||
el.textContent='';
|
||
var total=d.total_operations||0;
|
||
if(total===0&&(!d.playbooks||!d.playbooks.length))return; // nothing to show yet
|
||
|
||
var card=document.createElement('div');card.className='insight info';
|
||
var lb=document.createElement('div');lb.className='label';lb.textContent='SYSTEM LEARNING';
|
||
var hl=document.createElement('div');hl.className='headline';hl.textContent='The System Gets Smarter With Every Use';
|
||
var sub=document.createElement('div');sub.className='sub';
|
||
sub.textContent='Every search, every placement, every simulation teaches the system what works. '+total+' operations logged so far.';
|
||
card.appendChild(lb);card.appendChild(hl);card.appendChild(sub);
|
||
|
||
// Stats row
|
||
var stats=document.createElement('div');stats.style.cssText='display:flex;gap:16px;margin-bottom:12px';
|
||
addLearnStat(stats,d.fill_count||0,'Contract Fills','#3fb950');
|
||
addLearnStat(stats,d.search_count||0,'Searches','#58a6ff');
|
||
addLearnStat(stats,(d.learned_patterns||[]).length,'Patterns','#bc8cff');
|
||
card.appendChild(stats);
|
||
|
||
// Learned patterns
|
||
if(d.learned_patterns&&d.learned_patterns.length){
|
||
var ph=document.createElement('div');ph.style.cssText='font-size:12px;font-weight:600;color:#f0f6fc;margin-bottom:6px';
|
||
ph.textContent='Learned Search Patterns';card.appendChild(ph);
|
||
d.learned_patterns.slice(0,5).forEach(function(p){
|
||
var row=document.createElement('div');row.style.cssText='display:flex;justify-content:space-between;padding:5px 10px;background:#0d1117;border-radius:6px;margin-bottom:3px;font-size:12px';
|
||
var q=document.createElement('span');q.style.color='#c9d1d9';q.textContent='"'+p.query+'"';
|
||
var c=document.createElement('span');c.style.cssText='color:#58a6ff;font-weight:600';c.textContent=p.times+'x';
|
||
row.appendChild(q);row.appendChild(c);card.appendChild(row);
|
||
});
|
||
}
|
||
|
||
// Recent activity feed
|
||
if(d.playbooks&&d.playbooks.length){
|
||
var ah=document.createElement('div');ah.style.cssText='font-size:12px;font-weight:600;color:#f0f6fc;margin:10px 0 6px';
|
||
ah.textContent='Recent Activity';card.appendChild(ah);
|
||
d.playbooks.slice(0,5).forEach(function(p){
|
||
var row=document.createElement('div');row.style.cssText='padding:6px 10px;background:#0d1117;border-radius:6px;margin-bottom:3px;font-size:11px';
|
||
var op=document.createElement('div');op.style.color='#f0f6fc';op.textContent=p.operation||'';
|
||
var det=document.createElement('div');det.style.cssText='color:#484f58;margin-top:2px';
|
||
det.textContent=(p.result||'')+(p.context?' · '+p.context:'');
|
||
var ts=document.createElement('div');ts.style.cssText='color:#2d333b;font-size:10px;margin-top:2px';
|
||
ts.textContent=p.timestamp?new Date(p.timestamp).toLocaleString():'';
|
||
row.appendChild(op);row.appendChild(det);row.appendChild(ts);card.appendChild(row);
|
||
});
|
||
}
|
||
|
||
// Explainer
|
||
var ex=document.createElement('div');ex.style.cssText='font-size:11px;color:#484f58;margin-top:10px;font-style:italic;padding:8px;background:#0d1117;border-radius:6px';
|
||
ex.textContent='Every time you search and select a worker, the system records what worked. Over time, it learns which workers are best for which situations — turning your decisions into institutional knowledge that never leaves when a staffer does.';
|
||
card.appendChild(ex);
|
||
|
||
el.appendChild(card);
|
||
}).catch(function(){});
|
||
}
|
||
|
||
function addLearnStat(parent,n,label,color){
|
||
var d=document.createElement('div');d.style.cssText='text-align:center;flex:1';
|
||
var num=document.createElement('div');num.style.cssText='font-size:24px;font-weight:800;color:'+color;num.textContent=n;
|
||
var lb=document.createElement('div');lb.style.cssText='font-size:10px;color:#484f58';lb.textContent=label;
|
||
d.appendChild(num);d.appendChild(lb);parent.appendChild(d);
|
||
}
|
||
</script></body></html>
|