From bb1b471c674bfe4582a253ff7fa9303efbe020ee Mon Sep 17 00:00:00 2001 From: root Date: Mon, 20 Apr 2026 17:24:17 -0500 Subject: [PATCH] Predictive staffing forecast + per-contract timeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit J's ask: move the system from retrospective ranking to predictive anticipation. Show it tracks the clock, not just the roster. New endpoint /intelligence/staffing_forecast: - Pulls 30-day Chicago permit window (200 permits) - Maps work_type → role via industry heuristic - Aggregates predicted worker demand per role - Joins IL bench supply (workers_500k state='IL' group by role) - Computes coverage_pct, reliable_coverage_pct - Classifies risk: critical/tight/watch/ok - Computes earliest staffing deadline per role (permit issue_date + 31d = 45d construction start - 14d window) - Surfaces recent Chicago playbook ops for the role-specific memory New UI 'Staffing Forecast' section ABOVE Live Contracts: - Top card: total construction value, permit count, workers needed, critical/tight role count - Per-role rows: demand vs available supply, coverage %, deadline with red/amber/green urgency coloring Per-contract timeline on Live Contracts: - estimated_construction_start, staffing_window_opens, days_to_deadline - urgency classification: overdue/urgent/soon/scheduled - card border colored by urgency - timeline line explicitly shows recruiter: OVERDUE/URGENT + days count This is the 'system already thinks about when, not just who' surface J was asking for. CRMs store; this anticipates. --- mcp-server/index.ts | 136 +++++++++++++++++++++++++++++++++++++++++ mcp-server/search.html | 75 +++++++++++++++++++++-- 2 files changed, 207 insertions(+), 4 deletions(-) diff --git a/mcp-server/index.ts b/mcp-server/index.ts index 5198847..8945ba4 100644 --- a/mcp-server/index.ts +++ b/mcp-server/index.ts @@ -1288,6 +1288,122 @@ tr:hover{background:#111827} } } + // Predictive staffing forecast — aggregate demand inferred from + // recent Chicago permits, compared to our bench supply. Answers + // "what's coming in the next 30-60 days and can we cover it?" + // — the contextual-awareness dimension beyond retrospective rank. + if (url.pathname === "/intelligence/staffing_forecast" && req.method === "POST") { + const start = Date.now(); + try { + const permitUrl = "https://data.cityofchicago.org/resource/ydr8-5enu.json"; + // Last 30 days of permits — that's our forward demand window + const thirtyDaysAgo = new Date(Date.now() - 30 * 86400e3).toISOString().slice(0, 10); + const permits: any[] = await fetch( + `${permitUrl}?$select=work_type,reported_cost,issue_date` + + `&$where=reported_cost>100000 AND issue_date>'${thirtyDaysAgo}'` + + `&$limit=200` + ).then(r => r.json()).catch(() => []); + + // Construction heuristic: permit filing → construction start + // averages ~45 days. Staffing window opens 14 days before. + const typeToRole: Record = { + "Electrical Work": "Electrician", + "Masonry Work": "Production Worker", + "Mechanical Work": "Maintenance Tech", + "Reroofing": "Production Worker", + "Plumbing Work": "Maintenance Tech", + }; + + // Aggregate demand by role + const demandByRole: Record = {}; + for (const p of permits) { + const role = typeToRole[p.work_type || ""] || "Production Worker"; + const cost = parseFloat(p.reported_cost || 0); + const workers = Math.max(2, Math.min(Math.round(cost / 150000), 8)); + const issueDate = new Date(p.issue_date); + const stagingDate = new Date(issueDate.getTime() + 31 * 86400e3); // 45d - 14d window + if (!demandByRole[role]) { + demandByRole[role] = { permits: 0, total_cost: 0, est_workers: 0, + earliest_need: stagingDate.toISOString().slice(0, 10) }; + } + demandByRole[role].permits += 1; + demandByRole[role].total_cost += cost; + demandByRole[role].est_workers += workers; + const cur = new Date(demandByRole[role].earliest_need); + if (stagingDate < cur) demandByRole[role].earliest_need = stagingDate.toISOString().slice(0, 10); + } + + // Bench supply in IL + const benchR = await api("POST", "/query/sql", { + sql: `SELECT role, COUNT(*) as total, ` + + `SUM(CASE WHEN CAST(availability AS DOUBLE) > 0.5 THEN 1 ELSE 0 END) as available, ` + + `SUM(CASE WHEN CAST(reliability AS DOUBLE) > 0.8 THEN 1 ELSE 0 END) as reliable ` + + `FROM workers_500k WHERE state = 'IL' ` + + `GROUP BY role`, + }); + const bench: Record = {}; + for (const r of (benchR.rows || [])) bench[r.role] = r; + + // Past playbook fill-speed + success signal per role + const playbookR = await api("POST", "/query/sql", { + sql: `SELECT operation, COUNT(*) as fills ` + + `FROM successful_playbooks_live ` + + `WHERE operation LIKE '%Chicago, IL%' ` + + `GROUP BY operation ORDER BY fills DESC LIMIT 20`, + }); + const recentChicagoOps = playbookR.rows || []; + + // Build forecast entries with risk flag + const forecast: any[] = []; + for (const [role, d] of Object.entries(demandByRole)) { + const b = bench[role] || { total: 0, available: 0, reliable: 0 }; + const coverage = d.est_workers > 0 ? Math.round((b.available / d.est_workers) * 100) : 999; + const reliable_coverage = d.est_workers > 0 ? Math.round((b.reliable / d.est_workers) * 100) : 999; + let risk = "ok"; + if (coverage < 100) risk = "critical"; + else if (coverage < 300) risk = "tight"; + else if (reliable_coverage < 200) risk = "watch"; + // Days until earliest staffing deadline + const days_to_deadline = Math.round((new Date(d.earliest_need).getTime() - Date.now()) / 86400e3); + forecast.push({ + role, + demand_permits: d.permits, + demand_workers: d.est_workers, + demand_total_cost: d.total_cost, + earliest_staffing_deadline: d.earliest_need, + days_to_deadline, + bench_total: b.total, + bench_available: b.available, + bench_reliable: b.reliable, + coverage_pct: Math.min(coverage, 9999), + reliable_coverage_pct: Math.min(reliable_coverage, 9999), + risk, + }); + } + forecast.sort((a, b) => { + const order: Record = { critical: 0, tight: 1, watch: 2, ok: 3 }; + if (order[a.risk] !== order[b.risk]) return order[a.risk] - order[b.risk]; + return a.days_to_deadline - b.days_to_deadline; + }); + + return ok({ + generated_at: new Date().toISOString(), + window_days: 30, + permit_count: permits.length, + total_cost: permits.reduce((s, p) => s + parseFloat(p.reported_cost || 0), 0), + total_estimated_workers: forecast.reduce((s, f) => s + f.demand_workers, 0), + critical_roles: forecast.filter(f => f.risk === "critical").length, + tight_roles: forecast.filter(f => f.risk === "tight").length, + forecast, + recent_chicago_operations: recentChicagoOps, + duration_ms: Date.now() - start, + note: "Demand inferred from Chicago permit filings last 30 days. Construction starts ~45d after permit. Staffing window opens ~14d before construction. Supply = IL bench in workers_500k.", + }); + } catch (e: any) { + return err(`staffing_forecast: ${e.message}`, 500); + } + } + // Intelligence: Chicago permits → assumed staffing contracts with // Phase 19-ranked candidates and Path-2 discovered patterns. Each // card pairs a REAL permit (live from data.cityofchicago.org) with @@ -1357,6 +1473,20 @@ tr:hover{background:#111827} }; }); + // Timeline heuristic — permits filed now → construction + // starts ~45d later → staffing window opens ~14d before + // start. days_to_deadline is negative when we're past the + // window (fill urgency is imminent). + const issueDate = new Date(p.issue_date || Date.now()); + const estStart = new Date(issueDate.getTime() + 45 * 86400e3); + const stagingDate = new Date(issueDate.getTime() + 31 * 86400e3); + const daysToDeadline = Math.round((stagingDate.getTime() - Date.now()) / 86400e3); + let urgency = "scheduled"; + if (daysToDeadline < 0) urgency = "overdue"; + else if (daysToDeadline <= 7) urgency = "urgent"; + else if (daysToDeadline <= 21) urgency = "soon"; + else urgency = "scheduled"; + contracts.push({ permit: { cost, @@ -1366,6 +1496,12 @@ tr:hover{background:#111827} community_area: p.community_area, issue_date: (p.issue_date || "").substring(0, 10), }, + timeline: { + estimated_construction_start: estStart.toISOString().slice(0, 10), + staffing_window_opens: stagingDate.toISOString().slice(0, 10), + days_to_deadline: daysToDeadline, + urgency, + }, proposed: { role, count, diff --git a/mcp-server/search.html b/mcp-server/search.html index 56e9e86..e228907 100644 --- a/mcp-server/search.html +++ b/mcp-server/search.html @@ -112,6 +112,14 @@ body{font-family:'Inter',-apple-system,system-ui,'Segoe UI',sans-serif;backgroun
Analyzing contracts and workers...
+
+
+ Staffing Forecast — Next 30 Days + +
+
Loading forecast...
+
+
Live Contracts — Chicago Permits → Proposed Fills @@ -155,7 +163,56 @@ 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();loadLiveContracts();loadMarket();loadLearning()}); +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()}) @@ -171,16 +228,26 @@ function loadLiveContracts(){ el.textContent='No permits returned.';return; } r.contracts.forEach(function(c){ - var p=c.permit||{}, prop=c.proposed||{}; + 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 #388bfd'; + 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 · '+(p.issue_date||''); + 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;