mcp: contractor entity-brief drill-down + mobile UX pass
Adds /contractor page route plus /intelligence/contractor_profile endpoint that fans out across OSHA, ticker, history, parent_link, federal contracts, debarment, NLRB, ILSOS, news, diversity certs, BLS macro — single per-contractor portfolio view across every wired source. search.html: mobile responsive layout, fixed bottom dock with horizontal scroll-snap, legacy bridge row stacking, viewport overflow guards. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a6c83b03e5
commit
b843a23433
@ -18,6 +18,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
||||
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
||||
import { z } from "zod";
|
||||
import { startTrace, logSpan, logGeneration, scoreTrace, flush as flushTraces } from "./tracing.js";
|
||||
import { buildPermitBrief } from "./entity.js";
|
||||
|
||||
const BASE = process.env.LAKEHOUSE_URL || "http://localhost:3100";
|
||||
const PORT = parseInt(process.env.MCP_PORT || "3700");
|
||||
@ -960,6 +961,61 @@ async function main() {
|
||||
return new Response(Bun.file(import.meta.dir + "/console.html"));
|
||||
}
|
||||
|
||||
// ─── Contractor / entity drill-down page ───
|
||||
// Single-contractor portfolio view across every wired source:
|
||||
// OSHA national, Chicago history, ticker chart, parent link,
|
||||
// federal contracts, debarment, unions, training. Click any
|
||||
// contractor name in a permit Entity Brief to land here.
|
||||
if (url.pathname === "/contractor") {
|
||||
return new Response(Bun.file(import.meta.dir + "/contractor.html"), {
|
||||
headers: { ...cors, "Content-Type": "text/html" },
|
||||
});
|
||||
}
|
||||
if (url.pathname === "/intelligence/contractor_profile" && req.method === "POST") {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const b = (await req.json().catch(() => ({}))) as { name?: string };
|
||||
if (!b.name) return err("missing name", 400);
|
||||
// Use the entity-brief library directly — single entity, all sources.
|
||||
const { fetchOshaBrief, fetchTickerBrief, fetchContractorHistory, fetchParentLink, fetchFederalContracts, fetchDebarmentBrief, fetchNlrbBriefReal, fetchIlsosBrief, fetchNewsMentions, fetchDiversityCerts, scoreNewsSentiment, fetchBlsConstructionTrend, normalizeEntityName, entityTicker } = await import("./entity.js");
|
||||
const [osha, stock, history, parent_link, federal, debarment, nlrb, ilsos, news, diversity, macro] = await Promise.all([
|
||||
fetchOshaBrief(b.name),
|
||||
fetchTickerBrief(b.name),
|
||||
fetchContractorHistory(b.name),
|
||||
fetchParentLink(b.name),
|
||||
fetchFederalContracts(b.name),
|
||||
fetchDebarmentBrief(b.name),
|
||||
fetchNlrbBriefReal(b.name),
|
||||
fetchIlsosBrief(b.name),
|
||||
fetchNewsMentions(b.name),
|
||||
fetchDiversityCerts(b.name),
|
||||
fetchBlsConstructionTrend(),
|
||||
]);
|
||||
const news_sentiment = news ? scoreNewsSentiment(news) : null;
|
||||
return ok({
|
||||
key: normalizeEntityName(b.name),
|
||||
display_name: b.name,
|
||||
ticker: entityTicker(b.name),
|
||||
osha,
|
||||
stock,
|
||||
history,
|
||||
parent_link,
|
||||
federal,
|
||||
debarment,
|
||||
nlrb,
|
||||
ilsos,
|
||||
news,
|
||||
news_sentiment,
|
||||
diversity,
|
||||
macro,
|
||||
generated_at: new Date().toISOString(),
|
||||
duration_ms: Date.now() - start,
|
||||
});
|
||||
} catch (e: any) {
|
||||
return err(`contractor_profile: ${e.message}`, 500);
|
||||
}
|
||||
}
|
||||
|
||||
// Intelligence: Market data — public building permits → staffing demand forecast
|
||||
if (url.pathname === "/intelligence/market" && req.method === "POST") {
|
||||
const start = Date.now();
|
||||
@ -1232,8 +1288,12 @@ async function main() {
|
||||
const permitUrl = "https://data.cityofchicago.org/resource/ydr8-5enu.json";
|
||||
// Recent + substantial permits only — skip tiny ones that
|
||||
// don't imply real staffing demand.
|
||||
// Include contact_1 + contact_2 fields so the Entity Brief
|
||||
// panel on each card can populate without a second fetch.
|
||||
// Contacts identify the applicant / contractor by name —
|
||||
// those are the keys we pass to OSHA/ILSOS enrichment.
|
||||
const permits: any[] = await fetch(
|
||||
`${permitUrl}?$select=permit_type,work_type,work_description,reported_cost,street_number,street_direction,street_name,community_area,issue_date&`
|
||||
`${permitUrl}?$select=id,permit_type,work_type,work_description,reported_cost,street_number,street_direction,street_name,community_area,issue_date,contact_1_name,contact_1_type,contact_2_name,contact_2_type&`
|
||||
+ `$where=reported_cost>250000 AND issue_date>'2025-06-01'`
|
||||
+ `&$order=issue_date DESC&$limit=6`
|
||||
).then(r => r.json()).catch(() => []);
|
||||
@ -1367,12 +1427,19 @@ async function main() {
|
||||
|
||||
contracts.push({
|
||||
permit: {
|
||||
id: p.id,
|
||||
cost,
|
||||
work_type: p.work_type || "General construction",
|
||||
description: (p.work_description || "").substring(0, 140),
|
||||
address: `${p.street_number || ""} ${p.street_direction || ""} ${p.street_name || ""}`.trim(),
|
||||
community_area: p.community_area,
|
||||
issue_date: (p.issue_date || "").substring(0, 10),
|
||||
// Contacts — used by /intelligence/permit_entities to
|
||||
// enrich each card with OSHA + ILSOS on expand.
|
||||
contact_1_name: p.contact_1_name || "",
|
||||
contact_1_type: p.contact_1_type || "",
|
||||
contact_2_name: p.contact_2_name || "",
|
||||
contact_2_type: p.contact_2_type || "",
|
||||
},
|
||||
implied_bill_rate: contractBillRate,
|
||||
timeline: {
|
||||
@ -1426,6 +1493,58 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Intelligence: per-permit entity brief — OSHA + ILSOS + property
|
||||
// Takes a permit identifier (we look it up from Chicago Socrata) or
|
||||
// raw contact fields directly from the client. Returns an "ETF
|
||||
// basket" shape: property + entities + per-entity risk factors.
|
||||
// OSHA is live-scraped (cached 30d). ILSOS returns a structured
|
||||
// placeholder because apps.ilsos.gov blocks our ASN.
|
||||
if (url.pathname === "/intelligence/permit_entities" && req.method === "POST") {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const b = await req.json().catch(() => ({})) as {
|
||||
permit_id?: string;
|
||||
address?: string;
|
||||
work_type?: string;
|
||||
contact_1_name?: string;
|
||||
contact_1_type?: string;
|
||||
contact_2_name?: string;
|
||||
contact_2_type?: string;
|
||||
fetch_osha?: boolean;
|
||||
fetch_ilsos?: boolean;
|
||||
};
|
||||
// If the caller didn't pass contact fields but did pass a
|
||||
// permit_id, go pull the record from Chicago Socrata.
|
||||
let permit = b;
|
||||
if (b.permit_id && !b.contact_1_name) {
|
||||
const u = `https://data.cityofchicago.org/resource/ydr8-5enu.json?$where=id='${encodeURIComponent(b.permit_id)}'`;
|
||||
const rows = (await fetch(u).then((r) => r.json())) as any[];
|
||||
const p = rows?.[0];
|
||||
if (p) {
|
||||
const addr = [p.street_number, p.street_direction, p.street_name]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
permit = {
|
||||
permit_id: b.permit_id,
|
||||
address: addr,
|
||||
work_type: p.work_type,
|
||||
contact_1_name: p.contact_1_name,
|
||||
contact_1_type: p.contact_1_type,
|
||||
contact_2_name: p.contact_2_name,
|
||||
contact_2_type: p.contact_2_type,
|
||||
};
|
||||
}
|
||||
}
|
||||
const brief = await buildPermitBrief(permit, {
|
||||
fetchOsha: b.fetch_osha !== false,
|
||||
fetchIlsos: b.fetch_ilsos !== false,
|
||||
});
|
||||
return ok({ ...brief, duration_ms: Date.now() - start });
|
||||
} catch (e: any) {
|
||||
return err(`permit_entities: ${e.message}`, 500);
|
||||
}
|
||||
}
|
||||
|
||||
// Removed 2026-04-20: /intelligence/learn was a legacy CSV writer
|
||||
// that destructively re-wrote successful_playbooks. /log and
|
||||
// /log_failure replace it cleanly via /vectors/playbook_memory/seed
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user