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 { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { startTrace, logSpan, logGeneration, scoreTrace, flush as flushTraces } from "./tracing.js";
|
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 BASE = process.env.LAKEHOUSE_URL || "http://localhost:3100";
|
||||||
const PORT = parseInt(process.env.MCP_PORT || "3700");
|
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"));
|
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
|
// Intelligence: Market data — public building permits → staffing demand forecast
|
||||||
if (url.pathname === "/intelligence/market" && req.method === "POST") {
|
if (url.pathname === "/intelligence/market" && req.method === "POST") {
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
@ -1232,8 +1288,12 @@ async function main() {
|
|||||||
const permitUrl = "https://data.cityofchicago.org/resource/ydr8-5enu.json";
|
const permitUrl = "https://data.cityofchicago.org/resource/ydr8-5enu.json";
|
||||||
// Recent + substantial permits only — skip tiny ones that
|
// Recent + substantial permits only — skip tiny ones that
|
||||||
// don't imply real staffing demand.
|
// 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(
|
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'`
|
+ `$where=reported_cost>250000 AND issue_date>'2025-06-01'`
|
||||||
+ `&$order=issue_date DESC&$limit=6`
|
+ `&$order=issue_date DESC&$limit=6`
|
||||||
).then(r => r.json()).catch(() => []);
|
).then(r => r.json()).catch(() => []);
|
||||||
@ -1367,12 +1427,19 @@ async function main() {
|
|||||||
|
|
||||||
contracts.push({
|
contracts.push({
|
||||||
permit: {
|
permit: {
|
||||||
|
id: p.id,
|
||||||
cost,
|
cost,
|
||||||
work_type: p.work_type || "General construction",
|
work_type: p.work_type || "General construction",
|
||||||
description: (p.work_description || "").substring(0, 140),
|
description: (p.work_description || "").substring(0, 140),
|
||||||
address: `${p.street_number || ""} ${p.street_direction || ""} ${p.street_name || ""}`.trim(),
|
address: `${p.street_number || ""} ${p.street_direction || ""} ${p.street_name || ""}`.trim(),
|
||||||
community_area: p.community_area,
|
community_area: p.community_area,
|
||||||
issue_date: (p.issue_date || "").substring(0, 10),
|
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,
|
implied_bill_rate: contractBillRate,
|
||||||
timeline: {
|
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
|
// Removed 2026-04-20: /intelligence/learn was a legacy CSV writer
|
||||||
// that destructively re-wrote successful_playbooks. /log and
|
// that destructively re-wrote successful_playbooks. /log and
|
||||||
// /log_failure replace it cleanly via /vectors/playbook_memory/seed
|
// /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