diff --git a/mcp-server/index.ts b/mcp-server/index.ts index 3a74d80..b6f6fed 100644 --- a/mcp-server/index.ts +++ b/mcp-server/index.ts @@ -1034,6 +1034,82 @@ tr:hover{background:#111827} return new Response(Bun.file(import.meta.dir + "/console.html")); } + // Intelligence: Market data — public building permits → staffing demand forecast + if (url.pathname === "/intelligence/market" && req.method === "POST") { + const start = Date.now(); + try { + // Fetch Chicago building permits (public Socrata API — real data) + const permitUrl = "https://data.cityofchicago.org/resource/ydr8-5enu.json"; + const [bigR, byTypeR, recentR, benchR] = await Promise.all([ + // Top 8 largest permits by cost + fetch(`${permitUrl}?$select=permit_type,work_type,work_description,reported_cost,street_number,street_direction,street_name,community_area,issue_date,latitude,longitude&$where=reported_cost>1000000 AND issue_date>'2025-06-01'&$order=reported_cost DESC&$limit=8`).then(r => r.json()), + // Permits grouped by work type + fetch(`${permitUrl}?$select=work_type,count(*) as cnt,sum(reported_cost) as total_cost&$where=reported_cost>10000 AND issue_date>'2025-06-01'&$group=work_type&$order=total_cost DESC&$limit=10`).then(r => r.json()), + // Most recent permits + fetch(`${permitUrl}?$select=work_type,work_description,reported_cost,street_name,issue_date&$where=reported_cost>50000&$order=issue_date DESC&$limit=5`).then(r => r.json()), + // Our worker bench in IL (cross-reference) + api("POST", "/query/sql", { sql: "SELECT role, COUNT(*) supply, SUM(CASE WHEN CAST(reliability AS DOUBLE)>0.8 THEN 1 ELSE 0 END) reliable, SUM(CASE WHEN CAST(availability AS DOUBLE)>0.5 THEN 1 ELSE 0 END) available FROM workers_500k WHERE state='IL' GROUP BY role ORDER BY supply DESC" }), + ]); + + // Map construction types to staffing roles + const typeToRoles: Record = { + "Electrical Work": ["Electrician","Maintenance Tech"], + "Masonry Work": ["Production Worker","Loader","Material Handler"], + "Mechanical Work": ["Maintenance Tech","Machine Operator","Welder"], + "Reroofing": ["Production Worker","Loader"], + "Plumbing Work": ["Maintenance Tech"], + "": ["Forklift Operator","Loader","Material Handler","Production Worker","Warehouse Associate"], + }; + + // Build demand forecast from permit types + const forecast: any[] = []; + for (const t of (byTypeR || [])) { + const wtype = t.work_type || "(general construction)"; + const totalCost = parseFloat(t.total_cost || 0); + const cnt = parseInt(t.cnt || 0); + const estWorkers = Math.round(totalCost / 150000); // industry heuristic + const roles = typeToRoles[t.work_type || ""] || typeToRoles[""]; + forecast.push({ work_type: wtype, permits: cnt, total_cost: totalCost, estimated_workers: estWorkers, needed_roles: roles }); + } + + // Cross-reference with our bench + const ilBench = (benchR.rows || []).reduce((m: any, r: any) => { m[r.role] = r; return m; }, {}); + const gaps: any[] = []; + for (const f of forecast) { + for (const role of f.needed_roles) { + const b = ilBench[role]; + if (b) { + const coverage = Math.round((b.available / Math.max(f.estimated_workers, 1)) * 100); + gaps.push({ role, demand: f.estimated_workers, supply: b.supply, available: b.available, reliable: b.reliable, coverage_pct: Math.min(coverage, 999), source: f.work_type }); + } + } + } + + return ok({ + major_permits: (bigR || []).map((p: any) => ({ + cost: parseFloat(p.reported_cost || 0), + description: (p.work_description || "").substring(0, 100), + address: `${p.street_number || ""} ${p.street_direction || ""} ${p.street_name || ""}`.trim(), + type: p.work_type || p.permit_type || "", + date: (p.issue_date || "").substring(0, 10), + lat: p.latitude, lng: p.longitude, + })), + by_type: forecast, + recent: (recentR || []).map((p: any) => ({ + type: p.work_type || "", description: (p.work_description || "").substring(0, 80), + cost: parseFloat(p.reported_cost || 0), street: p.street_name || "", date: (p.issue_date || "").substring(0, 10), + })), + il_bench: benchR.rows || [], + gaps, + total_construction_value: forecast.reduce((s: number, f: any) => s + f.total_cost, 0), + total_estimated_workers: forecast.reduce((s: number, f: any) => s + f.estimated_workers, 0), + duration_ms: Date.now() - start, + }); + } catch (e: any) { + return ok({ error: e.message, duration_ms: Date.now() - start }); + } + } + // Intelligence: Log a search → selection as a learned pattern if (url.pathname === "/intelligence/learn" && req.method === "POST") { const b = await json(); diff --git a/mcp-server/search.html b/mcp-server/search.html index cf33a7c..b9f16d7 100644 --- a/mcp-server/search.html +++ b/mcp-server/search.html @@ -56,6 +56,7 @@ body{font-family:-apple-system,system-ui,sans-serif;background:#0b0f19;color:#c9
Analyzing your contracts and workers...
+
Search workers...
@@ -70,7 +71,7 @@ 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();loadLearning()}); +window.addEventListener('load',function(){loadDay();loadMarket();loadLearning()}); 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()}) @@ -602,6 +603,63 @@ function doSearch(){ }).catch(function(e){out.textContent='Error: '+e.message}); } +// ─── Market Intelligence ─── +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'; + var lb=document.createElement('div');lb.className='label';lb.textContent='MARKET INTELLIGENCE'; + var hl=document.createElement('div');hl.className='headline';hl.textContent='Chicago Construction Pipeline — Live Permit Data'; + var sub=document.createElement('div');sub.className='sub'; + sub.textContent='$'+(d.total_construction_value/1e9).toFixed(1)+'B in active permits → estimated '+d.total_estimated_workers.toLocaleString()+' workers needed. Source: City of Chicago Open Data'; + card.appendChild(lb);card.appendChild(hl);card.appendChild(sub); + + // Major permits + if(d.major_permits&&d.major_permits.length){ + 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); + }); + } + + // Supply gaps — where demand exceeds our bench + 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'; + var color=g.available>g.demand?'#3fb950':'#d29922'; + nums.style.color=color; + nums.textContent=g.available.toLocaleString()+' available / '+g.reliable.toLocaleString()+' reliable ('+g.supply.toLocaleString()+' total)'; + row.appendChild(role);row.appendChild(nums);card.appendChild(row); + }); + } + + // Insight callout + 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='This data updates from live city records. When a $50M warehouse gets permitted, you\'ll know about it here before the contractor calls looking for workers. The system cross-references permits with your worker bench to show where you\'re covered and where to recruit.'; + card.appendChild(insight); + + el.appendChild(card); + }).catch(function(){}); +} + // ─── Learning Loop ─── function logSelection(workerData){ if(!lastQuery||!workerData)return;