Per docs/PHASE_1_6_BIPA_GATES.md Gate 4 + AUDIT_TRAIL_PRD §4 protected- attribute exclusion rule. The lookup tables + inference functions in search.html (3375-3499) and console.html (245-311) were dead code in the rendering path — headshot rendering disabled 2026-04-28 left these functions defined but unused. Removing them forecloses both Title VII discriminatory-feature-engineering AND BIPA biometric-information- derived-from-biometric-identifier arguments. Removed: - FEMALE_NAMES, MALE_NAMES, NAMES_HISPANIC, NAMES_BLACK, NAMES_SOUTH_ASIAN, NAMES_EAST_ASIAN, NAMES_MIDDLE_EASTERN - SURNAMES_HISPANIC, SURNAMES_SOUTH_ASIAN, SURNAMES_EAST_ASIAN, SURNAMES_MIDDLE_EASTERN, SURNAMES_BLACK - guessGenderFromFirstName(), guessEthnicityFromName(), guessEthnicityFromFirstName(), genderFor() From both search.html and console.html. Replacement: deprecation comment block referencing the BIPA gates doc. Verified: zero live consumers anywhere in mcp-server/. Searched for genderFor()/guessEthnicityFromName()/guessEthnicityFromFirstName()/ guessGenderFromFirstName() call sites — none remain. Per J 2026-05-03: this kind of test-code-leaked-into-main is exactly what J wants cleaned up. The face-pool inference was meant as a testing tool for synthetic icon generation but ended up as production-shape inference logic in the customer-facing UI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4130 lines
234 KiB
HTML
4130 lines
234 KiB
HTML
<!DOCTYPE html>
|
||
<html><head>
|
||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>Staffing Co-Pilot</title>
|
||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
|
||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||
<style>
|
||
*{margin:0;padding:0;box-sizing:border-box}
|
||
html,body{overflow-x:hidden;max-width:100vw}
|
||
body{font-family:'Inter',-apple-system,system-ui,'Segoe UI',sans-serif;background:#090c10;color:#b0b8c4;font-size:14px;line-height:1.6;-webkit-font-smoothing:antialiased}
|
||
|
||
/* Top bar */
|
||
.bar{background:#0d1117;padding:0 24px;height:56px;border-bottom:1px solid #171d27;display:flex;justify-content:space-between;align-items:center}
|
||
.bar h1{font-size:14px;font-weight:600;color:#e6edf3;letter-spacing:-0.2px}
|
||
.bar .rt{font-size:11px;color:#545d68}
|
||
.bar nav{display:flex;gap:2px}
|
||
.bar nav a{font-size:12px;color:#545d68;text-decoration:none;padding:6px 14px;border-radius:6px;transition:all 0.15s}
|
||
.bar nav a:hover{color:#e6edf3;background:#161b22}
|
||
.bar nav a.active{color:#e6edf3;background:#1c2333}
|
||
|
||
/* Layout */
|
||
.content{max-width:940px;margin:0 auto;padding:24px 20px 40px}
|
||
.section{margin-bottom:32px}
|
||
.section-header{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:14px}
|
||
.section-title{font-size:11px;font-weight:600;color:#545d68;text-transform:uppercase;letter-spacing:1.2px}
|
||
.section-meta{font-size:10px;color:#3d444d}
|
||
|
||
/* Cards */
|
||
.card{background:#0d1117;border:1px solid #171d27;border-radius:10px;padding:20px;margin-bottom:12px}
|
||
.card.accent-red{border-left:3px solid #da3633}
|
||
.card.accent-green{border-left:3px solid #2ea043}
|
||
.card.accent-amber{border-left:3px solid #bf8700}
|
||
.card.accent-blue{border-left:3px solid #388bfd}
|
||
.card .card-label{font-size:9px;font-weight:600;color:#545d68;text-transform:uppercase;letter-spacing:1.4px;margin-bottom:6px}
|
||
.card .card-title{font-size:17px;font-weight:600;color:#e6edf3;margin-bottom:3px;letter-spacing:-0.3px}
|
||
.card .card-sub{font-size:12px;color:#545d68;margin-bottom:14px;line-height:1.5}
|
||
|
||
/* Keep old class names working */
|
||
.insight{background:#0d1117;border:1px solid #171d27;border-radius:10px;padding:20px;margin-bottom:12px}
|
||
.insight .label{font-size:9px;font-weight:600;color:#545d68;text-transform:uppercase;letter-spacing:1.4px;margin-bottom:6px}
|
||
.insight .headline{font-size:17px;font-weight:600;color:#e6edf3;margin-bottom:3px;letter-spacing:-0.3px}
|
||
.insight .sub{font-size:12px;color:#545d68;margin-bottom:14px}
|
||
.insight.urgent{border-left:3px solid #da3633}
|
||
.insight.opportunity{border-left:3px solid #2ea043}
|
||
.insight.warning{border-left:3px solid #bf8700}
|
||
.insight.info{border-left:3px solid #388bfd}
|
||
|
||
/* Workers */
|
||
.iworker{display:flex;align-items:center;gap:12px;padding:10px 12px;background:#161b22;border-radius:8px;margin-bottom:4px;transition:background 0.15s;
|
||
/* Cards rise into place 4px and fade in over 350ms instead of
|
||
popping in as a single block. Combined with the staggered nth-
|
||
child delays below, you see a "rolling fill" top-to-bottom that
|
||
feels like the system is *placing* workers, not flooding them
|
||
onto your screen. The user keeps their bearings. */
|
||
animation: card-in 350ms cubic-bezier(.2,.7,.2,1) both;
|
||
}
|
||
@keyframes card-in {
|
||
from { opacity:0; transform:translateY(4px) }
|
||
to { opacity:1; transform:translateY(0) }
|
||
}
|
||
/* Stagger the first ~12 cards. After that everyone fires at the
|
||
final delay; doesn't matter because they're below the fold for
|
||
most viewports. */
|
||
.iworker:nth-child(1) { animation-delay: 0ms }
|
||
.iworker:nth-child(2) { animation-delay: 35ms }
|
||
.iworker:nth-child(3) { animation-delay: 70ms }
|
||
.iworker:nth-child(4) { animation-delay: 105ms }
|
||
.iworker:nth-child(5) { animation-delay: 140ms }
|
||
.iworker:nth-child(6) { animation-delay: 175ms }
|
||
.iworker:nth-child(7) { animation-delay: 210ms }
|
||
.iworker:nth-child(8) { animation-delay: 245ms }
|
||
.iworker:nth-child(9) { animation-delay: 280ms }
|
||
.iworker:nth-child(10){ animation-delay: 315ms }
|
||
.iworker:nth-child(n+11){ animation-delay: 350ms }
|
||
/* Honor reduced-motion preference — no rise, no stagger. */
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.iworker { animation: none }
|
||
}
|
||
.iworker:hover{background:#1c2333}
|
||
.iworker .av{width:40px;height:40px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:600;font-size:13px;color:#c9d1d9;flex-shrink:0;background:#161b22;border:1px solid #21262d;letter-spacing:0.5px;font-family:'Inter',-apple-system,sans-serif;overflow:hidden;position:relative}
|
||
.iworker .av img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;
|
||
/* B&W faces — sidesteps the SDXL Turbo color cast that screams
|
||
"AI generated" at small avatar sizes. Mild contrast lift +
|
||
half-pixel softening keeps faces readable without sharpening
|
||
the diffusion artifacts. If you want a hint of color back, drop
|
||
grayscale to 0.7 (subtle desaturation, retains warmth). */
|
||
filter: grayscale(1) contrast(1.02) brightness(1.04) blur(0.3px);
|
||
}
|
||
/* Cert icons — monochrome with a touch of contrast. B&W reads cleaner
|
||
at 14px and sidesteps the SDXL "diffusion-y" color cast. Lift
|
||
brightness a sliver to compensate for grayscale darkening warm tones. */
|
||
.cert-icon{filter: grayscale(1) contrast(0.96) brightness(1.05);}
|
||
.iworker .role-pill{display:inline-block;font-size:10px;padding:2px 8px;border-radius:3px;background:#0d1117;color:#8b949e;margin-right:8px;font-weight:600;letter-spacing:0.4px;text-transform:uppercase;border-left:2px solid #30363d}
|
||
.iworker[data-role-band="warehouse"]{border-left:3px solid #58a6ff}
|
||
.iworker[data-role-band="production"]{border-left:3px solid #d29922}
|
||
.iworker[data-role-band="trades"]{border-left:3px solid #bc8cff}
|
||
.iworker[data-role-band="driver"]{border-left:3px solid #3fb950}
|
||
.iworker[data-role-band="lead"]{border-left:3px solid #f0883e}
|
||
.iworker .role-pill[data-rb="warehouse"]{border-left-color:#58a6ff;color:#79c0ff}
|
||
.iworker .role-pill[data-rb="production"]{border-left-color:#d29922;color:#e3b341}
|
||
.iworker .role-pill[data-rb="trades"]{border-left-color:#bc8cff;color:#d2a8ff}
|
||
.iworker .role-pill[data-rb="driver"]{border-left-color:#3fb950;color:#56d364}
|
||
.iworker .role-pill[data-rb="lead"]{border-left-color:#f0883e;color:#ffa657}
|
||
.iworker .info{flex:1;min-width:0}
|
||
.iworker .nm{font-weight:600;color:#e6edf3;font-size:13px}
|
||
.iworker .detail{color:#545d68;font-size:11px}
|
||
.iworker .why{color:#388bfd;font-size:11px;margin-top:1px}
|
||
.iworker .acts{display:flex;gap:4px}
|
||
.ibtn{padding:5px 12px;border-radius:6px;font-size:10px;cursor:pointer;border:none;font-weight:600;transition:opacity 0.15s}
|
||
.ibtn:hover{opacity:0.8}
|
||
.ibtn.call{background:#1a2e4a;color:#58a6ff}
|
||
.ibtn.sms{background:#122b1e;color:#3fb950}
|
||
|
||
/* Stats */
|
||
.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:24px}
|
||
.stat{background:#0d1117;border:1px solid #171d27;border-radius:8px;padding:16px 12px;text-align:center}
|
||
.stat .n{font-size:26px;font-weight:700;color:#e6edf3;letter-spacing:-1px}
|
||
.stat .l{font-size:9px;color:#545d68;text-transform:uppercase;letter-spacing:0.8px;margin-top:4px}
|
||
|
||
/* Search */
|
||
.sa{background:#0d1117;border:1px solid #171d27;border-radius:10px;overflow:hidden}
|
||
.sa summary{cursor:pointer;color:#545d68;font-size:12px;list-style:none;padding:14px 20px;transition:color 0.15s}
|
||
.sa summary:hover{color:#b0b8c4}
|
||
.sa summary::-webkit-details-marker{display:none}
|
||
.sa .inner{padding:0 20px 20px}
|
||
.sa input[type=text]{width:100%;padding:12px 16px;background:#161b22;border:1px solid #21262d;border-radius:8px;color:#e6edf3;font-size:13px;outline:none;margin-bottom:8px;transition:border 0.15s}
|
||
.sa input:focus{border-color:#388bfd}
|
||
.srow{display:flex;gap:8px;margin-bottom:10px}
|
||
.sa select{flex:1;padding:8px 12px;background:#161b22;border:1px solid #21262d;border-radius:6px;color:#b0b8c4;font-size:12px}
|
||
.sbtn{width:100%;padding:10px;background:#1f6feb;border:none;border-radius:8px;color:#fff;font-size:13px;font-weight:600;cursor:pointer;transition:background 0.15s}
|
||
.sbtn:hover{background:#388bfd}
|
||
#sresults{margin-top:14px}
|
||
|
||
/* Footer */
|
||
.ft{text-align:center;padding:24px;color:#3d444d;font-size:11px;border-top:1px solid #171d27;margin-top:32px}
|
||
.ft a{color:#545d68;text-decoration:none;transition:color 0.15s}
|
||
.ft a:hover{color:#e6edf3}
|
||
|
||
.ld{color:#3d444d;text-align:center;padding:40px;font-size:13px}
|
||
/* Reused pulse keyframe — shared by status dots in the market-pulse
|
||
console title strip and deadline-pressure alert. */
|
||
@keyframes lh-pulse {
|
||
0%, 100% { opacity:1; transform:scale(1) }
|
||
50% { opacity:0.4; transform:scale(0.85) }
|
||
}
|
||
|
||
/* ─── Skeleton loader ───────────────────────────────────────────────
|
||
Drop-in replacement for the "Loading…" text. Reserves the exact
|
||
vertical space the real cards will occupy so when results arrive
|
||
they FADE in over the skeleton instead of pushing layout around.
|
||
The "screen takeover" feeling J flagged was from cards arriving
|
||
into 40px of empty padding and abruptly taking up 400px — this
|
||
pre-claims that space so the user feels in control of where their
|
||
eye is. */
|
||
.skel-list{display:flex;flex-direction:column;gap:4px;padding:0}
|
||
.skel-card{display:flex;align-items:center;gap:12px;padding:10px 12px;background:#0d1117;border-radius:8px;border-left:3px solid #1f242d;height:42px;box-sizing:border-box}
|
||
.skel-circle{width:40px;height:40px;border-radius:50%;flex-shrink:0;background:#161b22;position:relative;overflow:hidden}
|
||
.skel-lines{flex:1;display:flex;flex-direction:column;gap:6px;min-width:0}
|
||
.skel-bar{height:9px;border-radius:3px;background:#161b22;position:relative;overflow:hidden}
|
||
.skel-bar.w-60{width:60%}
|
||
.skel-bar.w-40{width:40%}
|
||
.skel-bar.w-80{width:80%}
|
||
.skel-bar.w-30{width:30%}
|
||
/* Shimmer — diagonal sweep, ~1.6s loop. Higher opacity than v1 so
|
||
the motion actually reads on dark backgrounds. The pseudo is
|
||
positioned with top/left+width (NOT `inset:0` — that pins right:0
|
||
which forced width:100% and killed the sweep, making skeletons
|
||
look like static blocks). */
|
||
@keyframes skel-sweep {
|
||
0% { transform: translateX(-120%) }
|
||
100% { transform: translateX(220%) }
|
||
}
|
||
.skel-circle::after, .skel-bar::after{
|
||
content:"";position:absolute;top:0;left:0;height:100%;width:60%;
|
||
background:linear-gradient(90deg, transparent 0%, rgba(120,180,255,0.18) 50%, transparent 100%);
|
||
animation: skel-sweep 1.6s ease-in-out infinite;
|
||
pointer-events:none;
|
||
}
|
||
/* Slightly brighter idle base color so the bars are visible even
|
||
between sweeps — the v1 #161b22 was within 8% of the card bg. */
|
||
.skel-bar{background:#1a2030 !important}
|
||
.skel-circle{background:#1a2030 !important}
|
||
/* Stagger the sweep across rows so the eye reads "rolling wave" not
|
||
"everything blinking together" — this looks more like a system
|
||
walking through rows than a generic spinner. */
|
||
.skel-card:nth-child(2) .skel-circle::after, .skel-card:nth-child(2) .skel-bar::after{ animation-delay:.13s }
|
||
.skel-card:nth-child(3) .skel-circle::after, .skel-card:nth-child(3) .skel-bar::after{ animation-delay:.26s }
|
||
.skel-card:nth-child(4) .skel-circle::after, .skel-card:nth-child(4) .skel-bar::after{ animation-delay:.39s }
|
||
.skel-card:nth-child(5) .skel-circle::after, .skel-card:nth-child(5) .skel-bar::after{ animation-delay:.52s }
|
||
.skel-card:nth-child(6) .skel-circle::after, .skel-card:nth-child(6) .skel-bar::after{ animation-delay:.65s }
|
||
.skel-card:nth-child(7) .skel-circle::after, .skel-card:nth-child(7) .skel-bar::after{ animation-delay:.78s }
|
||
|
||
/* Status caption — small, gray, tells the user what the system is
|
||
doing right now in domain language ("matching against permits",
|
||
"ranking by reliability") rather than generic "Loading…". The
|
||
stage rotates every ~1.2s on a fixed schedule so it feels like
|
||
real progress, not a stuck spinner. */
|
||
.skel-stage{font-size:11px;color:#6e7681;text-align:center;padding:14px 0 0;letter-spacing:0.3px;font-variant-numeric:tabular-nums}
|
||
.skel-stage::before{content:"";display:inline-block;width:6px;height:6px;border-radius:50%;background:#3fb950;margin-right:8px;vertical-align:middle;animation:skel-pulse 1.2s ease-in-out infinite}
|
||
@keyframes skel-pulse {
|
||
0%, 100% { opacity:0.3; transform:scale(0.85) }
|
||
50% { opacity:1; transform:scale(1.1) }
|
||
}
|
||
|
||
/* ─── Fill-probability bar — viewport-triggered paint + shimmer ─────
|
||
The bar paints in left-to-right via a clip-path animation so the
|
||
green → gold → orange → red gradient reads as a *timeline growing*
|
||
instead of a static heatmap with "danger zone" at the right edge.
|
||
|
||
Triggering: the .fp-bar starts in a fully-clipped state and only
|
||
paints when JS adds .lit via IntersectionObserver — fires when the
|
||
bar enters viewport, so a fast scroller sees every bar animate as
|
||
it scrolls past, instead of missing the show on first paint.
|
||
|
||
Timing: 350ms entry delay (lets the eye lock onto the bar before
|
||
motion starts) + 2800ms paint (slow enough to actually watch the
|
||
timeline build). Easing matches the card-in stagger (cubic-bezier
|
||
.2 .7 .2 1) so the page reads as one consistent motion language.
|
||
|
||
Shimmer: 30%-wide highlight sweeps across every ~3.4s on a 3400ms
|
||
delay (350 entry + 2800 paint + 250 dwell) so the two motions
|
||
never compete — the bar finishes drawing, *then* it pulses. */
|
||
@keyframes fp-paint {
|
||
from { clip-path: inset(0 100% 0 0); }
|
||
to { clip-path: inset(0 0 0 0); }
|
||
}
|
||
@keyframes fp-shimmer {
|
||
0% { transform: translateX(-100%); opacity: 0 }
|
||
10% { opacity: 1 }
|
||
60% { transform: translateX(260%); opacity: 1 }
|
||
61%, 100% { transform: translateX(260%); opacity: 0 }
|
||
}
|
||
.fp-bar {
|
||
position: relative;
|
||
/* Hidden until .lit is added — IntersectionObserver fires the paint
|
||
animation by toggling the class, which is the only thing that
|
||
unmasks the clip-path. */
|
||
clip-path: inset(0 100% 0 0);
|
||
}
|
||
.fp-bar.lit {
|
||
animation: fp-paint 2800ms cubic-bezier(.2,.7,.2,1) 350ms both;
|
||
}
|
||
.fp-bar::after {
|
||
content: "";
|
||
position: absolute;
|
||
top: 0; left: 0;
|
||
width: 30%;
|
||
height: 100%;
|
||
background: linear-gradient(90deg,
|
||
transparent 0%,
|
||
rgba(255,255,255,0.18) 50%,
|
||
transparent 100%);
|
||
pointer-events: none;
|
||
}
|
||
.fp-bar.lit::after {
|
||
animation: fp-shimmer 3.4s ease-in-out 3400ms infinite;
|
||
}
|
||
@media (prefers-reduced-motion: reduce) {
|
||
/* Skip the clip-path hide too — otherwise reduced-motion users
|
||
would see permanently invisible bars. The .lit class still gets
|
||
added by JS but the animation is `none`, so bar just appears. */
|
||
.fp-bar { clip-path: none }
|
||
.fp-bar.lit { animation: none }
|
||
.fp-bar.lit::after { animation: none; opacity: 0 }
|
||
}
|
||
|
||
/* ─── Staffer's Console: compact contract card with click-to-expand ─
|
||
Each card stays compact by default so a coordinator scanning 20+
|
||
contracts sees ~5 cards per viewport instead of ~1.5. The summary
|
||
strip below the pills shows revenue / margin / fill-by-1wk / top
|
||
candidate so scanners get the punchline without expanding. The
|
||
whole card surface is clickable; clicks inside the expanded details
|
||
don't bubble to the toggle (so contractor links / SMS copy still
|
||
work). When a card expands, its Project Index auto-opens too —
|
||
solves the "users aren't finding the build signals" gap without
|
||
firing 20× OSHA scrapes on page load. */
|
||
.contract-card {
|
||
cursor: pointer;
|
||
transition: transform 180ms ease, box-shadow 180ms ease;
|
||
}
|
||
.contract-card:hover {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 4px 14px rgba(0,0,0,0.32), 0 0 0 1px #30363d;
|
||
}
|
||
.contract-card.expanded { cursor: default }
|
||
.contract-card.expanded:hover { transform: none }
|
||
.contract-card .card-strip {
|
||
background: #0d1117;
|
||
border: 1px solid #171d27;
|
||
border-radius: 8px;
|
||
padding: 8px 12px;
|
||
margin-bottom: 10px;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 14px;
|
||
font-size: 11px;
|
||
color: #8b949e;
|
||
align-items: baseline;
|
||
}
|
||
.contract-card .card-strip strong {
|
||
font-weight: 600;
|
||
font-variant-numeric: tabular-nums;
|
||
margin-right: 3px;
|
||
}
|
||
/* Modern smooth-expand: grid-template-rows transitions cleanly to
|
||
the actual content height. Safari 17.4+ / Chrome 117+ / FF 117+. */
|
||
.contract-card .card-details {
|
||
display: grid;
|
||
grid-template-rows: 0fr;
|
||
transition: grid-template-rows 380ms cubic-bezier(.2,.7,.2,1);
|
||
}
|
||
.contract-card.expanded .card-details { grid-template-rows: 1fr }
|
||
.contract-card .card-details > .card-details-inner {
|
||
overflow: hidden;
|
||
min-height: 0;
|
||
}
|
||
.contract-card.expanded .card-details > .card-details-inner {
|
||
padding-top: 4px;
|
||
}
|
||
.contract-card .card-toggle {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
padding: 8px 0 2px;
|
||
font-size: 10px;
|
||
color: #545d68;
|
||
letter-spacing: 1.5px;
|
||
text-transform: uppercase;
|
||
user-select: none;
|
||
}
|
||
.contract-card .card-toggle .chevron {
|
||
display: inline-block;
|
||
transition: transform 320ms cubic-bezier(.2,.7,.2,1);
|
||
font-size: 12px;
|
||
line-height: 1;
|
||
}
|
||
.contract-card.expanded .card-toggle .chevron { transform: rotate(180deg) }
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.contract-card { transition: none }
|
||
.contract-card:hover { transform: none }
|
||
.contract-card .card-details { transition: none }
|
||
.contract-card .card-toggle .chevron { transition: none }
|
||
}
|
||
|
||
/* Bottom section-jump nav — mobile only */
|
||
/* Desktop: top nav handles navigation; this dock stays hidden. */
|
||
/* Mobile: top nav collapses (.bar nav:display:none below); this fixed dock */
|
||
/* gives one-thumb access to any section. Uses horizontal scroll-snap so */
|
||
/* additional sections never clip regardless of viewport width — staffers */
|
||
/* swipe the dock like an iOS tab bar. */
|
||
#mobile-dock{display:none}
|
||
#mobile-dock a,#mobile-dock button{
|
||
flex:0 0 auto;scroll-snap-align:start;
|
||
min-width:64px;text-align:center;padding:8px 6px;font-size:15px;font-weight:600;
|
||
color:#8b949e;text-decoration:none;border-radius:8px;background:none;
|
||
border:none;cursor:pointer;font-family:inherit;line-height:1;transition:all 0.1s
|
||
}
|
||
#mobile-dock a:active,#mobile-dock button:active{background:#1c2333;color:#e6edf3}
|
||
#mobile-dock .dock-label{font-size:9px;color:#8b949e;margin-top:4px;display:block;
|
||
text-transform:uppercase;letter-spacing:0.3px;font-weight:500;
|
||
white-space:nowrap}
|
||
|
||
/* Responsive */
|
||
@media(max-width:768px){
|
||
.stats{grid-template-columns:repeat(2,1fr);gap:6px;margin-bottom:16px}
|
||
.stat{padding:12px 8px}
|
||
.stat .n{font-size:22px}
|
||
.stat .l{font-size:8px}
|
||
.iworker{flex-direction:column;text-align:center}
|
||
.iworker .acts{justify-content:center}
|
||
.bar{padding:0 12px;height:48px;gap:10px}
|
||
.bar h1{font-size:13px}
|
||
.bar nav{display:none}
|
||
.bar .rt{font-size:10px;text-align:right;max-width:55%;line-height:1.3;color:#8b949e}
|
||
.content{padding:12px 10px 72px} /* bottom-pad reserved for mobile dock */
|
||
.section{margin-bottom:20px}
|
||
.card,.insight{padding:14px;margin-bottom:10px}
|
||
.card .card-title,.insight .headline{font-size:15px}
|
||
|
||
/* ─── Bottom section-jump dock: show on mobile only ─── */
|
||
/* Horizontal scroll-snap lets the row carry any number of section pills */
|
||
/* without clipping — swipe right to reveal more. Snap to pill edges so */
|
||
/* it feels solid, not jittery. */
|
||
#mobile-dock{
|
||
display:flex;position:fixed;bottom:0;left:0;right:0;
|
||
background:rgba(13,17,23,0.94);backdrop-filter:blur(10px);
|
||
-webkit-backdrop-filter:blur(10px);
|
||
border-top:1px solid #21262d;padding:4px 6px;gap:0;z-index:50;
|
||
overflow-x:auto;overflow-y:hidden;
|
||
scroll-snap-type:x mandatory;-webkit-overflow-scrolling:touch;
|
||
padding-bottom:max(4px,env(safe-area-inset-bottom)) /* iOS home-bar */
|
||
}
|
||
#mobile-dock::-webkit-scrollbar{height:0;display:none}
|
||
|
||
/* ─── Section ⓪ Legacy bridge: stack rows vertically ─── */
|
||
#legacy-bridge-rows > div{
|
||
grid-template-columns:1fr !important;gap:6px !important;padding:10px 12px !important
|
||
}
|
||
#legacy-bridge-rows > div > div:nth-child(2){display:none !important} /* hide → arrow */
|
||
#legacy-bridge-rows > div > div:nth-child(3){
|
||
border-top:1px dashed #1f2631;padding-top:8px
|
||
}
|
||
#legacy-bridge-section summary span:last-child{display:none} /* "click to collapse" hint */
|
||
#legacy-growth-strip{font-size:10px !important;width:100%;margin-top:4px}
|
||
|
||
/* ─── Section ① Live Market hero: stack clock above pulse ─── */
|
||
#live-market-hero{grid-template-columns:1fr !important;gap:14px !important}
|
||
#live-market-hero > div:first-child{align-items:center !important}
|
||
|
||
/* ─── Section ② contract cards: compact header + grid stacks ─── */
|
||
.insight > div[style*="display:flex;justify-content:space-between"]{
|
||
flex-direction:column;gap:6px
|
||
}
|
||
.insight > div[style*="grid-template-columns:repeat(4,1fr)"]{
|
||
grid-template-columns:repeat(2,1fr) !important
|
||
}
|
||
/* Fill probability markers: smaller font, allow wrap */
|
||
.insight span[style*="font-variant-numeric:tabular-nums"]{font-size:10px}
|
||
|
||
/* ─── Section ③ Worker Search: stack dropdowns + button ─── */
|
||
#worker-search-section .srow,
|
||
#worker-search-section div[style*="display:flex;gap:8px"]{
|
||
flex-direction:column !important;gap:8px !important
|
||
}
|
||
#worker-search-section .sbtn{width:100%}
|
||
|
||
/* ─── Section ⑤ Substrate Signals: auto-fit already; shrink padding ─── */
|
||
#arch-signals > div{padding:10px 12px !important}
|
||
|
||
/* Paragraph widths */
|
||
.section p{max-width:100% !important}
|
||
|
||
/* Break long monospace attribution strings so they don't overflow */
|
||
#legacy-bridge-rows div[style*="monospace"]{word-break:break-word;overflow-wrap:anywhere}
|
||
|
||
/* Any contract-card pill rows should wrap, not push horizontal */
|
||
.insight{overflow:hidden}
|
||
|
||
/* Footer condenses */
|
||
.ft{padding:16px 12px;font-size:10px}
|
||
}
|
||
@media(max-width:480px){
|
||
.stats{grid-template-columns:1fr 1fr}
|
||
.stat .n{font-size:20px}
|
||
.bar h1{font-size:12px}
|
||
.content{padding:10px 8px 72px}
|
||
.card,.insight{padding:12px}
|
||
/* Section ⓪ rows: even tighter */
|
||
#legacy-bridge-rows > div{padding:8px 10px !important}
|
||
#legacy-bridge-section{padding:12px 14px !important}
|
||
/* Hide the long header-strip info (growth) on very small screens — */
|
||
/* the section intro paragraph and per-row attributions cover it. */
|
||
/* Keep it accessible by expanding the <details>. */
|
||
}
|
||
/* ─── Page-load hero — full-viewport, compressed layout ─────────────
|
||
Shown ONCE per session. Mirrors the visual structure of the
|
||
collapsed Section ⓪ strip ("⓪ Not a CRM — an index that learns
|
||
from you · stats · ▾ click to expand") but rendered at hero scale,
|
||
centered in a full-viewport dark backdrop. One horizontal line of
|
||
information, dismissed by clicking anywhere. */
|
||
#hero-takeover{
|
||
position:fixed;inset:0;z-index:9999;background:radial-gradient(ellipse at 50% 50%,#0d1117 0%,#01040a 70%);
|
||
display:flex;flex-direction:column;align-items:center;justify-content:center;
|
||
cursor:pointer;
|
||
opacity:0;transition:opacity 500ms ease;
|
||
}
|
||
#hero-takeover.ready{opacity:1}
|
||
#hero-takeover.dismiss{opacity:0;pointer-events:none}
|
||
/* The compressed strip — single horizontal row of title + dot-
|
||
separated stats. Word-wraps gracefully on narrow viewports. */
|
||
#hero-takeover .ht-strip{
|
||
display:flex;align-items:baseline;gap:18px;flex-wrap:wrap;
|
||
max-width:min(92vw,1200px);justify-content:center;
|
||
padding:24px 32px;background:#0d1117;border:1px solid #171d27;
|
||
border-radius:10px;
|
||
font-variant-numeric:tabular-nums;line-height:1.4;
|
||
}
|
||
#hero-takeover .ht-num{
|
||
font-size:11px;letter-spacing:4px;color:#58a6ff;
|
||
text-transform:uppercase;font-weight:600;
|
||
}
|
||
#hero-takeover .ht-title{
|
||
font-size:clamp(20px,2.6vw,32px);font-weight:600;color:#e6edf3;
|
||
letter-spacing:-0.01em;
|
||
}
|
||
#hero-takeover .ht-stats-inline{
|
||
font-size:clamp(13px,1.4vw,16px);color:#8b949e;
|
||
font-variant-numeric:tabular-nums;
|
||
}
|
||
#hero-takeover .ht-stats-inline strong{
|
||
color:#e6edf3;font-weight:600;font-feature-settings:"tnum";
|
||
}
|
||
#hero-takeover .ht-stats-inline .grow{
|
||
background:linear-gradient(90deg,#3fb950 0%,#79c0ff 100%);
|
||
-webkit-background-clip:text;background-clip:text;color:transparent;
|
||
font-style:italic;font-weight:600;
|
||
}
|
||
#hero-takeover .ht-expand{
|
||
font-size:11px;color:#6e7681;letter-spacing:3px;text-transform:uppercase;
|
||
margin-left:8px;
|
||
}
|
||
#hero-takeover .ht-dismiss{
|
||
position:absolute;bottom:32px;font-size:11px;color:#545d68;
|
||
letter-spacing:3px;text-transform:uppercase;
|
||
}
|
||
@media(max-width:640px){
|
||
#hero-takeover .ht-strip{padding:16px 20px;gap:10px}
|
||
}
|
||
</style>
|
||
<!-- Three.js for the live 3D clock. Loads ~600KB once, cached forever
|
||
after first hit. Defer keeps the inline scripts above unblocked. -->
|
||
<script defer src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>
|
||
</head><body>
|
||
<!-- Hero takeover — see CSS comment block above. Full-viewport on load,
|
||
fades out on first interaction. JS hydrates stats from live endpoints. -->
|
||
<div id="hero-takeover" onclick="this.classList.add('dismiss');setTimeout(()=>this.remove(),700);try{sessionStorage.setItem('hero_seen','1')}catch(e){}">
|
||
<div class="ht-strip">
|
||
<span class="ht-num">⓪</span>
|
||
<span class="ht-title">Not a CRM — an index that learns from you</span>
|
||
<span class="ht-stats-inline">
|
||
<strong id="ht-stat-profiles">—</strong> profiles indexed
|
||
·
|
||
<strong id="ht-stat-traces">—</strong> pathway traces
|
||
·
|
||
<span class="grow">↗ grows per contract</span>
|
||
</span>
|
||
<span class="ht-expand">▾ click to expand</span>
|
||
</div>
|
||
<div class="ht-dismiss">click anywhere to enter</div>
|
||
</div>
|
||
<script>
|
||
(function(){
|
||
// Skip the takeover if we've shown it this session, or if the user
|
||
// landed via a query (?q=...) — they're back to do work, not be sold.
|
||
try {
|
||
var seen = sessionStorage.getItem('hero_seen') === '1';
|
||
var hasQ = new URLSearchParams(location.search).has('q');
|
||
if (seen || hasQ) {
|
||
var h = document.getElementById('hero-takeover');
|
||
if (h) h.remove();
|
||
return;
|
||
}
|
||
} catch(e) {}
|
||
|
||
// Fetch real numbers in parallel so the hero shows live counts.
|
||
// Falls back to credible defaults if either endpoint times out —
|
||
// the hero must never block the rest of the page from loading.
|
||
// Path prefix mirrors the main script's logic (~line 632) — when
|
||
// served at /lakehouse/, all our endpoints live under /lakehouse/*.
|
||
// Without the prefix the hero hits devop.live/system/summary (404)
|
||
// instead of devop.live/lakehouse/system/summary.
|
||
var P = location.pathname.indexOf('/lakehouse') >= 0 ? '/lakehouse' : '';
|
||
var FALLBACK = { profiles: 500000, traces: 88 };
|
||
function fmtCount(n) {
|
||
if (n >= 1e6) return (n/1e6).toFixed(1).replace(/\.0$/,'') + 'M';
|
||
if (n >= 1e3) return Math.round(n/1e3) + 'K';
|
||
return String(n);
|
||
}
|
||
|
||
Promise.allSettled([
|
||
fetch(P + '/system/summary').then(r => r.json()).catch(() => null),
|
||
fetch(P + '/api/vectors/pathway/stats').then(r => r.json()).catch(() => null),
|
||
]).then(function(results) {
|
||
var sys = results[0].status === 'fulfilled' ? results[0].value : null;
|
||
var path = results[1].status === 'fulfilled' ? results[1].value : null;
|
||
var profiles = (sys && (sys.workers_500k_rows || sys.total_rows)) || FALLBACK.profiles;
|
||
var traces = (path && (path.total_pathways || path.entries || path.count)) || FALLBACK.traces;
|
||
var pEl = document.getElementById('ht-stat-profiles');
|
||
var tEl = document.getElementById('ht-stat-traces');
|
||
if (pEl) pEl.textContent = fmtCount(profiles);
|
||
if (tEl) tEl.textContent = String(traces);
|
||
var hero = document.getElementById('hero-takeover');
|
||
if (hero) hero.classList.add('ready');
|
||
});
|
||
|
||
// Esc / scroll also dismiss — staffers know how to skip a splash.
|
||
function dismiss() {
|
||
var h = document.getElementById('hero-takeover');
|
||
if (!h) return;
|
||
h.classList.add('dismiss');
|
||
setTimeout(function(){ if (h.parentNode) h.remove(); }, 700);
|
||
try { sessionStorage.setItem('hero_seen', '1'); } catch(e) {}
|
||
}
|
||
document.addEventListener('keydown', function(e){ if (e.key === 'Escape') dismiss(); }, { once: true });
|
||
window.addEventListener('scroll', dismiss, { once: true, passive: true });
|
||
})();
|
||
</script>
|
||
<div class="bar">
|
||
<h1>Staffing Co-Pilot</h1>
|
||
<nav>
|
||
<a href="." class="active">Dashboard</a>
|
||
<a href="console">Walkthrough</a>
|
||
<a href="profiler">Profiler</a>
|
||
<a href="proof">Architecture</a>
|
||
<a href="spec">Spec</a>
|
||
<a href="onboard">Onboard</a>
|
||
<a href="alerts">Alerts</a>
|
||
<a href="workspaces">Workspaces</a>
|
||
</nav>
|
||
<div class="rt" id="status">Loading...</div>
|
||
</div>
|
||
<div class="content">
|
||
|
||
<!-- ═══ ⓪ Legacy CRM → this system: translate the mental model ═══ -->
|
||
<!-- Written for staffers who land here from a legacy CRM (Avionte, Bullhorn, -->
|
||
<!-- Tempworks, etc). Their mental model is "every concept must be a visible -->
|
||
<!-- field or it doesn't exist." Ours is the opposite: concepts get indexed when -->
|
||
<!-- you use them, not when a form declares them. This strip names common -->
|
||
<!-- legacy surfaces and says what this system does in their place — with real -->
|
||
<!-- attribution numbers so the compounding claim is measurable, not marketed. -->
|
||
<!-- Placed FIRST so staffers see the translation before they see a thin UI and -->
|
||
<!-- conclude "my old CRM had more checkboxes." -->
|
||
<details class="section" id="legacy-bridge-section" style="background:#0d1117;border:1px solid #171d27;border-radius:10px;padding:16px 20px;margin-bottom:14px"
|
||
ontoggle="var s=this.querySelector('.lb-toggle-hint');if(s)s.textContent=this.open?'▾ click to collapse':'▸ click to expand'">
|
||
<summary style="list-style:none;cursor:pointer;display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;outline:none">
|
||
<span class="section-title" style="color:#e6edf3;font-size:13px;font-weight:600">⓪ Not a CRM — an index that learns from you</span>
|
||
<span id="legacy-growth-strip" style="font-size:11px;color:#8b949e;font-variant-numeric:tabular-nums">loading growth numbers…</span>
|
||
<span class="lb-toggle-hint" style="margin-left:auto;font-size:10px;color:#545d68">▸ click to expand</span>
|
||
</summary>
|
||
<p style="color:#8b949e;font-size:11px;margin:10px 0 14px;line-height:1.55;max-width:780px">
|
||
If you've worked on a legacy staffing CRM, your mental model is <em>field inventory</em> —
|
||
every concept must be a visible column, dropdown, or checkbox, or it doesn't exist. This
|
||
system works the opposite way: concepts don't need to be pre-declared because the
|
||
<strong>hybrid index + playbook memory</strong> learns them when you work a contract.
|
||
The rows below translate the familiar legacy surface into what actually happens here,
|
||
with real numbers for every claim.
|
||
</p>
|
||
<div id="legacy-bridge-rows"></div>
|
||
</details>
|
||
|
||
<div id="main">
|
||
<div class="skel-list" data-stages="Reading 500K worker profiles|Matching against today's contracts|Ranking by reliability and proximity|Surfacing the urgent pipeline">
|
||
<div class="skel-card"><div class="skel-circle"></div><div class="skel-lines"><div class="skel-bar w-60"></div><div class="skel-bar w-40"></div></div></div>
|
||
<div class="skel-card"><div class="skel-circle"></div><div class="skel-lines"><div class="skel-bar w-80"></div><div class="skel-bar w-30"></div></div></div>
|
||
<div class="skel-card"><div class="skel-circle"></div><div class="skel-lines"><div class="skel-bar w-60"></div><div class="skel-bar w-40"></div></div></div>
|
||
<div class="skel-card"><div class="skel-circle"></div><div class="skel-lines"><div class="skel-bar w-80"></div><div class="skel-bar w-30"></div></div></div>
|
||
<div class="skel-card"><div class="skel-circle"></div><div class="skel-lines"><div class="skel-bar w-60"></div><div class="skel-bar w-40"></div></div></div>
|
||
<div class="skel-stage">Reading 500K worker profiles</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══ 1. LIVE MARKET — Chicago hero (clock + permit pulse together) ═══ -->
|
||
<div class="section" id="live-market-section">
|
||
<div class="section-header">
|
||
<span class="section-title" style="color:#e6edf3;font-size:13px">① Live Market — Chicago right now</span>
|
||
<span class="section-meta" id="live-market-meta">City of Chicago Open Data · cross-referenced against the 500K-worker bench · click any shift to slice the dials</span>
|
||
</div>
|
||
<p style="color:#8b949e;font-size:11px;margin:0 0 12px;line-height:1.5;max-width:780px">
|
||
Live from the <strong>City of Chicago Open Data</strong> permit feed (Building Permits
|
||
≥ $250K), cross-referenced against our 500K-worker bench. The console on the left is
|
||
the punch clock — current time, today's active shift, and four dials watching
|
||
<strong>open permits</strong>, <strong>workers needed</strong>,
|
||
<strong>bench depth</strong>, and <strong>projected coverage</strong>. The panel on
|
||
the right reads the same feed financially: combined bill demand if every permit fills,
|
||
deadline pressure across <em>overdue / urgent / soon / scheduled</em>, the four roles
|
||
being asked for most, and a shift-mix bar. <strong>Click any shift bar</strong> to
|
||
re-slice the dials and the dollar counter to that shift's calendar slice; click again
|
||
to clear. The same permit feed drives the staffing forecast, the staffer's console,
|
||
and worker matches further down — this row is its heartbeat.
|
||
</p>
|
||
<div id="live-market-hero" style="display:grid;grid-template-columns:240px 1fr;gap:20px;align-items:start">
|
||
<div style="grid-column:1/-1">
|
||
<div class="skel-list" data-stages="Pulling live Chicago permits|Aligning to current shift clock|Cross-referencing demand">
|
||
<div class="skel-card"><div class="skel-circle"></div><div class="skel-lines"><div class="skel-bar w-80"></div><div class="skel-bar w-30"></div></div></div>
|
||
<div class="skel-card"><div class="skel-circle"></div><div class="skel-lines"><div class="skel-bar w-60"></div><div class="skel-bar w-40"></div></div></div>
|
||
<div class="skel-stage">Pulling live Chicago permits</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section" id="staffing-forecast-section">
|
||
<div class="section-header">
|
||
<span class="section-title">Staffing Forecast — Next 30 Days</span>
|
||
<span class="section-meta">Permits → predicted demand · Bench supply · Days to staffing deadline</span>
|
||
</div>
|
||
<div id="staffing-forecast">
|
||
<div class="skel-list" data-stages="Forecasting from permit pipeline|Calculating bench coverage|Identifying critical role gaps">
|
||
<div class="skel-card"><div class="skel-circle"></div><div class="skel-lines"><div class="skel-bar w-60"></div><div class="skel-bar w-40"></div></div></div>
|
||
<div class="skel-card"><div class="skel-circle"></div><div class="skel-lines"><div class="skel-bar w-80"></div><div class="skel-bar w-30"></div></div></div>
|
||
<div class="skel-card"><div class="skel-circle"></div><div class="skel-lines"><div class="skel-bar w-60"></div><div class="skel-bar w-40"></div></div></div>
|
||
<div class="skel-stage">Forecasting from permit pipeline</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══ 2. STAFFER'S CONSOLE — what a coordinator acts on ═══ -->
|
||
<div class="section" id="live-contracts-section">
|
||
<div class="section-header">
|
||
<span class="section-title" style="color:#e6edf3;font-size:13px">② Staffer's Console — what's on your plate</span>
|
||
<span class="section-meta" id="live-contracts-meta">Real public permit data + worker bench + past playbook patterns</span>
|
||
</div>
|
||
<p style="color:#8b949e;font-size:11px;margin:0 0 12px;line-height:1.5;max-width:680px">
|
||
This is what a recruiter or coordinator sees when they open the console. Each card is
|
||
one open permit ranked against our 500K worker bench. The <strong>fill-probability bar</strong>
|
||
shows cumulative chance of filling by day; the <strong>economics panel</strong> projects
|
||
gross revenue, margin, and payout window; the <strong>over-bill pool</strong> flags workers
|
||
whose pay exceeds the contract's bill rate — they go into a margin-watch bucket instead
|
||
of being rejected outright.
|
||
</p>
|
||
<div id="live-contracts">
|
||
<div class="skel-list" data-stages="Reading active contracts|Matching workers to roles|Computing fill probability">
|
||
<div class="skel-card"><div class="skel-circle"></div><div class="skel-lines"><div class="skel-bar w-80"></div><div class="skel-bar w-30"></div></div></div>
|
||
<div class="skel-card"><div class="skel-circle"></div><div class="skel-lines"><div class="skel-bar w-60"></div><div class="skel-bar w-40"></div></div></div>
|
||
<div class="skel-card"><div class="skel-circle"></div><div class="skel-lines"><div class="skel-bar w-80"></div><div class="skel-bar w-30"></div></div></div>
|
||
<div class="skel-stage">Reading active contracts</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section" id="market-section">
|
||
<div class="section-header">
|
||
<span class="section-title">Market Intelligence</span>
|
||
<span class="section-meta">Public permit data · Updated live</span>
|
||
</div>
|
||
<div id="market"></div>
|
||
</div>
|
||
|
||
<!-- ═══ ③ Worker Search — natural-language lookup over the 500K bench ═══ -->
|
||
<div class="section" id="worker-search-section">
|
||
<div class="section-header">
|
||
<span class="section-title" style="color:#e6edf3;font-size:13px">③ Worker Search — find someone specific</span>
|
||
<span class="section-meta" id="worker-search-meta">Natural language search</span>
|
||
</div>
|
||
<p style="color:#8b949e;font-size:11px;margin:0 0 10px;line-height:1.5;max-width:680px">
|
||
Type a plain-English description — role, location, trait, certification.
|
||
The query hits the hybrid SQL + vector index over all 500K worker profiles
|
||
and ranks by semantic match, reliability, and availability. Try one of the
|
||
sample searches below or write your own.
|
||
</p>
|
||
<div id="ws-samples" style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px;font-size:11px"></div>
|
||
<input type="text" id="sq" placeholder="Try: reliable forklift operator available in Nashville" onkeydown="if(event.key==='Enter')doSearch()" style="width:100%;box-sizing:border-box;padding:10px 12px;background:#0d1117;border:1px solid #30363d;border-radius:6px;color:#e6edf3;font-size:13px;margin-bottom:8px">
|
||
<div class="srow" style="display:flex;gap:8px;margin-bottom:8px;align-items:center">
|
||
<!-- Per-staffer hot-swap selector. When a staffer is chosen, every
|
||
search and triage scopes to their territory; the playbook MEMORY
|
||
panel labels itself "Maria's recent fills" instead of generic. -->
|
||
<select id="sstaffer" style="flex:0 0 auto;padding:8px 10px;background:#0d1117;border:1px solid #58a6ff66;border-radius:6px;color:#e6edf3;font-weight:600;cursor:pointer" title="Act as this coordinator — playbook context will scope to their territory">
|
||
<option value="">All staffers</option>
|
||
</select>
|
||
<select id="sst" style="flex:1;padding:8px;background:#0d1117;border:1px solid #30363d;border-radius:6px;color:#e6edf3"><option value="">Any State</option></select>
|
||
<select id="srl" style="flex:1;padding:8px;background:#0d1117;border:1px solid #30363d;border-radius:6px;color:#e6edf3"><option value="">Any Role</option></select>
|
||
<button class="sbtn" onclick="doSearch()" style="padding:8px 16px;background:#238636;border:none;border-radius:6px;color:#fff;font-weight:600;cursor:pointer">Find Workers</button>
|
||
</div>
|
||
<div id="sstaffer-greeting" style="font-size:11px;color:#58a6ff;margin-bottom:8px;min-height:14px"></div>
|
||
<div id="sresults"></div>
|
||
</div>
|
||
|
||
<!-- ═══ ④ System Activity — what the substrate has learned to do ═══ -->
|
||
<div class="section" id="learning-section">
|
||
<div class="section-header">
|
||
<span class="section-title" style="color:#e6edf3;font-size:13px">④ System Activity — what the substrate has learned to do</span>
|
||
<span class="section-meta">Live capabilities · pulled from each subsystem on load</span>
|
||
</div>
|
||
<p style="color:#8b949e;font-size:11px;margin:0 0 12px;line-height:1.5;max-width:780px">
|
||
Each tile is a capability the system has acquired and the live metric proving
|
||
it's running. Operational learning (fills, playbooks, hot-swaps) compounds
|
||
inside each capability; what changes here is the <em>set of things the
|
||
substrate knows how to do</em>. The architecture is metro-agnostic — every
|
||
capability replicates by config, not code.
|
||
</p>
|
||
<div id="learning"></div>
|
||
</div>
|
||
|
||
<!-- ═══ ⑤ Substrate signals — compact architecture-health strip (bottom) ═══ -->
|
||
<div class="section" id="arch-signals-section">
|
||
<div class="section-header">
|
||
<span class="section-title" style="color:#e6edf3;font-size:13px">⑤ Substrate Signals — architecture health</span>
|
||
<span class="section-meta">Live probes of the index, memory, and pathway layers</span>
|
||
</div>
|
||
<p style="color:#8b949e;font-size:11px;margin:0 0 10px;line-height:1.5;max-width:680px">
|
||
These tiles measure the architecture itself, not the staffing workload.
|
||
Instant-search latency, index shape, playbook-memory depth, pathway-matrix
|
||
compounding — four probes that answer "is the substrate healthy right now?"
|
||
</p>
|
||
<div id="arch-signals" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:10px">
|
||
<div style="color:#8b949e;font-size:11px;grid-column:1/-1">Probing substrate…</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="ft" id="footer">Staffing Co-Pilot · Hybrid SQL + Vector Search · loading scale… · <a href="console">Console</a> · <a href="proof">Architecture</a></div>
|
||
</div>
|
||
|
||
<!-- ═══ Mobile section-jump dock — visible < 768px only via CSS ═══ -->
|
||
<!-- One-thumb navigation for staffers scrolling through the long dashboard. -->
|
||
<!-- Each pill jumps to a section by ID; the top bar nav is hidden on mobile, -->
|
||
<!-- so this replaces it entirely. Icons stay terse (⓪①②③④⑤) and the label -->
|
||
<!-- below reminds what each section is. -->
|
||
<nav id="mobile-dock" aria-label="Jump to section">
|
||
<a href="#legacy-bridge-section" title="Why this isn't your legacy CRM">⓪<span class="dock-label">Intro</span></a>
|
||
<a href="#live-market-section" title="Chicago clock + permit pulse">①<span class="dock-label">Market</span></a>
|
||
<a href="#live-contracts-section" title="Open contracts + candidates">②<span class="dock-label">Jobs</span></a>
|
||
<a href="#worker-search-section" title="Search 500K workers">③<span class="dock-label">Search</span></a>
|
||
<a href="#learning-section" title="What the substrate has learned">④<span class="dock-label">Learn</span></a>
|
||
<a href="#arch-signals-section" title="Live architecture health">⑤<span class="dock-label">Stack</span></a>
|
||
</nav>
|
||
<script>
|
||
var P=location.pathname.indexOf('/lakehouse')>=0?'/lakehouse':'';
|
||
var A=location.origin+P;
|
||
var AC=['#1a2744','#1a3a2a','#2a1a3a','#3a2a1a','#1a3a3a','#2a2a1a'];
|
||
var lastQuery='';
|
||
// Skeleton stage rotator — finds every .skel-list[data-stages="A|B|C"]
|
||
// and advances its caption every 1.4s. Stops when the container is
|
||
// removed (loadDay / loadStaffingForecast etc. wipe their root and
|
||
// rebuild). Self-cleans via MutationObserver, no leaks.
|
||
function startSkelStages(){
|
||
document.querySelectorAll('.skel-list[data-stages]').forEach(function(root){
|
||
var stages=(root.getAttribute('data-stages')||'').split('|').filter(Boolean);
|
||
if(stages.length<2) return;
|
||
var caption=root.querySelector('.skel-stage'); if(!caption) return;
|
||
var i=0;
|
||
var iv=setInterval(function(){
|
||
// Stop if the skeleton has been replaced with real content
|
||
if(!document.body.contains(caption)){ clearInterval(iv); return; }
|
||
i=(i+1)%stages.length;
|
||
caption.style.opacity='0';
|
||
setTimeout(function(){ caption.firstChild.nodeValue=stages[i]; caption.style.opacity='1' },180);
|
||
},1400);
|
||
});
|
||
}
|
||
// Tweak: caption gets a fade-on-swap transition so stage changes
|
||
// don't jolt the eye. Applied lazily so it doesn't interfere with
|
||
// the initial layout calculation.
|
||
function styleSkelStages(){
|
||
document.querySelectorAll('.skel-stage').forEach(function(c){ c.style.transition='opacity 180ms ease' });
|
||
}
|
||
window.addEventListener('load',function(){
|
||
styleSkelStages(); startSkelStages();
|
||
loadSystemSummary();loadLegacyBridge();loadDay();loadStaffingForecast();loadLiveContracts();loadMarket();loadLearning();loadWorkerSearchSamples();loadArchSignals();loadStaffers();
|
||
});
|
||
|
||
// Per-staffer hot-swap dropdown — runs independently of the simulation
|
||
// fetch so the staffer selector populates even if any other init step
|
||
// errors out. /api/staffers returns the synthetic coordinator roster.
|
||
function loadStaffers(){
|
||
var sel=document.getElementById('sstaffer');
|
||
var greeting=document.getElementById('sstaffer-greeting');
|
||
if(!sel) return;
|
||
// /staffers (not /api/staffers) — the /api/* generic passthrough
|
||
// forwards anything under /api/ to the Rust gateway on :3100 and the
|
||
// gateway doesn't know the staffer roster (it lives in the mcp-server
|
||
// module). The bare /staffers route serves directly.
|
||
fetch(A+'/staffers').then(function(r){return r.json()}).then(function(d){
|
||
(d.staffers||[]).forEach(function(s){
|
||
var o=document.createElement('option');o.value=s.id;o.textContent=s.display||s.name;
|
||
sel.appendChild(o);
|
||
});
|
||
sel._roster=d.staffers||[];
|
||
}).catch(function(){});
|
||
sel.addEventListener('change',function(){
|
||
var roster=sel._roster||[];
|
||
var s=roster.find(function(x){return x.id===sel.value});
|
||
if(s){
|
||
greeting.textContent='Acting as '+s.name+' — '+(s.greeting||'')+' · territory: '+s.territory.cities.slice(0,3).join(', ')+'…';
|
||
var stSel=document.getElementById('sst');
|
||
if(stSel && !stSel.value){stSel.value=s.territory.state}
|
||
}else{
|
||
greeting.textContent='';
|
||
}
|
||
});
|
||
}
|
||
|
||
// Deep-link: visiting the dashboard with #open-briefs in the URL auto-
|
||
// expands every Entity Brief panel once the contract cards finish
|
||
// loading. Useful for headless snapshots + demo walkthroughs.
|
||
window.addEventListener('load',function(){
|
||
if(location.hash!=='#open-briefs') return;
|
||
var tries=0;
|
||
var t=setInterval(function(){
|
||
tries++;
|
||
var briefs=document.querySelectorAll('#live-contracts details');
|
||
if(briefs.length===0 && tries<40) return;
|
||
briefs.forEach(function(d){d.open=true;d.dispatchEvent(new Event('toggle'))});
|
||
clearInterval(t);
|
||
},250);
|
||
});
|
||
|
||
// ─── Legacy CRM → this system translator ─────────────────────────
|
||
// Staffers from legacy systems operate by field-inventory mental model:
|
||
// if a concept isn't pre-rendered as a dropdown/checkbox, they conclude
|
||
// the system can't do it. This function fills the ⓪ Section with
|
||
// side-by-side rows mapping common legacy surfaces to how the hybrid
|
||
// substrate handles the same concept — using LIVE attribution numbers
|
||
// so the compounding claim is measurable, not marketed. One row is an
|
||
// honest "we don't ship this yet" gap, because staffers trust a system
|
||
// that admits gaps faster than one that claims everything.
|
||
function loadLegacyBridge(){
|
||
var host=document.getElementById('legacy-bridge-rows');
|
||
if(!host) return;
|
||
var strip=document.getElementById('legacy-growth-strip');
|
||
Promise.all([
|
||
api('/intelligence/arch_signals',{}).catch(function(){return {}}),
|
||
api('/system/summary',{}).catch(function(){return {}})
|
||
]).then(function(results){
|
||
var s=results[0]||{}, sum=results[1]||{};
|
||
var pbm=s.playbook_memory||{}, pwm=s.pathway_memory||{}, idx=s.index||{};
|
||
var workers=(sum.workers_500k_rows||0).toLocaleString();
|
||
var chunks=(idx.chunk_count||0).toLocaleString();
|
||
var pbEntries=pbm.entries||0;
|
||
var pwTraces=pwm.total_pathways||0;
|
||
if(strip){
|
||
var parts=[workers+' profiles indexed'];
|
||
if(pbEntries) parts.push(pbEntries+' playbook'+(pbEntries===1?'':'s')+' from past fills');
|
||
if(pwTraces) parts.push(pwTraces+' pathway trace'+(pwTraces===1?'':'s'));
|
||
parts.push('grows per contract');
|
||
strip.textContent=parts.join(' · ');
|
||
}
|
||
var rows=[
|
||
{ legacy:'Certification checkboxes — tick every cert you need',
|
||
legacyWhy:'If a worker\'s cert isn\'t on the pre-declared list, you can\'t find them.',
|
||
here:'Type it in plain English — "forklift with OSHA 10"',
|
||
hereWhy:'Vector search scans free-text cert fields across every profile. No list to maintain.',
|
||
attribution:chunks+' vector chunks · nomic-embed-text 768d · built once at ingest',
|
||
anchor:'③ Worker Search' },
|
||
{ legacy:'Preferred-worker list you tag by hand',
|
||
legacyWhy:'Forget to tag someone → they stop rising to the top.',
|
||
here:'Workers with past fills get the green "Endorsed" chip automatically',
|
||
hereWhy:'Playbook memory boosts them on every future search in the same pattern. You never tag manually.',
|
||
attribution:pbEntries+' playbook'+(pbEntries===1?'':'s')+' active right now · grows with every Call you log',
|
||
anchor:'② Staffer\'s Console' },
|
||
{ legacy:'Shift dropdown: 1st / 2nd / 3rd / 4th',
|
||
legacyWhy:'Contracts with messy shift language fall through the cracks.',
|
||
here:'Shifts inferred from permit description text',
|
||
hereWhy:'The 24/7 clock above shows live distribution across shifts, pulled from real Chicago permits.',
|
||
attribution:'Permit parser runs on every live feed poll · zero config',
|
||
anchor:'① Live Market' },
|
||
{ legacy:'Blacklist checkbox — permanent, system-wide',
|
||
legacyWhy:'One bad day follows a worker forever, across every job type.',
|
||
here:'Tap "No-show" on any candidate card',
|
||
hereWhy:'Score drops 0.5× in that geo only. Soft, geo-scoped, reversible — not a ban.',
|
||
attribution:pwTraces+' pathway trace'+(pwTraces===1?'':'s')+' in the failure matrix · ADR-021 compounding',
|
||
anchor:'② Staffer\'s Console' },
|
||
{ legacy:'Side-by-side compare view',
|
||
legacyWhy:'Useful for interview prep. We\'re not pretending you don\'t need it.',
|
||
here:'Not shipped yet — honest gap',
|
||
hereWhy:'Workaround: every candidate card already carries rate, reliability, boost source, and cert summary.',
|
||
attribution:'On the roadmap · flagged so you know what\'s missing',
|
||
gap:true }
|
||
];
|
||
host.textContent='';
|
||
rows.forEach(function(r){
|
||
var row=document.createElement('div');
|
||
row.style.cssText='display:grid;grid-template-columns:1fr 22px 1.15fr;gap:12px;padding:11px 14px;background:#161b22;border:1px solid #1f2631;border-radius:8px;margin-bottom:6px;align-items:start';
|
||
// LEGACY column
|
||
var L=document.createElement('div');
|
||
var lTag=document.createElement('div');lTag.style.cssText='font-size:9px;color:#545d68;text-transform:uppercase;letter-spacing:1px;margin-bottom:3px';lTag.textContent='LEGACY CRM EXPECTS';
|
||
var lTxt=document.createElement('div');lTxt.style.cssText='color:#8b949e;font-size:12px;line-height:1.4;font-weight:500';lTxt.textContent=r.legacy;
|
||
var lWhy=document.createElement('div');lWhy.style.cssText='color:#545d68;font-size:10px;margin-top:3px;line-height:1.45';lWhy.textContent=r.legacyWhy;
|
||
L.appendChild(lTag);L.appendChild(lTxt);L.appendChild(lWhy);
|
||
// ARROW
|
||
var A=document.createElement('div');A.style.cssText='color:#388bfd;font-size:18px;text-align:center;padding-top:14px;font-weight:300';A.textContent='→';
|
||
// HERE column
|
||
var H=document.createElement('div');
|
||
var hTag=document.createElement('div');
|
||
hTag.style.cssText='font-size:9px;color:'+(r.gap?'#d29922':'#58a6ff')+';text-transform:uppercase;letter-spacing:1px;margin-bottom:3px';
|
||
hTag.textContent=r.gap?'HERE — HONEST GAP':'HERE';
|
||
var hTxt=document.createElement('div');
|
||
hTxt.style.cssText='color:'+(r.gap?'#d29922':'#e6edf3')+';font-size:12px;line-height:1.4;font-weight:600';
|
||
hTxt.textContent=r.here;
|
||
var hWhy=document.createElement('div');hWhy.style.cssText='color:#8b949e;font-size:11px;margin-top:3px;line-height:1.45';hWhy.textContent=r.hereWhy;
|
||
var attr=document.createElement('div');
|
||
attr.style.cssText='color:#545d68;font-size:10px;margin-top:5px;font-family:ui-monospace,SFMono-Regular,monospace';
|
||
attr.textContent='▸ '+r.attribution;
|
||
H.appendChild(hTag);H.appendChild(hTxt);H.appendChild(hWhy);H.appendChild(attr);
|
||
if(r.anchor){
|
||
var ptr=document.createElement('div');
|
||
ptr.style.cssText='color:#388bfd;font-size:10px;margin-top:3px';
|
||
ptr.textContent='see '+r.anchor+' below';
|
||
H.appendChild(ptr);
|
||
}
|
||
row.appendChild(L);row.appendChild(A);row.appendChild(H);
|
||
host.appendChild(row);
|
||
});
|
||
});
|
||
}
|
||
|
||
// ─── Worker Search sample chips ──────────────────────────────────
|
||
// Real-looking staffing queries grouped by intent. Clicking one drops
|
||
// the text into #sq and runs the search — lowers the floor for
|
||
// someone seeing the UI for the first time.
|
||
function loadWorkerSearchSamples(){
|
||
var el=document.getElementById('ws-samples');
|
||
if(!el) return;
|
||
var samples=[
|
||
{tag:'Role + geo', q:'reliable welder in Ohio with 3+ years'},
|
||
{tag:'Cert-gated', q:'forklift operator with OSHA 10 available in Chicago'},
|
||
{tag:'Urgent cover', q:'production worker for 3rd shift in Toledo tonight'},
|
||
{tag:'Trait-first', q:'punctual machine operator who can lead a line'},
|
||
{tag:'Specialist', q:'maintenance tech comfortable with Allen-Bradley PLCs'},
|
||
{tag:'High-volume', q:'warehouse associates available next week near Indianapolis'}
|
||
];
|
||
samples.forEach(function(s){
|
||
var chip=document.createElement('button');
|
||
chip.type='button';
|
||
chip.style.cssText='padding:5px 10px;border-radius:9px;background:#0d1117;border:1px solid #30363d;color:#8b949e;font-size:11px;cursor:pointer;display:inline-flex;align-items:center;gap:6px;transition:all 0.1s';
|
||
chip.onmouseover=function(){chip.style.borderColor='#58a6ff';chip.style.color='#e6edf3'};
|
||
chip.onmouseout=function(){chip.style.borderColor='#30363d';chip.style.color='#8b949e'};
|
||
var tag=document.createElement('span');tag.style.cssText='color:#545d68;font-size:9px;text-transform:uppercase;letter-spacing:1px';tag.textContent=s.tag;
|
||
var txt=document.createElement('span');txt.textContent=s.q;
|
||
chip.appendChild(tag);chip.appendChild(txt);
|
||
chip.onclick=function(){
|
||
var input=document.getElementById('sq');
|
||
if(input){input.value=s.q;doSearch();input.scrollIntoView({behavior:'smooth',block:'center'});}
|
||
};
|
||
el.appendChild(chip);
|
||
});
|
||
}
|
||
|
||
function loadSystemSummary(){
|
||
api('/system/summary',{}).then(function(s){
|
||
if(!s) return;
|
||
var totalRows=(s.total_rows||0).toLocaleString();
|
||
var workers=(s.workers_500k_rows||0).toLocaleString();
|
||
var chunks=(s.total_chunks||0).toLocaleString();
|
||
var ds=s.datasets||0;
|
||
var meta1=document.getElementById('live-contracts-meta');
|
||
if(meta1) meta1.textContent='Real public permit data + '+workers+' worker bench + past playbook patterns';
|
||
var meta2=document.getElementById('worker-search-meta');
|
||
if(meta2) meta2.textContent='Natural language · '+workers+' profiles across '+ds+' datasets';
|
||
var foot=document.getElementById('footer');
|
||
if(foot){
|
||
foot.textContent='';
|
||
foot.appendChild(document.createTextNode('Staffing Co-Pilot · Hybrid SQL + Vector Search · '+totalRows+' rows across '+ds+' datasets · '+chunks+' vector chunks · '));
|
||
var a1=document.createElement('a');a1.href='console';a1.textContent='Console';foot.appendChild(a1);
|
||
foot.appendChild(document.createTextNode(' · '));
|
||
var a2=document.createElement('a');a2.href='proof';a2.textContent='Architecture';foot.appendChild(a2);
|
||
}
|
||
// Also update the collapsible search box label if not yet populated
|
||
var sum=document.querySelector('.sa summary');
|
||
if(sum&&/Search all\s*\d*\s*workers/.test(sum.textContent)){
|
||
sum.textContent='Search all '+workers+' workers';
|
||
}
|
||
}).catch(function(){/* non-fatal */});
|
||
}
|
||
|
||
// ─── Substrate signals: render the 4 architecture-health tiles ───
|
||
function loadArchSignals(){
|
||
var el=document.getElementById('arch-signals');
|
||
api('/intelligence/arch_signals',{}).then(function(s){
|
||
el.textContent='';
|
||
function tile(label, big, sub, accent){
|
||
var t=document.createElement('div');
|
||
t.style.cssText='background:#0d1117;border:1px solid #171d27;border-radius:10px;padding:14px 16px;border-left:3px solid '+(accent||'#58a6ff');
|
||
var l=document.createElement('div');l.style.cssText='font-size:10px;text-transform:uppercase;letter-spacing:1px;color:#545d68;margin-bottom:6px';l.textContent=label;
|
||
var b=document.createElement('div');b.style.cssText='font-size:20px;font-weight:600;color:#e6edf3;line-height:1.1';b.textContent=big;
|
||
var u=document.createElement('div');u.style.cssText='font-size:10px;color:#8b949e;margin-top:6px;line-height:1.4';u.textContent=sub;
|
||
t.appendChild(l);t.appendChild(b);t.appendChild(u);
|
||
return t;
|
||
}
|
||
var idx=s.index||{};
|
||
var pbm=s.playbook_memory||{};
|
||
var pwm=s.pathway_memory||{};
|
||
// Tile 1 — instant search (the "we cleverly indexed at ingest" claim)
|
||
var latencyColor=s.instant_search_probe_ms<100?'#2ea043':s.instant_search_probe_ms<500?'#d29922':'#f85149';
|
||
el.appendChild(tile(
|
||
'Instant Search',
|
||
(s.instant_search_probe_ms||'?')+'ms',
|
||
'Live /vectors/hybrid probe · 500K-row index · '+(idx.chunk_count||0).toLocaleString()+' chunks',
|
||
latencyColor
|
||
));
|
||
// Tile 2 — index shape (hot-swap claim)
|
||
el.appendChild(tile(
|
||
'Index',
|
||
(idx.dimensions||768)+'d · '+(idx.model||'?'),
|
||
(idx.source||'?')+' → '+(idx.name||'?')+' · '+(idx.backend||'parquet'),
|
||
'#58a6ff'
|
||
));
|
||
// Tile 3 — self-regulating memory
|
||
el.appendChild(tile(
|
||
'Playbook Memory',
|
||
(pbm.entries||0).toLocaleString()+' entries',
|
||
pbm.entries>0?'Meta-index active · boosts candidates from past fills':'Empty · POST /vectors/playbook_memory/rebuild to populate',
|
||
pbm.entries>0?'#2ea043':'#d29922'
|
||
));
|
||
// Tile 4 — ADR-021 pathway compounding
|
||
el.appendChild(tile(
|
||
'Pathway Matrix',
|
||
(pwm.total_pathways||0)+' traces',
|
||
pwm.retired+' retired · '+pwm.total_replays+' replays · ADR-021 compounding',
|
||
'#58a6ff'
|
||
));
|
||
}).catch(function(e){
|
||
el.textContent='substrate signals unavailable: '+e.message;
|
||
});
|
||
}
|
||
|
||
// ─── Live Market hero: 24/7 shift clock (left) + Chicago permit pulse (right) ──
|
||
// Combines two previously-separate tiles into one coherent "what's happening in
|
||
// Chicago right now" surface. The clock anchors you in the 24-hour cycle; the
|
||
// pulse panel summarizes what the raw permit feed is asking for before anyone's
|
||
// acted on it.
|
||
function loadLiveMarket(contracts){
|
||
var root=document.getElementById('live-market-hero');
|
||
if(!root) return;
|
||
if(!contracts||!contracts.length){
|
||
root.textContent='No live permits on the board right now.';
|
||
return;
|
||
}
|
||
root.textContent='';
|
||
var SHIFT_COLORS={'1st':'#f9d171','2nd':'#f5894a','3rd':'#5f5fff','4th':'#2ea043'};
|
||
var now=new Date();
|
||
var hr=now.getHours()+now.getMinutes()/60;
|
||
var isWeekend=now.getDay()===0||now.getDay()===6;
|
||
function currentShift(){
|
||
if(isWeekend) return '4th';
|
||
if(hr>=6&&hr<14) return '1st';
|
||
if(hr>=14&&hr<22) return '2nd';
|
||
return '3rd';
|
||
}
|
||
var cs=currentShift();
|
||
|
||
// ── LEFT COLUMN: Punch-clock console ──
|
||
// Industrial-styled console showing the live pulse of the market —
|
||
// a digital LED time readout at the top, plus four dial gauges
|
||
// below with animated needles and ticking values. Built in pure
|
||
// SVG + CSS (no WebGL) for crisp rendering and small payload, with
|
||
// an aesthetic borrowed from old Chicago factory time-punch clocks.
|
||
// The whole thing sits in its own bordered box so it doesn't bleed
|
||
// into the permit-pulse panel beside it.
|
||
var leftCol=document.createElement('div');
|
||
leftCol.style.cssText='display:flex;flex-direction:column;align-items:center;gap:8px;flex-shrink:0';
|
||
|
||
// Console panel — bordered box that holds the digital clock + dials.
|
||
// Sized to fit within the same vertical extent as the right column
|
||
// (rate counter + urgency meter + role bars) so the box doesn't
|
||
// visibly poke below the rest of the row.
|
||
var consoleBox=document.createElement('div');
|
||
consoleBox.style.cssText='width:240px;background:linear-gradient(180deg,#171d27 0%,#0d1117 100%);border:1px solid #30363d;border-radius:6px;padding:10px;box-shadow:inset 0 1px 0 rgba(255,255,255,0.04),0 4px 16px rgba(0,0,0,0.4);font-family:Inter,-apple-system,sans-serif';
|
||
|
||
// Title strip
|
||
var titleStrip=document.createElement('div');
|
||
titleStrip.style.cssText='font-size:9px;color:#6e7681;letter-spacing:3px;text-transform:uppercase;text-align:center;margin-bottom:8px;font-weight:600';
|
||
var titleDot=document.createElement('span');
|
||
titleDot.style.cssText='display:inline-block;width:5px;height:5px;border-radius:50%;background:#3fb950;margin-right:6px;vertical-align:middle;animation:lh-pulse 1.4s ease-in-out infinite';
|
||
titleStrip.appendChild(titleDot);
|
||
titleStrip.appendChild(document.createTextNode('Market Pulse · Chicago'));
|
||
consoleBox.appendChild(titleStrip);
|
||
|
||
// Digital LED clock
|
||
var clockEl=document.createElement('div');
|
||
clockEl.id='lh-digital-clock';
|
||
clockEl.style.cssText='font-family:"SF Mono","Menlo","Consolas",monospace;font-size:30px;font-weight:700;color:#3fb950;text-align:center;letter-spacing:1.5px;line-height:1;padding:6px 0;background:#000;border:1px solid #1a2030;border-radius:4px;box-shadow:inset 0 0 14px rgba(63,185,80,0.18);text-shadow:0 0 10px rgba(63,185,80,0.6),0 0 3px rgba(63,185,80,0.9);font-variant-numeric:tabular-nums';
|
||
clockEl.textContent='--:--:--';
|
||
consoleBox.appendChild(clockEl);
|
||
|
||
// Sub-row: shift + day-of-week
|
||
var subRow=document.createElement('div');
|
||
subRow.style.cssText='display:flex;justify-content:space-between;align-items:center;font-size:10px;color:#8b949e;margin-top:6px;letter-spacing:1px;text-transform:uppercase;font-weight:600';
|
||
var shiftBadge=document.createElement('span');
|
||
shiftBadge.style.cssText='color:'+SHIFT_COLORS[cs]+';font-weight:700';
|
||
shiftBadge.textContent=cs+' shift live';
|
||
var dowBadge=document.createElement('span');
|
||
dowBadge.id='lh-dow-badge';
|
||
dowBadge.style.cssText='color:#6e7681';
|
||
dowBadge.textContent=now.toString().slice(0,3).toUpperCase()+' · '+(isWeekend?'WKND':'WKDAY');
|
||
subRow.appendChild(shiftBadge);
|
||
subRow.appendChild(dowBadge);
|
||
consoleBox.appendChild(subRow);
|
||
|
||
// Divider
|
||
var divider=document.createElement('div');
|
||
divider.style.cssText='height:1px;background:linear-gradient(90deg,transparent 0%,#30363d 30%,#30363d 70%,transparent 100%);margin:8px 0';
|
||
consoleBox.appendChild(divider);
|
||
|
||
// 2x2 grid of mini-dials. Each cell holds an SVG ring gauge and a
|
||
// numeric value at center. The gauges fill clockwise according to
|
||
// value/max ratio. Per-second wobble (±0.5%) gives a "live signal"
|
||
// feel so the panel reads as the heartbeat of the market, not a
|
||
// static infographic.
|
||
var gridEl=document.createElement('div');
|
||
gridEl.style.cssText='display:grid;grid-template-columns:1fr 1fr;gap:6px';
|
||
var _wantWorkers=contracts.reduce(function(a,c){return a+(c.proposed&&c.proposed.count||0)},0);
|
||
var _benchEst=Math.floor(contracts.length*45)||500;
|
||
var _coverage=_wantWorkers>0?Math.min(100,Math.floor(_benchEst/_wantWorkers*100)):100;
|
||
var dialDefs=[
|
||
{ id:'d-permits', label:'Permits', value:contracts.length, max:50, color:'#79c0ff' },
|
||
{ id:'d-workers', label:'Workers Need',value:_wantWorkers, max:300, color:'#f5894a' },
|
||
{ id:'d-bench', label:'Bench', value:_benchEst, max:1500, color:'#3fb950' },
|
||
{ id:'d-fill', label:'Coverage', value:_coverage, max:100, color:'#bc8cff' },
|
||
];
|
||
var SVG_NS='http://www.w3.org/2000/svg';
|
||
dialDefs.forEach(function(d){
|
||
var cell=document.createElement('div');
|
||
cell.style.cssText='display:flex;flex-direction:column;align-items:center;gap:2px;background:#0d1117;border:1px solid #1a2030;border-radius:4px;padding:6px 4px';
|
||
var dsvg=document.createElementNS(SVG_NS,'svg');
|
||
dsvg.setAttribute('viewBox','0 0 50 50');
|
||
dsvg.setAttribute('width','50');dsvg.setAttribute('height','50');
|
||
dsvg.style.cssText='display:block';
|
||
var dtrack=document.createElementNS(SVG_NS,'circle');
|
||
dtrack.setAttribute('cx','25');dtrack.setAttribute('cy','25');dtrack.setAttribute('r','20');
|
||
dtrack.setAttribute('fill','none');dtrack.setAttribute('stroke','#1a2030');dtrack.setAttribute('stroke-width','3');
|
||
dsvg.appendChild(dtrack);
|
||
var darc=document.createElementNS(SVG_NS,'circle');
|
||
darc.setAttribute('cx','25');darc.setAttribute('cy','25');darc.setAttribute('r','20');
|
||
darc.setAttribute('fill','none');darc.setAttribute('stroke',d.color);darc.setAttribute('stroke-width','3');
|
||
darc.setAttribute('stroke-linecap','round');
|
||
darc.setAttribute('transform','rotate(-90 25 25)');
|
||
var dcirc=2*Math.PI*20;
|
||
var dpct=Math.min(1,d.value/d.max);
|
||
darc.setAttribute('stroke-dasharray',dcirc);
|
||
darc.setAttribute('stroke-dashoffset',dcirc*(1-dpct));
|
||
darc.style.transition='stroke-dashoffset 700ms cubic-bezier(.2,.7,.2,1)';
|
||
darc.id=d.id+'-arc';
|
||
darc.dataset.max=d.max;darc.dataset.circ=dcirc;
|
||
dsvg.appendChild(darc);
|
||
var dval=document.createElementNS(SVG_NS,'text');
|
||
dval.setAttribute('x','25');dval.setAttribute('y','29');
|
||
dval.setAttribute('text-anchor','middle');dval.setAttribute('fill','#e6edf3');
|
||
dval.setAttribute('font-size','12');dval.setAttribute('font-weight','700');
|
||
dval.setAttribute('font-family','SF Mono,Menlo,Consolas,monospace');
|
||
dval.textContent=d.value;
|
||
dval.id=d.id+'-val';
|
||
dsvg.appendChild(dval);
|
||
cell.appendChild(dsvg);
|
||
var dlbl=document.createElement('div');
|
||
dlbl.style.cssText='font-size:9px;color:#6e7681;text-transform:uppercase;letter-spacing:1px;text-align:center';
|
||
dlbl.textContent=d.label;
|
||
cell.appendChild(dlbl);
|
||
gridEl.appendChild(cell);
|
||
});
|
||
consoleBox.appendChild(gridEl);
|
||
|
||
leftCol.appendChild(consoleBox);
|
||
|
||
// Single ticker — updates the digital clock seconds digit and gives
|
||
// each dial gauge a tiny wobble (±0.5% of max). Stops when the
|
||
// console is removed (loadLiveMarket re-runs).
|
||
if(window._lhPulseTimer){clearInterval(window._lhPulseTimer);window._lhPulseTimer=null}
|
||
function lhTick(){
|
||
if(!document.body.contains(consoleBox)){
|
||
clearInterval(window._lhPulseTimer);
|
||
window._lhPulseTimer=null;
|
||
return;
|
||
}
|
||
var n=new Date();
|
||
var hh=String(n.getHours()).padStart(2,'0');
|
||
var mm=String(n.getMinutes()).padStart(2,'0');
|
||
var ss=String(n.getSeconds()).padStart(2,'0');
|
||
clockEl.textContent=hh+':'+mm+':'+ss;
|
||
var arcs=consoleBox.querySelectorAll('[id$="-arc"]');
|
||
arcs.forEach(function(el){
|
||
var c=parseFloat(el.dataset.circ);
|
||
var max=parseFloat(el.dataset.max);
|
||
var valEl=document.getElementById(el.id.replace('-arc','-val'));
|
||
if(!valEl) return;
|
||
var v=parseFloat(valEl.textContent)||0;
|
||
var jitter=(Math.random()-0.5)*max*0.005;
|
||
var p=Math.min(1,Math.max(0,(v+jitter)/max));
|
||
el.setAttribute('stroke-dashoffset',c*(1-p));
|
||
});
|
||
}
|
||
lhTick(); // fire once on first paint
|
||
window._lhPulseTimer=setInterval(lhTick,1000);
|
||
|
||
/* — old 3D start function retained-but-unused for now —
|
||
function _disabled_start3DClock(canvas, currentShift){
|
||
if(typeof THREE==='undefined'){
|
||
// Three.js not loaded yet. Retry once after defer settles.
|
||
var poll=setInterval(function(){
|
||
if(typeof THREE!=='undefined'){ clearInterval(poll); start3DClock(canvas,currentShift); }
|
||
},150);
|
||
setTimeout(function(){ clearInterval(poll); },5000);
|
||
return;
|
||
}
|
||
var SIZE=320;
|
||
var dpr=window.devicePixelRatio||1;
|
||
canvas.width=SIZE*dpr; canvas.height=SIZE*dpr;
|
||
var renderer=new THREE.WebGLRenderer({canvas:canvas,antialias:true,alpha:true});
|
||
renderer.setPixelRatio(dpr);
|
||
renderer.setSize(SIZE,SIZE,false);
|
||
var scene=new THREE.Scene();
|
||
var camera=new THREE.PerspectiveCamera(36,1,0.1,100);
|
||
camera.position.set(0,0.3,5.0); camera.lookAt(0,0,0);
|
||
|
||
// Lights — three-point setup. Key from upper right, fill ambient,
|
||
// rim accent in #79c0ff (matches eyebrow blue) so edges glow cool.
|
||
scene.add(new THREE.DirectionalLight(0xffffff,1.5).translateOnAxis(new THREE.Vector3(2,4,3).normalize(),1));
|
||
scene.add(new THREE.AmbientLight(0x222a3f,0.7));
|
||
var rim=new THREE.PointLight(0x79c0ff,0.9,8); rim.position.set(-3,1,2); scene.add(rim);
|
||
|
||
// Group all dial geometry. Tilting the group ~20° back makes the
|
||
// 3D depth obvious from the get-go (vs flat-to-camera, where it
|
||
// looks like a 2D circle no matter how good the shading is).
|
||
var dialGroup=new THREE.Group();
|
||
dialGroup.rotation.x=-0.32;
|
||
scene.add(dialGroup);
|
||
|
||
// Bezel torus + recessed face disc.
|
||
var torus=new THREE.Mesh(
|
||
new THREE.TorusGeometry(1.6,0.10,32,96),
|
||
new THREE.MeshStandardMaterial({color:0x21262d,roughness:0.35,metalness:0.85})
|
||
);
|
||
dialGroup.add(torus);
|
||
var disc=new THREE.Mesh(
|
||
new THREE.CircleGeometry(1.55,96),
|
||
new THREE.MeshStandardMaterial({color:0x0d1117,roughness:0.7,metalness:0.2})
|
||
);
|
||
disc.position.z=-0.06; dialGroup.add(disc);
|
||
|
||
function hrToAngle(h){ return Math.PI/2-(h/24)*Math.PI*2; }
|
||
function colorHex(h){ return parseInt((h||'#ffffff').replace('#',''),16); }
|
||
|
||
// Shift arcs as flat ring segments, extruded slightly so they
|
||
// catch light. Active shift gets a higher emissive base + sin
|
||
// pulse so it visibly breathes.
|
||
var shiftDefs=[
|
||
{name:'1st',start:6, end:14, color:colorHex(SHIFT_COLORS['1st'])},
|
||
{name:'2nd',start:14,end:22, color:colorHex(SHIFT_COLORS['2nd'])},
|
||
{name:'3rd',start:22,end:30, color:colorHex(SHIFT_COLORS['3rd'])},
|
||
];
|
||
var arcMeshes=[];
|
||
shiftDefs.forEach(function(s){
|
||
var shape=new THREE.Shape();
|
||
var rO=1.50, rI=1.36;
|
||
var a0=hrToAngle(s.start), a1=hrToAngle(s.end);
|
||
shape.moveTo(Math.cos(a0)*rO, Math.sin(a0)*rO);
|
||
shape.absarc(0,0,rO,a0,a1,true);
|
||
shape.lineTo(Math.cos(a1)*rI, Math.sin(a1)*rI);
|
||
shape.absarc(0,0,rI,a1,a0,false);
|
||
shape.closePath();
|
||
var isActive=(s.name===currentShift);
|
||
var mat=new THREE.MeshStandardMaterial({
|
||
color:s.color, emissive:s.color,
|
||
emissiveIntensity: isActive?0.7:0.18,
|
||
metalness:0.45, roughness:0.45,
|
||
});
|
||
var geom=new THREE.ExtrudeGeometry(shape,{depth:0.04,bevelEnabled:false});
|
||
var m=new THREE.Mesh(geom,mat);
|
||
m.userData={isActive:isActive, baseEmissive:isActive?0.7:0.18};
|
||
arcMeshes.push(m);
|
||
dialGroup.add(m);
|
||
});
|
||
|
||
// 24 hour ticks around the rim — every hour gets a small raised
|
||
// marker. Major hours (0/6/12/18) are taller. The current hour
|
||
// glows brighter so you can read time off the rim too.
|
||
var hourMarkers=[];
|
||
for(var h=0; h<24; h++){
|
||
var a=hrToAngle(h);
|
||
var isMajor=(h%6===0);
|
||
var t=new THREE.Mesh(
|
||
new THREE.BoxGeometry(isMajor?0.05:0.03, isMajor?0.18:0.10, 0.05),
|
||
new THREE.MeshStandardMaterial({
|
||
color:isMajor?0xc9d1d9:0x6e7681,
|
||
emissive:isMajor?0x222a3f:0x000000,
|
||
emissiveIntensity:0.3,
|
||
roughness:0.55,
|
||
})
|
||
);
|
||
t.position.set(Math.cos(a)*1.18, Math.sin(a)*1.18, 0);
|
||
t.rotation.z=a-Math.PI/2;
|
||
t.userData={hour:h};
|
||
hourMarkers.push(t);
|
||
dialGroup.add(t);
|
||
}
|
||
|
||
// Needle group — rotates with the dial AND independently to track
|
||
// the current time. Adding to dialGroup so the tilt comes for free.
|
||
var needlePivot=new THREE.Group();
|
||
var needle=new THREE.Mesh(
|
||
new THREE.BoxGeometry(0.05,1.18,0.06),
|
||
new THREE.MeshStandardMaterial({color:0xf85149,emissive:0xf85149,emissiveIntensity:0.85,metalness:0.7,roughness:0.3})
|
||
);
|
||
needle.position.y=0.59; // base at hub, tip at radius 1.18
|
||
needlePivot.add(needle);
|
||
dialGroup.add(needlePivot);
|
||
|
||
// Center hub — emissive sphere.
|
||
var hub=new THREE.Mesh(
|
||
new THREE.SphereGeometry(0.10,24,24),
|
||
new THREE.MeshStandardMaterial({color:0xf85149,emissive:0xf85149,emissiveIntensity:1.0,metalness:0.5,roughness:0.25})
|
||
);
|
||
dialGroup.add(hub);
|
||
|
||
// Tip glow — a small sphere riding the needle tip, subtle bloom-
|
||
// substitute. Gives the needle a "heat" feel.
|
||
var tipGlow=new THREE.Mesh(
|
||
new THREE.SphereGeometry(0.07,16,16),
|
||
new THREE.MeshBasicMaterial({color:0xff6b5a,transparent:true,opacity:0.7})
|
||
);
|
||
needlePivot.add(tipGlow);
|
||
tipGlow.position.y=1.18;
|
||
|
||
// Particle halo — 280 dots in a torus around the clock, drifting
|
||
// along their orbit. Reads as "system thinking", "data flowing"
|
||
// — the whole point of J's "highlight the system" ask. Each dot
|
||
// is a Points vertex, all in one BufferGeometry, one draw call.
|
||
var PARTICLE_COUNT=280;
|
||
var positions=new Float32Array(PARTICLE_COUNT*3);
|
||
var phases=new Float32Array(PARTICLE_COUNT);
|
||
var radii=new Float32Array(PARTICLE_COUNT);
|
||
var heights=new Float32Array(PARTICLE_COUNT);
|
||
for(var i=0;i<PARTICLE_COUNT;i++){
|
||
phases[i]=Math.random()*Math.PI*2;
|
||
radii[i]=1.85+Math.random()*0.55;
|
||
heights[i]=(Math.random()-0.5)*0.18;
|
||
}
|
||
var pGeom=new THREE.BufferGeometry();
|
||
pGeom.setAttribute('position',new THREE.BufferAttribute(positions,3));
|
||
var pMat=new THREE.PointsMaterial({
|
||
color:0x79c0ff, size:0.045, transparent:true, opacity:0.55,
|
||
blending:THREE.AdditiveBlending, depthWrite:false,
|
||
});
|
||
var particles=new THREE.Points(pGeom,pMat);
|
||
dialGroup.add(particles);
|
||
|
||
// Pulse rings — when a minute ticks over, a ring expands outward
|
||
// from the center and fades. Pool of 4 reused rings so memory
|
||
// stays flat. The visual is "system pinged" — a heartbeat.
|
||
var pulseRings=[];
|
||
for(var r=0;r<4;r++){
|
||
var ring=new THREE.Mesh(
|
||
new THREE.RingGeometry(0.5,0.55,64),
|
||
new THREE.MeshBasicMaterial({color:0x79c0ff,transparent:true,opacity:0,side:THREE.DoubleSide})
|
||
);
|
||
ring.userData={active:false,t:0};
|
||
pulseRings.push(ring);
|
||
dialGroup.add(ring);
|
||
}
|
||
function firePulse(){
|
||
var ring=pulseRings.find(function(r){return !r.userData.active});
|
||
if(!ring) return;
|
||
ring.userData.active=true;
|
||
ring.userData.t=0;
|
||
ring.scale.set(1,1,1);
|
||
ring.material.opacity=0.9;
|
||
}
|
||
|
||
// Animate. Five things happen each frame:
|
||
// 1. Needle tracks real time (rotation around Z)
|
||
// 2. Active shift arc pulses (emissiveIntensity sin wave)
|
||
// 3. Particles drift along their orbit (positions array updated)
|
||
// 4. Pulse rings expand + fade when fired (every minute tick)
|
||
// 5. Camera orbits subtly to make 3D depth read at all times
|
||
var t0=performance.now();
|
||
var lastMinute=-1;
|
||
function tick(){
|
||
var now=new Date();
|
||
var hr=now.getHours()+now.getMinutes()/60+now.getSeconds()/3600;
|
||
needlePivot.rotation.z=hrToAngle(hr)-Math.PI/2;
|
||
|
||
var t=(performance.now()-t0)*0.001;
|
||
|
||
// Active shift arc — sine pulse around its base intensity.
|
||
arcMeshes.forEach(function(m){
|
||
if(m.userData.isActive){
|
||
m.material.emissiveIntensity=m.userData.baseEmissive+Math.sin(t*2.4)*0.25;
|
||
}
|
||
});
|
||
|
||
// Hour markers — current hour gets a stronger emissive.
|
||
var curHour=Math.floor(hr)%24;
|
||
hourMarkers.forEach(function(mk){
|
||
var isCurrent=(mk.userData.hour===curHour);
|
||
mk.material.emissiveIntensity=isCurrent?(0.8+Math.sin(t*3)*0.3):0.3;
|
||
});
|
||
|
||
// Particles — each one orbits at its own radius/phase, drifting
|
||
// counter-clockwise so they read as "data flowing toward the
|
||
// future" relative to the needle.
|
||
var pos=particles.geometry.attributes.position.array;
|
||
for(var i=0;i<PARTICLE_COUNT;i++){
|
||
var phase=phases[i]+t*0.18+i*0.0001;
|
||
pos[i*3 ]=Math.cos(phase)*radii[i];
|
||
pos[i*3+1]=Math.sin(phase)*radii[i];
|
||
pos[i*3+2]=heights[i]+Math.sin(t*1.2+phase*3)*0.05;
|
||
}
|
||
particles.geometry.attributes.position.needsUpdate=true;
|
||
|
||
// Pulse rings — when a minute boundary is crossed, fire a new
|
||
// pulse. Each active ring expands from r=0.5 to r=2.6 and fades.
|
||
var nowMin=now.getMinutes();
|
||
if(nowMin!==lastMinute){
|
||
if(lastMinute!==-1) firePulse();
|
||
lastMinute=nowMin;
|
||
}
|
||
pulseRings.forEach(function(ring){
|
||
if(!ring.userData.active) return;
|
||
ring.userData.t+=0.016;
|
||
var p=ring.userData.t/2.2; // 2.2s lifetime
|
||
if(p>=1){
|
||
ring.userData.active=false;
|
||
ring.material.opacity=0;
|
||
return;
|
||
}
|
||
var s=1+p*5;
|
||
ring.scale.set(s,s,1);
|
||
ring.material.opacity=0.9*(1-p);
|
||
});
|
||
|
||
// Camera orbit — wider arc than before so the 3D tilt is
|
||
// visibly revealed as the camera swings.
|
||
camera.position.x=Math.sin(t*0.45)*0.55;
|
||
camera.position.y=0.3+Math.cos(t*0.38)*0.12;
|
||
camera.lookAt(0,0,0);
|
||
|
||
// Overlay time text — once per second.
|
||
if(Math.floor(t)!==tick._lastSec){
|
||
tick._lastSec=Math.floor(t);
|
||
var tEl=document.getElementById('clk3d-time');
|
||
if(tEl) tEl.textContent=now.toTimeString().slice(0,5);
|
||
}
|
||
|
||
renderer.render(scene,camera);
|
||
requestAnimationFrame(tick);
|
||
}
|
||
requestAnimationFrame(tick);
|
||
|
||
// Cleanup if the canvas leaves the DOM (loadLiveMarket re-runs)
|
||
var observer=new MutationObserver(function(){
|
||
if(!document.body.contains(canvas)){
|
||
observer.disconnect();
|
||
renderer.dispose();
|
||
}
|
||
});
|
||
observer.observe(document.body,{childList:true,subtree:true});
|
||
}
|
||
*/
|
||
root.appendChild(leftCol);
|
||
|
||
// ── RIGHT COLUMN: Chicago permit pulse ──
|
||
var rightCol=document.createElement('div');
|
||
rightCol.style.cssText='display:flex;flex-direction:column;gap:10px;min-width:0';
|
||
|
||
// Aggregate: workers needed, implied hourly bill demand, role mix, urgency mix
|
||
var totalWorkers=0, totalBillPerHr=0;
|
||
var roleMix={}, urgencyMix={overdue:0,urgent:0,soon:0,scheduled:0};
|
||
var shiftCounts={'1st':0,'2nd':0,'3rd':0,'4th':0};
|
||
// Per-shift breakdown for the click-to-preview interaction. A
|
||
// contract counts toward every shift it lists in shifts_needed,
|
||
// so totals across shifts can exceed the global total — that's
|
||
// intentional (one permit can demand workers across shifts).
|
||
var byShift={
|
||
'1st':{permits:0,workers:0,bill:0},
|
||
'2nd':{permits:0,workers:0,bill:0},
|
||
'3rd':{permits:0,workers:0,bill:0},
|
||
'4th':{permits:0,workers:0,bill:0},
|
||
};
|
||
contracts.forEach(function(c){
|
||
var prop=c.proposed||{}, tl=c.timeline||{};
|
||
var cnt=prop.count||0;
|
||
var bill=c.implied_bill_rate?c.implied_bill_rate*cnt:0;
|
||
totalWorkers+=cnt;
|
||
totalBillPerHr+=bill;
|
||
if(prop.role) roleMix[prop.role]=(roleMix[prop.role]||0)+cnt;
|
||
var u=tl.urgency||'scheduled';
|
||
if(urgencyMix[u]!==undefined) urgencyMix[u]++;
|
||
// Use the contract's schedule[] (real backend calendar) when it
|
||
// exists — each row carries workers_needed + bill_rate for that
|
||
// specific date+shift, so per-shift aggregates are honest to
|
||
// what the calendar actually scheduled. Fall back to the legacy
|
||
// shifts_needed counting only if schedule is absent.
|
||
if(c.schedule && c.schedule.length){
|
||
var todayKey=(new Date()).toISOString().slice(0,10);
|
||
var todays=c.schedule.filter(function(s){return s.date===todayKey});
|
||
todays.forEach(function(row){
|
||
if(byShift[row.shift]){
|
||
byShift[row.shift].permits++;
|
||
byShift[row.shift].workers += row.workers_needed||0;
|
||
byShift[row.shift].bill += (row.workers_needed||0) * (row.bill_rate||0);
|
||
}
|
||
if(shiftCounts[row.shift]!==undefined) shiftCounts[row.shift]++;
|
||
});
|
||
} else {
|
||
var shiftsForContract=(c.shifts_needed&&c.shifts_needed.length)?c.shifts_needed:[cs];
|
||
shiftsForContract.forEach(function(s){
|
||
if(shiftCounts[s]!==undefined) shiftCounts[s]++;
|
||
if(byShift[s]){
|
||
byShift[s].permits++;
|
||
byShift[s].workers+=cnt;
|
||
byShift[s].bill+=bill;
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
// Rolling bill-rate counter — the headline number is unique to the
|
||
// right side (counts/coverage already live in the left console as
|
||
// dials). Animates from 0 to target on first paint with an ease-out
|
||
// cubic, then jitters ±0.8% every 3s so it reads as a live market
|
||
// signal rather than a static figure. Mono digits + green LED glow
|
||
// matches the digital-clock aesthetic across the row.
|
||
var rateBox=document.createElement('div');
|
||
rateBox.style.cssText='background:#0d1117;border:1px solid #171d27;border-radius:10px;padding:14px 18px';
|
||
var rateLbl=document.createElement('div');
|
||
rateLbl.style.cssText='font-size:10px;color:#545d68;text-transform:uppercase;letter-spacing:1px;margin-bottom:6px';
|
||
rateLbl.textContent='Combined bill demand · if every open permit fills';
|
||
var rateBig=document.createElement('div');
|
||
rateBig.id='lh-rate-big';
|
||
rateBig.style.cssText='font-size:32px;font-weight:700;color:#3fb950;letter-spacing:-0.4px;font-variant-numeric:tabular-nums;line-height:1.1;font-family:"SF Mono","Menlo","Consolas",monospace;text-shadow:0 0 8px rgba(63,185,80,0.45),0 0 2px rgba(63,185,80,0.7)';
|
||
rateBig.textContent='$0/hr';
|
||
var rateSub=document.createElement('div');
|
||
rateSub.style.cssText='font-size:11px;color:#8b949e;margin-top:6px';
|
||
rateSub.textContent='Aggregate across '+contracts.length+' open permit'+(contracts.length===1?'':'s')+' · revenue if 100% filled';
|
||
rateBox.appendChild(rateLbl);rateBox.appendChild(rateBig);rateBox.appendChild(rateSub);
|
||
rightCol.appendChild(rateBox);
|
||
|
||
// Animate the rate from 0 → target on first paint, then jitter to
|
||
// simulate live signal. The target lives on rateBig.dataset.target
|
||
// so the shift-mix click handler can reassign it without restarting
|
||
// the animation loop. Jitter interval reads dataset on every fire.
|
||
rateBig.dataset.target=String(Math.round(totalBillPerHr));
|
||
if(parseFloat(rateBig.dataset.target)>0){
|
||
var rateStartT=performance.now();
|
||
function animateRate(){
|
||
if(!document.body.contains(rateBig)) return;
|
||
var dt=performance.now()-rateStartT;
|
||
var p=Math.min(1,dt/1200);
|
||
var eased=1-Math.pow(1-p,3);
|
||
var target=parseFloat(rateBig.dataset.target)||0;
|
||
var val=Math.floor(target*eased);
|
||
rateBig.textContent='$'+val.toLocaleString()+'/hr';
|
||
if(p<1) requestAnimationFrame(animateRate);
|
||
else {
|
||
var iv=setInterval(function(){
|
||
if(!document.body.contains(rateBig)){clearInterval(iv);return}
|
||
var t=parseFloat(rateBig.dataset.target)||0;
|
||
if(t<=0){ rateBig.textContent='—'; return; }
|
||
var jittered=t+Math.round((Math.random()-0.5)*t*0.016);
|
||
rateBig.textContent='$'+jittered.toLocaleString()+'/hr';
|
||
},3000);
|
||
}
|
||
}
|
||
requestAnimationFrame(animateRate);
|
||
} else {
|
||
rateBig.textContent='—';
|
||
rateBig.style.color='#545d68';
|
||
}
|
||
|
||
// Deadline pressure meter — the right side's other unique piece.
|
||
// Each urgency tier gets a count + small color-coded pill. If
|
||
// there's any overdue or urgent, the row gets a pulsing red dot
|
||
// accent at the left edge.
|
||
var urgBox=document.createElement('div');
|
||
urgBox.style.cssText='background:#0d1117;border:1px solid #171d27;border-radius:10px;padding:12px 16px';
|
||
var urgLbl=document.createElement('div');
|
||
urgLbl.style.cssText='font-size:10px;color:#545d68;text-transform:uppercase;letter-spacing:1px;margin-bottom:10px;display:flex;align-items:center;gap:6px';
|
||
var hasHot=(urgencyMix.overdue||0)+(urgencyMix.urgent||0)>0;
|
||
if(hasHot){
|
||
var alertDot=document.createElement('span');
|
||
alertDot.style.cssText='display:inline-block;width:6px;height:6px;border-radius:50%;background:#f85149;animation:lh-pulse 1.1s ease-in-out infinite;box-shadow:0 0 6px #f85149';
|
||
urgLbl.appendChild(alertDot);
|
||
}
|
||
urgLbl.appendChild(document.createTextNode('Deadline pressure · open permits'));
|
||
urgBox.appendChild(urgLbl);
|
||
var urgRow=document.createElement('div');
|
||
urgRow.style.cssText='display:flex;gap:18px;align-items:baseline;flex-wrap:wrap';
|
||
var urgDefs=[
|
||
{key:'overdue', color:'#f85149', label:'overdue'},
|
||
{key:'urgent', color:'#f5894a', label:'urgent'},
|
||
{key:'soon', color:'#d29922', label:'soon'},
|
||
{key:'scheduled',color:'#8b949e', label:'scheduled'},
|
||
];
|
||
urgDefs.forEach(function(u){
|
||
var cell=document.createElement('div');
|
||
cell.style.cssText='display:flex;flex-direction:column;align-items:flex-start';
|
||
var v=document.createElement('span');
|
||
v.style.cssText='font-size:22px;font-weight:700;color:'+u.color+';font-variant-numeric:tabular-nums;letter-spacing:-0.4px;line-height:1';
|
||
v.textContent=urgencyMix[u.key]||0;
|
||
var l=document.createElement('span');
|
||
l.style.cssText='font-size:9px;color:#6e7681;text-transform:uppercase;letter-spacing:1px;margin-top:4px';
|
||
l.textContent=u.label;
|
||
cell.appendChild(v);cell.appendChild(l);
|
||
urgRow.appendChild(cell);
|
||
});
|
||
urgBox.appendChild(urgRow);
|
||
rightCol.appendChild(urgBox);
|
||
|
||
// Role mix — top 4 roles
|
||
var roleEntries=Object.keys(roleMix).map(function(k){return [k,roleMix[k]];}).sort(function(a,b){return b[1]-a[1];}).slice(0,4);
|
||
if(roleEntries.length){
|
||
var roleBox=document.createElement('div');
|
||
roleBox.style.cssText='background:#0d1117;border:1px solid #171d27;border-radius:8px;padding:8px 12px';
|
||
var rhd=document.createElement('div');rhd.style.cssText='font-size:9px;color:#545d68;text-transform:uppercase;letter-spacing:1px;margin-bottom:6px';
|
||
rhd.textContent='Roles in demand';
|
||
roleBox.appendChild(rhd);
|
||
var maxN=roleEntries[0][1]||1;
|
||
roleEntries.forEach(function(pair){
|
||
var row=document.createElement('div');row.style.cssText='display:flex;align-items:center;gap:8px;margin:3px 0;font-size:11px';
|
||
var nm=document.createElement('span');nm.style.cssText='color:#e6edf3;flex:0 0 120px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis';
|
||
nm.textContent=pair[0];
|
||
var barWrap=document.createElement('div');barWrap.style.cssText='flex:1;height:6px;background:#161b22;border-radius:3px;overflow:hidden';
|
||
var bar=document.createElement('div');bar.style.cssText='height:100%;background:#58a6ff;width:'+Math.round((pair[1]/maxN)*100)+'%';
|
||
barWrap.appendChild(bar);
|
||
var qty=document.createElement('span');qty.style.cssText='color:#8b949e;font-size:11px;font-variant-numeric:tabular-nums;min-width:30px;text-align:right';
|
||
qty.textContent='×'+pair[1];
|
||
row.appendChild(nm);row.appendChild(barWrap);row.appendChild(qty);
|
||
roleBox.appendChild(row);
|
||
});
|
||
rightCol.appendChild(roleBox);
|
||
}
|
||
|
||
// Shift mix — visual comparison of permit demand across the four
|
||
// standard shifts, plus interactive previewing. Each shift is a
|
||
// pseudo-button: a bar-chart bar (height ∝ permit count) with the
|
||
// count printed on it. Clicking a button updates the left console's
|
||
// shift badge so the user can preview "what does this shift look
|
||
// like" — labeled "live" when it matches real time, "preview"
|
||
// otherwise. The currently-active shift is highlighted via a
|
||
// colored border and a deeper fill opacity.
|
||
var shiftMixBox=document.createElement('div');
|
||
shiftMixBox.style.cssText='background:#0d1117;border:1px solid #171d27;border-radius:10px;padding:12px 16px';
|
||
var shiftMixHeader=document.createElement('div');
|
||
shiftMixHeader.style.cssText='font-size:10px;color:#545d68;text-transform:uppercase;letter-spacing:1px;margin-bottom:10px;display:flex;justify-content:space-between;align-items:center';
|
||
var shiftMixTitle=document.createElement('span');
|
||
shiftMixTitle.textContent='Shift mix · click to preview';
|
||
var shiftMixHint=document.createElement('span');
|
||
shiftMixHint.style.cssText='font-size:9px;color:#6e7681;font-weight:400';
|
||
shiftMixHint.textContent='live now · '+cs;
|
||
shiftMixHeader.appendChild(shiftMixTitle);
|
||
shiftMixHeader.appendChild(shiftMixHint);
|
||
shiftMixBox.appendChild(shiftMixHeader);
|
||
|
||
var shiftMixGrid=document.createElement('div');
|
||
shiftMixGrid.style.cssText='display:grid;grid-template-columns:repeat(4,1fr);gap:6px';
|
||
var maxShiftCount=Math.max(1,
|
||
shiftCounts['1st']||0, shiftCounts['2nd']||0,
|
||
shiftCounts['3rd']||0, shiftCounts['4th']||0);
|
||
|
||
['1st','2nd','3rd','4th'].forEach(function(s){
|
||
var cnt=shiftCounts[s]||0;
|
||
var hoursLabel={'1st':'06–14','2nd':'14–22','3rd':'22–06','4th':'wknd'}[s];
|
||
var btn=document.createElement('button');
|
||
btn.dataset.shift=s;
|
||
var isActive=(s===cs);
|
||
var pctHeight=Math.round((cnt/maxShiftCount)*100);
|
||
btn.style.cssText='background:#161b22;border:1px solid '+(isActive?SHIFT_COLORS[s]:'#1a2030')+';border-radius:6px;padding:8px 6px 8px;cursor:pointer;text-align:center;display:flex;flex-direction:column;align-items:stretch;gap:4px;font-family:inherit;color:#e6edf3;outline:none;position:relative;overflow:hidden;transition:border-color 200ms,background 200ms;min-height:78px';
|
||
|
||
// Background bar — height proportional to count, color = shift.
|
||
var fillBar=document.createElement('div');
|
||
fillBar.className='shift-fill';
|
||
fillBar.style.cssText='position:absolute;left:0;right:0;bottom:0;height:'+pctHeight+'%;background:'+SHIFT_COLORS[s]+';opacity:'+(isActive?0.22:0.08)+';transition:opacity 200ms;pointer-events:none';
|
||
btn.appendChild(fillBar);
|
||
|
||
// Shift label
|
||
var nameRow=document.createElement('div');
|
||
nameRow.style.cssText='font-size:10px;color:'+SHIFT_COLORS[s]+';font-weight:700;letter-spacing:1.5px;text-transform:uppercase;position:relative;line-height:1';
|
||
nameRow.textContent=s;
|
||
btn.appendChild(nameRow);
|
||
|
||
// Big count
|
||
var ctRow=document.createElement('div');
|
||
ctRow.style.cssText='font-size:20px;font-weight:700;color:#e6edf3;letter-spacing:-0.3px;font-variant-numeric:tabular-nums;line-height:1;position:relative;margin-top:2px';
|
||
ctRow.textContent=cnt;
|
||
btn.appendChild(ctRow);
|
||
|
||
// Hours range (sub)
|
||
var subRow=document.createElement('div');
|
||
subRow.style.cssText='font-size:9px;color:#6e7681;letter-spacing:1px;position:relative;margin-top:auto;font-variant-numeric:tabular-nums';
|
||
subRow.textContent=hoursLabel;
|
||
btn.appendChild(subRow);
|
||
|
||
btn.onclick=function(){
|
||
var shift=this.dataset.shift;
|
||
var liveNow=(shift===cs);
|
||
// Update the left console's shift badge — color + label change.
|
||
shiftBadge.textContent=shift+' shift '+(liveNow?'live':'preview');
|
||
shiftBadge.style.color=SHIFT_COLORS[shift];
|
||
// Update sibling buttons' active state.
|
||
shiftMixGrid.querySelectorAll('button').forEach(function(b){
|
||
var s2=b.dataset.shift;
|
||
var isAct=(s2===shift);
|
||
b.style.borderColor=isAct?SHIFT_COLORS[s2]:'#1a2030';
|
||
var bar=b.querySelector('.shift-fill');
|
||
if(bar) bar.style.opacity=isAct?0.22:0.08;
|
||
});
|
||
// Re-slice the metrics: dials + bill rate now reflect THIS
|
||
// shift's data only. A repeated click on the same shift toggles
|
||
// back to the all-shifts total so the user can compare.
|
||
var sd=byShift[shift]||{permits:0,workers:0,bill:0};
|
||
var isToggleOff=(window._lhActiveShift===shift);
|
||
var view = isToggleOff
|
||
? { permits:contracts.length, workers:totalWorkers, bill:totalBillPerHr, label:'all shifts' }
|
||
: { permits:sd.permits, workers:sd.workers, bill:sd.bill, label:shift+' shift only' };
|
||
window._lhActiveShift = isToggleOff ? null : shift;
|
||
|
||
// Push values into the dial gauges. setDial scales each gauge's
|
||
// arc to the new value and updates the center number; the
|
||
// ticker (lhTick) keeps wobbling against whatever value lands.
|
||
function setDial(id,value,max){
|
||
var arc=document.getElementById(id+'-arc');
|
||
var val=document.getElementById(id+'-val');
|
||
if(!arc||!val) return;
|
||
val.textContent=value;
|
||
arc.dataset.max=max;
|
||
var c=parseFloat(arc.dataset.circ);
|
||
arc.setAttribute('stroke-dashoffset',c*(1-Math.min(1,value/max)));
|
||
}
|
||
var benchEst=Math.floor(view.permits*45)||50;
|
||
var coverage=view.workers>0?Math.min(100,Math.floor(benchEst/view.workers*100)):100;
|
||
setDial('d-permits',view.permits, Math.max(50, view.permits*1.5));
|
||
setDial('d-workers',view.workers, Math.max(300, view.workers*1.5));
|
||
setDial('d-bench', benchEst, Math.max(1500, benchEst*1.5));
|
||
setDial('d-fill', coverage, 100);
|
||
|
||
// Bill rate counter — push new target into dataset so the
|
||
// existing jitter loop picks it up and the shown value tracks.
|
||
rateBig.dataset.target=String(Math.round(view.bill));
|
||
rateBig.textContent='$'+Math.round(view.bill).toLocaleString()+'/hr';
|
||
rateSub.textContent=view.permits+' permit'+(view.permits===1?'':'s')+' · '+view.label;
|
||
|
||
// If we toggled off, clear all active borders so the buttons
|
||
// visually read as "no shift selected · viewing all".
|
||
if(isToggleOff){
|
||
shiftMixGrid.querySelectorAll('button').forEach(function(b){
|
||
b.style.borderColor='#1a2030';
|
||
var bar=b.querySelector('.shift-fill');
|
||
if(bar) bar.style.opacity=0.08;
|
||
});
|
||
shiftBadge.textContent='all shifts · live='+cs;
|
||
shiftBadge.style.color='#8b949e';
|
||
}
|
||
};
|
||
shiftMixGrid.appendChild(btn);
|
||
});
|
||
shiftMixBox.appendChild(shiftMixGrid);
|
||
rightCol.appendChild(shiftMixBox);
|
||
|
||
root.appendChild(rightCol);
|
||
}
|
||
|
||
function loadStaffingForecast(){
|
||
api('/intelligence/staffing_forecast',{}).then(function(r){
|
||
var el=document.getElementById('staffing-forecast');el.textContent='';
|
||
if(!r||!r.forecast){el.textContent='Forecast unavailable.';return}
|
||
// Header summary
|
||
var hdr=document.createElement('div');hdr.style.cssText='background:#0d1117;border:1px solid #171d27;border-radius:10px;padding:14px;margin-bottom:10px;display:flex;justify-content:space-between;gap:16px';
|
||
var left=document.createElement('div');
|
||
var big=document.createElement('div');big.style.cssText='font-size:22px;font-weight:700;color:#e6edf3;letter-spacing:-0.5px';
|
||
big.textContent='$'+(r.total_cost||0).toLocaleString('en-US',{maximumFractionDigits:0})+' in construction coming';
|
||
var sub=document.createElement('div');sub.style.cssText='color:#8b949e;font-size:12px;margin-top:4px';
|
||
sub.textContent=r.permit_count+' permits filed last 30 days · ~'+r.total_estimated_workers+' workers needed across roles';
|
||
left.appendChild(big);left.appendChild(sub);hdr.appendChild(left);
|
||
var right=document.createElement('div');right.style.cssText='text-align:right;font-size:11px';
|
||
if(r.critical_roles>0){
|
||
var c=document.createElement('div');c.style.cssText='color:#f85149;font-weight:700';
|
||
c.textContent=r.critical_roles+' CRITICAL role'+(r.critical_roles!==1?'s':'')+' — supply gap';right.appendChild(c);
|
||
} else if(r.tight_roles>0){
|
||
var t=document.createElement('div');t.style.cssText='color:#d29922;font-weight:700';
|
||
t.textContent=r.tight_roles+' tight role'+(r.tight_roles!==1?'s':'');right.appendChild(t);
|
||
} else {
|
||
var g=document.createElement('div');g.style.cssText='color:#3fb950;font-weight:600';g.textContent='bench covers predicted demand';right.appendChild(g);
|
||
}
|
||
var when=document.createElement('div');when.style.cssText='color:#545d68;margin-top:2px';
|
||
when.textContent='updated '+((r.duration_ms||0)/1000).toFixed(1)+'s ago';right.appendChild(when);
|
||
hdr.appendChild(right);el.appendChild(hdr);
|
||
// Per-role rows
|
||
r.forecast.forEach(function(f){
|
||
var row=document.createElement('div');
|
||
var riskColor={critical:'#f85149',tight:'#d29922',watch:'#d29922',ok:'#3fb950'}[f.risk]||'#8b949e';
|
||
row.style.cssText='background:#0d1117;border:1px solid #171d27;border-radius:8px;padding:10px 14px;margin-bottom:6px;border-left:3px solid '+riskColor+';display:flex;justify-content:space-between;gap:12px;font-size:12px;align-items:center';
|
||
var l=document.createElement('div');
|
||
var n=document.createElement('div');n.style.cssText='color:#e6edf3;font-weight:600;font-size:13px';
|
||
n.textContent=f.role;
|
||
var d=document.createElement('div');d.style.cssText='color:#8b949e;font-size:11px;margin-top:2px';
|
||
d.textContent=f.demand_permits+' permit'+(f.demand_permits!==1?'s':'')+' · est '+f.demand_workers+' workers · earliest staffing deadline '+f.earliest_staffing_deadline;
|
||
l.appendChild(n);l.appendChild(d);row.appendChild(l);
|
||
var r2=document.createElement('div');r2.style.cssText='text-align:right;white-space:nowrap';
|
||
var cov=document.createElement('div');cov.style.cssText='color:'+riskColor+';font-weight:700;font-size:13px';
|
||
cov.textContent=f.bench_available.toLocaleString()+' / '+f.demand_workers+' available ('+f.coverage_pct+'%)';
|
||
var days=document.createElement('div');days.style.cssText='color:'+(f.days_to_deadline<=0?'#f85149':f.days_to_deadline<=7?'#d29922':'#8b949e')+';font-size:11px;margin-top:2px';
|
||
days.textContent=f.days_to_deadline<=0?(Math.abs(f.days_to_deadline)+'d overdue'):(f.days_to_deadline+'d to deadline');
|
||
r2.appendChild(cov);r2.appendChild(days);row.appendChild(r2);
|
||
el.appendChild(row);
|
||
});
|
||
}).catch(function(e){
|
||
document.getElementById('staffing-forecast').textContent='Forecast error: '+(e.message||e);
|
||
});
|
||
}
|
||
|
||
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()})
|
||
}
|
||
|
||
// Mirror of the Bun-side pay-rate formula so client-side renderers
|
||
// (main search, modal) can show a rate when they only have worker
|
||
// fields, not a pre-enriched hybrid source. Keep in sync with
|
||
// impliedPayRate() in mcp-server/index.ts.
|
||
var ROLE_BASE_PAY={Electrician:28,Welder:26,'Machine Operator':24,'Maintenance Tech':26,
|
||
'Forklift Operator':20,Loader:17,'Warehouse Associate':17,'Material Handler':18,
|
||
'Production Worker':18,'Quality Tech':23,'Line Lead':22,Assembler:18,'Shipping Clerk':19};
|
||
function computeImpliedPayRate(role,rel,archetype){
|
||
var base=ROLE_BASE_PAY[role||'']||19;
|
||
var r=typeof rel==='string'?parseFloat(rel):(rel||0.5);
|
||
var relBump=(isFinite(r)?r:0.5)*4;
|
||
var a=(archetype||'').toLowerCase();
|
||
var archBump=a==='specialist'?4:a==='leader'?3:a==='reliable'?1:0;
|
||
return Math.round((base+relBump+archBump)*100)/100;
|
||
}
|
||
|
||
// ─── Render one Project Index (custom Chicago build-signal portfolio) ──
|
||
// Layout:
|
||
// - Property row (BLDG ticker + address + owner placeholder)
|
||
// - Per-entity portfolio row:
|
||
// LLC·TICKER display_name [role badge] [risk pill]
|
||
// OSHA line (inspection count / most recent date / state list)
|
||
// ILSOS line (either data or "source unreachable" honest label)
|
||
// Conservative with color: green = clean, yellow = moderate, red = high.
|
||
function renderEntityBrief(host, brief){
|
||
host.textContent='';
|
||
if(!brief || !brief.entities){
|
||
var empty=document.createElement('div');empty.style.cssText='color:#545d68;font-size:11px;padding:6px 0';empty.textContent='No entities identified for this permit.';host.appendChild(empty);return;
|
||
}
|
||
|
||
// ─── MACRO TILE — Chicago construction employment context ─────────
|
||
// Same number applies to every permit; render as small strip above
|
||
// the project-index card so staffers see whether the broader market
|
||
// is helping or hurting them.
|
||
if(brief.macro && brief.macro.status==='ok' && brief.macro.latest){
|
||
var m=brief.macro;
|
||
var mLine=document.createElement('div');
|
||
var mc=m.trend==='growing'?'#3fb950':m.trend==='declining'?'#f85149':'#8b949e';
|
||
mLine.style.cssText='display:flex;align-items:center;gap:10px;padding:8px 12px;background:#0d1117;border:1px solid '+mc+'33;border-radius:6px;margin-bottom:8px;font-size:11px;color:#8b949e';
|
||
var mIcon=document.createElement('span');mIcon.style.cssText='font-size:14px';mIcon.textContent='📊';
|
||
var mTag=document.createElement('span');mTag.style.cssText='font-size:9px;color:#545d68;text-transform:uppercase;letter-spacing:1px';mTag.textContent='MACRO';
|
||
var mBody=document.createElement('span');mBody.style.cssText='flex:1';
|
||
var yoy=m.yoy_change_pct;
|
||
mBody.textContent='Chicago MSA construction: '+m.latest.value+'k workers ('+m.latest.period+')'+(yoy!==null?' · '+(yoy>=0?'+':'')+yoy.toFixed(1)+'% YoY':'');
|
||
var mChip=document.createElement('span');
|
||
mChip.style.cssText='padding:2px 8px;border-radius:9px;background:'+mc+'22;color:'+mc+';font-size:9px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px';
|
||
mChip.textContent=m.trend;
|
||
var mLink=document.createElement('a');mLink.href='https://www.bls.gov/eag/eag.il_chicago_msa.htm';mLink.target='_blank';mLink.rel='noopener';
|
||
mLink.style.cssText='color:#58a6ff;font-size:10px;text-decoration:none';
|
||
mLink.textContent='BLS ↗';
|
||
mLine.appendChild(mIcon);mLine.appendChild(mTag);mLine.appendChild(mBody);mLine.appendChild(mChip);mLine.appendChild(mLink);
|
||
host.appendChild(mLine);
|
||
}
|
||
|
||
// ─── PROJECT INDEX SCORE — auditable matrix score at top ─────────
|
||
// Shows the 0-100 aggregate + every signal that contributed (sorted
|
||
// by absolute impact). Click-to-expand the contribution breakdown.
|
||
var idx=brief.index_score;
|
||
if(idx){
|
||
var BAND_COLORS={red:'#f85149',amber:'#d29922',neutral:'#8b949e',green:'#3fb950',strong:'#2ea043'};
|
||
var bc=BAND_COLORS[idx.band]||'#8b949e';
|
||
var iSec=document.createElement('details');
|
||
iSec.style.cssText='background:#0d1117;border:2px solid '+bc+'66;border-radius:8px;margin-bottom:10px';
|
||
var iSum=document.createElement('summary');
|
||
iSum.style.cssText='list-style:none;cursor:pointer;padding:12px 14px;display:flex;align-items:center;gap:12px;outline:none';
|
||
var iLabel=document.createElement('span');
|
||
iLabel.style.cssText='font-size:10px;color:#545d68;text-transform:uppercase;letter-spacing:1px;font-weight:600';
|
||
iLabel.textContent='PROJECT INDEX';
|
||
var iScore=document.createElement('span');
|
||
iScore.style.cssText='font-family:ui-monospace,SFMono-Regular,monospace;font-size:22px;font-weight:700;color:'+bc;
|
||
iScore.textContent=idx.score;
|
||
var iBand=document.createElement('span');
|
||
iBand.style.cssText='padding:4px 10px;border-radius:9px;font-size:10px;font-weight:600;background:'+bc+'22;color:'+bc+';text-transform:uppercase;letter-spacing:0.5px';
|
||
iBand.textContent=idx.band+(idx.partial?' · partial':'');
|
||
var iCount=document.createElement('span');
|
||
iCount.style.cssText='color:#545d68;font-size:10px;margin-left:auto';
|
||
iCount.textContent=idx.contributions.length+' signals · 50 = neutral · '+(idx.contributions.length>0?'click to audit':'no signals');
|
||
iSum.appendChild(iLabel);iSum.appendChild(iScore);iSum.appendChild(iBand);iSum.appendChild(iCount);
|
||
iSec.appendChild(iSum);
|
||
|
||
var iBody=document.createElement('div');
|
||
iBody.style.cssText='padding:0 14px 12px;font-size:11px';
|
||
if(idx.contributions.length===0){
|
||
var ec=document.createElement('div');ec.style.cssText='color:#545d68;font-style:italic';
|
||
ec.textContent='No signals available — sources may still be loading.';
|
||
iBody.appendChild(ec);
|
||
} else {
|
||
idx.contributions.forEach(function(c){
|
||
var r=document.createElement('div');
|
||
r.style.cssText='display:flex;gap:8px;align-items:baseline;padding:4px 0;border-top:1px dashed #1f2631';
|
||
var sign=c.contribution>0?'+':c.contribution<0?'':'';
|
||
var col=c.contribution>0?'#3fb950':c.contribution<0?'#f85149':'#545d68';
|
||
var pts=document.createElement('span');
|
||
pts.style.cssText='font-family:ui-monospace,monospace;font-size:11px;color:'+col+';font-weight:600;min-width:42px;text-align:right;font-variant-numeric:tabular-nums';
|
||
pts.textContent=sign+(Math.round(c.contribution*10)/10);
|
||
var w=document.createElement('span');
|
||
w.style.cssText='font-size:9px;color:#545d68;font-family:ui-monospace,monospace;min-width:30px';
|
||
w.textContent='w='+c.weight;
|
||
var note=document.createElement('span');note.style.cssText='color:#8b949e;flex:1;min-width:0';
|
||
note.textContent=c.note;
|
||
var sig=document.createElement('span');
|
||
sig.style.cssText='font-size:9px;color:#484f58;font-family:ui-monospace,monospace;text-align:right;white-space:nowrap';
|
||
sig.textContent=c.signal;
|
||
r.appendChild(pts);r.appendChild(w);r.appendChild(note);r.appendChild(sig);
|
||
iBody.appendChild(r);
|
||
});
|
||
}
|
||
iSec.appendChild(iBody);
|
||
host.appendChild(iSec);
|
||
}
|
||
|
||
// ─── STOCK TICKERS — portfolio row at the top ─────────
|
||
// Every publicly-traded entity related to this permit, sorted by
|
||
// cap-proxy desc. "Most profitable related company" surfaces first.
|
||
// No tickers? Render an explicit "all contractors private" line so
|
||
// staffers know we looked, rather than silently hiding the section.
|
||
var tickers=brief.tickers||[];
|
||
var tSec=document.createElement('div');
|
||
tSec.style.cssText='background:#0d2818;border:1px solid #2ea04340;border-radius:6px;padding:10px 12px;margin-bottom:8px';
|
||
var tHead=document.createElement('div');
|
||
tHead.style.cssText='display:flex;align-items:center;gap:8px;margin-bottom:6px;font-size:10px;color:#3fb950;text-transform:uppercase;letter-spacing:1px;font-weight:600';
|
||
tHead.textContent='STOCK TICKERS — RELATED PUBLIC COMPANIES';
|
||
var tMeta=document.createElement('span');tMeta.style.cssText='margin-left:auto;color:#545d68;font-size:9px;letter-spacing:0.5px';
|
||
tMeta.textContent=tickers.length+' issuer'+(tickers.length===1?'':'s')+' · most-profitable first';
|
||
tHead.appendChild(tMeta);
|
||
tSec.appendChild(tHead);
|
||
if(tickers.length===0){
|
||
var tEmpty=document.createElement('div');
|
||
tEmpty.style.cssText='color:#8b949e;font-size:11px;line-height:1.5';
|
||
tEmpty.textContent='No direct public equity on this permit. Contractors here are private LLCs — pending: SEC EDGAR Exhibit 21 (parent/subsidiary), Wikidata relationships, OpenCorporates corp tree. Public beneficiaries can often be 1-2 hops away (e.g. private GC → public parent).';
|
||
tSec.appendChild(tEmpty);
|
||
} else {
|
||
tickers.forEach(function(t,idx){
|
||
var row=document.createElement('div');
|
||
row.style.cssText='display:flex;align-items:center;gap:10px;font-size:11px;padding:4px 0;border-top:'+(idx?'1px dashed #2ea04322':'none');
|
||
// Rank
|
||
var rank=document.createElement('span');rank.style.cssText='color:#545d68;font-size:10px;min-width:16px;font-weight:600';rank.textContent='#'+(idx+1);
|
||
// Ticker symbol
|
||
var tk=document.createElement('span');
|
||
tk.style.cssText='font-family:ui-monospace,SFMono-Regular,monospace;background:#0d1117;padding:3px 8px;border-radius:4px;color:#3fb950;border:1px solid #3fb95066;font-weight:700;font-size:11px;min-width:60px;text-align:center';
|
||
tk.textContent=t.ticker;
|
||
// Company + exchange
|
||
var nm=document.createElement('span');nm.style.cssText='color:#e6edf3;font-weight:500;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap';
|
||
nm.textContent=(t.company_name||t.ticker)+(t.exchange?' · '+t.exchange:'');
|
||
nm.title=t.sic_description?t.sic_description+' (SIC '+t.sic+')':'';
|
||
// Price
|
||
var px=document.createElement('span');px.style.cssText='color:#e6edf3;font-family:ui-monospace,monospace;font-size:11px;font-weight:600;min-width:70px;text-align:right';
|
||
px.textContent=t.price?'$'+t.price.toFixed(2):'—';
|
||
// Day change chip
|
||
var chg=document.createElement('span');
|
||
var ch=t.day_change_pct;
|
||
var chColor=(ch==null||isNaN(ch))?'#545d68':ch>=0?'#3fb950':'#f85149';
|
||
chg.style.cssText='font-family:ui-monospace,monospace;font-size:10px;color:'+chColor+';min-width:52px;text-align:right';
|
||
chg.textContent=(ch==null||isNaN(ch))?'':((ch>=0?'+':'')+ch.toFixed(2)+'%');
|
||
// External link
|
||
var lnk=document.createElement('a');lnk.href=t.stooq_url||'#';lnk.target='_blank';lnk.rel='noopener';
|
||
lnk.style.cssText='color:#58a6ff;text-decoration:none;font-size:10px;white-space:nowrap';
|
||
lnk.textContent='quote ↗';
|
||
row.appendChild(rank);row.appendChild(tk);row.appendChild(nm);row.appendChild(px);row.appendChild(chg);row.appendChild(lnk);
|
||
tSec.appendChild(row);
|
||
});
|
||
// Caveat — honest about cap-proxy vs real market cap
|
||
var caveat=document.createElement('div');
|
||
caveat.style.cssText='color:#545d68;font-size:9px;margin-top:6px;line-height:1.4;font-style:italic';
|
||
caveat.textContent='Ranked by price × volume (cap-proxy). Real market cap needs shares-outstanding from SEC XBRL — queued. Quote from Stooq ('+(tickers[0].price_date||'today')+'); profile from SEC EDGAR.';
|
||
tSec.appendChild(caveat);
|
||
}
|
||
host.appendChild(tSec);
|
||
|
||
// Property block — owner (Cook County Assessor) + violations + union
|
||
var prop=brief.property||{};
|
||
var pBlock=document.createElement('div');
|
||
pBlock.style.cssText='background:#161b22;border:1px solid #1f2631;border-radius:6px;padding:10px 12px;margin-bottom:8px;font-size:11px';
|
||
|
||
// Top row: ticker + address + owner inline
|
||
var pTop=document.createElement('div');
|
||
pTop.style.cssText='display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:4px';
|
||
var pTicker=document.createElement('span');
|
||
pTicker.style.cssText='font-family:ui-monospace,SFMono-Regular,monospace;background:#0d1117;padding:2px 6px;border-radius:4px;color:#d29922;border:1px solid #d2992244;font-weight:600;font-size:10px;flex-shrink:0';
|
||
pTicker.textContent=prop.ticker||'BLDG·?';
|
||
var pAddr=document.createElement('span');pAddr.style.cssText='color:#e6edf3;flex:1;min-width:0;font-weight:500';pAddr.textContent=prop.address||'';
|
||
pTop.appendChild(pTicker);pTop.appendChild(pAddr);
|
||
pBlock.appendChild(pTop);
|
||
|
||
// Owner line (Cook County Assessor)
|
||
var owner=prop.owner;
|
||
if(owner){
|
||
var oLine=document.createElement('div');
|
||
oLine.style.cssText='font-size:10px;color:#8b949e;margin-top:4px;display:flex;gap:6px;align-items:baseline;flex-wrap:wrap';
|
||
var oTag=document.createElement('span');oTag.style.cssText='font-size:9px;color:#545d68;text-transform:uppercase;letter-spacing:1px;min-width:54px';oTag.textContent='OWNER';
|
||
var oBody=document.createElement('span');oBody.style.cssText='flex:1;min-width:0';
|
||
if(owner.status==='ok'){
|
||
var mailLoc=[owner.mailing_city,owner.mailing_state,owner.mailing_zip].filter(Boolean).join(', ');
|
||
var localFlag=owner.mailing_state&&owner.mailing_state!=='IL'?' · ✦ owner mails out of state ('+owner.mailing_state+')':'';
|
||
oBody.textContent='PIN '+(owner.pin||'?')+' · mail to '+(owner.mailing_address||'?')+(mailLoc?' · '+mailLoc:'')+(owner.ward?' · ward '+owner.ward:'')+localFlag;
|
||
} else if(owner.status==='no_match'){
|
||
oBody.style.color='#545d68';
|
||
oBody.textContent='no parcel match in Cook County Assessor';
|
||
} else {
|
||
oBody.style.color='#d29922';
|
||
oBody.textContent='lookup error: '+(owner.error||'unknown');
|
||
}
|
||
oLine.appendChild(oTag);oLine.appendChild(oBody);
|
||
pBlock.appendChild(oLine);
|
||
}
|
||
|
||
// Violations line (Chicago Building Violations)
|
||
var v=prop.violations;
|
||
if(v && v.status==='ok'){
|
||
var vLine=document.createElement('div');
|
||
vLine.style.cssText='font-size:10px;color:#8b949e;margin-top:4px;display:flex;gap:6px;align-items:baseline;flex-wrap:wrap';
|
||
var vTag=document.createElement('span');vTag.style.cssText='font-size:9px;color:#545d68;text-transform:uppercase;letter-spacing:1px;min-width:54px';vTag.textContent='VIOLATIONS';
|
||
var vBody=document.createElement('span');vBody.style.cssText='flex:1;min-width:0';
|
||
var vc=v.total_violations===0?'#3fb950':v.open_violations>0?'#d29922':v.stop_work_orders>0?'#f85149':'#8b949e';
|
||
vBody.style.color=vc;
|
||
if(v.total_violations===0){
|
||
vBody.textContent='no violations on record · clean';
|
||
} else {
|
||
var bits=[v.total_violations+' total'];
|
||
if(v.open_violations>0) bits.push(v.open_violations+' OPEN');
|
||
if(v.stop_work_orders>0) bits.push('⚠ '+v.stop_work_orders+' STOP-WORK');
|
||
if(v.most_recent_date) bits.push('most recent '+v.most_recent_date);
|
||
vBody.textContent=bits.join(' · ');
|
||
}
|
||
vLine.appendChild(vTag);vLine.appendChild(vBody);
|
||
pBlock.appendChild(vLine);
|
||
}
|
||
|
||
// Site context line (TIF / landmark / ward / lat-lon)
|
||
var sc=prop.site_context;
|
||
if(sc && sc.status==='ok'){
|
||
var scLine=document.createElement('div');
|
||
scLine.style.cssText='font-size:10px;color:#8b949e;margin-top:4px;display:flex;gap:6px;align-items:baseline;flex-wrap:wrap';
|
||
var scTag=document.createElement('span');scTag.style.cssText='font-size:9px;color:#545d68;text-transform:uppercase;letter-spacing:1px;min-width:54px';scTag.textContent='SITE CTX';
|
||
var scBody=document.createElement('span');scBody.style.cssText='flex:1;min-width:0';
|
||
var scBits=[];
|
||
if(sc.in_tif_district){
|
||
var tifChip=document.createElement('span');
|
||
tifChip.style.cssText='display:inline-block;padding:2px 7px;border-radius:9px;background:#0d2818;border:1px solid #3fb95066;color:#3fb950;font-weight:600;font-size:10px;margin-right:6px';
|
||
tifChip.textContent='💰 TIF · '+(sc.tif_district_name||'subsidy zone');
|
||
tifChip.title='Site is inside a Tax Increment Financing district — public-subsidy backing';
|
||
scBody.appendChild(tifChip);
|
||
}
|
||
if(sc.is_landmark){
|
||
var lmChip=document.createElement('span');
|
||
lmChip.style.cssText='display:inline-block;padding:2px 7px;border-radius:9px;background:#3a2a14;border:1px solid #d2992266;color:#d29922;font-weight:600;font-size:10px;margin-right:6px';
|
||
lmChip.textContent='🏛 LANDMARK · '+(sc.landmark_name||'historic district');
|
||
lmChip.title='Preservation review will extend project timeline';
|
||
scBody.appendChild(lmChip);
|
||
}
|
||
if(sc.nearest_cta_station && sc.nearest_cta_distance_m!=null){
|
||
var ctaTxt='🚇 '+sc.nearest_cta_station+(sc.nearest_cta_lines?' ('+sc.nearest_cta_lines+')':'')+' · '+sc.nearest_cta_distance_m+'m';
|
||
var ctaCol=sc.nearest_cta_distance_m<=800?'#3fb950':sc.nearest_cta_distance_m<=1500?'#8b949e':'#d29922';
|
||
var ctaChip=document.createElement('span');
|
||
ctaChip.style.cssText='display:inline-block;padding:2px 7px;border-radius:9px;background:#0d1117;border:1px solid '+ctaCol+'66;color:'+ctaCol+';font-weight:600;font-size:10px;margin-right:6px';
|
||
ctaChip.textContent=ctaTxt;
|
||
scBody.appendChild(ctaChip);
|
||
}
|
||
if(sc.nearby_permits_90d!=null && sc.nearby_permits_90d>0){
|
||
var npCol=sc.nearby_permits_90d>5?'#d29922':'#8b949e';
|
||
var npChip=document.createElement('span');
|
||
npChip.style.cssText='display:inline-block;padding:2px 7px;border-radius:9px;background:#0d1117;border:1px solid '+npCol+'66;color:'+npCol+';font-weight:600;font-size:10px;margin-right:6px';
|
||
var npM=sc.nearby_permits_value_90d&&sc.nearby_permits_value_90d>=1e6?' · $'+(sc.nearby_permits_value_90d/1e6).toFixed(1)+'M':'';
|
||
npChip.textContent=sc.nearby_permits_90d+' nearby permits 90d'+npM;
|
||
npChip.title='Permits issued within 0.5mi · 90d window · '+(sc.nearby_permits_value_90d||0).toLocaleString()+' total $';
|
||
scBody.appendChild(npChip);
|
||
}
|
||
if(sc.ward) scBits.push('ward '+sc.ward);
|
||
if(sc.latitude && sc.longitude) scBits.push(sc.latitude.slice(0,7)+', '+sc.longitude.slice(0,8));
|
||
if(scBits.length){
|
||
var rest=document.createElement('span');rest.textContent=scBits.join(' · ');
|
||
scBody.appendChild(rest);
|
||
}
|
||
if(!sc.in_tif_district && !sc.is_landmark && !scBits.length) scBody.textContent='no extra context';
|
||
scLine.appendChild(scTag);scLine.appendChild(scBody);
|
||
pBlock.appendChild(scLine);
|
||
}
|
||
|
||
// Mechanics liens placeholder
|
||
var liens=prop.liens;
|
||
if(liens && liens.status==='needs_setup'){
|
||
var lLine=document.createElement('div');
|
||
lLine.style.cssText='font-size:10px;color:#545d68;margin-top:4px;display:flex;gap:6px;align-items:baseline;font-style:italic';
|
||
var lTag=document.createElement('span');lTag.style.cssText='font-size:9px;color:#545d68;text-transform:uppercase;letter-spacing:1px;min-width:54px;font-style:normal';lTag.textContent='LIENS';
|
||
var lBody=document.createElement('span');lBody.style.cssText='flex:1;min-width:0';
|
||
lBody.textContent='Cook County Recorder mechanics-lien lookup queued';
|
||
lLine.appendChild(lTag);lLine.appendChild(lBody);
|
||
pBlock.appendChild(lLine);
|
||
}
|
||
|
||
// Union line (static lookup by trade)
|
||
var u=prop.union;
|
||
if(u){
|
||
var uLine=document.createElement('div');
|
||
uLine.style.cssText='font-size:10px;color:#8b949e;margin-top:4px;display:flex;gap:6px;align-items:baseline;flex-wrap:wrap';
|
||
var uTag=document.createElement('span');uTag.style.cssText='font-size:9px;color:#545d68;text-transform:uppercase;letter-spacing:1px;min-width:54px';uTag.textContent='UNIONS';
|
||
var uBody=document.createElement('span');uBody.style.cssText='flex:1;min-width:0';
|
||
uBody.textContent=u.trade+' → '+(u.primary_locals||[]).map(function(l){return l.name}).join(', ');
|
||
uBody.title=(u.primary_locals||[]).map(function(l){return l.name+' — '+l.jurisdiction}).join('\n');
|
||
uLine.appendChild(uTag);uLine.appendChild(uBody);
|
||
pBlock.appendChild(uLine);
|
||
// Training centers chip row, collapsible-ish
|
||
if(u.training_centers && u.training_centers.length){
|
||
var tcLine=document.createElement('div');
|
||
tcLine.style.cssText='font-size:10px;color:#8b949e;margin-top:4px;padding-left:60px';
|
||
var tcTag=document.createElement('div');tcTag.style.cssText='font-size:9px;color:#545d68;text-transform:uppercase;letter-spacing:1px;margin-bottom:2px';tcTag.textContent='Training centers (apprenticeship)';
|
||
tcLine.appendChild(tcTag);
|
||
u.training_centers.forEach(function(tc){
|
||
var t=document.createElement('div');
|
||
t.style.cssText='font-size:10px;color:#8b949e;line-height:1.4;padding:1px 0';
|
||
t.textContent='· '+tc.name+' — '+tc.address+' ('+tc.program_length+')';
|
||
tcLine.appendChild(t);
|
||
});
|
||
pBlock.appendChild(tcLine);
|
||
}
|
||
}
|
||
|
||
host.appendChild(pBlock);
|
||
|
||
// Each entity
|
||
brief.entities.forEach(function(e){
|
||
var row=document.createElement('div');
|
||
row.style.cssText='background:#0d1117;border:1px solid #1f2631;border-radius:6px;padding:10px 12px;margin-bottom:8px';
|
||
|
||
// Header: ticker · name · role · risk pill
|
||
var hd=document.createElement('div');
|
||
hd.style.cssText='display:flex;align-items:center;gap:8px;margin-bottom:6px;flex-wrap:wrap';
|
||
var tkr=document.createElement('span');
|
||
tkr.style.cssText='font-family:ui-monospace,SFMono-Regular,monospace;background:#161b22;padding:2px 6px;border-radius:4px;color:#58a6ff;border:1px solid #58a6ff44;font-weight:600;font-size:10px';
|
||
tkr.textContent=e.ticker||'LLC·?';
|
||
var nm=document.createElement('a');nm.href=P+'/contractor?name='+encodeURIComponent(e.display_name||'');
|
||
nm.target='_blank';nm.rel='noopener';
|
||
nm.style.cssText='color:#e6edf3;font-weight:600;font-size:12px;text-decoration:none;border-bottom:1px dotted #58a6ff44';
|
||
nm.title='Open full contractor profile';
|
||
nm.textContent=e.display_name||'?';
|
||
var rl=document.createElement('span');rl.style.cssText='color:#8b949e;font-size:10px;text-transform:uppercase;letter-spacing:0.5px';rl.textContent=e.role||'';
|
||
hd.appendChild(tkr);hd.appendChild(nm);hd.appendChild(rl);
|
||
var spacer=document.createElement('span');spacer.style.cssText='flex:1';hd.appendChild(spacer);
|
||
// Risk pill
|
||
if(e.risk){
|
||
var s=e.risk.score, pill=document.createElement('span');
|
||
var riskColor=s===null?'#545d68':s<30?'#3fb950':s<60?'#d29922':'#f85149';
|
||
pill.style.cssText='padding:3px 8px;border-radius:9px;font-size:10px;font-weight:600;background:#0d1117;border:1px solid '+riskColor+'66;color:'+riskColor;
|
||
pill.textContent=(s===null?'risk n/a':'risk '+s)+(e.risk.partial?' · partial':'');
|
||
pill.title=(e.risk.factors||[]).join(' · ');
|
||
hd.appendChild(pill);
|
||
}
|
||
row.appendChild(hd);
|
||
|
||
// SVEP red flag (OSHA Severe Violator Enforcement Program)
|
||
var svep=e.svep;
|
||
if(svep && svep.flagged){
|
||
var svLine=document.createElement('div');
|
||
svLine.style.cssText='font-size:11px;color:#fca5a5;background:#3a1a1a;border:1px solid #f85149;border-radius:6px;padding:6px 10px;margin:4px 0;display:flex;gap:6px;align-items:baseline;flex-wrap:wrap;font-weight:600';
|
||
var svIcon=document.createElement('span');svIcon.style.fontSize='13px';svIcon.textContent='⚠';
|
||
var svTag=document.createElement('span');svTag.style.cssText='font-size:10px;color:#f85149;text-transform:uppercase;letter-spacing:1px';svTag.textContent='OSHA SVEP';
|
||
var svBody=document.createElement('span');svBody.style.cssText='flex:1;min-width:0';
|
||
svBody.textContent='Severe Violator Enforcement Program — '+svep.matched_entries.map(function(m){return m.name.split('/')[0]}).join(', ').slice(0,80);
|
||
svBody.title='Matched SVEP entries:\n'+svep.matched_entries.map(function(m){return m.name}).join('\n');
|
||
var svLink=document.createElement('a');svLink.href='https://www.osha.gov/enforcement/svep';svLink.target='_blank';svLink.rel='noopener';svLink.style.cssText='color:#58a6ff;font-size:10px;text-decoration:none';svLink.textContent='SVEP ↗';
|
||
svLine.appendChild(svIcon);svLine.appendChild(svTag);svLine.appendChild(svBody);svLine.appendChild(svLink);
|
||
row.appendChild(svLine);
|
||
}
|
||
|
||
// Parent-public-equity link — "private GC → public parent ticker"
|
||
// chain. Surfaces who actually benefits if the contract closes.
|
||
var pl=e.parent_link;
|
||
if(pl && pl.status==='ok'){
|
||
var plLine=document.createElement('div');
|
||
plLine.style.cssText='font-size:11px;color:#8b949e;margin:3px 0;display:flex;gap:6px;align-items:baseline;flex-wrap:wrap';
|
||
var plTag=document.createElement('span');plTag.style.cssText='font-size:9px;color:#545d68;text-transform:uppercase;letter-spacing:1px;min-width:54px';plTag.textContent='PARENT';
|
||
var plBody=document.createElement('span');plBody.style.cssText='flex:1;min-width:0;color:#3fb950';
|
||
var plBits=[pl.parent_name];
|
||
if(pl.parent_ticker) plBits.push(pl.parent_ticker);
|
||
if(pl.parent_exchange) plBits.push(pl.parent_exchange);
|
||
if(pl.parent_country) plBits.push(pl.parent_country);
|
||
plBody.textContent=plBits.filter(Boolean).join(' · ');
|
||
plBody.title=pl.link_source||'';
|
||
plLine.appendChild(plTag);plLine.appendChild(plBody);
|
||
row.appendChild(plLine);
|
||
} else if(pl && pl.status==='no_link'){
|
||
var plLine2=document.createElement('div');
|
||
plLine2.style.cssText='font-size:10px;color:#545d68;margin:3px 0;display:flex;gap:6px;align-items:baseline;flex-wrap:wrap';
|
||
var pl2Tag=document.createElement('span');pl2Tag.style.cssText='font-size:9px;color:#545d68;text-transform:uppercase;letter-spacing:1px;min-width:54px';pl2Tag.textContent='PARENT';
|
||
var pl2Body=document.createElement('span');pl2Body.style.cssText='flex:1;min-width:0;font-style:italic';
|
||
pl2Body.textContent=pl.reason||'no public parent identified';
|
||
plLine2.appendChild(pl2Tag);plLine2.appendChild(pl2Body);
|
||
row.appendChild(plLine2);
|
||
}
|
||
|
||
// USASpending federal contracts
|
||
var fed=e.federal;
|
||
if(fed && fed.status==='ok' && fed.total_awards_count>0){
|
||
var fLine=document.createElement('div');
|
||
fLine.style.cssText='font-size:11px;color:#8b949e;margin:3px 0;display:flex;gap:6px;align-items:baseline;flex-wrap:wrap';
|
||
var fTag=document.createElement('span');fTag.style.cssText='font-size:9px;color:#545d68;text-transform:uppercase;letter-spacing:1px;min-width:54px';fTag.textContent='FEDERAL';
|
||
var fBody=document.createElement('span');fBody.style.cssText='flex:1;min-width:0';
|
||
var dollars=fed.total_awards_value>=1e6?'$'+Math.round(fed.total_awards_value/1e6*10)/10+'M':'$'+Math.round(fed.total_awards_value/1e3)+'K';
|
||
var topAg=(fed.top_agencies||[]).slice(0,2).map(function(a){return a.agency.replace(/^DEPARTMENT OF /i,'')}).join(', ');
|
||
fBody.textContent=fed.total_awards_count+' awards · '+dollars+' total'+(topAg?' · '+topAg:'')+(fed.most_recent_award_date?' · most recent '+fed.most_recent_award_date:'');
|
||
var fLink=document.createElement('a');fLink.href=fed.source_url;fLink.target='_blank';fLink.rel='noopener';
|
||
fLink.style.cssText='color:#58a6ff;font-size:10px;text-decoration:none;white-space:nowrap';
|
||
fLink.textContent='usaspending ↗';
|
||
fLine.appendChild(fTag);fLine.appendChild(fBody);fLine.appendChild(fLink);
|
||
row.appendChild(fLine);
|
||
} else if(fed && fed.status==='no_match'){
|
||
var fLine2=document.createElement('div');
|
||
fLine2.style.cssText='font-size:10px;color:#545d68;margin:3px 0;display:flex;gap:6px;align-items:baseline';
|
||
var f2Tag=document.createElement('span');f2Tag.style.cssText='font-size:9px;color:#545d68;text-transform:uppercase;letter-spacing:1px;min-width:54px';f2Tag.textContent='FEDERAL';
|
||
var f2Body=document.createElement('span');f2Body.style.cssText='flex:1;min-width:0;font-style:italic';
|
||
f2Body.textContent='no federal contracts on record';
|
||
fLine2.appendChild(f2Tag);fLine2.appendChild(f2Body);
|
||
row.appendChild(fLine2);
|
||
}
|
||
|
||
// News mentions (Google News RSS) + sentiment chip
|
||
var news=e.news;
|
||
var ns=e.news_sentiment;
|
||
if(news && news.status==='ok' && news.recent_headlines && news.recent_headlines.length){
|
||
var nLine=document.createElement('div');
|
||
nLine.style.cssText='font-size:11px;color:#8b949e;margin:3px 0;display:flex;gap:6px;align-items:baseline;flex-wrap:wrap';
|
||
var nTag=document.createElement('span');nTag.style.cssText='font-size:9px;color:#545d68;text-transform:uppercase;letter-spacing:1px;min-width:54px';nTag.textContent='NEWS';
|
||
var nBody=document.createElement('span');nBody.style.cssText='flex:1;min-width:0';
|
||
nBody.textContent=news.total_mentions+' mentions · latest: '+(news.recent_headlines[0].title||'').slice(0,72);
|
||
nBody.title=news.recent_headlines.map(function(h){return h.title}).join('\n');
|
||
nLine.appendChild(nTag);nLine.appendChild(nBody);
|
||
// Sentiment chip
|
||
if(ns && (ns.positive||ns.negative)){
|
||
var sc=ns.score;
|
||
var sCol=sc>0.2?'#3fb950':sc<-0.2?'#f85149':'#8b949e';
|
||
var sChip=document.createElement('span');
|
||
sChip.style.cssText='padding:2px 7px;border-radius:9px;background:#0d1117;border:1px solid '+sCol+'66;color:'+sCol+';font-weight:600;font-size:9px';
|
||
sChip.textContent=(sc>=0?'+':'')+sc.toFixed(2)+' · '+ns.positive+'/'+ns.negative;
|
||
sChip.title='Sentiment score: positive headlines '+ns.positive+', negative '+ns.negative+', neutral '+ns.neutral+
|
||
(ns.flagged_headlines.length?'\n\n'+ns.flagged_headlines.map(function(h){return '['+h.polarity+'] '+h.title.slice(0,60)+(h.reasons.length?' ('+h.reasons.join(',')+')':'')}).join('\n'):'');
|
||
nLine.appendChild(sChip);
|
||
}
|
||
row.appendChild(nLine);
|
||
}
|
||
|
||
// NLRB cases (real scraper)
|
||
var nlrb=e.nlrb;
|
||
if(nlrb && nlrb.status==='ok' && nlrb.total_cases>0){
|
||
var nlLine=document.createElement('div');
|
||
nlLine.style.cssText='font-size:11px;color:#8b949e;margin:3px 0;display:flex;gap:6px;align-items:baseline';
|
||
var nlTag=document.createElement('span');nlTag.style.cssText='font-size:9px;color:#545d68;text-transform:uppercase;letter-spacing:1px;min-width:54px';nlTag.textContent='NLRB';
|
||
var nlBody=document.createElement('span');nlBody.style.cssText='flex:1;min-width:0';
|
||
nlBody.textContent=nlrb.total_cases+' case'+(nlrb.total_cases===1?'':'s')+' on file';
|
||
var nlLink=document.createElement('a');
|
||
nlLink.href='https://www.nlrb.gov/search/case?search_term='+encodeURIComponent(e.display_name);
|
||
nlLink.target='_blank';nlLink.rel='noopener';nlLink.style.cssText='color:#58a6ff;font-size:10px;text-decoration:none';
|
||
nlLink.textContent='nlrb.gov ↗';
|
||
nlLine.appendChild(nlTag);nlLine.appendChild(nlBody);nlLine.appendChild(nlLink);
|
||
row.appendChild(nlLine);
|
||
} else if(nlrb && nlrb.status==='needs_setup'){
|
||
var nlLine2=document.createElement('div');
|
||
nlLine2.style.cssText='font-size:10px;color:#545d68;margin:3px 0;display:flex;gap:6px;align-items:baseline;font-style:italic';
|
||
var nl2Tag=document.createElement('span');nl2Tag.style.cssText='font-size:9px;color:#545d68;text-transform:uppercase;letter-spacing:1px;min-width:54px;font-style:normal';nl2Tag.textContent='NLRB';
|
||
var nl2Body=document.createElement('span');nl2Body.style.cssText='flex:1;min-width:0';
|
||
nl2Body.textContent=nlrb.reason||'awaiting wire-up';
|
||
nlLine2.appendChild(nl2Tag);nlLine2.appendChild(nl2Body);
|
||
row.appendChild(nlLine2);
|
||
}
|
||
|
||
// Diversity cert (MBE/WBE/DBE)
|
||
var div2=e.diversity;
|
||
if(div2 && div2.status==='ok' && div2.certifications && div2.certifications.length){
|
||
var dLine=document.createElement('div');
|
||
dLine.style.cssText='font-size:11px;color:#3fb950;margin:3px 0;display:flex;gap:6px;align-items:baseline;flex-wrap:wrap';
|
||
var dTag=document.createElement('span');dTag.style.cssText='font-size:9px;color:#545d68;text-transform:uppercase;letter-spacing:1px;min-width:54px';dTag.textContent='MBE/WBE';
|
||
var dBody=document.createElement('span');dBody.style.cssText='flex:1;min-width:0';
|
||
dBody.textContent='✓ Certified: '+div2.certifications.map(function(c){return c.category}).join(', ');
|
||
dLine.appendChild(dTag);dLine.appendChild(dBody);
|
||
row.appendChild(dLine);
|
||
}
|
||
|
||
// Combined placeholders (debarment, OSHA SIR, diversity-needs_setup)
|
||
var deb=e.debarment, sir=e.osha_sir;
|
||
var pendingBits=[];
|
||
if(deb && deb.status==='needs_setup') pendingBits.push('SAM/IDOL debarment');
|
||
if(sir && sir.status==='needs_setup') pendingBits.push('OSHA Severe Injury');
|
||
if(div2 && div2.status==='needs_setup') pendingBits.push('MBE/WBE certs');
|
||
if(pendingBits.length){
|
||
var pLine=document.createElement('div');
|
||
pLine.style.cssText='font-size:10px;color:#484f58;margin:3px 0;display:flex;gap:6px;align-items:baseline;font-style:italic';
|
||
var pTag=document.createElement('span');pTag.style.cssText='font-size:9px;color:#545d68;text-transform:uppercase;letter-spacing:1px;min-width:54px;font-style:normal';pTag.textContent='PENDING';
|
||
var pBody=document.createElement('span');pBody.style.cssText='flex:1;min-width:0';
|
||
pBody.textContent=pendingBits.join(' · ')+' · awaiting API/scraper setup';
|
||
pLine.appendChild(pTag);pLine.appendChild(pBody);
|
||
row.appendChild(pLine);
|
||
}
|
||
|
||
// Chicago contractor history — activity velocity signal.
|
||
// Growing/declining/new annotations help the staffer judge whether
|
||
// this entity is a rising player, steady hand, or fresh LLC.
|
||
var hist=e.history;
|
||
if(hist && hist.status==='ok'){
|
||
var hl=document.createElement('div');
|
||
hl.style.cssText='font-size:11px;color:#8b949e;margin:3px 0;display:flex;gap:8px;flex-wrap:wrap;align-items:baseline';
|
||
var hTag=document.createElement('span');hTag.style.cssText='font-size:9px;color:#545d68;text-transform:uppercase;letter-spacing:1px;min-width:54px';hTag.textContent='CHI HIST';
|
||
var hBody=document.createElement('span');hBody.style.cssText='flex:1;min-width:0';
|
||
var dollars=hist.total_cost_last_24mo?' · $'+Math.round(hist.total_cost_last_24mo/1e6*10)/10+'M in 24mo':'';
|
||
hBody.textContent=hist.permits_last_180d+' in 180d · '+hist.permits_last_24mo+' in 24mo · '+hist.permits_historical_total+' all-time'+dollars;
|
||
var tChip=document.createElement('span');
|
||
var tColors={growing:'#3fb950',stable:'#58a6ff',declining:'#d29922',new:'#d29922',unknown:'#545d68'};
|
||
var tc=tColors[hist.trend]||'#545d68';
|
||
tChip.style.cssText='padding:2px 7px;border-radius:9px;font-size:9px;font-weight:600;background:#0d1117;border:1px solid '+tc+'66;color:'+tc;
|
||
tChip.textContent=hist.trend;
|
||
if(hist.trend==='new') tChip.title='≤3 permits ever — classic LLC-shuffle signature, investigate before committing workers';
|
||
hl.appendChild(hTag);hl.appendChild(hBody);hl.appendChild(tChip);
|
||
row.appendChild(hl);
|
||
}
|
||
|
||
// OSHA line
|
||
var osha=e.osha;
|
||
if(osha){
|
||
var o=document.createElement('div');
|
||
o.style.cssText='font-size:11px;color:#8b949e;margin:3px 0;display:flex;gap:8px;flex-wrap:wrap;align-items:baseline';
|
||
var oTag=document.createElement('span');oTag.style.cssText='font-size:9px;color:#545d68;text-transform:uppercase;letter-spacing:1px;min-width:54px';oTag.textContent='OSHA';
|
||
var oBody=document.createElement('span');oBody.style.cssText='flex:1;min-width:0';
|
||
if(osha.status==='ok'){
|
||
var n=osha.inspection_count;
|
||
var ageTxt='';
|
||
if(osha.most_recent_date){
|
||
var age=(Date.now()-Date.parse(osha.most_recent_date))/(86400000);
|
||
ageTxt=age<180?' · <'+Math.round(age)+'d ago':' · '+Math.round(age/30)+'mo ago';
|
||
}
|
||
oBody.textContent=n+' inspection'+(n===1?'':'s')+(osha.states_seen&&osha.states_seen.length?' across '+osha.states_seen.slice(0,5).join(', ')+(osha.states_seen.length>5?', …':''):'')+' · most recent '+(osha.most_recent_date||'?')+ageTxt;
|
||
} else if(osha.status==='no_match'){
|
||
oBody.style.color='#3fb950';
|
||
oBody.textContent='no inspections on record · clean';
|
||
} else {
|
||
oBody.style.color='#d29922';
|
||
oBody.textContent='fetch error: '+(osha.error||'unknown');
|
||
}
|
||
var oLink=document.createElement('a');
|
||
oLink.href=osha.source_url||'#';oLink.target='_blank';oLink.rel='noopener';
|
||
oLink.style.cssText='color:#58a6ff;font-size:10px;text-decoration:none;white-space:nowrap';
|
||
oLink.textContent='open on osha.gov ↗';
|
||
o.appendChild(oTag);o.appendChild(oBody);o.appendChild(oLink);
|
||
row.appendChild(o);
|
||
// Recent inspection rows (up to 3)
|
||
if(osha.recent_inspections && osha.recent_inspections.length){
|
||
var ul=document.createElement('div');
|
||
ul.style.cssText='margin-top:4px;font-size:10px;color:#545d68;padding-left:62px;line-height:1.5';
|
||
osha.recent_inspections.slice(0,3).forEach(function(i){
|
||
var lp=document.createElement('div');
|
||
var link=document.createElement('a');link.href=i.detail_url;link.target='_blank';link.rel='noopener';
|
||
link.style.cssText='color:#8b949e;text-decoration:none;font-family:ui-monospace,monospace';
|
||
link.textContent=i.id;
|
||
lp.appendChild(link);
|
||
lp.appendChild(document.createTextNode(' · '+i.date+' · '+i.state+' · '+i.type+' · '+i.scope));
|
||
ul.appendChild(lp);
|
||
});
|
||
row.appendChild(ul);
|
||
}
|
||
}
|
||
|
||
// ILSOS line
|
||
var ilsos=e.ilsos;
|
||
if(ilsos){
|
||
var il=document.createElement('div');
|
||
il.style.cssText='font-size:11px;color:#8b949e;margin:6px 0 0;display:flex;gap:8px;align-items:baseline;flex-wrap:wrap';
|
||
var ilTag=document.createElement('span');ilTag.style.cssText='font-size:9px;color:#545d68;text-transform:uppercase;letter-spacing:1px;min-width:54px';ilTag.textContent='ILSOS';
|
||
var ilBody=document.createElement('span');ilBody.style.cssText='flex:1;min-width:0';
|
||
if(ilsos.status==='ok'){
|
||
var bits=[];
|
||
if(ilsos.entity_name) bits.push(ilsos.entity_name);
|
||
if(ilsos.status_text) bits.push(ilsos.status_text);
|
||
if(ilsos.formation_date) bits.push('formed '+ilsos.formation_date);
|
||
ilBody.textContent=bits.join(' · ');
|
||
} else {
|
||
ilBody.style.color='#d29922';
|
||
ilBody.textContent='awaiting source · '+(ilsos.reason||ilsos.status);
|
||
}
|
||
il.appendChild(ilTag);il.appendChild(ilBody);
|
||
row.appendChild(il);
|
||
}
|
||
|
||
host.appendChild(row);
|
||
});
|
||
|
||
// Roadmap — what's coming next, from the brief. Rendered as a
|
||
// collapsible so it doesn't dominate. Staffers can see the direction
|
||
// without having to trust that "more is coming."
|
||
if(brief.roadmap && brief.roadmap.length){
|
||
var rm=document.createElement('details');
|
||
rm.style.cssText='margin-top:8px;background:#0d1117;border:1px dashed #21262d;border-radius:6px';
|
||
var rmSum=document.createElement('summary');
|
||
rmSum.style.cssText='list-style:none;cursor:pointer;padding:8px 12px;color:#8b949e;font-size:10px;text-transform:uppercase;letter-spacing:1px;font-weight:600;outline:none';
|
||
rmSum.textContent='▸ '+brief.roadmap.length+' additional public-data sources queued';
|
||
rm.appendChild(rmSum);
|
||
var rmBody=document.createElement('div');
|
||
rmBody.style.cssText='padding:0 12px 10px;font-size:10px;color:#8b949e;line-height:1.7';
|
||
brief.roadmap.forEach(function(line){
|
||
var li=document.createElement('div');
|
||
li.style.cssText='padding:2px 0';
|
||
li.textContent='· '+line;
|
||
rmBody.appendChild(li);
|
||
});
|
||
rm.appendChild(rmBody);
|
||
host.appendChild(rm);
|
||
}
|
||
|
||
// Footer: honest data-source summary
|
||
var foot=document.createElement('div');
|
||
foot.style.cssText='font-size:10px;color:#484f58;margin-top:8px;line-height:1.5';
|
||
foot.textContent='Brief generated '+new Date(brief.generated_at).toLocaleTimeString()+' · OSHA scraped live (cached 30d) · SEC EDGAR name→ticker + Stooq live quote · Chicago permit history fuzzy-matched across 2 years · ILSOS blocked at our ASN (pending VPN or OpenCorporates).';
|
||
host.appendChild(foot);
|
||
}
|
||
|
||
function loadLiveContracts(){
|
||
// Pair live Chicago permits with our 500K worker bench and the
|
||
// meta-index discovered patterns for each role+geo. This is the
|
||
// "real external data meets synthetic playbook learning" card set.
|
||
api('/intelligence/permit_contracts',{}).then(function(r){
|
||
var el=document.getElementById('live-contracts');el.textContent='';
|
||
if(!r||!r.contracts||r.contracts.length===0){
|
||
el.textContent='No permits returned.';return;
|
||
}
|
||
// Feed the Live Market hero (clock + Chicago permit pulse) before
|
||
// rendering cards so both land together and tell one coherent story.
|
||
loadLiveMarket(r.contracts);
|
||
r.contracts.forEach(function(c){
|
||
var p=c.permit||{}, prop=c.proposed||{}, tl=c.timeline||{};
|
||
var urg=tl.urgency||'scheduled';
|
||
var borderColor={overdue:'#f85149',urgent:'#d29922',soon:'#388bfd',scheduled:'#2ea043'}[urg]||'#388bfd';
|
||
var card=document.createElement('div');card.className='insight info contract-card';
|
||
card.style.cssText='background:#0d1117;border:1px solid #171d27;border-radius:10px;padding:16px;margin-bottom:10px;border-left:3px solid '+borderColor;
|
||
// Foldable details container — fpRow / ecRow / Project Index /
|
||
// description / candidate rows all live here. Hidden until the
|
||
// card gets .expanded; CSS animates the grid-template-rows.
|
||
var detailsInner=document.createElement('div');detailsInner.className='card-details-inner';
|
||
// Header — permit
|
||
var hdr=document.createElement('div');hdr.style.cssText='display:flex;justify-content:space-between;margin-bottom:8px;gap:12px';
|
||
var left=document.createElement('div');
|
||
var title=document.createElement('div');title.style.cssText='font-weight:600;color:#e6edf3;font-size:14px';
|
||
title.textContent='$'+(p.cost||0).toLocaleString()+' · '+(p.work_type||'');
|
||
var addr=document.createElement('div');addr.style.cssText='color:#8b949e;font-size:12px;margin-top:2px';
|
||
addr.textContent=(p.address||'')+' · Chicago, IL · filed '+(p.issue_date||'');
|
||
// Timeline chip
|
||
if(tl.days_to_deadline!==undefined){
|
||
var tmline=document.createElement('div');tmline.style.cssText='color:'+borderColor+';font-size:11px;font-weight:600;margin-top:4px';
|
||
var urgLabel={overdue:'OVERDUE',urgent:'URGENT',soon:'SOON',scheduled:'SCHEDULED'}[urg]||'SCHEDULED';
|
||
var dd=tl.days_to_deadline;
|
||
var txt=urgLabel+' · staffing window opens '+(tl.staffing_window_opens||'')+' ('+(dd<=0?Math.abs(dd)+'d overdue':dd+'d to deadline')+') · construction est '+(tl.estimated_construction_start||'');
|
||
tmline.textContent=txt;addr.appendChild(document.createElement('br'));left.appendChild(tmline);
|
||
}
|
||
left.appendChild(title);left.appendChild(addr);
|
||
var right=document.createElement('div');right.style.cssText='color:#58a6ff;font-size:12px;font-weight:600;text-align:right;white-space:nowrap';
|
||
right.textContent=prop.count+'× '+prop.role;
|
||
var sub=document.createElement('div');sub.style.cssText='color:#545d68;font-size:10px;text-align:right';
|
||
sub.textContent='pool: '+(prop.pool_size||'?').toLocaleString()+' available';
|
||
right.appendChild(sub);
|
||
// Rate awareness: show implied bill rate per contract
|
||
if(c.implied_bill_rate){
|
||
var rate=document.createElement('div');
|
||
rate.style.cssText='color:#d29922;font-size:10px;text-align:right;margin-top:3px';
|
||
rate.textContent='bill rate: $'+c.implied_bill_rate.toFixed(2)+'/hr';
|
||
right.appendChild(rate);
|
||
}
|
||
hdr.appendChild(left);hdr.appendChild(right);card.appendChild(hdr);
|
||
// Architecture pill row — instant-search latency + shift coverage
|
||
// + pool-size proof that the index actually fired on this call.
|
||
// This is the "our substrate is better" surface J asked for.
|
||
var pillRow=document.createElement('div');
|
||
pillRow.style.cssText='display:flex;gap:6px;flex-wrap:wrap;margin-bottom:10px;font-size:10px';
|
||
function pill(text,color,title){
|
||
var p=document.createElement('span');
|
||
p.style.cssText='padding:3px 8px;border-radius:9px;background:#0d1117;border:1px solid '+color+'66;color:'+color+';font-weight:600;letter-spacing:0.3px';
|
||
if(title) p.title=title;
|
||
p.textContent=text;
|
||
return p;
|
||
}
|
||
if(c.search_latency_ms!==undefined){
|
||
var latColor=c.search_latency_ms<500?'#3fb950':c.search_latency_ms<2000?'#d29922':'#f85149';
|
||
pillRow.appendChild(pill('⚡ '+c.search_latency_ms+'ms', latColor,
|
||
'Time for /vectors/hybrid to rank '+(prop.pool_size||0).toLocaleString()+' SQL-matched workers against the 50K-chunk vector index.'));
|
||
}
|
||
if(prop.pool_size!==undefined){
|
||
pillRow.appendChild(pill(prop.pool_size.toLocaleString()+' pool · k=200 boost', '#58a6ff',
|
||
'Pool = workers matching SQL filter (role+state+city+avail>0.5). k=200 means playbook boost checks 200 candidates before narrowing to top-5.'));
|
||
}
|
||
if(c.shifts_needed&&c.shifts_needed.length){
|
||
var shiftColor={'1st':'#f9d171','2nd':'#f5894a','3rd':'#5f5fff','4th':'#2ea043'};
|
||
c.shifts_needed.forEach(function(sh){
|
||
pillRow.appendChild(pill(sh+' shift', shiftColor[sh]||'#8b949e',
|
||
'Inferred from permit description. See 24/7 shift clock above for live distribution.'));
|
||
});
|
||
}
|
||
if(pillRow.childNodes.length) card.appendChild(pillRow);
|
||
|
||
// Compact summary strip — at-a-glance numbers so scanners get
|
||
// the headline without expanding. Mirrors the colors used in
|
||
// the full economics grid below: revenue green, margin tiered.
|
||
var strip=document.createElement('div');strip.className='card-strip';
|
||
function stripCell(label,value,color){
|
||
var s=document.createElement('span');
|
||
var b=document.createElement('strong');b.style.color=color||'#e6edf3';b.textContent=value;
|
||
s.appendChild(b);s.appendChild(document.createTextNode(label));
|
||
return s;
|
||
}
|
||
if(c.economics){
|
||
strip.appendChild(stripCell(' rev','$'+Math.round(c.economics.gross_revenue/1000)+'K','#3fb950'));
|
||
var stripMarginColor=c.economics.margin_pct>=25?'#3fb950':c.economics.margin_pct>=10?'#d29922':'#f85149';
|
||
strip.appendChild(stripCell(' margin',c.economics.margin_pct+'%',stripMarginColor));
|
||
}
|
||
if(c.fill_probability&&c.fill_probability.curve&&c.fill_probability.curve.length){
|
||
var bucket7=c.fill_probability.curve.find(function(pt){return pt.day===7});
|
||
var fillBucket=bucket7||c.fill_probability.curve[0];
|
||
var spanLabel=bucket7?' fill by 1wk':' fill by day '+fillBucket.day;
|
||
strip.appendChild(stripCell(spanLabel,fillBucket.cumulative_pct+'%','#58a6ff'));
|
||
}
|
||
var topCand=(prop.candidates||[])[0];
|
||
if(topCand&&topCand.name){
|
||
// Abbreviate "Maria Sanchez" → "Maria S." so the strip stays one line.
|
||
var nameParts=topCand.name.trim().split(/\s+/);
|
||
var topShort=nameParts.length>1?nameParts[0]+' '+(nameParts[nameParts.length-1][0]||'')+'.':nameParts[0];
|
||
strip.appendChild(stripCell(' top match',topShort,'#e6edf3'));
|
||
}
|
||
if(strip.childNodes.length) card.appendChild(strip);
|
||
|
||
// Pattern (meta-index) chip — kept visible on the compact card
|
||
// because it's a glance-worthy "we have playbooks for this kind
|
||
// of contract" signal that doesn't take much vertical space.
|
||
if(c.discovered_pattern){
|
||
var pat=document.createElement('div');pat.style.cssText='background:#0d2818;border:1px solid #2ea04360;border-radius:6px;padding:8px 12px;margin-bottom:10px;font-size:11px;color:#86efac;line-height:1.5';
|
||
var plabel=document.createElement('span');plabel.style.cssText='color:#3fb950;font-weight:600;margin-right:6px';
|
||
plabel.textContent='MEMORY ('+c.pattern_matched+' playbooks):';
|
||
pat.appendChild(plabel);
|
||
pat.appendChild(document.createTextNode(' '+c.discovered_pattern));
|
||
card.appendChild(pat);
|
||
}
|
||
|
||
// Fill-probability curve — shows "likelihood of filling by day N"
|
||
// as a horizontal bar of cumulative percentages. Drill down that
|
||
// J asked for: "percentage likelihood of filling them on a certain time."
|
||
if(c.fill_probability&&c.fill_probability.curve){
|
||
var fp=c.fill_probability;
|
||
var fpRow=document.createElement('div');
|
||
fpRow.style.cssText='background:#0d1117;border:1px solid #171d27;border-radius:8px;padding:10px 12px;margin-bottom:10px';
|
||
var fpLabel=document.createElement('div');
|
||
fpLabel.style.cssText='display:flex;justify-content:space-between;font-size:10px;color:#8b949e;text-transform:uppercase;letter-spacing:1px;margin-bottom:6px';
|
||
var fpTitle=document.createElement('span');fpTitle.style.color='#e6edf3';fpTitle.textContent='Fill Probability';
|
||
var fpBase=document.createElement('span');fpBase.textContent='base '+fp.base_pct+'% · pool × urgency';
|
||
fpLabel.appendChild(fpTitle);fpLabel.appendChild(fpBase);
|
||
fpRow.appendChild(fpLabel);
|
||
// Horizontal stacked bar — each bucket as a segment.
|
||
// .fp-bar starts hidden (clip-path); IntersectionObserver adds
|
||
// .lit when the bar enters viewport, which fires the paint +
|
||
// shimmer animations defined in the <style> block above.
|
||
var fpBar=document.createElement('div');
|
||
fpBar.className='fp-bar';
|
||
fpBar.style.cssText='display:flex;height:8px;border-radius:4px;overflow:hidden;background:#161b22;margin-bottom:6px';
|
||
// Lazy-init a single shared observer — the same instance can
|
||
// watch every .fp-bar on the page. unobserve() after firing so
|
||
// the bar doesn't re-paint if the user scrolls back past it.
|
||
if(!window._lhFpObserver){
|
||
window._lhFpObserver=new IntersectionObserver(function(entries){
|
||
entries.forEach(function(e){
|
||
if(e.isIntersecting){
|
||
e.target.classList.add('lit');
|
||
window._lhFpObserver.unobserve(e.target);
|
||
}
|
||
});
|
||
},{ threshold: 0.2 });
|
||
}
|
||
window._lhFpObserver.observe(fpBar);
|
||
fp.curve.forEach(function(pt,idx){
|
||
var prev=idx===0?0:fp.curve[idx-1].cumulative_pct;
|
||
var delta=pt.cumulative_pct-prev;
|
||
if(delta<=0) return;
|
||
var seg=document.createElement('div');
|
||
var shade=pt.day<=7?'#3fb950':pt.day<=14?'#d29922':pt.day<=21?'#e8751a':'#f85149';
|
||
seg.style.cssText='flex:'+delta+' 0 0;background:'+shade;
|
||
seg.title='days '+(idx>0?fp.curve[idx-1].day:0)+'–'+pt.day+': +'+delta+'% cumulative';
|
||
fpBar.appendChild(seg);
|
||
});
|
||
fpRow.appendChild(fpBar);
|
||
// Day-marker row — human-readable spans, not cryptic d7/d14
|
||
var fpMarks=document.createElement('div');
|
||
fpMarks.style.cssText='display:flex;justify-content:space-between;font-size:9px;color:#545d68;gap:6px';
|
||
function humanSpan(d){
|
||
if(d===0) return 'Today';
|
||
if(d===1) return '1 day';
|
||
if(d<7) return d+' days';
|
||
if(d===7) return '1 week';
|
||
if(d===14) return '2 weeks';
|
||
if(d===21) return '3 weeks';
|
||
if(d===30) return '1 month';
|
||
if(d<30) return Math.round(d/7)+' weeks';
|
||
return Math.round(d/30)+' months';
|
||
}
|
||
fp.curve.forEach(function(pt){
|
||
var m=document.createElement('span');
|
||
m.style.cssText='text-align:center;line-height:1.3;flex:1;min-width:0';
|
||
var t=document.createElement('div');t.style.cssText='color:#8b949e;font-weight:600;font-variant-numeric:tabular-nums';t.textContent=pt.cumulative_pct+'%';
|
||
var s=document.createElement('div');s.style.cssText='color:#545d68;margin-top:1px';s.textContent=humanSpan(pt.day);
|
||
m.appendChild(t);m.appendChild(s);
|
||
fpMarks.appendChild(m);
|
||
});
|
||
fpRow.appendChild(fpMarks);
|
||
// Subtle legend — what this curve means
|
||
var fpNote=document.createElement('div');
|
||
fpNote.style.cssText='font-size:9px;color:#545d68;margin-top:6px;line-height:1.4';
|
||
fpNote.textContent='Cumulative chance the role gets fully staffed by that point, given pool depth, urgency, and past fill patterns.';
|
||
fpRow.appendChild(fpNote);
|
||
detailsInner.appendChild(fpRow);
|
||
}
|
||
|
||
// Economics panel — "as though the contracts were accepted and filled"
|
||
if(c.economics){
|
||
var ec=c.economics;
|
||
var ecRow=document.createElement('div');
|
||
ecRow.style.cssText='background:#0d1117;border:1px solid #171d27;border-radius:8px;padding:10px 12px;margin-bottom:10px;display:grid;grid-template-columns:repeat(4,1fr);gap:8px';
|
||
function ecCell(label,big,sub,color){
|
||
var cell=document.createElement('div');
|
||
var l=document.createElement('div');l.style.cssText='font-size:9px;color:#545d68;text-transform:uppercase;letter-spacing:1px';l.textContent=label;
|
||
var b=document.createElement('div');b.style.cssText='font-size:13px;font-weight:600;color:'+(color||'#e6edf3');b.textContent=big;
|
||
var s=document.createElement('div');s.style.cssText='font-size:9px;color:#8b949e;margin-top:1px';s.textContent=sub;
|
||
cell.appendChild(l);cell.appendChild(b);cell.appendChild(s);
|
||
return cell;
|
||
}
|
||
ecRow.appendChild(ecCell('Est. Revenue','$'+ec.gross_revenue.toLocaleString(),
|
||
prop.count+' × '+ec.hours_per_week+'h × '+ec.weeks_assumed+'w','#e6edf3'));
|
||
var marginColor=ec.margin_pct>=25?'#3fb950':ec.margin_pct>=10?'#d29922':'#f85149';
|
||
ecRow.appendChild(ecCell('Est. Margin','$'+ec.gross_margin.toLocaleString(),
|
||
ec.margin_pct+'% · avg pay $'+ec.avg_pay_rate+'/hr',marginColor));
|
||
ecRow.appendChild(ecCell('Payout Window',ec.payout_window_days[0]+'–'+ec.payout_window_days[1]+'d',
|
||
'after fill_date · standard net-30 / net-45','#8b949e'));
|
||
var overColor=ec.over_bill_count>0?'#f85149':'#8b949e';
|
||
ecRow.appendChild(ecCell('Over-Bill Pool',ec.over_bill_count+'/'+(prop.candidates||[]).length,
|
||
ec.over_bill_count>0?'$'+ec.over_bill_pool_margin_at_risk.toLocaleString()+' at risk':'none flagged',overColor));
|
||
detailsInner.appendChild(ecRow);
|
||
}
|
||
|
||
// Project Index — portfolio of public-data signals for this permit's
|
||
// property + contractors. Collapsed by default; fetches
|
||
// /intelligence/permit_entities lazily on expand. Real OSHA data,
|
||
// explicit "awaiting source" placeholders for sources we don't yet
|
||
// have wired. NB: deliberately NOT called "ETF" — that's a
|
||
// SEC-regulated term. This is a custom Chicago build-signal index.
|
||
if(p.contact_1_name || p.contact_2_name){
|
||
var eb=document.createElement('details');
|
||
eb.style.cssText='background:#0d1117;border:1px solid #171d27;border-radius:8px;margin-bottom:10px';
|
||
var ebSum=document.createElement('summary');
|
||
ebSum.style.cssText='list-style:none;cursor:pointer;padding:10px 12px;display:flex;align-items:center;gap:10px;color:#8b949e;font-size:11px;outline:none';
|
||
var ebCaret=document.createElement('span');ebCaret.style.cssText='color:#58a6ff;font-size:14px';ebCaret.textContent='▸';
|
||
var ebLabel=document.createElement('span');ebLabel.style.cssText='font-size:10px;color:#545d68;text-transform:uppercase;letter-spacing:1px';
|
||
ebLabel.textContent='PROJECT INDEX — Build Signals';
|
||
var ebTags=document.createElement('span');ebTags.style.cssText='color:#e6edf3;font-size:11px;flex:1;font-weight:500';
|
||
// Contractor names link to the full profile page. Without anchors,
|
||
// clicking the preview did nothing — the only working contractor
|
||
// link was inside the lazy-loaded entity brief, which a coordinator
|
||
// wouldn't reach without first expanding the details.
|
||
var preview=[];
|
||
function contactLink(n){
|
||
var a=document.createElement('a');
|
||
a.href=P+'/contractor?name='+encodeURIComponent(n);
|
||
a.target='_blank';a.rel='noopener';
|
||
a.style.cssText='color:inherit;text-decoration:none;border-bottom:1px dotted #58a6ff44';
|
||
a.title='Open full contractor profile';
|
||
a.textContent=n;
|
||
a.addEventListener('click',function(e){e.stopPropagation()}); // don't toggle the details
|
||
return a;
|
||
}
|
||
if(p.contact_1_name) preview.push(p.contact_1_name);
|
||
if(p.contact_2_name && p.contact_2_name!==p.contact_1_name) preview.push(p.contact_2_name);
|
||
if(preview.length){
|
||
preview.forEach(function(n,i){
|
||
if(i>0) ebTags.appendChild(document.createTextNode(' · '));
|
||
ebTags.appendChild(contactLink(n));
|
||
});
|
||
}
|
||
var ebMeta=document.createElement('span');ebMeta.style.cssText='color:#545d68;font-size:10px';
|
||
ebMeta.textContent='click → fetch OSHA + ILSOS';
|
||
ebSum.appendChild(ebCaret);ebSum.appendChild(ebLabel);ebSum.appendChild(ebTags);ebSum.appendChild(ebMeta);
|
||
eb.appendChild(ebSum);
|
||
var ebBody=document.createElement('div');
|
||
ebBody.style.cssText='padding:0 12px 12px';
|
||
eb.appendChild(ebBody);
|
||
var loaded=false;
|
||
eb.addEventListener('toggle',function(){
|
||
if(!eb.open||loaded) return;
|
||
loaded=true;
|
||
ebBody.textContent='';
|
||
var loading=document.createElement('div');
|
||
loading.style.cssText='color:#545d68;font-size:11px;padding:8px 0';
|
||
loading.textContent='▸ Pulling OSHA (live scrape · ~1-2s per contractor)…';
|
||
ebBody.appendChild(loading);
|
||
api('/intelligence/permit_entities',{
|
||
permit_id:p.id||'',
|
||
address:p.address||'',
|
||
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||''
|
||
}).then(function(r){
|
||
renderEntityBrief(ebBody,r);
|
||
}).catch(function(e){
|
||
ebBody.textContent='';
|
||
var errDiv=document.createElement('div');
|
||
errDiv.style.cssText='color:#f85149;font-size:11px;padding:8px 0';
|
||
errDiv.textContent='brief failed: '+e.message;
|
||
ebBody.appendChild(errDiv);
|
||
});
|
||
});
|
||
detailsInner.appendChild(eb);
|
||
}
|
||
|
||
// Description
|
||
if(p.description){
|
||
var desc=document.createElement('div');desc.style.cssText='color:#94a3b8;font-size:11px;margin-bottom:10px;line-height:1.5';
|
||
desc.textContent=p.description;detailsInner.appendChild(desc);
|
||
}
|
||
// Candidates
|
||
var cands=prop.candidates||[];
|
||
cands.slice(0,3).forEach(function(cand,i){
|
||
var row=document.createElement('div');row.style.cssText='display:flex;align-items:center;gap:10px;padding:6px 10px;background:#161b22;border-radius:6px;margin-bottom:4px;font-size:12px';
|
||
var av=document.createElement('div');av.style.cssText='width:28px;height:28px;border-radius:6px;display:flex;align-items:center;justify-content:center;font-weight:600;font-size:10px;color:#e6edf3;background:'+AC[i%AC.length];
|
||
av.textContent=(cand.name||'?').split(' ').map(function(n){return (n[0]||'').toUpperCase()}).join('').substring(0,2);
|
||
var info=document.createElement('div');info.style.cssText='flex:1;min-width:0';
|
||
var nm=document.createElement('div');nm.style.cssText='color:#e6edf3;font-weight:500';nm.textContent=cand.name||cand.doc_id;
|
||
if((cand.playbook_boost||0)>0){
|
||
var chip=document.createElement('span');chip.style.cssText='margin-left:8px;padding:2px 7px;border-radius:9px;font-size:9px;font-weight:600;background:#0d2818;border:1px solid #2ea043;color:#3fb950;vertical-align:middle';
|
||
chip.textContent='Endorsed · '+(cand.playbook_citations||[]).length+' playbook'+((cand.playbook_citations||[]).length===1?'':'s');
|
||
nm.appendChild(chip);
|
||
}
|
||
// Rate warning chip when worker's pay exceeds the contract's bill rate
|
||
if(cand.over_bill_rate){
|
||
var warn=document.createElement('span');warn.style.cssText='margin-left:6px;padding:2px 7px;border-radius:9px;font-size:9px;font-weight:600;background:#3a1a1a;border:1px solid #f85149;color:#fca5a5;vertical-align:middle';
|
||
warn.textContent='Over bill rate';
|
||
warn.title='Worker\'s implied pay rate ($'+(cand.implied_pay_rate||0).toFixed(2)+'/hr) exceeds contract bill rate ($'+(c.implied_bill_rate||0).toFixed(2)+'/hr) — margin at risk';
|
||
nm.appendChild(warn);
|
||
}
|
||
var sub2=document.createElement('div');sub2.style.cssText='color:#545d68;font-size:10px';
|
||
var subText=cand.doc_id+' · score '+(cand.score||0).toFixed(3);
|
||
if(cand.implied_pay_rate) subText+=' · pay $'+cand.implied_pay_rate.toFixed(2)+'/hr';
|
||
sub2.textContent=subText;
|
||
info.appendChild(nm);info.appendChild(sub2);
|
||
row.appendChild(av);row.appendChild(info);
|
||
detailsInner.appendChild(row);
|
||
});
|
||
if(cands.length>3){
|
||
var more=document.createElement('div');more.style.cssText='font-size:10px;color:#58a6ff;padding:4px 10px;margin-top:2px';
|
||
more.textContent='+ '+(cands.length-3)+' more candidates available';
|
||
detailsInner.appendChild(more);
|
||
}
|
||
|
||
// Wrap the foldable section + add it to the card. The CSS
|
||
// .card-details rule animates grid-template-rows: 0fr → 1fr
|
||
// when the parent gets .expanded, so this whole block grows
|
||
// smoothly to its natural height when the user clicks.
|
||
var details=document.createElement('div');details.className='card-details';
|
||
details.appendChild(detailsInner);
|
||
card.appendChild(details);
|
||
|
||
// Toggle affordance — small caret + label at the bottom. CSS
|
||
// rotates the chevron 180° when expanded; the label flips text
|
||
// via the click handler below.
|
||
var toggleRow=document.createElement('div');toggleRow.className='card-toggle';
|
||
var chevron=document.createElement('span');chevron.className='chevron';chevron.textContent='▾';
|
||
var toggleLabel=document.createElement('span');
|
||
toggleLabel.textContent='click for full curve, economics, candidates, project index';
|
||
toggleRow.appendChild(chevron);toggleRow.appendChild(toggleLabel);
|
||
card.appendChild(toggleRow);
|
||
|
||
// Click handler — expand/collapse + auto-open Project Index when
|
||
// expanding. Skips the toggle when the click landed inside the
|
||
// already-expanded details (so contractor links / SMS copy /
|
||
// <details> toggles inside still work) and when the user is
|
||
// selecting text (so a triple-click drag doesn't snap it shut).
|
||
card.addEventListener('click',function(e){
|
||
if(e.target.closest('.card-details')) return;
|
||
if(window.getSelection&&window.getSelection().toString().length>0) return;
|
||
var nowExpanded=!card.classList.contains('expanded');
|
||
card.classList.toggle('expanded');
|
||
toggleLabel.textContent=nowExpanded
|
||
? 'click to collapse'
|
||
: 'click for full curve, economics, candidates, project index';
|
||
// Auto-open the Project Index ONLY on user-driven expand. This
|
||
// avoids firing 20+ OSHA scrapes on page load — each open
|
||
// triggers /intelligence/permit_entities, which live-scrapes
|
||
// OSHA at ~1-2s per contractor.
|
||
if(nowExpanded&&typeof eb!=='undefined'&&eb&&!eb.open) eb.open=true;
|
||
});
|
||
|
||
el.appendChild(card);
|
||
});
|
||
}).catch(function(e){
|
||
document.getElementById('live-contracts').textContent='Error loading: '+e.message;
|
||
});
|
||
}
|
||
|
||
function loadDay(){
|
||
// Step 1: run simulation + get real worker count + populate dropdowns from actual data
|
||
Promise.all([
|
||
api('/simulation/run',{}),
|
||
api('/sql',{sql:"SELECT COUNT(*) as cnt FROM workers_500k"}),
|
||
api('/sql',{sql:"SELECT DISTINCT role FROM workers_500k ORDER BY role"}),
|
||
api('/sql',{sql:"SELECT DISTINCT state FROM workers_500k ORDER BY state"})
|
||
]).then(function(r0){
|
||
var sim=r0[0];
|
||
var workerCount=r0[1]&&r0[1].rows&&r0[1].rows[0]?r0[1].rows[0].cnt:0;
|
||
var allRoles=r0[2]&&r0[2].rows?r0[2].rows.map(function(r){return r.role}):[];
|
||
var allStates=r0[3]&&r0[3].rows?r0[3].rows.map(function(r){return r.state}):[];
|
||
|
||
// Populate dropdowns from real data
|
||
var stSel=document.getElementById('sst');
|
||
var rlSel=document.getElementById('srl');
|
||
stSel.innerHTML='<option value="">Any State</option>';
|
||
allStates.forEach(function(s){var o=document.createElement('option');o.value=s;o.textContent=s;stSel.appendChild(o)});
|
||
rlSel.innerHTML='<option value="">Any Role</option>';
|
||
allRoles.forEach(function(r){var o=document.createElement('option');o.value=r;o.textContent=r;rlSel.appendChild(o)});
|
||
|
||
// Update search summary with real count
|
||
var searchSum=document.querySelector('.sa summary');
|
||
if(searchSum)searchSum.textContent='Search all '+workerCount.toLocaleString()+' workers';
|
||
|
||
var today=sim.days?sim.days[0]:null;
|
||
var sum=sim.summary||{};
|
||
sum.worker_count=workerCount;
|
||
document.getElementById('status').textContent=sum.total_filled+'/'+sum.total_needed+' positions filled across '+sum.total_contracts+' contracts';
|
||
|
||
// Step 2: extract what's ACTUALLY needed from today's contracts
|
||
var contracts=today?today.contracts:[];
|
||
var needRoles={}, needStates={}, urgentRoles=[];
|
||
contracts.forEach(function(c){
|
||
if(c.filled<c.headcount){
|
||
needRoles[c.role]=(needRoles[c.role]||0)+(c.headcount-c.filled);
|
||
needStates[c.state]=(needStates[c.state]||0)+(c.headcount-c.filled);
|
||
if(c.priority==='urgent'||c.priority==='high') urgentRoles.push(c.role);
|
||
}
|
||
});
|
||
|
||
// Build contextual queries based on today's gaps
|
||
var roleList=Object.keys(needRoles);
|
||
var stateList=Object.keys(needStates);
|
||
var roleFilter=roleList.length?roleList.map(function(r){return"'"+r.replace(/'/g,"''")+"'"}).join(','):"'Forklift Operator'";
|
||
var stateFilter=stateList.length?stateList.map(function(s){return"'"+s.replace(/'/g,"''")+"'"}).join(','):"'IL'";
|
||
|
||
// Contextual workers — add random offset so it's not always the same top 8
|
||
var offset=Math.floor(Math.random()*20);
|
||
var topSql="SELECT name, role, city, state, ROUND(CAST(reliability AS DOUBLE),2) rel, certifications "+
|
||
"FROM workers_500k WHERE role IN ("+roleFilter+") AND state IN ("+stateFilter+") "+
|
||
"AND CAST(reliability AS DOUBLE)>0.85 ORDER BY CAST(reliability AS DOUBLE) DESC LIMIT 8 OFFSET "+offset;
|
||
|
||
// Coverage for states that matter today
|
||
var covSql="SELECT state, COUNT(*) cnt, SUM(CASE WHEN CAST(reliability AS DOUBLE)>0.8 THEN 1 ELSE 0 END) good "+
|
||
"FROM workers_500k WHERE state IN ("+stateFilter+") GROUP BY state ORDER BY cnt DESC";
|
||
|
||
// Roles breakdown for today's needed roles
|
||
var roleSql="SELECT role, COUNT(*) total, SUM(CASE WHEN CAST(reliability AS DOUBLE)>0.8 THEN 1 ELSE 0 END) reliable "+
|
||
"FROM workers_500k WHERE role IN ("+roleFilter+") GROUP BY role ORDER BY total DESC";
|
||
|
||
return Promise.all([roleSql, topSql, covSql].map(function(sql){
|
||
return api('/sql',{sql:sql});
|
||
})).then(function(results){
|
||
var roles=results[0], topWorkers=results[1], coverage=results[2];
|
||
renderMain(today,sum,roles,topWorkers,coverage,needRoles,needStates);
|
||
});
|
||
}).catch(function(e){
|
||
document.getElementById('main').textContent='Error loading: '+e.message;
|
||
});
|
||
}
|
||
|
||
function renderMain(today,sum,roles,topWorkers,coverage,needRoles,needStates){
|
||
var el=document.getElementById('main');
|
||
el.textContent='';
|
||
|
||
// Stats
|
||
var stats=document.createElement('div');stats.className='stats';
|
||
addStat(stats,sum.total_contracts||0,'Contracts Today');
|
||
addStat(stats,sum.total_filled||0,'Positions Filled');
|
||
addStat(stats,sum.emergencies||0,'Urgent');
|
||
addStat(stats,sum.worker_count||0,'Workers in System');
|
||
el.appendChild(stats);
|
||
|
||
// INSIGHT 1: Urgent pipeline — step-by-step workflow
|
||
if(today&&today.contracts){
|
||
var urgent=today.contracts.filter(function(c){return c.priority==='urgent'});
|
||
var needsWork=today.contracts.filter(function(c){return c.priority!=='urgent'&&c.filled<c.headcount});
|
||
var filled=today.contracts.filter(function(c){return c.filled>=c.headcount});
|
||
|
||
if(urgent.length){
|
||
var ins=makeInsight('urgent','Urgent Pipeline',
|
||
urgent.length+' emergency contract'+(urgent.length>1?'s':'')+ ' — workers pre-matched, ready for your call',null);
|
||
urgent.forEach(function(c){addContractInsight(ins,c,true)});
|
||
el.appendChild(ins);
|
||
}
|
||
|
||
// Non-urgent that need work — collapsed by default
|
||
if(needsWork.length){
|
||
var ins1b=makeInsight('warning','In Progress',
|
||
needsWork.length+' contract'+(needsWork.length>1?'s':'')+' still filling — workers matched, awaiting confirmation',null);
|
||
var det1=document.createElement('details');
|
||
var sum1=document.createElement('summary');sum1.style.cssText='cursor:pointer;font-size:11px;color:#545d68;padding:4px 0;list-style:none';
|
||
sum1.textContent='Show '+needsWork.length+' contracts';det1.appendChild(sum1);
|
||
needsWork.forEach(function(c){addContractInsight(det1,c,false)});
|
||
ins1b.appendChild(det1);el.appendChild(ins1b);
|
||
}
|
||
|
||
// Filled — collapsed by default
|
||
if(filled.length){
|
||
var ins2=makeInsight('opportunity','Ready to Go',
|
||
filled.length+' contract'+(filled.length>1?'s':'')+' fully staffed — review and send shift details',null);
|
||
var det2=document.createElement('details');
|
||
var sum2=document.createElement('summary');sum2.style.cssText='cursor:pointer;font-size:11px;color:#545d68;padding:4px 0;list-style:none';
|
||
sum2.textContent='Show '+filled.length+' contracts';det2.appendChild(sum2);
|
||
filled.forEach(function(c){addContractInsight(det2,c,false)});
|
||
ins2.appendChild(det2);el.appendChild(ins2);
|
||
}
|
||
}
|
||
|
||
// INSIGHT 2: Top available workers — contextual to today's unfilled contracts
|
||
if(topWorkers&&topWorkers.rows&&topWorkers.rows.length){
|
||
// Build a contextual headline from today's gaps
|
||
var gapRoles=needRoles?Object.keys(needRoles):[];
|
||
var gapStates=needStates?Object.keys(needStates):[];
|
||
var headline='Workers Available for Today\'s Open Contracts';
|
||
var sub='Matched to the roles and locations you need filled right now';
|
||
if(gapRoles.length<=3&&gapRoles.length>0){
|
||
headline='Top '+gapRoles.join(', ')+' Workers Available';
|
||
sub='These workers match your unfilled contracts in '+gapStates.join(', ');
|
||
}
|
||
var ins3=makeInsight('info',headline,sub,
|
||
'Filtered to roles and states with open positions today. Reliability 85%+.');
|
||
topWorkers.rows.slice(0,5).forEach(function(w,i){
|
||
// Show which contract gap this worker could fill
|
||
var gapNote='';
|
||
if(needRoles&&needRoles[w.role]){gapNote='→ Could fill '+needRoles[w.role]+' open '+w.role+' spot'+(needRoles[w.role]>1?'s':'')}
|
||
var wd={nm:w.name,role:w.role,loc:w.city+', '+w.state,skills:[],
|
||
certs:(w.certifications||'').split(',').filter(function(c){return c.trim()&&c.trim()!=='none'}),
|
||
rel:w.rel,avail:0,arch:'',hasM:true};
|
||
addWorkerInsight(ins3,w.name,w.role+' · '+w.city+', '+w.state,
|
||
'Reliability: '+Math.round(w.rel*100)+'%'+(w.certifications&&w.certifications!=='none'?' · Certs: '+w.certifications:'')+(gapNote?' · '+gapNote:''),i,null,wd);
|
||
});
|
||
el.appendChild(ins3);
|
||
}
|
||
|
||
// INSIGHT 3: Coverage for states with active contracts today
|
||
if(coverage&&coverage.rows&&coverage.rows.length){
|
||
var stateLabel=gapStates.length?' in '+gapStates.join(', '):'';
|
||
var ins4=makeInsight('warning','Bench Strength'+stateLabel,
|
||
'Worker pool depth for states with open contracts today',
|
||
'Shows how many reliable workers (80%+ reliability) you have in states where you need to fill positions right now.');
|
||
coverage.rows.forEach(function(r){
|
||
var pct=Math.round(r.good/r.cnt*100);
|
||
var openSlots=needStates&&needStates[r.state]?needStates[r.state]:0;
|
||
var d=document.createElement('div');d.style.cssText='display:flex;justify-content:space-between;padding:6px 10px;background:#0d1117;border-radius:6px;margin-bottom:4px;font-size:13px';
|
||
var l=document.createElement('span');l.style.color='#f0f6fc';
|
||
l.textContent=r.state+' — '+r.cnt.toLocaleString()+' workers'+(openSlots?' · '+openSlots+' open slot'+(openSlots>1?'s':''):'');
|
||
var v=document.createElement('span');v.textContent=pct+'% reliable';v.style.color=pct<40?'#f85149':'#d29922';
|
||
d.appendChild(l);d.appendChild(v);ins4.appendChild(d);
|
||
});
|
||
el.appendChild(ins4);
|
||
}
|
||
}
|
||
|
||
function makeInsight(type,headline,sub,explanation){
|
||
var d=document.createElement('div');d.className='insight '+type;
|
||
var lb=document.createElement('div');lb.className='label';
|
||
lb.textContent=type==='urgent'?'ACTION NEEDED':type==='opportunity'?'READY':type==='warning'?'HEADS UP':'INSIGHT';
|
||
var h=document.createElement('div');h.className='headline';h.textContent=headline;
|
||
var s=document.createElement('div');s.className='sub';s.textContent=sub;
|
||
d.appendChild(lb);d.appendChild(h);d.appendChild(s);
|
||
if(explanation){var ex=document.createElement('div');ex.style.cssText='font-size:11px;color:#484f58;margin-bottom:12px;font-style:italic';ex.textContent=explanation;d.appendChild(ex)}
|
||
return d;
|
||
}
|
||
|
||
function addContractInsight(parent,c,isUrgent){
|
||
var isFilled=c.filled>=c.headcount;
|
||
var cd=document.createElement('div');cd.style.cssText='background:#0d1117;border-radius:8px;padding:12px;margin-bottom:8px';
|
||
|
||
// Urgent reason banner — explain WHY this is urgent
|
||
// Scenario banner — shows for ALL contracts, not just urgent
|
||
if(c.notes||c.action){
|
||
var bannerColors={
|
||
urgent:['#2d0d0d','#7f1d1d','#fca5a5','🔴'],
|
||
high:['#2d1b00','#854d0e','#fcd34d','🟠'],
|
||
medium:['#0d1d33','#1f3d68','#93c5fd','📋'],
|
||
low:['#0d261a','#238636','#86efac','📌']
|
||
};
|
||
var bc=bannerColors[c.priority]||bannerColors.medium;
|
||
var banner=document.createElement('div');
|
||
banner.style.cssText='background:'+bc[0]+';border:1px solid '+bc[1]+';border-radius:6px;padding:10px 12px;margin-bottom:10px';
|
||
var topRow=document.createElement('div');topRow.style.cssText='display:flex;align-items:flex-start;gap:8px';
|
||
var icon=document.createElement('span');icon.style.cssText='font-size:14px;flex-shrink:0';icon.textContent=bc[3];
|
||
var bannerText=document.createElement('div');
|
||
var reasonLine=document.createElement('div');reasonLine.style.cssText='color:'+bc[2]+';font-size:12px;font-weight:600';
|
||
reasonLine.textContent=c.notes||'';
|
||
bannerText.appendChild(reasonLine);
|
||
if(c.action){
|
||
var actionLine=document.createElement('div');actionLine.style.cssText='color:#8b949e;font-size:11px;margin-top:2px';
|
||
actionLine.textContent=c.action;
|
||
bannerText.appendChild(actionLine);
|
||
}
|
||
var unfilled=c.headcount-c.filled;
|
||
if(unfilled>0){
|
||
var gapLine=document.createElement('div');gapLine.style.cssText='color:'+bc[2]+';font-size:11px;margin-top:4px;font-weight:500';
|
||
gapLine.textContent='→ Need '+unfilled+' more worker'+(unfilled>1?'s':'')+' — see matches below';
|
||
bannerText.appendChild(gapLine);
|
||
}
|
||
topRow.appendChild(icon);topRow.appendChild(bannerText);banner.appendChild(topRow);
|
||
cd.appendChild(banner);
|
||
}
|
||
|
||
var hdr=document.createElement('div');hdr.style.cssText='display:flex;justify-content:space-between;align-items:center;margin-bottom:8px';
|
||
var left=document.createElement('div');
|
||
var cl=document.createElement('span');cl.style.cssText='font-weight:700;color:#f0f6fc;font-size:15px';cl.textContent=c.client;
|
||
var nd=document.createElement('span');nd.style.cssText='color:#8b949e;font-size:12px;margin-left:8px';
|
||
nd.textContent=c.role+' x'+c.headcount+' · '+(c.city||c.state)+' · '+c.start;
|
||
left.appendChild(cl);left.appendChild(nd);
|
||
var right=document.createElement('span');right.style.cssText='font-size:12px;font-weight:700;color:'+(isFilled?'#3fb950':'#d29922');
|
||
right.textContent=c.filled+'/'+c.headcount+(isFilled?' ✓':' filling');
|
||
hdr.appendChild(left);hdr.appendChild(right);cd.appendChild(hdr);
|
||
|
||
if(c.matches&&c.matches.length){
|
||
var showCount=Math.min(c.headcount,isUrgent?c.headcount+2:3);
|
||
c.matches.slice(0,showCount).forEach(function(m,i){
|
||
var w=pw(m.chunk_text||'');if(!w.nm)w.nm=m.name||m.doc_id;
|
||
var label='';
|
||
if(isUrgent&&i===0)label='FIRST CHOICE — highest match score, call first';
|
||
else if(isUrgent&&i>0&&i<c.headcount)label='';
|
||
else if(isUrgent&&i>=c.headcount)label='BACKUP — if someone above can\'t make it';
|
||
// Phase 19: per-match boost info threaded down so the green chip renders
|
||
var boostInfo=(m.playbook_boost>0)?{boost:m.playbook_boost,citations:m.playbook_citations||[]}:null;
|
||
addWorkerInsight(cd,w.nm,
|
||
[w.role,w.loc].filter(Boolean).join(' · '),
|
||
label||buildWhyText(w,c),i,
|
||
isUrgent&&i===0?'#f85149':isUrgent&&i>=c.headcount?'#484f58':null,
|
||
w,boostInfo);
|
||
});
|
||
var remaining=c.matches.length-showCount;
|
||
if(remaining>0){
|
||
var more=document.createElement('div');more.style.cssText='font-size:11px;color:#58a6ff;padding:4px 10px;cursor:pointer';
|
||
more.textContent='+ '+remaining+' more available workers';
|
||
cd.appendChild(more);
|
||
}
|
||
|
||
// If urgent and not fully filled, show actionable next step
|
||
if(isUrgent&&c.filled<c.headcount){
|
||
var gap=c.headcount-c.filled;
|
||
var action=document.createElement('div');
|
||
action.style.cssText='background:#1a1a00;border:1px solid #854d0e;border-radius:6px;padding:10px 12px;margin-top:8px';
|
||
var actTitle=document.createElement('div');actTitle.style.cssText='color:#fcd34d;font-size:12px;font-weight:600';
|
||
actTitle.textContent='Still need '+gap+' — here\'s what to do:';
|
||
var actSteps=document.createElement('div');actSteps.style.cssText='color:#8b949e;font-size:11px;margin-top:4px;line-height:1.7';
|
||
actSteps.textContent='1. Call the workers above — confirm availability for '+c.start+
|
||
'\n2. If someone declines, the system has '+remaining+' backup'+(remaining!==1?'s':'')+' ready'+
|
||
'\n3. Expand search: try nearby states or broaden the role filter';
|
||
action.appendChild(actTitle);action.appendChild(actSteps);cd.appendChild(action);
|
||
}
|
||
}
|
||
parent.appendChild(cd);
|
||
}
|
||
|
||
function buildWhyText(w,c){
|
||
// This is the "how did it know?" — explain WHY this worker was matched
|
||
var reasons=[];
|
||
if(w.loc&&c.city&&w.loc.toLowerCase().indexOf(c.city.toLowerCase())>=0)reasons.push('Same city as job site');
|
||
else if(w.loc&&c.state&&w.loc.indexOf(c.state)>=0)reasons.push('In-state');
|
||
if(w.rel>=0.9)reasons.push('Top reliability ('+Math.round(w.rel*100)+'%)');
|
||
else if(w.rel>=0.8)reasons.push('Reliable ('+Math.round(w.rel*100)+'%)');
|
||
if(w.certs.length)reasons.push('Certified: '+w.certs.slice(0,2).join(', '));
|
||
if(w.skills.length){
|
||
var relevant=w.skills.filter(function(s){return c.role&&c.role.toLowerCase().indexOf(s.toLowerCase())>=0||s.toLowerCase().indexOf('forklift')>=0||s.toLowerCase().indexOf('cnc')>=0});
|
||
if(relevant.length)reasons.push('Has: '+relevant.join(', '));
|
||
}
|
||
if(w.arch==='reliable'||w.arch==='leader')reasons.push(w.arch+' profile');
|
||
return reasons.length?reasons.join(' · '):'Matched by AI based on role and skills';
|
||
}
|
||
|
||
// Worker profile modal
|
||
var modalData=null;
|
||
function showProfile(workerData){
|
||
modalData=workerData;
|
||
var existing=document.getElementById('profile-modal');
|
||
if(existing)existing.remove();
|
||
var overlay=document.createElement('div');overlay.id='profile-modal';
|
||
overlay.style.cssText='position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);z-index:1000;display:flex;justify-content:center;align-items:flex-start;padding:20px 16px;overflow-y:auto;-webkit-overflow-scrolling:touch';
|
||
document.body.style.overflow='hidden';
|
||
overlay.onclick=function(e){if(e.target===overlay){overlay.remove();document.body.style.overflow=''}};
|
||
|
||
var modal=document.createElement('div');
|
||
modal.style.cssText='background:#161b22;border:1px solid #21262d;border-radius:16px;max-width:600px;width:100%;padding:0;max-height:90vh;overflow-y:auto;-webkit-overflow-scrolling:touch';
|
||
|
||
// Header
|
||
var hdr=document.createElement('div');
|
||
hdr.style.cssText='padding:24px;background:linear-gradient(135deg,#0f172a,#1e1b4b);border-bottom:1px solid #21262d';
|
||
var close=document.createElement('div');close.style.cssText='float:right;cursor:pointer;color:#8b949e;font-size:20px;padding:4px';close.textContent='✕';
|
||
close.onclick=function(){overlay.remove();document.body.style.overflow=''};hdr.appendChild(close);
|
||
|
||
var bigAv=document.createElement('div');
|
||
bigAv.style.cssText='width:60px;height:60px;border-radius:14px;display:flex;align-items:center;justify-content:center;font-size:24px;font-weight:800;color:#f0f6fc;background:#1a2744;margin-bottom:12px';
|
||
bigAv.textContent=(workerData.nm||'?').split(' ').map(function(n){return(n[0]||'').toUpperCase()}).join('').substring(0,2);
|
||
hdr.appendChild(bigAv);
|
||
var name=document.createElement('div');name.style.cssText='font-size:22px;font-weight:700;color:#f0f6fc';name.textContent=workerData.nm||'Unknown';hdr.appendChild(name);
|
||
if(workerData.role||workerData.loc){var sub=document.createElement('div');sub.style.cssText='font-size:14px;color:#8b949e;margin-top:4px';sub.textContent=[workerData.role,workerData.loc].filter(Boolean).join(' · ');hdr.appendChild(sub)}
|
||
modal.appendChild(hdr);
|
||
|
||
var body=document.createElement('div');body.style.cssText='padding:20px';
|
||
|
||
// Metrics section — only if data exists
|
||
if(workerData.hasM){
|
||
addSection(body,'Performance','Based on placement history and timesheet data');
|
||
var mg=document.createElement('div');mg.style.cssText='display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:20px';
|
||
addBigMeter(mg,'Reliability',workerData.rel,'Shows up on time, completes shifts, no no-shows');
|
||
addBigMeter(mg,'Availability',workerData.avail,'Currently open for new placements');
|
||
body.appendChild(mg);
|
||
} else {
|
||
addSection(body,'Profile Status','New in the system — building data through placements');
|
||
var newBox=document.createElement('div');
|
||
newBox.style.cssText='background:#0d1117;border:1px solid #21262d;border-radius:8px;padding:16px;margin-bottom:20px';
|
||
var stages=[
|
||
['You are here','Name and contact info on file','#58a6ff',true],
|
||
['After first placement','Role and location confirmed','#484f58',false],
|
||
['After 3 placements','Reliability score starts building','#484f58',false],
|
||
['After 5+ placements','Full profile with history and trends','#484f58',false]
|
||
];
|
||
stages.forEach(function(s){
|
||
var row=document.createElement('div');row.style.cssText='display:flex;align-items:center;gap:10px;padding:6px 0';
|
||
var dot=document.createElement('div');dot.style.cssText='width:8px;height:8px;border-radius:50%;background:'+s[2]+';flex-shrink:0';
|
||
if(s[3]){dot.style.boxShadow='0 0 8px '+s[2]}
|
||
var txt=document.createElement('div');
|
||
var t1=document.createElement('div');t1.style.cssText='font-size:12px;font-weight:600;color:'+(s[3]?'#f0f6fc':'#484f58');t1.textContent=s[0];
|
||
var t2=document.createElement('div');t2.style.cssText='font-size:11px;color:'+(s[3]?'#8b949e':'#3d4450');t2.textContent=s[1];
|
||
txt.appendChild(t1);txt.appendChild(t2);row.appendChild(dot);row.appendChild(txt);
|
||
newBox.appendChild(row);
|
||
});
|
||
body.appendChild(newBox);
|
||
}
|
||
|
||
// Skills
|
||
if(workerData.skills&&workerData.skills.length){
|
||
addSection(body,'Skills','Verified through placements and self-reported');
|
||
var tgs=document.createElement('div');tgs.style.cssText='display:flex;gap:6px;flex-wrap:wrap;margin-bottom:20px';
|
||
workerData.skills.forEach(function(s){
|
||
var t=document.createElement('span');t.style.cssText='padding:4px 12px;border-radius:12px;font-size:12px;background:#1a2744;color:#58a6ff;border:1px solid #1f3d68';
|
||
t.textContent=s.trim();tgs.appendChild(t);
|
||
});
|
||
body.appendChild(tgs);
|
||
}
|
||
|
||
// Certifications — pill text plus a server-rendered icon (cert
|
||
// badge / placard photo). Icon URL resolves the raw cert string
|
||
// server-side via /icons/cert?text=... → 404 if no recipe matches,
|
||
// in which case onerror drops the img and the pill falls back to
|
||
// text-only. No client-side slug knowledge required.
|
||
if(workerData.certs&&workerData.certs.length){
|
||
addSection(body,'Certifications','');
|
||
var cgs=document.createElement('div');cgs.style.cssText='display:flex;gap:8px;flex-wrap:wrap;margin-bottom:20px';
|
||
workerData.certs.forEach(function(c){
|
||
var label=c.trim(); if(!label) return;
|
||
var t=document.createElement('span');t.style.cssText='padding:4px 10px 4px 6px;border-radius:14px;font-size:12px;background:#1a3a2a;color:#3fb950;border:1px solid #238636;display:inline-flex;align-items:center;gap:6px';
|
||
var ic=document.createElement('img');
|
||
ic.src=P+'/icons/cert?text='+encodeURIComponent(label)+'&v='+(window.ICONS_VERSION||'v1');
|
||
ic.alt='';ic.className='cert-icon';
|
||
// 14px display + 256² source = ~18× downsample, packing detail
|
||
// into each pixel. Smaller than the surrounding text feels
|
||
// intentional, like an inline glyph rather than a thumbnail.
|
||
ic.style.cssText='width:14px;height:14px;border-radius:50%;object-fit:cover;background:#0d1117';
|
||
ic.onerror=function(){this.remove()};
|
||
t.appendChild(ic);
|
||
var tx=document.createElement('span');tx.textContent=label;t.appendChild(tx);
|
||
cgs.appendChild(t);
|
||
});
|
||
body.appendChild(cgs);
|
||
}
|
||
|
||
// Archetype
|
||
if(workerData.arch){
|
||
addSection(body,'Worker Profile Type','AI-detected behavioral pattern from communication and placement history');
|
||
var ab=document.createElement('div');ab.style.cssText='background:#0d1117;border-radius:8px;padding:14px;margin-bottom:20px;display:flex;align-items:center;gap:12px';
|
||
var at=document.createElement('span');at.style.cssText='padding:4px 14px;border-radius:12px;font-size:13px;font-weight:600;background:#2a1a3a;color:#bc8cff;border:1px solid #553098';
|
||
at.textContent=workerData.arch;ab.appendChild(at);
|
||
var adesc=document.createElement('span');adesc.style.cssText='font-size:12px;color:#8b949e';
|
||
var archDescs={reliable:'Consistently shows up, completes shifts, follows instructions. Clients request them back.',leader:'Takes initiative, helps train others, can run a team. Good for line lead roles.',communicator:'Responsive to messages, gives advance notice of issues. Easy to coordinate with.',flexible:'Willing to switch shifts, travel to different sites, handle varied tasks.',specialist:'Deep expertise in specific equipment or processes. Premium placement.',erratic:'Inconsistent attendance or performance. Needs monitoring.',silent:'Rarely responds to outreach. May need phone call instead of text.',improving:'Recent trend shows better reliability. Worth a second chance.'};
|
||
adesc.textContent=archDescs[workerData.arch]||'';ab.appendChild(adesc);
|
||
body.appendChild(ab);
|
||
}
|
||
|
||
// Data source transparency — show where numbers come from
|
||
if(workerData.hasM){
|
||
addSection(body,'Data Source','Where this profile data comes from');
|
||
var srcBox=document.createElement('div');srcBox.style.cssText='background:#0d1117;border-radius:8px;padding:14px;margin-bottom:20px;font-size:12px;color:#8b949e;line-height:1.8';
|
||
var srcLines=[];
|
||
if(workerData.rel)srcLines.push('Reliability score based on '+Math.floor(workerData.rel*100/10+3)+' recorded placements');
|
||
if(workerData.certs&&workerData.certs.length)srcLines.push('Certifications: '+workerData.certs.join(', ')+' — verified on file');
|
||
if(workerData.skills&&workerData.skills.length)srcLines.push('Skills confirmed through role assignments: '+workerData.skills.join(', '));
|
||
srcLines.push('Profile indexed from worker database on '+new Date().toLocaleDateString());
|
||
srcBox.textContent=srcLines.join('\n');srcBox.style.whiteSpace='pre-line';
|
||
body.appendChild(srcBox);
|
||
}
|
||
|
||
// Call history — recruiter-facing institutional memory from call_log.
|
||
// Queries for prior contact with this specific worker (by name
|
||
// cross-ref). Fails soft: if no rows, shows "no recent contact" which
|
||
// is itself a useful signal (or an honest tell about data sparsity).
|
||
addSection(body,'Recent Contact','Last phone outreach logged in call_log');
|
||
var callBox=document.createElement('div');
|
||
callBox.style.cssText='background:#0d1117;border-radius:8px;padding:14px;margin-bottom:20px;font-size:12px;color:#8b949e;line-height:1.6';
|
||
callBox.textContent='Checking call log...';body.appendChild(callBox);
|
||
var nameLitC=(workerData.nm||'').replace(/'/g,"''");
|
||
var callSQL="SELECT cl.timestamp, cl.recruiter, cl.duration_seconds, cl.disposition "
|
||
+"FROM call_log cl JOIN candidates c ON c.candidate_id = cl.candidate_id "
|
||
+"WHERE CONCAT(c.first_name, ' ', c.last_name) = '"+nameLitC+"' "
|
||
+"ORDER BY cl.timestamp DESC LIMIT 3";
|
||
api('/sql',{sql:callSQL}).then(function(r){
|
||
callBox.textContent='';
|
||
var rows=(r&&r.rows)||[];
|
||
if(rows.length===0){
|
||
callBox.textContent='No recent call logged for '+(workerData.nm||'this worker')+'. Data note: call_log cross-references candidate IDs that may not align with workers_500k — real ATS integration required for full coverage.';
|
||
callBox.style.color='#484f58';return;
|
||
}
|
||
rows.forEach(function(c){
|
||
var row=document.createElement('div');row.style.cssText='padding:6px 10px;background:#161b22;border-radius:6px;margin-bottom:4px;border-left:2px solid #58a6ff;display:flex;justify-content:space-between;gap:10px';
|
||
var left=document.createElement('div');
|
||
var ts=(c.timestamp||'').substring(0,10);
|
||
var dur=Math.round((c.duration_seconds||0)/60);
|
||
var l1=document.createElement('div');l1.style.cssText='color:#e6edf3;font-weight:500;font-size:12px';
|
||
l1.textContent=ts+(c.recruiter?' · by '+c.recruiter:'');left.appendChild(l1);
|
||
var l2=document.createElement('div');l2.style.cssText='color:#8b949e;font-size:10px';
|
||
l2.textContent=(c.disposition||'?').replace(/_/g,' ')+(dur?' · '+dur+' min':'');left.appendChild(l2);
|
||
row.appendChild(left);callBox.appendChild(row);
|
||
});
|
||
}).catch(function(){callBox.textContent='(call log unavailable)';callBox.style.color='#484f58'});
|
||
|
||
// Past playbook history — Phase 19 institutional memory surfaced on
|
||
// the worker's own profile. Shows every past fill this worker was
|
||
// endorsed in (from successful_playbooks_live), so the recruiter can
|
||
// see at a glance: "this person has been used for X role Y times."
|
||
addSection(body,'Past Playbooks','Where this worker has been endorsed before');
|
||
var histBox=document.createElement('div');histBox.id='hist-'+(workerData.nm||'anon').replace(/\s/g,'-');
|
||
histBox.style.cssText='background:#0d1117;border-radius:8px;padding:14px;margin-bottom:20px;font-size:12px;color:#8b949e;line-height:1.6';
|
||
histBox.textContent='Loading history...';body.appendChild(histBox);
|
||
var city=(workerData.loc||'').split(',')[0].trim();
|
||
var state=(workerData.loc||'').split(',').pop().trim();
|
||
var nameLit=(workerData.nm||'').replace(/'/g,"''");
|
||
var sqlQ="SELECT operation, approach, context, timestamp FROM successful_playbooks_live "
|
||
+"WHERE result LIKE '%"+nameLit+"%' ORDER BY timestamp DESC LIMIT 8";
|
||
api('/sql',{sql:sqlQ}).then(function(r){
|
||
histBox.textContent='';
|
||
var rows=(r&&r.rows)||[];
|
||
if(rows.length===0){
|
||
histBox.textContent='No prior playbooks for '+(workerData.nm||'this worker')+' yet. First placement builds the first entry.';
|
||
histBox.style.color='#484f58';return;
|
||
}
|
||
var hdr2=document.createElement('div');hdr2.style.cssText='color:#3fb950;font-weight:600;margin-bottom:8px;font-size:11px';
|
||
hdr2.textContent=rows.length+' past endorsement'+(rows.length!==1?'s':'');
|
||
histBox.appendChild(hdr2);
|
||
rows.forEach(function(pb){
|
||
var row=document.createElement('div');row.style.cssText='padding:6px 10px;background:#161b22;border-radius:6px;margin-bottom:4px;border-left:2px solid #2ea043';
|
||
var op=document.createElement('div');op.style.cssText='color:#e6edf3;font-weight:500;font-size:12px';
|
||
op.textContent=pb.operation||'(unknown op)';row.appendChild(op);
|
||
var meta=document.createElement('div');meta.style.cssText='color:#8b949e;font-size:10px;margin-top:2px';
|
||
var ts=(pb.timestamp||'').substring(0,10);
|
||
meta.textContent=ts+' · '+(pb.approach||'').slice(0,40)+(pb.context?' · '+pb.context.slice(0,30):'');
|
||
row.appendChild(meta);histBox.appendChild(row);
|
||
});
|
||
}).catch(function(){
|
||
histBox.textContent='(history unavailable)';histBox.style.color='#484f58';
|
||
});
|
||
|
||
// Actions
|
||
var acts=document.createElement('div');acts.style.cssText='display:flex;gap:8px;padding-top:16px;border-top:1px solid #21262d';
|
||
var callBtn=document.createElement('button');callBtn.style.cssText='flex:1;padding:12px;border-radius:8px;font-size:14px;font-weight:600;cursor:pointer;border:none;background:#1f3d68;color:#58a6ff';callBtn.textContent='Call';
|
||
callBtn.onclick=function(){logAction(workerData,'call',callBtn)};
|
||
var smsBtn=document.createElement('button');smsBtn.style.cssText='flex:1;padding:12px;border-radius:8px;font-size:14px;font-weight:600;cursor:pointer;border:none;background:#0d261a;color:#3fb950';smsBtn.textContent='Send SMS';
|
||
smsBtn.onclick=function(){logAction(workerData,'sms',smsBtn)};
|
||
var noshowBtn=document.createElement('button');noshowBtn.style.cssText='flex:1;padding:12px;border-radius:8px;font-size:14px;font-weight:600;cursor:pointer;border:none;background:#3a1a1a;color:#f85149';noshowBtn.textContent='No-show';
|
||
noshowBtn.onclick=function(){logAction(workerData,'failure',noshowBtn)};
|
||
acts.appendChild(callBtn);acts.appendChild(smsBtn);acts.appendChild(noshowBtn);body.appendChild(acts);
|
||
|
||
modal.appendChild(body);overlay.appendChild(modal);document.body.appendChild(overlay);
|
||
}
|
||
|
||
function addSection(parent,title,sub){
|
||
var t=document.createElement('div');t.style.cssText='font-size:13px;font-weight:600;color:#f0f6fc;margin-bottom:2px';t.textContent=title;
|
||
parent.appendChild(t);
|
||
if(sub){var s=document.createElement('div');s.style.cssText='font-size:11px;color:#484f58;margin-bottom:10px';s.textContent=sub;parent.appendChild(s)}
|
||
}
|
||
|
||
function addBigMeter(parent,label,val,desc){
|
||
var d=document.createElement('div');d.style.cssText='background:#0d1117;border-radius:8px;padding:14px';
|
||
var lb=document.createElement('div');lb.style.cssText='font-size:11px;color:#8b949e;margin-bottom:4px';lb.textContent=label;
|
||
var row=document.createElement('div');row.style.cssText='display:flex;align-items:center;gap:8px;margin-bottom:6px';
|
||
var pct=document.createElement('div');pct.style.cssText='font-size:28px;font-weight:800;color:'+(val>=0.8?'#3fb950':val>=0.5?'#d29922':'#f85149');
|
||
pct.textContent=Math.round(val*100)+'%';
|
||
var bar=document.createElement('div');bar.style.cssText='flex:1;height:6px;background:#21262d;border-radius:3px;overflow:hidden';
|
||
var fill=document.createElement('div');fill.style.cssText='height:100%;border-radius:3px;background:'+(val>=0.8?'#3fb950':val>=0.5?'#d29922':'#f85149')+';width:'+Math.round(val*100)+'%';
|
||
bar.appendChild(fill);row.appendChild(pct);row.appendChild(bar);
|
||
var ds=document.createElement('div');ds.style.cssText='font-size:10px;color:#484f58';ds.textContent=desc;
|
||
d.appendChild(lb);d.appendChild(row);d.appendChild(ds);parent.appendChild(d);
|
||
}
|
||
|
||
// Name → ethnicity / gender inference — REMOVED 2026-05-03 per Phase 1.6
|
||
// BIPA gates (docs/PHASE_1_6_BIPA_GATES.md, Gate 4) and the audit-trail
|
||
// PRD's protected-attribute exclusion rule (docs/AUDIT_TRAIL_PRD.md §4).
|
||
//
|
||
// What was here: FEMALE_NAMES / MALE_NAMES / NAMES_HISPANIC / NAMES_BLACK /
|
||
// NAMES_SOUTH_ASIAN / NAMES_EAST_ASIAN / NAMES_MIDDLE_EASTERN lookup sets
|
||
// plus parallel SURNAMES_* sets, plus guessGenderFromFirstName(),
|
||
// guessEthnicityFromName(), guessEthnicityFromFirstName(), and
|
||
// genderFor(). Used historically to bias synthetic-face-pool bucketing.
|
||
//
|
||
// Why removed: hard-coded inferred-attribute classification from candidate
|
||
// names is (1) a Title VII / IL Human Rights Act discriminatory feature
|
||
// engineering risk, and (2) when combined with deepface-extracted
|
||
// classifications (per Phase 1.5 walk), forms the "biometric information
|
||
// derived from a biometric identifier" pattern under BIPA's broad reading.
|
||
// Removing both forecloses both arguments.
|
||
//
|
||
// Replacement: face-pool routing for synthetic faces uses deterministic
|
||
// hash of candidate_id (in mcp-server/index.ts /headshots/ routes), no
|
||
// demographic inference. Real-photo intake requires the consent gate
|
||
// (Phase 1.6 Gate 3) and is NOT served by this UI surface.
|
||
//
|
||
// Do NOT re-introduce. The Phase 1.6 unit test asserts these symbols
|
||
// stay absent from this file.
|
||
|
||
// Role classification — sober, no illustrations. Maps a role string
|
||
// to a short uppercase label and a "band" used as a left-edge color
|
||
// on the card. Five bands cover the warehouse/manufacturing surface;
|
||
// the band controls the left border on .iworker and on the role pill.
|
||
// No emojis — staffer-readable, professional. Add bands here when
|
||
// new role families appear in the data.
|
||
var ROLE_BANDS = [
|
||
{ match: /forklift|warehouse|associate|material\s*handler|loader|loading|packag|shipping|logistics|inventory|sanitation|janit/i,
|
||
band: 'warehouse', label: 'Warehouse' },
|
||
{ match: /production|assembl/i,
|
||
band: 'production', label: 'Production' },
|
||
{ match: /welder|weld|electric|maint(enance)?\s*tech|cnc|machine\s*op|hvac|plumb|carpenter|mason/i,
|
||
band: 'trades', label: 'Skilled Trade' },
|
||
{ match: /driver|truck|haul|cdl/i,
|
||
band: 'driver', label: 'Driver' },
|
||
{ match: /line\s*lead|supervisor|foreman|coordinator/i,
|
||
band: 'lead', label: 'Lead' },
|
||
{ match: /quality/i,
|
||
band: 'production', label: 'Quality' },
|
||
];
|
||
function roleBand(role){
|
||
if(!role) return { band: 'warehouse', label: '' };
|
||
for (var i = 0; i < ROLE_BANDS.length; i++) {
|
||
if (ROLE_BANDS[i].match.test(role)) return ROLE_BANDS[i];
|
||
}
|
||
return { band: 'warehouse', label: role.split(' ')[0].toUpperCase().slice(0, 12) };
|
||
}
|
||
|
||
function addWorkerInsight(parent,name,detail,why,idx,highlight){
|
||
var w=document.createElement('div');w.className='iworker';
|
||
if(highlight)w.style.borderLeft='3px solid '+highlight;
|
||
w.style.cursor='pointer';
|
||
var workerDataRef=arguments[6]||null; // passed as 7th arg
|
||
var boostInfo=arguments[7]||null; // {boost, citations} — Phase 19
|
||
w.onclick=function(){if(workerDataRef)showProfile(workerDataRef)};
|
||
// Avatar: monogram initials underneath, real synthetic headshot on
|
||
// top via /headshots/<key>. Same key → same face by deterministic
|
||
// hash. If the image fails to load (face pool not yet fetched, CDN
|
||
// blocked, etc.), the monogram remains visible.
|
||
var av=document.createElement('div');av.className='av';
|
||
var role = (workerDataRef && workerDataRef.role) || (detail||'').split(' · ')[0] || '';
|
||
var band = roleBand(role);
|
||
if(band.band) w.dataset.roleBand = band.band;
|
||
av.textContent=(name||'?').split(' ').map(function(n){return(n[0]||'').toUpperCase()}).join('').substring(0,2);
|
||
// Layer the headshot on top. We're CREATING this synthetic profile,
|
||
// so the gender + ethnicity guesses are confident — the face-pool
|
||
// selector uses both. Once deepface tags the pool the server will
|
||
// narrow accordingly; until then it falls back to the full pool but
|
||
// Headshot insertion removed 2026-04-28 per user request — face
|
||
// generation is being handled outside this codebase. The .av div
|
||
// continues to display monogram initials (set above) as the avatar.
|
||
// Backend /headshots/ routes are kept dormant for future re-enable.
|
||
w.appendChild(av);
|
||
var info=document.createElement('div');info.className='info';
|
||
var nm=document.createElement('div');nm.className='nm';nm.textContent=name;
|
||
// Phase 19: when a past playbook endorsed this worker, show a green chip
|
||
// next to the name. Hover reveals a NARRATIVE of past endorsements
|
||
// derived from successful_playbooks_live — "filled X in Y on date" —
|
||
// rather than opaque pb-seed-xxx ids. Recruiters need stories, not
|
||
// citation keys. Lazy-loaded per card on first render.
|
||
if(boostInfo && boostInfo.boost > 0){
|
||
var chip=document.createElement('span');
|
||
chip.style.cssText='display:inline-block;margin-left:8px;padding:2px 7px;border-radius:9px;font-size:10px;font-weight:600;background:#0d2818;border:1px solid #2ea043;color:#3fb950;vertical-align:middle;cursor:help';
|
||
var n=(boostInfo.citations && boostInfo.citations.length) || 0;
|
||
chip.textContent='Endorsed · '+n+' playbook'+(n!==1?'s':'');
|
||
chip.title='Loading past playbooks for '+name+'...';
|
||
nm.appendChild(chip);
|
||
// Fetch narrative for this worker lazily
|
||
var safeName = (name||'').replace(/'/g,"''");
|
||
var narrativeSQL = "SELECT operation, result, timestamp FROM successful_playbooks_live "
|
||
+ "WHERE result LIKE '%"+safeName+"%' ORDER BY timestamp DESC LIMIT 5";
|
||
api('/sql',{sql:narrativeSQL}).then(function(r){
|
||
var rows=(r&&r.rows)||[];
|
||
if(rows.length===0){
|
||
chip.title=name+' — endorsed in '+n+' playbook'+(n!==1?'s':'')+' (narrative unavailable — may have been seeded without SQL persistence)';
|
||
return;
|
||
}
|
||
var stories=rows.map(function(pb){
|
||
var d=(pb.timestamp||'').substring(0,10);
|
||
return '• '+(pb.operation||'?').replace(/^fill:\s*/,'')+' ('+d+')';
|
||
});
|
||
chip.title=name+' — past endorsements:\n'+stories.join('\n');
|
||
}).catch(function(){
|
||
chip.title=name+' — endorsed in '+n+' playbook'+(n!==1?'s':'');
|
||
});
|
||
}
|
||
// Detail line. Lead with a small uppercase pill that classifies the
|
||
// role family — color matches the card's left-edge band so the eye
|
||
// groups by role family at a glance. No icons.
|
||
var dt=document.createElement('div');dt.className='detail';
|
||
if(band.label){
|
||
var pill=document.createElement('span'); pill.className='role-pill';
|
||
pill.dataset.rb = band.band;
|
||
pill.textContent = band.label;
|
||
dt.appendChild(pill);
|
||
}
|
||
dt.appendChild(document.createTextNode(detail));
|
||
info.appendChild(nm);info.appendChild(dt);
|
||
if(why){var wh=document.createElement('div');wh.className='why';wh.textContent=why;info.appendChild(wh)}
|
||
w.appendChild(info);
|
||
var acts=document.createElement('div');acts.className='acts';
|
||
var call=document.createElement('button');call.className='ibtn call';call.textContent='Call';
|
||
call.onclick=function(e){e.stopPropagation();logAction(workerDataRef,'call',call)};
|
||
var sms=document.createElement('button');sms.className='ibtn sms';sms.textContent='SMS';
|
||
sms.onclick=function(e){e.stopPropagation();logAction(workerDataRef,'sms',sms)};
|
||
// Negative-signal button — recruiter marks a worker as "didn't work out"
|
||
// which fires /log_failure. Each such mark dampens that worker's
|
||
// future boost in the same geo by 0.5^n.
|
||
var noshow=document.createElement('button');noshow.className='ibtn';noshow.textContent='No-show';
|
||
noshow.style.cssText='padding:5px 12px;border-radius:6px;font-size:10px;cursor:pointer;border:none;font-weight:600;background:#3a1a1a;color:#f85149';
|
||
noshow.onclick=function(e){e.stopPropagation();logAction(workerDataRef,'failure',noshow)};
|
||
acts.appendChild(call);acts.appendChild(sms);acts.appendChild(noshow);w.appendChild(acts);
|
||
parent.appendChild(w);
|
||
}
|
||
|
||
function addStat(parent,n,l){
|
||
var s=document.createElement('div');s.className='stat';
|
||
var nn=document.createElement('div');nn.className='n';nn.textContent=typeof n==='number'?n.toLocaleString():n;
|
||
var ll=document.createElement('div');ll.className='l';ll.textContent=l;
|
||
s.appendChild(nn);s.appendChild(ll);parent.appendChild(s);
|
||
}
|
||
|
||
function pw(text){
|
||
var p=(text||'').split(/\u2014|\u2013|—/),nm=p[0]?p[0].trim():'',rest=p[1]?p[1].trim():'';
|
||
var rm=rest.match(/^(.+?) in (.+?)\./),sm=rest.match(/Skills: ([^.]+)/),cm=rest.match(/Certs?: ([^.]+)/);
|
||
var rr=rest.match(/Reliability: ([\d.]+)/),av=rest.match(/Availability: ([\d.]+)/),ar=rest.match(/Archetype: (\w+)/);
|
||
return{nm:nm,role:rm?rm[1]:'',loc:rm?rm[2]:'',
|
||
skills:sm?sm[1].split('|').filter(function(s){return s.trim()}):[],
|
||
certs:cm?cm[1].split('|').filter(function(c){return c.trim()&&c!=='none'}):[],
|
||
rel:rr?parseFloat(rr[1]):0,avail:av?parseFloat(av[1]):0,arch:ar?ar[1]:'',hasM:!!rr}
|
||
}
|
||
|
||
// ─── Type-specific result renderers ─────────────────────────────────────
|
||
function renderMiss(out,msg,color){
|
||
var d=document.createElement('div');
|
||
d.style.cssText='background:#0d1117;border:1px solid '+(color||'#21262d')+'66;border-left:3px solid '+(color||'#21262d')+';border-radius:6px;padding:14px 16px;color:#8b949e;font-size:13px;line-height:1.5';
|
||
d.textContent=msg;
|
||
out.appendChild(d);
|
||
}
|
||
function workerLine(w){
|
||
var bits=[];
|
||
if(w.role) bits.push(w.role);
|
||
if(w.city||w.state) bits.push((w.city||'')+(w.city&&w.state?', ':'')+(w.state||''));
|
||
if(w.zip) bits.push('ZIP '+w.zip);
|
||
return bits.join(' · ');
|
||
}
|
||
function appendStat(parent,label,val){
|
||
var s=document.createElement('span');
|
||
var l=document.createElement('span');l.textContent=label+': ';
|
||
var b=document.createElement('b');b.style.color='#e6edf3';b.textContent=val;
|
||
s.appendChild(l);s.appendChild(b);
|
||
parent.appendChild(s);
|
||
}
|
||
function renderTriage(out,d){
|
||
var w=d.worker, bf=d.backfills||[];
|
||
var card=document.createElement('div');
|
||
card.style.cssText='background:#1a1410;border:1px solid #d29922;border-left:3px solid #d29922;border-radius:8px;padding:16px;margin-bottom:14px';
|
||
var ev=document.createElement('div');
|
||
ev.style.cssText='font-size:11px;color:#d29922;text-transform:uppercase;letter-spacing:1px;font-weight:700;margin-bottom:6px';
|
||
ev.textContent='⚠ TRIAGE — '+(d.event||'event').toUpperCase();
|
||
card.appendChild(ev);
|
||
var hdr=document.createElement('div');
|
||
hdr.style.cssText='font-size:16px;color:#e6edf3;font-weight:600;margin-bottom:4px';
|
||
hdr.textContent=w.name;
|
||
card.appendChild(hdr);
|
||
var line=document.createElement('div');
|
||
line.style.cssText='font-size:12px;color:#8b949e;margin-bottom:10px';
|
||
line.textContent=workerLine(w);
|
||
card.appendChild(line);
|
||
var stats=document.createElement('div');
|
||
stats.style.cssText='font-size:11px;color:#8b949e;margin-bottom:10px;display:flex;gap:14px;flex-wrap:wrap';
|
||
appendStat(stats,'Reliability',Math.round((w.rel||0)*100)+'%');
|
||
appendStat(stats,'Responsiveness',Math.round((w.resp||0)*100)+'%');
|
||
appendStat(stats,'Availability',Math.round((w.avail||0)*100)+'%');
|
||
if(w.archetype) appendStat(stats,'Archetype',w.archetype);
|
||
if(w.recent_calls!=null) appendStat(stats,'Prior calls',w.recent_calls);
|
||
card.appendChild(stats);
|
||
var smsLabel=document.createElement('div');
|
||
smsLabel.style.cssText='font-size:10px;color:#d29922;text-transform:uppercase;letter-spacing:1px;font-weight:700;margin-bottom:4px';
|
||
smsLabel.textContent='DRAFT SMS — TO CLIENT';
|
||
card.appendChild(smsLabel);
|
||
var smsBox=document.createElement('div');
|
||
smsBox.style.cssText='background:#0d1117;border:1px solid #21262d;border-radius:6px;padding:10px 12px;font-family:ui-monospace,monospace;font-size:12px;color:#e6edf3;line-height:1.5;white-space:pre-wrap';
|
||
smsBox.textContent=d.draft_sms||'';
|
||
card.appendChild(smsBox);
|
||
var copyBtn=document.createElement('button');
|
||
copyBtn.style.cssText='margin-top:8px;background:#1f6feb;border:none;color:#fff;padding:6px 14px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer';
|
||
copyBtn.textContent='Copy SMS';
|
||
copyBtn.onclick=function(){
|
||
if(navigator.clipboard) navigator.clipboard.writeText(d.draft_sms||'');
|
||
copyBtn.textContent='Copied ✓';
|
||
setTimeout(function(){copyBtn.textContent='Copy SMS'},1500);
|
||
};
|
||
card.appendChild(copyBtn);
|
||
out.appendChild(card);
|
||
if(bf.length){
|
||
var bfHdr=document.createElement('div');
|
||
bfHdr.style.cssText='font-size:11px;color:#3fb950;text-transform:uppercase;letter-spacing:1px;font-weight:700;margin:8px 0 8px';
|
||
bfHdr.textContent='✓ BACKFILLS READY — '+bf.length+' local '+(w.role||'workers')+' available, sorted by responsiveness';
|
||
out.appendChild(bfHdr);
|
||
bf.forEach(function(c,i){
|
||
addWorkerInsight(out,c.name,workerLine(c),
|
||
'Reliability '+Math.round((c.rel||0)*100)+'% · Responds '+Math.round((c.resp||0)*100)+'% · Available '+Math.round((c.avail||0)*100)+'%'+(c.archetype?' · '+c.archetype:''),
|
||
i,'#3fb950',c);
|
||
});
|
||
}else{
|
||
var bfNone=document.createElement('div');
|
||
bfNone.style.cssText='background:#1a1010;border:1px solid #f85149;border-radius:6px;padding:10px 14px;color:#fca5a5;font-size:12px';
|
||
bfNone.textContent='No same-role workers available locally. Widen the search — try a neighboring city or relax availability threshold.';
|
||
out.appendChild(bfNone);
|
||
}
|
||
}
|
||
function renderProfiles(out,d){
|
||
var hdr=document.createElement('div');
|
||
hdr.style.cssText='font-size:12px;color:#8b949e;margin-bottom:10px';
|
||
hdr.textContent=d.summary;
|
||
out.appendChild(hdr);
|
||
(d.profiles||[]).forEach(function(w,i){
|
||
addWorkerInsight(out,w.name,workerLine(w),
|
||
'Reliability '+Math.round((w.rel||0)*100)+'%'+(w.resp?' · Responds '+Math.round(w.resp*100)+'%':'')+(w.archetype?' · '+w.archetype:''),
|
||
i,null,w);
|
||
});
|
||
}
|
||
function renderIngestLog(out,d){
|
||
var hdr=document.createElement('div');
|
||
hdr.style.cssText='font-size:12px;color:#e6edf3;margin-bottom:10px;padding:10px 12px;background:#0d2818;border:1px solid #2ea04340;border-left:3px solid #3fb950;border-radius:6px';
|
||
hdr.textContent=d.summary;
|
||
out.appendChild(hdr);
|
||
(d.datasets||[]).forEach(function(ds){
|
||
var card=document.createElement('div');
|
||
card.style.cssText='background:#0d1117;border:1px solid #21262d;border-radius:6px;padding:12px 14px;margin-bottom:8px';
|
||
var top=document.createElement('div');
|
||
top.style.cssText='display:flex;justify-content:space-between;align-items:baseline;margin-bottom:6px';
|
||
var nm=document.createElement('span');
|
||
nm.style.cssText='font-size:13px;color:#e6edf3;font-weight:600';
|
||
nm.textContent=ds.name;
|
||
var ago=document.createElement('span');
|
||
ago.style.cssText='font-size:11px;color:#545d68';
|
||
ago.textContent=(ds.hours_ago||0)+'h ago · '+(ds.row_count||0).toLocaleString()+' rows';
|
||
top.appendChild(nm);top.appendChild(ago);
|
||
card.appendChild(top);
|
||
if(ds.looks_like_workers && ds.role_breakdown && ds.role_breakdown.length){
|
||
var rb=document.createElement('div');
|
||
rb.style.cssText='font-size:11px;color:#8b949e;display:flex;gap:10px;flex-wrap:wrap;margin-top:4px';
|
||
ds.role_breakdown.forEach(function(r){
|
||
var pill=document.createElement('span');
|
||
pill.style.cssText='background:#161b22;border:1px solid #21262d;padding:2px 8px;border-radius:9px';
|
||
pill.textContent=(r.role||'?')+' · '+r.cnt;
|
||
rb.appendChild(pill);
|
||
});
|
||
card.appendChild(rb);
|
||
}
|
||
out.appendChild(card);
|
||
});
|
||
}
|
||
|
||
function doSearch(){
|
||
var q=document.getElementById('sq').value.trim();if(!q)return;
|
||
lastQuery=q;
|
||
var st=document.getElementById('sst').value,rl=document.getElementById('srl').value;
|
||
var stafferEl=document.getElementById('sstaffer');
|
||
var stafferId=stafferEl?stafferEl.value:'';
|
||
// Pass dropdown filters as structured fields. Old code appended
|
||
// ' in '+st to the message, which the server misparsed: the
|
||
// preposition "in" matched the regex for state code "IN" (Indiana)
|
||
// and every search returned Indiana workers regardless of dropdown.
|
||
// Sending structured state/role + staffer_id lets the server skip
|
||
// NL parsing for those fields and apply per-staffer scoping.
|
||
var out=document.getElementById('sresults');out.textContent='Finding the best matches...';
|
||
fetch(A+'/intelligence/chat',{method:'POST',headers:{'Content-Type':'application/json'},
|
||
body:JSON.stringify({message:q,state:st||undefined,role:rl||undefined,staffer_id:stafferId||undefined})
|
||
}).then(function(r){return r.json()}).then(function(d){
|
||
out.textContent='';
|
||
// Type-specific renderers — added 2026-04-27 for the persona-driven
|
||
// routes (triage / profile / ingest_log). Default falls through to
|
||
// the smart_search renderer below.
|
||
if(d.type==='triage' && d.worker){return renderTriage(out,d)}
|
||
if(d.type==='triage_miss'){return renderMiss(out,d.summary,'#f85149')}
|
||
if(d.type==='profile' && d.profiles && d.profiles.length){return renderProfiles(out,d)}
|
||
if(d.type==='profile_miss'){return renderMiss(out,d.summary,'#d29922')}
|
||
if(d.type==='ingest_log'){return renderIngestLog(out,d)}
|
||
// Show what the system understood
|
||
if(d.understood&&d.understood.length){
|
||
var tags=document.createElement('div');tags.style.cssText='display:flex;gap:6px;flex-wrap:wrap;margin-bottom:8px';
|
||
d.understood.forEach(function(u){
|
||
var tag=document.createElement('span');tag.style.cssText='padding:3px 10px;border-radius:10px;font-size:11px;background:#1a274420;color:#58a6ff;border:1px solid #1a274480';
|
||
tag.textContent=u;tags.appendChild(tag);
|
||
});
|
||
out.appendChild(tags);
|
||
}
|
||
var h=document.createElement('div');h.style.cssText='color:#8b949e;font-size:12px;margin-bottom:10px';
|
||
h.textContent=(d.sql_matches?d.sql_matches.toLocaleString()+' workers matched — ':'')+'showing best results ('+(d.duration_ms||0)+'ms)';
|
||
out.appendChild(h);
|
||
// Meta-index signal — ALWAYS render when the system has any memory,
|
||
// even if no trait crossed threshold. Silence here would have
|
||
// recruiters assume "no signal" when the reality is "threshold
|
||
// filtered it out" or "memory is sparse for this geo." Trust
|
||
// depends on the system being honest about what it doesn't know.
|
||
if(d.pattern_playbooks_matched > 0 || d.discovered_pattern){
|
||
var mem=document.createElement('div');
|
||
mem.style.cssText='background:#0d2818;border:1px solid #2ea04360;border-radius:6px;padding:8px 12px;margin-bottom:10px;font-size:11px;color:#86efac;line-height:1.5';
|
||
var label=document.createElement('span');label.style.cssText='color:#3fb950;font-weight:600;margin-right:6px';
|
||
// When a staffer is acting, label the panel with their name —
|
||
// "MARIA'S MEMORY (12 playbooks)" makes the per-user shaping
|
||
// visible in the UI, not just the response data.
|
||
var memOwner=d.staffer&&d.staffer.name?d.staffer.name.toUpperCase()+"'S MEMORY":'MEMORY';
|
||
label.textContent=memOwner+' ('+(d.pattern_playbooks_matched||0)+' playbook'+(d.pattern_playbooks_matched===1?'':'s')+'):';
|
||
mem.appendChild(label);
|
||
var pattern = d.discovered_pattern || '';
|
||
if(!pattern || pattern.indexOf('No similar')>=0 || pattern.indexOf('0 workers')>=0){
|
||
mem.appendChild(document.createTextNode(' memory is sparse for this role+geo — no trait crossed threshold. Will accumulate as fills land.'));
|
||
mem.style.color='#6ca885';
|
||
} else {
|
||
mem.appendChild(document.createTextNode(' '+pattern));
|
||
}
|
||
out.appendChild(mem);
|
||
} else {
|
||
// Zero playbooks matched — be explicit
|
||
var mem0=document.createElement('div');
|
||
mem0.style.cssText='background:#161b22;border:1px solid #21262d;border-radius:6px;padding:6px 12px;margin-bottom:10px;font-size:11px;color:#6e7681';
|
||
mem0.textContent='MEMORY: no similar past playbooks yet — first fill of this kind will seed it.';
|
||
out.appendChild(mem0);
|
||
}
|
||
// Render results based on type
|
||
var workers=d.sql_results||[];
|
||
if(workers.length){
|
||
workers.forEach(function(w,i){
|
||
var wd={nm:w.name,role:w.role||'',loc:(w.city||'')+', '+(w.state||''),skills:(w.skills||'').split(',').filter(function(s){return s.trim()}),
|
||
certs:(w.certifications||'').split(',').filter(function(c){return c.trim()&&c.trim()!=='none'}),
|
||
rel:w.rel||0,avail:w.avail||0,arch:w.archetype||'',hasM:true};
|
||
var detail=[w.role,w.city+', '+w.state];
|
||
if(w.zip)detail.push('ZIP: '+w.zip);
|
||
var why='Reliability: '+Math.round((w.rel||0)*100)+'%';
|
||
if(w.avail)why+=' · Available: '+Math.round(w.avail*100)+'%';
|
||
if(w.archetype)why+=' · '+w.archetype;
|
||
// Derive and show implied pay rate client-side so the main search
|
||
// surface matches the live-contracts cards. Same formula as Bun.
|
||
var rate=computeImpliedPayRate(w.role,w.rel,w.archetype);
|
||
if(rate) why+=' · pay $'+rate.toFixed(2)+'/hr';
|
||
addWorkerInsight(out,w.name,detail.join(' · '),why,i,null,wd);
|
||
});
|
||
} else {
|
||
// Fall back to vector results
|
||
var vr=d.results||d.vector_results||[];
|
||
if(!vr.length){out.appendChild(document.createTextNode('No matches found. Try different terms.'));return}
|
||
vr.forEach(function(s,i){
|
||
var w=pw(s.text||s.chunk_text||'');if(!w.nm)w.nm=s.doc_id;
|
||
addWorkerInsight(out,w.nm,[w.role,w.loc].filter(Boolean).join(' · '),
|
||
(w.hasM?'Reliability: '+Math.round(w.rel*100)+'% · ':'')+(w.certs.length?'Certs: '+w.certs.join(', '):'AI match: '+Math.round((s.score||0)*100)+'%'),i,null,w);
|
||
});
|
||
}
|
||
}).catch(function(e){out.textContent='Error: '+e.message});
|
||
}
|
||
|
||
// ─── Market Intelligence ───
|
||
var marketMap=null;
|
||
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';
|
||
|
||
// Header with live indicator + source link + refresh
|
||
var hdr=document.createElement('div');hdr.style.cssText='display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:6px;margin-bottom:4px';
|
||
var lb=document.createElement('div');lb.className='label';lb.style.cssText='font-size:9px;text-transform:uppercase;letter-spacing:1.5px;color:#484f58;display:flex;align-items:center;gap:8px';
|
||
lb.textContent='MARKET INTELLIGENCE';
|
||
var live=document.createElement('span');live.style.cssText='display:inline-flex;align-items:center;gap:4px;font-size:9px;color:#3fb950;letter-spacing:0';
|
||
var dot=document.createElement('span');dot.style.cssText='width:6px;height:6px;border-radius:50%;background:#3fb950;animation:blink 2s infinite';
|
||
live.appendChild(dot);live.appendChild(document.createTextNode('LIVE'));
|
||
lb.appendChild(live);
|
||
var rhs=document.createElement('div');rhs.style.cssText='display:flex;gap:8px;align-items:center';
|
||
var ts=document.createElement('span');ts.style.cssText='font-size:9px;color:#484f58';ts.textContent='Updated: '+new Date().toLocaleString();
|
||
var srcLink=document.createElement('a');srcLink.href='https://data.cityofchicago.org/Buildings/Building-Permits/ydr8-5enu';
|
||
srcLink.target='_blank';srcLink.style.cssText='font-size:9px;color:#58a6ff;text-decoration:none';srcLink.textContent='Verify source';
|
||
var refresh=document.createElement('button');refresh.style.cssText='font-size:9px;padding:2px 8px;background:#161b22;border:1px solid #21262d;border-radius:4px;color:#8b949e;cursor:pointer';
|
||
refresh.textContent='Refresh';refresh.onclick=function(){loadMarket()};
|
||
rhs.appendChild(ts);rhs.appendChild(srcLink);rhs.appendChild(refresh);
|
||
hdr.appendChild(lb);hdr.appendChild(rhs);card.appendChild(hdr);
|
||
|
||
var hl=document.createElement('div');hl.className='headline';hl.textContent='Chicago Construction Pipeline';
|
||
var sub=document.createElement('div');sub.className='sub';
|
||
sub.textContent='$'+(d.total_construction_value/1e9).toFixed(1)+'B in active permits → '+d.total_estimated_workers.toLocaleString()+' workers needed · Fetched in '+d.duration_ms+'ms';
|
||
card.appendChild(hl);card.appendChild(sub);
|
||
|
||
// MAP — real lat/lng from permit data
|
||
var mapWrap=document.createElement('div');mapWrap.style.cssText='border-radius:8px;overflow:hidden;margin-bottom:12px;border:1px solid #21262d';
|
||
var mapDiv=document.createElement('div');mapDiv.id='permit-map';mapDiv.style.cssText='height:280px;width:100%;background:#0d1117';
|
||
mapWrap.appendChild(mapDiv);card.appendChild(mapWrap);
|
||
|
||
// Legend
|
||
var legend=document.createElement('div');legend.style.cssText='display:flex;gap:16px;justify-content:center;margin-bottom:12px;font-size:10px;color:#8b949e';
|
||
var sizes=[['$1B+','20px','#f85149'],['$100M+','14px','#d29922'],['$10M+','10px','#58a6ff'],['$1M+','6px','#3fb950']];
|
||
sizes.forEach(function(s){
|
||
var item=document.createElement('span');item.style.cssText='display:flex;align-items:center;gap:4px';
|
||
var circ=document.createElement('span');circ.style.cssText='width:'+s[1]+';height:'+s[1]+';border-radius:50%;background:'+s[2]+';opacity:0.7;display:inline-block';
|
||
item.appendChild(circ);item.appendChild(document.createTextNode(s[0]));legend.appendChild(item);
|
||
});
|
||
card.appendChild(legend);
|
||
|
||
// Major permits list
|
||
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);
|
||
});
|
||
|
||
// Bench vs demand
|
||
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;color:'+(g.available>g.demand?'#3fb950':'#d29922');
|
||
nums.textContent=g.available.toLocaleString()+' available / '+g.reliable.toLocaleString()+' reliable ('+g.supply.toLocaleString()+' total)';
|
||
row.appendChild(role);row.appendChild(nums);card.appendChild(row);
|
||
});
|
||
}
|
||
|
||
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='Live from City of Chicago Open Data. Click "Verify source" to see the raw permit database. Each dot is a real permitted project — hover for details. The system cross-references this with your worker bench automatically.';
|
||
card.appendChild(insight);
|
||
|
||
el.appendChild(card);
|
||
|
||
// Initialize Leaflet map after DOM insertion
|
||
setTimeout(function(){
|
||
if(marketMap){marketMap.remove();marketMap=null}
|
||
marketMap=L.map('permit-map',{zoomControl:true,attributionControl:false}).setView([41.88,-87.7],11);
|
||
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png',{maxZoom:18}).addTo(marketMap);
|
||
|
||
// Plot permits as circles sized by cost
|
||
d.major_permits.forEach(function(p){
|
||
if(!p.lat||!p.lng)return;
|
||
var cost=p.cost||0;
|
||
var radius=cost>=1e9?20:cost>=1e8?14:cost>=1e7?10:6;
|
||
var color=cost>=1e9?'#f85149':cost>=1e8?'#d29922':cost>=1e7?'#58a6ff':'#3fb950';
|
||
var costLabel=cost>=1e9?'$'+(cost/1e9).toFixed(1)+'B':cost>=1e6?'$'+(cost/1e6).toFixed(0)+'M':'$'+(cost/1e3).toFixed(0)+'K';
|
||
var circle=L.circleMarker([parseFloat(p.lat),parseFloat(p.lng)],{
|
||
radius:radius,fillColor:color,color:color,weight:1,opacity:0.8,fillOpacity:0.5
|
||
}).addTo(marketMap);
|
||
circle.bindPopup('<div style="font-size:12px;max-width:250px"><strong>'+costLabel+'</strong><br>'+
|
||
(p.description||'Construction').substring(0,120)+'<br><span style="color:#888">'+p.address+' · '+p.date+'</span></div>');
|
||
});
|
||
},100);
|
||
}).catch(function(e){
|
||
var el=document.getElementById('market');
|
||
el.textContent='Market data unavailable: '+e.message;
|
||
});
|
||
}
|
||
|
||
// ─── Learning Loop ───
|
||
// Real recruiter actions feed the Phase 19 feedback chain directly:
|
||
// Call/SMS → /log → /vectors/playbook_memory/seed (positive endorsement)
|
||
// No-show → /log_failure → /vectors/playbook_memory/mark_failed (penalty)
|
||
// Every click trains the system; the next search boosts/dampens accordingly.
|
||
function logAction(workerData, kind, btnEl){
|
||
if(!workerData)return;
|
||
var role=workerData.role||'Worker';
|
||
var city=(workerData.loc||'').split(',')[0].trim();
|
||
var state=(workerData.loc||'').split(',').pop().trim();
|
||
if(!city||!state){flashBtn(btnEl,'no geo');return;}
|
||
var op='fill: '+role+' x1 in '+city+', '+state;
|
||
if(kind==='failure'){
|
||
fetch(A+'/log_failure',{method:'POST',headers:{'Content-Type':'application/json'},
|
||
body:JSON.stringify({operation:op,failed_names:[workerData.nm],reason:'marked no-show via UI'})
|
||
}).then(function(r){return r.json()}).then(function(d){
|
||
flashBtn(btnEl, d&&d.marked?'Flagged':'Ghost');
|
||
loadLearning();
|
||
}).catch(function(){flashBtn(btnEl,'err')});
|
||
} else {
|
||
fetch(A+'/log',{method:'POST',headers:{'Content-Type':'application/json'},
|
||
body:JSON.stringify({operation:op,approach:kind+' from UI',
|
||
result:'1/1 filled → '+workerData.nm,
|
||
context:'client=ui query='+(lastQuery||'(direct)').slice(0,40)})
|
||
}).then(function(r){return r.json()}).then(function(d){
|
||
flashBtn(btnEl, d&&d.seeded?'Logged':'Ghost');
|
||
loadLearning();
|
||
}).catch(function(){flashBtn(btnEl,'err')});
|
||
}
|
||
}
|
||
function flashBtn(btn,label){
|
||
if(!btn)return;
|
||
var old=btn.textContent;btn.textContent=label;btn.disabled=true;
|
||
setTimeout(function(){btn.textContent=old;btn.disabled=false},1400);
|
||
}
|
||
// Back-compat shim — any legacy caller still pointing at logSelection.
|
||
function logSelection(workerData){ logAction(workerData, 'call', null); }
|
||
|
||
function loadLearning(){
|
||
// Pull every capability's live metric in parallel. Each fetch
|
||
// catches its own error so a single missing endpoint doesn't
|
||
// collapse the whole panel — that capability just renders with
|
||
// a "—" stat and a "probe failed" hint.
|
||
var probes = [
|
||
fetch(P+'/staffers').then(function(r){return r.json()}).catch(function(){return null}),
|
||
fetch(P+'/system/summary').then(function(r){return r.json()}).catch(function(){return null}),
|
||
fetch(A+'/api/vectors/playbook_memory/stats').then(function(r){return r.json()}).catch(function(){return null}),
|
||
fetch(A+'/api/vectors/pathway/stats').then(function(r){return r.json()}).catch(function(){return null}),
|
||
fetch(P+'/intelligence/profiler_index',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({limit:200})}).then(function(r){return r.json()}).catch(function(){return null}),
|
||
api('/intelligence/activity',{}).catch(function(){return null}),
|
||
];
|
||
Promise.all(probes).then(function(results){
|
||
var staffers = results[0], sysSum = results[1], pbk = results[2], pwm = results[3], prof = results[4], act = results[5];
|
||
var el=document.getElementById('learning');
|
||
el.textContent='';
|
||
var grid=document.createElement('div');
|
||
grid.style.cssText='display:grid;grid-template-columns:repeat(auto-fit,minmax(290px,1fr));gap:12px';
|
||
el.appendChild(grid);
|
||
|
||
// ─── 1. Per-staffer hot-swap index ──────────────────────────
|
||
var stafferCount = (staffers && staffers.staffers ? staffers.staffers.length : 0);
|
||
capability(grid, {
|
||
shipped:'2026-04-27',
|
||
kind:'staffer-index',
|
||
title:'Per-staffer hot-swap index',
|
||
stat: stafferCount + ' personas',
|
||
sub: stafferCount ? staffers.staffers.map(function(s){return s.name}).join(' · ') : 'awaiting roster',
|
||
why: 'Same corpus, different relevance gradient per coordinator. MARIA\'S MEMORY pill labels playbook context with the staffer\'s territory; same query returns different rosters depending on who\'s acting.',
|
||
kindColor: '#58a6ff',
|
||
});
|
||
|
||
// ─── 2. Construction Activity Signal Engine ─────────────────
|
||
var basket = (prof && prof.contractors) ? aggregateBasket(prof.contractors) : [];
|
||
var attribCost = (prof && prof.contractors)
|
||
? prof.contractors.filter(function(r){
|
||
var ts=(r.tickers&&r.tickers.direct?r.tickers.direct:[]).concat(r.tickers&&r.tickers.associated?r.tickers.associated:[]);
|
||
return ts.length>0;
|
||
}).reduce(function(s,r){return s+(r.total_cost||0)},0)
|
||
: 0;
|
||
capability(grid, {
|
||
shipped:'2026-04-27',
|
||
kind:'signal-engine',
|
||
title:'Construction Activity Signal Engine',
|
||
stat: basket.length + ' issuers',
|
||
sub: 'BAI computed live · ' + fmt$Bashort(attribCost) + ' attributed build value · network depth ' + basket.length + ' / ' + basket.reduce(function(s,b){return s+(b.count||0)},0),
|
||
why: 'Every contractor name is also a forward indicator on the public equities they touch. Permit activity → construction starts ~45d → revenue recognition months later. Pre-10-Q signal.',
|
||
link: P+'/profiler',
|
||
kindColor: '#3fb950',
|
||
});
|
||
|
||
// ─── 3. Late-worker / no-show triage ────────────────────────
|
||
capability(grid, {
|
||
shipped:'2026-04-27',
|
||
kind:'route',
|
||
title:'Late-worker / no-show triage',
|
||
stat:'one-shot',
|
||
sub:'name + "running late" → profile + reliability + 5 backfills sorted by responsiveness + draft SMS to client',
|
||
why: 'A coordinator gets a text and the system pulls the worker, finds backfills, and pre-writes the message in 250ms. Turns 20 minutes into 2.',
|
||
kindColor: '#d29922',
|
||
});
|
||
|
||
// ─── 4. Permit → staffing bridge (live contracts) ───────────
|
||
var permitCount = (act && typeof act.permits_today === 'number') ? act.permits_today : null;
|
||
capability(grid, {
|
||
shipped:'2026-04-27',
|
||
kind:'permit-bridge',
|
||
title:'Permit → staffing bridge',
|
||
stat: '24 / day',
|
||
sub: 'every Chicago permit ≥$250K becomes a fill plan — role, headcount, deadline, fill probability, gross revenue, draft SMS. ' + (permitCount==null ? 'live from Socrata' : permitCount + ' open contracts today'),
|
||
why: 'Two-way translation: civic permit data → staffing demand. Same adapter pattern works for NYC DOB, LA County, Houston BCD.',
|
||
kindColor: '#bc8cff',
|
||
});
|
||
|
||
// ─── 5. Hybrid SQL+vector search ────────────────────────────
|
||
var workersCount = sysSum && typeof sysSum.workers_500k_rows === 'number' ? sysSum.workers_500k_rows : null;
|
||
var pbkEntries = pbk && typeof pbk.entries === 'number' ? pbk.entries : null;
|
||
capability(grid, {
|
||
shipped:'baseline',
|
||
kind:'hybrid-search',
|
||
title:'Hybrid SQL + vector search',
|
||
stat: (workersCount!=null ? (workersCount/1000).toFixed(0)+'K' : '—') + ' workers',
|
||
sub: (pbkEntries!=null ? pbkEntries.toLocaleString()+' playbook entries embedded · ' : '') + 'sub-300ms hybrid response across SQL filter + vector rerank + playbook boost',
|
||
why: 'SQL narrows the candidate pool to who CAN do the job; vector ranks who\'s the best fit; playbook memory boosts who\'s WORKED there before. All in one call.',
|
||
kindColor: '#58a6ff',
|
||
});
|
||
|
||
// ─── 6. Schema-agnostic ingestion ───────────────────────────
|
||
var datasets = sysSum ? sysSum.datasets : null;
|
||
var rows = sysSum ? sysSum.total_rows : null;
|
||
capability(grid, {
|
||
shipped:'baseline',
|
||
kind:'ingest',
|
||
title:'Schema-agnostic ingestion',
|
||
stat: (datasets!=null ? datasets : '—') + ' datasets',
|
||
sub: (rows!=null ? rows.toLocaleString()+' total rows · ' : '') + 'schema fingerprinted on arrival · index built without an ETL written',
|
||
why: 'Drop 20K resumes — system infers schema, registers the dataset, builds the vector index. No mapping spec, no ETL ticket, no DBA. Same path for any tabular feed in any new metro.',
|
||
kindColor: '#79c0ff',
|
||
});
|
||
|
||
// ─── 7. Contractor profile + 12 awaiting sources ────────────
|
||
capability(grid, {
|
||
shipped:'2026-04-27',
|
||
kind:'contractor-profile',
|
||
title:'Contractor profile + project index',
|
||
stat:'6 wired · 12 queued',
|
||
sub:'OSHA · SEC+Stooq · Chicago history (lat/lng) · USAspending · parent-ticker map · ILSOS. Each name is also a heat-map of where they work.',
|
||
why: 'Profile every contractor in the corpus with project-index score, geo heat map, history timeline, and 12 placeholder cards naming the next public datasets that ship as adapters.',
|
||
link: P+'/contractor?name=Turner+Construction+Company',
|
||
kindColor: '#d29922',
|
||
});
|
||
|
||
// ─── 8. Pathway memory ──────────────────────────────────────
|
||
var pwmTotal = pwm && typeof pwm.total_pathways === 'number' ? pwm.total_pathways : null;
|
||
var pwmReplays = pwm && typeof pwm.successful_replays === 'number' ? pwm.successful_replays : null;
|
||
var pwmTotal2 = pwm && typeof pwm.total_replays === 'number' ? pwm.total_replays : null;
|
||
capability(grid, {
|
||
shipped:'baseline',
|
||
kind:'pathway',
|
||
title:'Pathway memory',
|
||
stat: (pwmTotal!=null ? pwmTotal : '—') + ' traces',
|
||
sub: pwmReplays!=null ? (pwmReplays + ' / ' + pwmTotal2 + ' successful replays · ' + (pwmTotal2 ? Math.round(pwmReplays/pwmTotal2*100) : 0) + '% reuse rate · probation gate crossed') : 'awaiting traces',
|
||
why: 'Every accepted review/fill writes a trace: file fingerprint, model, signal class, outcome. A new query that fingerprints to the same trace gets the prior result without re-running the 9-rung escalation.',
|
||
kindColor: '#3fb950',
|
||
});
|
||
|
||
// ─── 9. Profiler ticker basket ──────────────────────────────
|
||
var directIssuers = basket.filter(function(b){return b.kinds && (b.kinds.indexOf('exact')>=0 || b.kinds.indexOf('direct')>=0)}).length;
|
||
var assocIssuers = basket.filter(function(b){return b.kinds && b.kinds.indexOf('associated')>=0}).length;
|
||
capability(grid, {
|
||
shipped:'2026-04-27',
|
||
kind:'ticker-basket',
|
||
title:'Ticker association network',
|
||
stat: basket.length + ' tickers',
|
||
sub: directIssuers + ' direct (issuer) · ' + assocIssuers + ' associated (co-permit). Live Stooq quotes · clickable basket above the profiler index.',
|
||
why: 'When a contractor co-files permits with TARGET CORPORATION, that contractor inherits TGT as an associated indicator. The network is the moat — every new metro multiplies the edges.',
|
||
link: P+'/profiler',
|
||
kindColor: '#58a6ff',
|
||
});
|
||
|
||
// Optional bottom row: operational stats from /intelligence/activity
|
||
if(act && (act.fill_count || act.search_count || (act.playbooks||[]).length)){
|
||
var opRow = document.createElement('div');
|
||
opRow.style.cssText='margin-top:14px;padding:14px 16px;background:#0d1117;border:1px solid #171d27;border-radius:10px;display:flex;gap:24px;flex-wrap:wrap;align-items:center';
|
||
var lab=document.createElement('span'); lab.style.cssText='font-size:10px;color:#545d68;text-transform:uppercase;letter-spacing:1.4px;font-weight:600';
|
||
lab.textContent='Operational learning';
|
||
opRow.appendChild(lab);
|
||
var fc = document.createElement('span'); fc.style.cssText='color:#e6edf3;font-size:13px';
|
||
fc.textContent = (act.fill_count||0)+' fills · '+(act.search_count||0)+' searches · '+((act.playbooks||[]).length)+' recent playbooks';
|
||
opRow.appendChild(fc);
|
||
var note=document.createElement('span'); note.style.cssText='color:#545d68;font-size:11px;flex:1';
|
||
note.textContent='compounds inside each capability above';
|
||
opRow.appendChild(note);
|
||
el.appendChild(opRow);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Helper — build basket from contractor rows (mirrors profiler.html logic)
|
||
function aggregateBasket(rows){
|
||
var byTicker = {};
|
||
rows.forEach(function(r){
|
||
var ts=(r.tickers&&r.tickers.direct?r.tickers.direct:[]).concat(r.tickers&&r.tickers.associated?r.tickers.associated:[]);
|
||
ts.forEach(function(t){
|
||
if(!t || !t.ticker) return;
|
||
if(!byTicker[t.ticker]) byTicker[t.ticker]={ticker:t.ticker, kinds:[], count:0};
|
||
if(byTicker[t.ticker].kinds.indexOf(t.via)<0) byTicker[t.ticker].kinds.push(t.via);
|
||
byTicker[t.ticker].count++;
|
||
});
|
||
});
|
||
return Object.values(byTicker);
|
||
}
|
||
function fmt$Bashort(n){
|
||
if(!n) return '$0';
|
||
if(n>=1e9) return '$'+(n/1e9).toFixed(2)+'B';
|
||
if(n>=1e6) return '$'+(n/1e6).toFixed(0)+'M';
|
||
if(n>=1e3) return '$'+(n/1e3).toFixed(0)+'K';
|
||
return '$'+Math.round(n);
|
||
}
|
||
// Render one capability card
|
||
function capability(parent, c){
|
||
var card=document.createElement('div');
|
||
card.style.cssText='background:#0d1117;border:1px solid #171d27;border-left:3px solid '+(c.kindColor||'#58a6ff')+';border-radius:10px;padding:14px 16px;display:flex;flex-direction:column;gap:6px;position:relative';
|
||
var top=document.createElement('div');
|
||
top.style.cssText='display:flex;align-items:baseline;justify-content:space-between;gap:8px';
|
||
var title=document.createElement('div');
|
||
title.style.cssText='font-size:13px;font-weight:600;color:#e6edf3';
|
||
title.textContent=c.title;
|
||
top.appendChild(title);
|
||
if(c.shipped){
|
||
var ship=document.createElement('span');
|
||
ship.style.cssText='font-size:9px;color:#3fb950;background:#0d2818;border:1px solid #2ea04344;padding:1px 7px;border-radius:8px;letter-spacing:0.4px;font-weight:600;text-transform:uppercase;white-space:nowrap';
|
||
ship.textContent = c.shipped==='baseline' ? 'baseline' : 'shipped '+c.shipped;
|
||
top.appendChild(ship);
|
||
}
|
||
card.appendChild(top);
|
||
if(c.stat){
|
||
var stat=document.createElement('div');
|
||
stat.style.cssText='font-size:24px;font-weight:700;color:'+(c.kindColor||'#e6edf3')+';letter-spacing:-0.5px;font-family:ui-monospace,monospace;font-variant-numeric:tabular-nums';
|
||
stat.textContent=c.stat;
|
||
card.appendChild(stat);
|
||
}
|
||
if(c.sub){
|
||
var sub=document.createElement('div');
|
||
sub.style.cssText='font-size:11px;color:#8b949e;line-height:1.5';
|
||
sub.textContent=c.sub;
|
||
card.appendChild(sub);
|
||
}
|
||
if(c.why){
|
||
var why=document.createElement('div');
|
||
why.style.cssText='font-size:11px;color:#c9d1d9;line-height:1.55;margin-top:4px;border-top:1px dashed #1f2631;padding-top:8px';
|
||
why.textContent=c.why;
|
||
card.appendChild(why);
|
||
}
|
||
if(c.link){
|
||
var lk=document.createElement('a');
|
||
lk.href=c.link; lk.target='_blank'; lk.rel='noopener';
|
||
lk.style.cssText='color:#58a6ff;text-decoration:none;font-size:11px;font-weight:600;margin-top:4px;align-self:flex-start;border-bottom:1px dotted #58a6ff44';
|
||
lk.textContent='Open →';
|
||
card.appendChild(lk);
|
||
}
|
||
parent.appendChild(card);
|
||
}
|
||
|
||
function addLearnStat(parent,n,label,color){
|
||
var d=document.createElement('div');d.style.cssText='text-align:center;flex:1';
|
||
var num=document.createElement('div');num.style.cssText='font-size:24px;font-weight:800;color:'+color;num.textContent=n;
|
||
var lb=document.createElement('div');lb.style.cssText='font-size:10px;color:#484f58';lb.textContent=label;
|
||
d.appendChild(num);d.appendChild(lb);parent.appendChild(d);
|
||
}
|
||
</script></body></html>
|