From aa56fbce61d77feaa93e3ec45d2bc317e0c3876e Mon Sep 17 00:00:00 2001 From: root Date: Mon, 27 Apr 2026 22:19:26 -0500 Subject: [PATCH] =?UTF-8?q?demo:=20profiler=20=E2=80=94=20scrolling=20tick?= =?UTF-8?q?er=20basket=20with=20live=20prices=20+=20click-to-filter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit J asked: "kind of like a scrolling ticker that has all of the companies and their stock prices and where they fit in the map." Implemented as a horizontal-scroll strip at the top of /profiler: 9 public issuers in this view · quotes via Stooq · 669ms ┌────┬────┬────┬────┬────┬────┬────┐ │TGT │JPM │BALY│ACRE│FCBC│NREF│LSBK│ ← live price + day-change per │129 │311 │... │... │... │... │... │ ticker, color-banded by │+.17│+1.5│... │... │... │... │... │ attribution kind └────┴────┴────┴────┴────┴────┴────┘ Each card carries: - ticker + live price + day-change % (red/green) - attribution count + kind (exact / direct / parent / associated) - left bar color = strongest attribution kind (green for direct issuer, amber for parent, blue for co-permit associated, gradient when both direct and associated apply) - tooltip on hover lists the contractors attributed to this ticker - click toggles a filter on the table below — clicking TGT cuts the 200-row list down to just TARGET CORPORATION + TORNOW, KYLE F (Target's primary co-permit contractor) Server-side: - entity.ts exports fetchStooqQuote (was internal) - new POST /intelligence/ticker_quotes — accepts {tickers: [...]}, fans out to Stooq.us in parallel, returns {ticker, price, price_date, open, high, low, day_change_pct, stooq_url} per symbol or null for non-US listings (HOC.DE, SKA-B.ST, LLC.AX). Capped at 50 symbols per call. Front-end: - mcp-server/profiler.html — new .basket-wrap section above the controls. buildBasket() runs after profiler_index loads: 1. Aggregates unique tickers from .tickers.direct + .associated across all surfaced contractors 2. Renders shells immediately (ticker symbol + "—" placeholder) 3. Batch-fetches quotes via /intelligence/ticker_quotes 4. Updates each card with price + day-change in place Click on a card sets a tickerFilter; render() skips rows whose attributions don't include that ticker. "clear filter" button on the basket strip resets it. Verified end-to-end on devop.live/lakehouse/profiler: Default load → 9 issuers, live prices populated in 669ms TGT click → table filters to TARGET CORPORATION + TORNOW, KYLE F (the contractor who runs 3 of Target's recent permits gets the TGT correlation indicator) JPM card → $311.63, +1.55% — JPMorgan-adjacent contractors Tooltip → list of contractors attributed to the ticker --- mcp-server/entity.ts | 2 +- mcp-server/index.ts | 39 +++++++++++ mcp-server/profiler.html | 136 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 174 insertions(+), 3 deletions(-) diff --git a/mcp-server/entity.ts b/mcp-server/entity.ts index 26a6dd2..627dad0 100644 --- a/mcp-server/entity.ts +++ b/mcp-server/entity.ts @@ -1106,7 +1106,7 @@ async function fetchSecProfile(cik: number | string): Promise<{ // Stooq — free daily quote CSV. Format (column order from the response): // Symbol,Date,Time,Open,High,Low,Close,Volume -async function fetchStooqQuote(ticker: string): Promise<{ +export async function fetchStooqQuote(ticker: string): Promise<{ price?: number; price_date?: string; open?: number; diff --git a/mcp-server/index.ts b/mcp-server/index.ts index 6772b07..a18bc50 100644 --- a/mcp-server/index.ts +++ b/mcp-server/index.ts @@ -876,6 +876,45 @@ async function main() { } } + // Batch ticker quotes — used by the profiler page's scrolling + // ticker basket. Stooq's HTTP CSV API is single-symbol per call, + // so this fans out N tickers in parallel and returns a flat + // map. Non-US tickers (HOC.DE, SKA-B.ST, LLC.AX) won't have a + // Stooq.us entry; we surface that as null so the UI can render + // them with a "—" placeholder. + if (url.pathname === "/intelligence/ticker_quotes" && req.method === "POST") { + const start = Date.now(); + const b = await json(); + const tickers: string[] = Array.isArray(b.tickers) ? b.tickers.map((t: any) => String(t).toUpperCase()).filter(Boolean) : []; + if (!tickers.length) return ok({ quotes: {}, duration_ms: 0 }); + const { fetchStooqQuote } = await import("./entity.js"); + const dedup = Array.from(new Set(tickers)).slice(0, 50); + const results = await Promise.all(dedup.map(async (t) => { + // Skip non-US suffixes — Stooq.us won't have them + if (t.includes(".")) return [t, null] as const; + try { + const q = await fetchStooqQuote(t); + if (!q || !q.price) return [t, null] as const; + const change_pct = q.open && q.open > 0 ? ((q.price - q.open) / q.open) * 100 : null; + return [t, { + ticker: t, + price: q.price, + price_date: q.price_date, + open: q.open, + high: q.high, + low: q.low, + day_change_pct: change_pct, + stooq_url: `https://stooq.com/q/?s=${t.toLowerCase()}.us`, + }] as const; + } catch { + return [t, null] as const; + } + })); + const quotes: Record = {}; + for (const [t, q] of results) quotes[t] = q; + return ok({ quotes, count: dedup.length, duration_ms: Date.now() - start }); + } + // Profiler index — directory of every contractor that has filed // a Chicago permit recently, ranked by permit count + total // cost. Each name in the response links to the full /contractor diff --git a/mcp-server/profiler.html b/mcp-server/profiler.html index c89e124..039f718 100644 --- a/mcp-server/profiler.html +++ b/mcp-server/profiler.html @@ -36,6 +36,34 @@ td.role .pill{display:inline-block;padding:2px 7px;border-radius:9px;font-size:9 .ticker-pill.parent{background:#1a1410;border:1px solid #d2992288;color:#d29922} .ticker-pill.associated{background:#0d1830;border:1px solid #58a6ff66;color:#58a6ff} .ticker-pill.exact{background:#0d2818;border:1px solid #2ea043;color:#3fb950} + +/* Scrolling ticker basket — top strip showing every public issuer + the profiler index has surfaced, with live price + day-change. */ +.basket-wrap{background:#0a0d12;border:1px solid #171d27;border-radius:10px;margin-bottom:14px;overflow:hidden;position:relative} +.basket-label{padding:10px 16px 4px;font-size:10px;color:#545d68;text-transform:uppercase;letter-spacing:1.3px;font-weight:600;display:flex;justify-content:space-between;align-items:baseline} +.basket-label .meta{font-weight:400;color:#3d444d;font-size:10px;text-transform:none;letter-spacing:0} +.basket-track{display:flex;gap:0;overflow-x:auto;scroll-behavior:smooth;padding:6px 8px 12px;scrollbar-width:thin;scrollbar-color:#21262d transparent} +.basket-track::-webkit-scrollbar{height:6px} +.basket-track::-webkit-scrollbar-thumb{background:#21262d;border-radius:3px} +.basket-track::-webkit-scrollbar-thumb:hover{background:#388bfd} +.bk-card{flex:0 0 auto;min-width:140px;background:#0d1117;border:1px solid #21262d;border-radius:8px;padding:10px 12px;margin:0 4px;cursor:pointer;transition:all 0.12s;position:relative} +.bk-card:hover{border-color:#58a6ff;background:#0d1a30;transform:translateY(-1px)} +.bk-card.selected{border-color:#58a6ff;background:#0d1a30;box-shadow:0 0 0 1px #58a6ff;} +.bk-card .tk{font-family:ui-monospace,SFMono-Regular,monospace;font-size:13px;font-weight:700;color:#e6edf3;letter-spacing:0.3px} +.bk-card .px{font-family:ui-monospace,SFMono-Regular,monospace;font-size:14px;font-weight:600;color:#e6edf3;margin-top:3px;font-variant-numeric:tabular-nums} +.bk-card .ch{font-family:ui-monospace,monospace;font-size:11px;margin-top:1px;font-variant-numeric:tabular-nums} +.bk-card .ch.up{color:#3fb950} +.bk-card .ch.down{color:#f85149} +.bk-card .ch.flat{color:#545d68} +.bk-card .meta{font-size:9px;color:#545d68;margin-top:5px;text-transform:uppercase;letter-spacing:0.6px} +.bk-card .kind-bar{position:absolute;left:0;top:0;bottom:0;width:3px;border-radius:8px 0 0 8px} +.bk-card .kind-bar.exact,.bk-card .kind-bar.direct{background:#3fb950} +.bk-card .kind-bar.parent{background:#d29922} +.bk-card .kind-bar.associated{background:#58a6ff} +.bk-card .kind-bar.mixed{background:linear-gradient(180deg,#3fb950 0%,#58a6ff 100%)} +.bk-card.no-quote .px{color:#545d68} +.basket-empty{padding:18px;font-size:11px;color:#545d68;font-style:italic;text-align:center} +.basket-clear{margin-left:8px;font-size:10px;color:#58a6ff;cursor:pointer;border:none;background:none;text-decoration:underline} .cost-band-1{color:#3fb950} .cost-band-2{color:#d29922} .cost-band-3{color:#f85149} @@ -53,6 +81,13 @@ td.role .pill{display:inline-block;padding:2px 7px;border-radius:9px;font-size:9
+