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:
root
2026-04-17 20:12:01 -05:00
co-authored by Claude Opus 4.6
parent b16e485be1
commit 9acbe5c369
2 changed files with 135 additions and 1 deletions
+76
View File
@@ -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();