Market Intelligence: live Chicago building permits → staffing demand forecast
/intelligence/market pulls real permit data from Chicago Open Data API: - $9.6B in active construction permits - O'Hare expansion ($730M), new casino ($580M), transit station ($445M) - Maps permit types to staffing roles (electrical→Electrician, masonry→Loader) - Cross-references with our IL worker bench to show coverage gaps - Electrician gap: only 1,036 reliable vs 63K estimated demand Datalake page now shows three intelligence layers: 1. Contract simulation with scenario-driven matching 2. Market Intelligence with live permit data + bench analysis 3. System Learning with fill history and detected patterns The staffing company sees demand forming before the phone rings. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b16e485be1
commit
9acbe5c369
@ -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<string, string[]> = {
|
||||
"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();
|
||||
|
||||
@ -56,6 +56,7 @@ body{font-family:-apple-system,system-ui,sans-serif;background:#0b0f19;color:#c9
|
||||
<div class="content">
|
||||
<div id="main"><div class="ld">Analyzing your contracts and workers...</div></div>
|
||||
|
||||
<div id="market"></div>
|
||||
<div id="learning"></div>
|
||||
|
||||
<details class="sa"><summary>Search workers...</summary><div class="inner">
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user