Spaces:
Sleeping
Sleeping
| <html lang="en" data-theme="dark"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>RedStack — Top-100 Candidate Triage Dashboard</title> | |
| <style> | |
| :root[data-theme="dark"]{ | |
| --bg:#0d1117; --bg-panel:#151b23; --bg-elevated:#1c2530; --bg-hover:#212b38; | |
| --border:#2a3441; --text:#e6edf3; --text-dim:#8b98a9; --text-faint:#5b6878; | |
| --accent:#34d399; --accent-dim:#1f5f48; --amber:#fbbf24; --amber-dim:#5c4a1a; | |
| --crimson:#f87171; --crimson-dim:#5c2424; --blue:#60a5fa; --blue-dim:#1e3a5c; | |
| --shadow:0 1px 2px rgba(0,0,0,.4), 0 12px 28px -8px rgba(0,0,0,.5); | |
| --shadow-sm:0 1px 3px rgba(0,0,0,.4); | |
| --glass-bg:rgba(21,27,35,.68); --glass-border:rgba(255,255,255,.06); | |
| --glow-1:rgba(52,211,153,.07); --glow-2:rgba(96,165,250,.06); | |
| --chip-clear-bg:#0f2e22; --chip-clear-fg:#5eead4; | |
| --chip-soft-bg:#3a2c0d; --chip-soft-fg:#fcd34d; | |
| --chip-dabbler-bg:#3a300d; --chip-dabbler-fg:#fde68a; | |
| --chip-ghost-bg:#3a1414; --chip-ghost-fg:#fca5a5; | |
| --chip-hard-bg:#4a1212; --chip-hard-fg:#fecaca; | |
| } | |
| :root[data-theme="light"]{ | |
| --bg:#f3f5f8; --bg-panel:#ffffff; --bg-elevated:#f7f9fb; --bg-hover:#eef2f6; | |
| --border:#dce2e8; --text:#161c24; --text-dim:#4a5568; --text-faint:#8896a6; | |
| --accent:#059669; --accent-dim:#d1fae5; --amber:#b45309; --amber-dim:#fef3c7; | |
| --crimson:#b91c1c; --crimson-dim:#fee2e2; --blue:#1d4ed8; --blue-dim:#dbeafe; | |
| --shadow:0 1px 2px rgba(20,30,50,.05), 0 10px 24px -8px rgba(20,30,50,.14); | |
| --shadow-sm:0 1px 3px rgba(20,30,50,.08); | |
| --glass-bg:rgba(255,255,255,.66); --glass-border:rgba(20,30,50,.05); | |
| --glow-1:rgba(5,150,105,.05); --glow-2:rgba(29,78,216,.05); | |
| --chip-clear-bg:#d1fae5; --chip-clear-fg:#065f46; | |
| --chip-soft-bg:#fef3c7; --chip-soft-fg:#92400e; | |
| --chip-dabbler-bg:#fef9c3; --chip-dabbler-fg:#854d0e; | |
| --chip-ghost-bg:#fee2e2; --chip-ghost-fg:#991b1b; | |
| --chip-hard-bg:#fecaca; --chip-hard-fg:#7f1d1d; | |
| } | |
| *{box-sizing:border-box;} | |
| html,body{margin:0;padding:0;} | |
| html{scrollbar-width:thin; scrollbar-color:var(--border) var(--bg);} | |
| ::-webkit-scrollbar{width:11px; height:11px;} | |
| ::-webkit-scrollbar-track{background:var(--bg);} | |
| ::-webkit-scrollbar-thumb{background:var(--border); border-radius:6px; border:2px solid var(--bg);} | |
| ::-webkit-scrollbar-thumb:hover{background:var(--text-faint);} | |
| body{ | |
| background: | |
| radial-gradient(1100px 560px at 8% -8%, var(--glow-1), transparent 60%), | |
| radial-gradient(900px 480px at 100% 0%, var(--glow-2), transparent 60%), | |
| var(--bg); | |
| color:var(--text); | |
| font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| font-size:14px; line-height:1.5; -webkit-font-smoothing:antialiased; | |
| } | |
| code{font-family:"JetBrains Mono","SF Mono",Menlo,Consolas,monospace; font-size:.92em;} | |
| a{color:var(--blue);} | |
| /* ---------- top bar ---------- */ | |
| .topbar{ | |
| display:flex; align-items:center; justify-content:space-between; | |
| padding:18px 28px; background:var(--glass-bg); backdrop-filter:blur(16px) saturate(180%); | |
| -webkit-backdrop-filter:blur(16px) saturate(180%); | |
| border-bottom:1px solid var(--glass-border); | |
| position:sticky; top:0; z-index:50; box-shadow:var(--shadow); | |
| } | |
| .topbar h1{margin:0; font-size:19px; font-weight:700; letter-spacing:-.01em;} | |
| .topbar h1 span{color:var(--text-dim); font-weight:500; margin-left:8px;} | |
| .subtitle{margin:4px 0 0; font-size:12px; color:var(--text-faint);} | |
| .btn-ghost{ | |
| background:var(--bg-elevated); color:var(--text); border:1px solid var(--border); | |
| border-radius:8px; padding:8px 14px; font-size:13px; cursor:pointer; transition:.15s; | |
| } | |
| .btn-ghost:hover{background:var(--bg-hover);} | |
| main{max-width:1320px; margin:0 auto; padding:24px 28px 60px;} | |
| /* ---------- KPI cards ---------- */ | |
| .kpi-grid{ | |
| display:grid; grid-template-columns:repeat(auto-fit,minmax(195px,1fr)); gap:14px; margin-bottom:24px; | |
| } | |
| .kpi-card{ | |
| background:var(--glass-bg); backdrop-filter:blur(14px) saturate(160%); -webkit-backdrop-filter:blur(14px) saturate(160%); | |
| border:1px solid var(--glass-border); border-left:4px solid var(--accent-card,var(--blue)); | |
| border-radius:10px; padding:16px 18px; box-shadow:var(--shadow); transition:transform .18s ease, box-shadow .18s ease; | |
| } | |
| .kpi-card:hover{transform:translateY(-2px); box-shadow:var(--shadow), 0 0 0 1px var(--accent-card,var(--blue)) inset;} | |
| .kpi-card .kpi-label{font-size:11.5px; text-transform:uppercase; letter-spacing:.05em; color:var(--text-faint); font-weight:600;} | |
| .kpi-card .kpi-value{font-size:28px; font-weight:700; margin:6px 0 2px; letter-spacing:-.02em;} | |
| .kpi-card .kpi-value small{font-size:14px; color:var(--text-dim); font-weight:500;} | |
| .kpi-card .kpi-sub{font-size:12px; color:var(--text-dim);} | |
| .kpi-card.status-good{--accent-card:var(--accent);} | |
| .kpi-card.status-warn{--accent-card:var(--amber);} | |
| .kpi-card.status-bad{--accent-card:var(--crimson);} | |
| .kpi-card.status-info{--accent-card:var(--blue);} | |
| /* ---------- panels ---------- */ | |
| .panel{ | |
| background:var(--glass-bg); backdrop-filter:blur(14px) saturate(160%); -webkit-backdrop-filter:blur(14px) saturate(160%); | |
| border:1px solid var(--glass-border); border-radius:12px; | |
| padding:20px 22px; margin-bottom:22px; box-shadow:var(--shadow); | |
| } | |
| .panel h2{margin:0 0 14px; font-size:15px; font-weight:700;} | |
| .panel h2 .hint{font-size:11.5px; color:var(--text-faint); font-weight:500; margin-left:8px;} | |
| /* ---------- gate histogram ---------- */ | |
| .gate-row{display:flex; align-items:center; gap:12px; padding:6px 0;} | |
| .gate-label{width:300px; flex:0 0 300px; font-size:12.5px; color:var(--text-dim);} | |
| .gate-label code{color:var(--text);} | |
| .gate-bar-track{flex:1; background:var(--bg-elevated); border-radius:5px; height:18px; overflow:hidden; position:relative;} | |
| .gate-bar-fill{height:100%; border-radius:5px; transition:width .4s ease;} | |
| .gate-value{width:130px; flex:0 0 130px; text-align:right; font-size:12px; color:var(--text-dim); font-variant-numeric:tabular-nums;} | |
| /* ---------- controls ---------- */ | |
| .controls-bar{display:flex; flex-wrap:wrap; align-items:center; gap:10px; margin-bottom:16px;} | |
| #search-input{ | |
| flex:1 1 280px; background:var(--bg-elevated); border:1px solid var(--border); color:var(--text); | |
| border-radius:8px; padding:9px 12px; font-size:13.5px; outline:none; | |
| } | |
| #search-input:focus{border-color:var(--blue);} | |
| .filter-badges{display:flex; flex-wrap:wrap; gap:8px;} | |
| .chip{ | |
| border:1px solid var(--border); background:var(--bg-elevated); color:var(--text-dim); | |
| border-radius:999px; padding:7px 13px; font-size:12.5px; cursor:pointer; white-space:nowrap; transition:.15s; | |
| } | |
| .chip:hover{background:var(--bg-hover);} | |
| .chip.active{background:var(--blue-dim); color:var(--blue); border-color:var(--blue);} | |
| .chip.active.chip-clear{background:var(--chip-clear-bg); color:var(--chip-clear-fg); border-color:transparent;} | |
| .chip.active.chip-dabbler{background:var(--chip-dabbler-bg); color:var(--chip-dabbler-fg); border-color:transparent;} | |
| .chip.active.chip-ghost{background:var(--chip-ghost-bg); color:var(--chip-ghost-fg); border-color:transparent;} | |
| .chip.active.chip-soft{background:var(--chip-soft-bg); color:var(--chip-soft-fg); border-color:transparent;} | |
| .result-count{margin-left:auto; font-size:12px; color:var(--text-faint); white-space:nowrap;} | |
| /* ---------- quick filter (YOE band) ---------- */ | |
| .quick-filter{ | |
| display:flex; align-items:center; gap:6px; font-size:12.5px; color:var(--text-dim); | |
| background:var(--bg-elevated); border:1px solid var(--border); border-radius:999px; padding:5px 6px 5px 12px; | |
| } | |
| .quick-filter .qf-label{white-space:nowrap; font-weight:600;} | |
| .quick-filter input[type=number]{ | |
| width:46px; background:transparent; border:none; color:var(--text); font-size:12.5px; | |
| text-align:center; outline:none; | |
| } | |
| .quick-filter .qf-sep{color:var(--text-faint);} | |
| .quick-filter .chip{padding:5px 11px;} | |
| /* ---------- table ---------- */ | |
| .triage-table{width:100%; border-collapse:collapse;} | |
| .triage-table thead th{ | |
| text-align:left; font-size:11px; text-transform:uppercase; letter-spacing:.04em; color:var(--text-faint); | |
| border-bottom:1px solid var(--border); padding:8px 10px; position:sticky; top:65px; background:var(--bg-panel); z-index:5; | |
| } | |
| .triage-table .col-rank,.triage-table .col-score,.triage-table .col-yoe{text-align:right;} | |
| .triage-table tbody tr.row{border-bottom:1px solid var(--border); cursor:pointer; transition:background .12s ease, box-shadow .15s ease;} | |
| .triage-table tbody tr.row:hover{background:var(--bg-hover); box-shadow:inset 3px 0 0 var(--accent);} | |
| .triage-table td{padding:10px 10px; vertical-align:top; font-size:13px;} | |
| .triage-table .col-expand{width:28px; text-align:center;} | |
| .chevron{display:inline-flex; align-items:center; transition:transform .18s cubic-bezier(.4,0,.2,1), color .15s; color:var(--text-faint);} | |
| .chevron svg{display:block;} | |
| .row:hover .chevron{color:var(--accent);} | |
| .row.expanded .chevron{transform:rotate(90deg); color:var(--accent);} | |
| .cell-rank{font-weight:700; color:var(--text-dim);} | |
| .cell-score{font-weight:700; font-variant-numeric:tabular-nums;} | |
| .candidate-cell{display:flex; align-items:flex-start; gap:10px;} | |
| .avatar{ | |
| width:32px; height:32px; border-radius:8px; background:var(--blue-dim); color:var(--blue); | |
| display:flex; align-items:center; justify-content:center; font-weight:700; font-size:12px; flex:0 0 32px; | |
| } | |
| .candidate-name{font-weight:600;} | |
| .candidate-id{color:var(--text-faint); font-size:11.5px; display:flex; align-items:center; gap:5px;} | |
| .copy-id-btn{ | |
| background:none; border:none; color:var(--text-faint); cursor:pointer; padding:2px; | |
| border-radius:5px; display:inline-flex; align-items:center; transition:.15s; | |
| } | |
| .copy-id-btn:hover{color:var(--accent); background:var(--bg-hover); transform:scale(1.15);} | |
| .copy-id-btn.copied{color:var(--accent);} | |
| .copy-id-btn svg{display:block;} | |
| .role-title{font-weight:600;} | |
| .role-company{color:var(--text-dim);} | |
| .role-loc{color:var(--text-faint); font-size:11.5px;} | |
| .competency-mini{font-size:11.5px; color:var(--text-dim); line-height:1.6;} | |
| .competency-mini b{color:var(--text);} | |
| .skill-match-line{margin-top:3px; font-weight:700; color:var(--accent); font-size:12px;} | |
| .logistics-mini{font-size:11.5px; color:var(--text-dim); line-height:1.6;} | |
| .logistics-mini .tag{color:var(--text);} | |
| .risk-cell{display:flex; flex-wrap:wrap; gap:5px; max-width:230px;} | |
| .badge{ | |
| font-size:10.5px; border-radius:999px; padding:3px 9px; font-weight:700; white-space:nowrap; | |
| letter-spacing:.01em; box-shadow:0 1px 2px rgba(0,0,0,.12); display:inline-flex; align-items:center; | |
| } | |
| .badge-clear{background:var(--chip-clear-bg); color:var(--chip-clear-fg);} | |
| .badge-soft{background:var(--chip-soft-bg); color:var(--chip-soft-fg);} | |
| .badge-dabbler{background:var(--chip-dabbler-bg); color:var(--chip-dabbler-fg);} | |
| .badge-ghost{background:var(--chip-ghost-bg); color:var(--chip-ghost-fg);} | |
| .badge-hard{background:var(--chip-hard-bg); color:var(--chip-hard-fg);} | |
| /* ---------- detail panel ---------- */ | |
| tr.detail-row td{padding:0; border-bottom:1px solid var(--border);} | |
| .detail-wrap{background:var(--bg-elevated); padding:18px 22px 22px; animation:fadeSlideIn .22s ease;} | |
| @keyframes fadeSlideIn{from{opacity:0; transform:translateY(-6px);} to{opacity:1; transform:translateY(0);}} | |
| .tabs{display:flex; gap:4px; border-bottom:1px solid var(--border); margin-bottom:16px; flex-wrap:wrap;} | |
| .tab-btn{ | |
| background:none; border:none; color:var(--text-dim); padding:8px 14px; font-size:12.5px; font-weight:600; | |
| cursor:pointer; border-bottom:2px solid transparent; margin-bottom:-1px; transition:color .15s, border-color .15s; | |
| } | |
| .tab-btn:hover{color:var(--text);} | |
| .tab-btn.active{color:var(--accent); border-bottom-color:var(--accent);} | |
| .tab-panel{display:none;} | |
| .tab-panel.active{display:block;} | |
| .detail-grid{display:grid; grid-template-columns:2fr 1fr; gap:22px;} | |
| .detail-section h4{margin:0 0 8px; font-size:12px; text-transform:uppercase; letter-spacing:.04em; color:var(--text-faint);} | |
| .profile-summary{font-size:13px; color:var(--text); line-height:1.65; margin:0 0 14px;} | |
| .kv-grid{display:grid; grid-template-columns:repeat(2,1fr); gap:8px 16px; font-size:12.5px;} | |
| .kv-grid div b{color:var(--text-dim); font-weight:600; display:block; font-size:10.5px; text-transform:uppercase; letter-spacing:.03em; margin-bottom:1px;} | |
| .timeline{border-left:2px solid var(--border); margin-left:6px;} | |
| .timeline-item{position:relative; padding:0 0 18px 20px;} | |
| .timeline-item::before{content:""; position:absolute; left:-6px; top:3px; width:10px; height:10px; border-radius:50%; background:var(--blue); border:2px solid var(--bg-elevated);} | |
| .timeline-item.current::before{background:var(--accent);} | |
| .timeline-head{font-weight:700; font-size:13px;} | |
| .timeline-meta{font-size:11.5px; color:var(--text-faint); margin:2px 0 6px;} | |
| .timeline-desc{font-size:12.5px; color:var(--text-dim); line-height:1.6;} | |
| .current-pill{font-size:10px; background:var(--chip-clear-bg); color:var(--chip-clear-fg); border-radius:5px; padding:1px 6px; margin-left:6px; font-weight:700;} | |
| table.mini-table{width:100%; border-collapse:collapse; font-size:12px;} | |
| table.mini-table th{text-align:left; color:var(--text-faint); font-weight:600; text-transform:uppercase; font-size:10.5px; letter-spacing:.03em; padding:5px 8px; border-bottom:1px solid var(--border);} | |
| table.mini-table td{padding:6px 8px; border-bottom:1px solid var(--border); vertical-align:top;} | |
| table.mini-table tr.top3 td:first-child{color:var(--accent); font-weight:700;} | |
| .bar-cell{display:flex; align-items:center; gap:6px;} | |
| .bar-track{flex:1; height:6px; background:var(--bg-panel); border-radius:3px; overflow:hidden;} | |
| .bar-fill{height:100%; background:var(--blue); border-radius:3px;} | |
| .tok{display:inline-block; background:var(--bg-panel); border:1px solid var(--border); border-radius:5px; padding:1px 6px; font-size:10.5px; margin:1px 2px 0 0; color:var(--text-dim);} | |
| .risk-list{list-style:none; margin:0; padding:0;} | |
| .risk-list li{display:flex; gap:10px; padding:8px 0; border-bottom:1px solid var(--border);} | |
| .risk-list li:last-child{border-bottom:none;} | |
| .risk-icon{flex:0 0 auto; font-size:15px;} | |
| .risk-text b{display:block; font-size:12.5px; margin-bottom:2px;} | |
| .risk-text span{font-size:12px; color:var(--text-dim);} | |
| .all-clear{color:var(--accent); font-weight:700; font-size:13px;} | |
| blockquote.reasoning{ | |
| margin:0; padding:14px 18px; background:var(--bg-panel); border-left:3px solid var(--accent); | |
| border-radius:8px; font-style:italic; color:var(--text); font-size:13px; line-height:1.65; | |
| } | |
| .reasoning-badges{margin-top:12px; display:flex; flex-wrap:wrap; gap:6px;} | |
| .cred-block{margin-bottom:16px;} | |
| .cred-item{font-size:12.5px; padding:6px 0; border-bottom:1px solid var(--border);} | |
| .cred-item:last-child{border-bottom:none;} | |
| .cred-item b{display:block;} | |
| .cred-item span{color:var(--text-dim); font-size:11.5px;} | |
| .empty-note{color:var(--text-faint); font-size:12px; font-style:italic;} | |
| footer{text-align:center; color:var(--text-faint); font-size:11.5px; padding:20px 0 40px;} | |
| .hidden{display:none ;} | |
| </style> | |
| </head> | |
| <body> | |
| <header class="topbar"> | |
| <div> | |
| <h1>RedStack <span>Top-100 Candidate Triage</span></h1> | |
| <p class="subtitle" id="run-meta-line">loading run metadata…</p> | |
| </div> | |
| <button id="theme-toggle" class="btn-ghost" type="button">☼ Light mode</button> | |
| </header> | |
| <main> | |
| <section class="kpi-grid" id="kpi-grid"></section> | |
| <section class="panel"> | |
| <h2>Hard-Gate Fired Rates <span class="hint">full pool · N = 100,000 · source: run_report.json</span></h2> | |
| <div id="gate-chart"></div> | |
| </section> | |
| <section class="panel"> | |
| <div class="controls-bar"> | |
| <input id="search-input" type="text" placeholder="Filter by name, title, company, location, or matched skill token…" autocomplete="off"> | |
| <div class="filter-badges" id="filter-badges"> | |
| <button class="chip chip-all active" data-filter="all">All</button> | |
| <button class="chip chip-clear" data-filter="clear">✅ Clear Only</button> | |
| <button class="chip chip-soft" data-filter="title_chaser">⚠ Title-Chasers</button> | |
| <button class="chip chip-dabbler" data-filter="dabbler">🥛 Dabblers</button> | |
| <button class="chip chip-ghost" data-filter="ghost">👻 Ghost Penalty</button> | |
| <button class="chip chip-soft" data-filter="notice_over_30">⌛ Notice > 30d</button> | |
| <button class="chip chip-soft" data-filter="outside_india">🌐 Outside India</button> | |
| </div> | |
| <div class="quick-filter" id="quick-filter"> | |
| <span class="qf-label">🎯 YOE band</span> | |
| <input type="number" id="yoe-min" min="0" max="60" step="0.1" value="6" aria-label="Minimum years of experience"> | |
| <span class="qf-sep">–</span> | |
| <input type="number" id="yoe-max" min="0" max="60" step="0.1" value="8" aria-label="Maximum years of experience"> | |
| <button class="chip" data-filter="yoe_band" id="yoe-band-toggle" title="Highlight only candidates inside this YOE band">Apply</button> | |
| </div> | |
| <div class="result-count" id="result-count"></div> | |
| </div> | |
| <table class="triage-table" id="triage-table"> | |
| <thead> | |
| <tr> | |
| <th class="col-expand"></th> | |
| <th class="col-rank">Rank</th> | |
| <th class="col-score">Score</th> | |
| <th class="col-candidate">Candidate</th> | |
| <th class="col-role">Title @ Company</th> | |
| <th class="col-yoe">YOE</th> | |
| <th class="col-competency">Top-3 Competency</th> | |
| <th class="col-logistics">Logistics</th> | |
| <th class="col-risk">Risk / Gate Alerts</th> | |
| </tr> | |
| </thead> | |
| <tbody id="table-body"></tbody> | |
| </table> | |
| </section> | |
| </main> | |
| <footer> | |
| Generated offline from <code>debug_top100_lean.json</code> + <code>data/raw/candidates.jsonl</code> + <code>run_report.json</code>. Single self-contained file — no external scripts, styles, or network calls. | |
| </footer> | |
| <script type="application/json" id="candidate-data">__DATA_JSON__</script> | |
| <script> | |
| (function () { | |
| "use strict"; | |
| // ---------------------------------------------------------------- | |
| // 1. LOAD DATA -- the JSON island above is the entire data source. | |
| // ---------------------------------------------------------------- | |
| var DATA = JSON.parse(document.getElementById("candidate-data").textContent); | |
| var CANDIDATES = DATA.candidates; | |
| var GROUP_LABELS = DATA.group_labels; | |
| var COMPONENT_LABELS = DATA.component_labels; | |
| // Risk-badge text -> { icon, cssClass } used by both the grid cell and the detail panel. | |
| function badgeStyle(level) { | |
| switch (level) { | |
| case "hard": return { icon: "⛔", cls: "badge-hard" }; | |
| case "soft": return { icon: "⚠", cls: "badge-soft" }; | |
| case "dabbler": return { icon: "🐥", cls: "badge-dabbler" }; | |
| case "ghost": return { icon: "👻", cls: "badge-ghost" }; | |
| default: return { icon: "✅", cls: "badge-clear" }; | |
| } | |
| } | |
| function fmtPct(x) { return (x * 100).toFixed(0) + "%"; } | |
| function fmt3(x) { return Number(x).toFixed(3); } | |
| function fmt6(x) { return Number(x).toFixed(6); } | |
| function esc(s) { | |
| return String(s).replace(/[&<>"']/g, function (c) { | |
| return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]; | |
| }); | |
| } | |
| function initials(name) { | |
| return name.split(/\s+/).map(function (p) { return p[0]; }).slice(0, 2).join("").toUpperCase(); | |
| } | |
| // ---------------------------------------------------------------- | |
| // 2. KPI CARDS | |
| // ---------------------------------------------------------------- | |
| function renderKpis() { | |
| var k = DATA.kpis; | |
| var cards = [ | |
| { label: "Full Pool Size", value: k.pool_size.toLocaleString(), sub: "candidates scored end-to-end", status: "info" }, | |
| { label: "Top-10 Avg YOE", value: k.avg_yoe_top10 + "y", sub: "years of experience, cohort average", status: "info" }, | |
| { label: "Honeypots Caught", value: k.honeypot_count + " / " + k.cohort_size, sub: "verified via IntegrityEngine", status: k.honeypot_count === 0 ? "good" : "bad" }, | |
| { label: "Hard-Blocked Leaking", value: k.hard_blocked_count + " / " + k.cohort_size, sub: "should always be 0", status: k.hard_blocked_count === 0 ? "good" : "bad" }, | |
| { label: "Dabbler Pattern Flagged", value: k.dabbler_count + " / " + k.cohort_size, sub: "in_career = 0, trust ≥ 0.5 on a top-3 group", status: "warn" }, | |
| { label: "Ghost-Profile Penalty", value: k.ghost_count + " / " + k.cohort_size, sub: "behavioral_multiplier < 0.85", status: "warn" } | |
| ]; | |
| var html = cards.map(function (c) { | |
| return '<div class="kpi-card status-' + c.status + '">' + | |
| '<div class="kpi-label">' + esc(c.label) + '</div>' + | |
| '<div class="kpi-value">' + c.value + '</div>' + | |
| '<div class="kpi-sub">' + esc(c.sub) + '</div></div>'; | |
| }).join(""); | |
| document.getElementById("kpi-grid").innerHTML = html; | |
| var rm = DATA.run_meta; | |
| document.getElementById("run-meta-line").textContent = | |
| "Run " + rm.run_id + " · " + rm.code_version + " · manifest " + rm.manifest_hash + | |
| " · budget honored: " + (rm.reproducible ? "yes" : "no"); | |
| } | |
| // ---------------------------------------------------------------- | |
| // 3. GATE HISTOGRAM (full pool, pure div bars, no chart library) | |
| // ---------------------------------------------------------------- | |
| function renderGateChart() { | |
| var gates = DATA.gates; | |
| var maxPct = Math.max.apply(null, gates.map(function (g) { return g.pct; })); | |
| var html = gates.map(function (g) { | |
| var color = g.pct > 50 ? "var(--crimson)" : g.pct > 20 ? "var(--amber)" : "var(--blue)"; | |
| var widthPct = (g.pct / maxPct) * 100; | |
| return '<div class="gate-row">' + | |
| '<div class="gate-label"><code>' + esc(g.code) + '</code><br>' + esc(g.label) + '</div>' + | |
| '<div class="gate-bar-track"><div class="gate-bar-fill" style="width:' + widthPct.toFixed(2) + '%;background:' + color + ';"></div></div>' + | |
| '<div class="gate-value">' + g.fired.toLocaleString() + ' · ' + g.pct.toFixed(2) + '%</div>' + | |
| '</div>'; | |
| }).join(""); | |
| document.getElementById("gate-chart").innerHTML = html; | |
| } | |
| // ---------------------------------------------------------------- | |
| // 4. TRIAGE TABLE -- rows are built once; detail panels are built | |
| // lazily on first expand and then cached on the DOM node. | |
| // ---------------------------------------------------------------- | |
| function topCompetencyHtml(c) { | |
| var groups = c.competency.groups; | |
| return c.competency.top3_groups.map(function (key) { | |
| var g = groups[key]; | |
| var toks = (g.matched_tokens || []).join(","); | |
| return "<b>" + esc(key) + "</b>=" + fmt3(g.competency) + " [" + esc(toks) + "]"; | |
| }).join("<br>") + '<div class="skill-match-line">skill_match=' + fmt6(c.competency.skill_match) + "</div>"; | |
| } | |
| function logisticsHtml(c) { | |
| var lg = c.logistics, bh = c.behavioral; | |
| return '<span class="tag">' + esc(lg.location_fit) + "</span> / " + esc(lg.notice_fit) + "<br>" + | |
| "notice " + lg.notice_period_days + "d<br>" + | |
| "resp " + fmtPct(bh.response_rate) + " (" + esc(bh.responsiveness_status) + ")"; | |
| } | |
| function riskCellHtml(c) { | |
| if (c.flags.clear) return '<span class="badge badge-clear">✅ CLEAR</span>'; | |
| return c.risk_items.map(function (r) { | |
| var st = badgeStyle(r.level); | |
| return '<span class="badge ' + st.cls + '" title="' + esc(r.desc) + '">' + st.icon + " " + esc(r.code) + "</span>"; | |
| }).join(""); | |
| } | |
| var CHEVRON_SVG = '<svg viewBox="0 0 16 16" width="11" height="11" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 3l6 5-6 5"></path></svg>'; | |
| var COPY_ICON_SVG = '<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="1.4"><rect x="5.5" y="5.5" width="8" height="8" rx="1.6"></rect><path d="M3 10.2V3.6C3 2.7 3.7 2 4.6 2h6.2"></path></svg>'; | |
| var CHECK_ICON_SVG = '<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 8.5l3.2 3.2L13 5"></path></svg>'; | |
| function fallbackCopyText(text) { | |
| var ta = document.createElement("textarea"); | |
| ta.value = text; | |
| ta.style.position = "fixed"; | |
| ta.style.left = "-9999px"; | |
| document.body.appendChild(ta); | |
| ta.focus(); | |
| ta.select(); | |
| try { document.execCommand("copy"); } catch (e) { /* clipboard unavailable, ignore */ } | |
| document.body.removeChild(ta); | |
| } | |
| function copyCandidateId(id, btn) { | |
| function onCopied() { | |
| var prevHtml = btn.innerHTML, prevTitle = btn.title; | |
| btn.innerHTML = CHECK_ICON_SVG; | |
| btn.classList.add("copied"); | |
| btn.title = "Copied!"; | |
| setTimeout(function () { | |
| btn.innerHTML = prevHtml; | |
| btn.classList.remove("copied"); | |
| btn.title = prevTitle; | |
| }, 1100); | |
| } | |
| if (navigator.clipboard && navigator.clipboard.writeText) { | |
| navigator.clipboard.writeText(id).then(onCopied).catch(function () { | |
| fallbackCopyText(id); | |
| onCopied(); | |
| }); | |
| } else { | |
| fallbackCopyText(id); | |
| onCopied(); | |
| } | |
| } | |
| function buildRow(c) { | |
| var tr = document.createElement("tr"); | |
| tr.className = "row"; | |
| tr.dataset.cid = c.candidate_id; | |
| tr.innerHTML = | |
| '<td class="col-expand"><span class="chevron">' + CHEVRON_SVG + '</span></td>' + | |
| '<td class="cell-rank">' + c.rank + '</td>' + | |
| '<td class="cell-score">' + fmt6(c.score) + '</td>' + | |
| '<td><div class="candidate-cell"><div class="avatar">' + esc(initials(c.name)) + '</div>' + | |
| '<div><div class="candidate-name">' + esc(c.name) + '</div>' + | |
| '<div class="candidate-id"><code>' + esc(c.candidate_id) + '</code>' + | |
| '<button class="copy-id-btn" type="button" data-cid="' + esc(c.candidate_id) + '" title="Copy candidate ID" aria-label="Copy candidate ID">' + COPY_ICON_SVG + '</button>' + | |
| '</div></div></div></td>' + | |
| '<td><div class="role-title">' + esc(c.current_title) + '</div>' + | |
| '<div class="role-company">@ ' + esc(c.current_company) + '</div>' + | |
| '<div class="role-loc">' + esc(c.location) + '</div></td>' + | |
| '<td>' + c.yoe.toFixed(1) + '</td>' + | |
| '<td><div class="competency-mini">' + topCompetencyHtml(c) + '</div></td>' + | |
| '<td><div class="logistics-mini">' + logisticsHtml(c) + '</div></td>' + | |
| '<td><div class="risk-cell">' + riskCellHtml(c) + '</div></td>'; | |
| var detailTr = document.createElement("tr"); | |
| detailTr.className = "detail-row hidden"; | |
| var td = document.createElement("td"); | |
| td.colSpan = 9; | |
| detailTr.appendChild(td); | |
| tr.addEventListener("click", function () { toggleDetail(tr, detailTr); }); | |
| var copyBtn = tr.querySelector(".copy-id-btn"); | |
| copyBtn.addEventListener("click", function (ev) { | |
| ev.stopPropagation(); | |
| copyCandidateId(c.candidate_id, copyBtn); | |
| }); | |
| return [tr, detailTr]; | |
| } | |
| function toggleDetail(tr, detailTr) { | |
| var willOpen = detailTr.classList.contains("hidden"); | |
| detailTr.classList.toggle("hidden"); | |
| tr.classList.toggle("expanded", willOpen); | |
| if (willOpen && !detailTr.dataset.built) { | |
| var c = CANDIDATES_BY_ID[tr.dataset.cid]; | |
| detailTr.firstElementChild.innerHTML = buildDetailHtml(c); | |
| wireTabs(detailTr.firstElementChild, c.candidate_id); | |
| detailTr.dataset.built = "1"; | |
| } | |
| } | |
| // ---------- detail panel builders ---------- | |
| function overviewTabHtml(c) { | |
| var p = c.profile, bh = c.behavioral, lg = c.logistics; | |
| var comp = Object.keys(c.components).map(function (key) { | |
| var item = c.components[key]; | |
| var pct = Math.round(item.raw * 100); | |
| return '<tr><td>' + esc(COMPONENT_LABELS[key] || key) + '</td>' + | |
| '<td class="bar-cell"><div class="bar-track"><div class="bar-fill" style="width:' + pct + '%"></div></div>' + fmt3(item.raw) + '</td>' + | |
| '<td>' + item.weight.toFixed(2) + '</td><td>' + fmt3(item.weighted) + '</td></tr>'; | |
| }).join(""); | |
| return '<div class="detail-grid">' + | |
| '<div>' + | |
| '<div class="detail-section"><h4>Profile Summary</h4>' + | |
| '<p class="profile-summary">' + esc(p.summary || "(no summary on file)") + '</p>' + | |
| '<div class="kv-grid">' + | |
| '<div><b>Headline</b>' + esc(p.headline || "—") + '</div>' + | |
| '<div><b>Location</b>' + esc(p.location) + ", " + esc(p.country) + '</div>' + | |
| '<div><b>Company Size</b>' + esc(p.company_size || "—") + '</div>' + | |
| '<div><b>Industry</b>' + esc(p.industry || "—") + '</div>' + | |
| '</div></div>' + | |
| '<div class="detail-section" style="margin-top:16px;"><h4>Score Composition (7 weighted components)</h4>' + | |
| '<table class="mini-table"><thead><tr><th>Component</th><th>Raw</th><th>Weight</th><th>Weighted</th></tr></thead><tbody>' + comp + '</tbody></table></div>' + | |
| '</div>' + | |
| '<div>' + | |
| '<div class="detail-section"><h4>Logistics & Availability</h4><div class="kv-grid">' + | |
| '<div><b>Location Fit</b>' + esc(lg.location_fit) + '</div>' + | |
| '<div><b>Notice Fit</b>' + esc(lg.notice_fit) + '</div>' + | |
| '<div><b>Notice Period</b>' + lg.notice_period_days + ' days</div>' + | |
| '<div><b>Logistics Mult.</b>' + fmt3(c.logistics_multiplier) + '</div>' + | |
| '</div></div>' + | |
| '<div class="detail-section" style="margin-top:16px;"><h4>Behavioral Signals</h4><div class="kv-grid">' + | |
| '<div><b>Availability</b>' + esc(bh.availability_status) + '</div>' + | |
| '<div><b>Responsiveness</b>' + esc(bh.responsiveness_status) + '</div>' + | |
| '<div><b>Response Rate</b>' + fmtPct(bh.response_rate) + '</div>' + | |
| '<div><b>GitHub Activity</b>' + bh.github_activity_score + '</div>' + | |
| '<div><b>Behavioral Mult.</b>' + fmt3(c.behavioral_multiplier) + '</div>' + | |
| '<div><b>Expected Salary</b>' + bh.expected_salary_min_max[0] + '–' + bh.expected_salary_min_max[1] + ' LPA</div>' + | |
| '</div></div>' + | |
| '</div>' + | |
| '</div>'; | |
| } | |
| function experienceTabHtml(c) { | |
| if (!c.career_history.length) return '<p class="empty-note">No career history on file.</p>'; | |
| var items = c.career_history.map(function (ch) { | |
| var end = ch.is_current ? '<span class="current-pill">CURRENT</span>' : esc(ch.end_date || "?"); | |
| return '<div class="timeline-item' + (ch.is_current ? " current" : "") + '">' + | |
| '<div class="timeline-head">' + esc(ch.title) + ' @ ' + esc(ch.company) + end + '</div>' + | |
| '<div class="timeline-meta">' + esc(ch.start_date || "?") + ' → ' + (ch.is_current ? "present" : esc(ch.end_date || "?")) + | |
| ' · ' + ch.duration_months + ' months · ' + esc(ch.industry || "") + ' · ' + esc(ch.company_size || "") + ' employees</div>' + | |
| '<div class="timeline-desc">' + esc(ch.description || "") + '</div></div>'; | |
| }).join(""); | |
| return '<div class="timeline">' + items + '</div>'; | |
| } | |
| function competencyTabHtml(c) { | |
| var groups = c.competency.groups; | |
| var top3 = c.competency.top3_groups; | |
| var rows = Object.keys(groups).sort(function (a, b) { return groups[b].competency - groups[a].competency; }).map(function (key) { | |
| var g = groups[key]; | |
| var isTop3 = top3.indexOf(key) !== -1; | |
| var toks = (g.matched_tokens || []).map(function (t) { return '<span class="tok">' + esc(t) + '</span>'; }).join(""); | |
| return '<tr class="' + (isTop3 ? "top3" : "") + '">' + | |
| '<td>' + esc(GROUP_LABELS[key] || key) + ' <code>' + esc(key) + '</code></td>' + | |
| '<td>' + fmt3(g.trust) + '</td><td>' + fmt3(g.in_career) + '</td><td>' + fmt3(g.semantic) + '</td>' + | |
| '<td class="bar-cell"><div class="bar-track"><div class="bar-fill" style="width:' + Math.round(g.competency * 100) + '%"></div></div>' + fmt3(g.competency) + '</td>' + | |
| '<td>' + (toks || '<span class="empty-note">none</span>') + '</td></tr>'; | |
| }).join(""); | |
| return '<table class="mini-table"><thead><tr><th>Skill Group</th><th>Trust</th><th>In-Career</th><th>Semantic</th><th>Competency</th><th>Matched Tokens</th></tr></thead><tbody>' + rows + '</tbody></table>' + | |
| '<p class="empty-note" style="margin-top:8px;">Rows highlighted in green are this candidate’s top-3 contributing groups. “In-Career” = 0 with Trust ≥ 0.5 is the self-learner / dabbler signature.</p>'; | |
| } | |
| function riskTabHtml(c) { | |
| if (c.flags.clear && !c.risk_items.length) { | |
| return '<p class="all-clear">✅ CLEAR — no hard blocks, soft penalties, dabbler patterns, or ghost-profile signals.</p>'; | |
| } | |
| var items = c.risk_items.map(function (r) { | |
| var st = badgeStyle(r.level); | |
| return '<li><span class="risk-icon">' + st.icon + '</span><div class="risk-text"><b>' + esc(r.code) + '</b><span>' + esc(r.desc) + '</span></div></li>'; | |
| }).join(""); | |
| var integrityLine = c.integrity_gate_passed | |
| ? '<p class="all-clear" style="margin-top:10px;">✅ IntegrityEngine: honeypot check passed.</p>' | |
| : '<p style="color:var(--crimson);font-weight:700;margin-top:10px;">⛔ IntegrityEngine: HONEYPOT FLAG — this candidate should not appear in a clean pool.</p>'; | |
| return '<ul class="risk-list">' + items + '</ul>' + integrityLine; | |
| } | |
| function credentialsTabHtml(c) { | |
| var edu = c.education_detail.length | |
| ? c.education_detail.map(function (e) { | |
| return '<div class="cred-item"><b>' + esc(e.degree) + ' · ' + esc(e.field_of_study) + '</b>' + | |
| '<span>' + esc(e.institution) + ' · ' + e.start_year + '–' + e.end_year + ' · ' + esc(e.grade || "") + ' · ' + esc(e.tier) + '</span></div>'; | |
| }).join("") | |
| : '<p class="empty-note">No education on file.</p>'; | |
| var certs = c.certifications.length | |
| ? c.certifications.map(function (cert) { | |
| return '<div class="cred-item"><b>' + esc(cert.name) + '</b><span>' + esc(cert.issuer) + ' · ' + cert.year + '</span></div>'; | |
| }).join("") | |
| : '<p class="empty-note">No certifications on file.</p>'; | |
| var langs = c.languages.length | |
| ? c.languages.map(function (l) { | |
| return '<div class="cred-item"><b>' + esc(l.language) + '</b><span>' + esc(l.proficiency) + '</span></div>'; | |
| }).join("") | |
| : '<p class="empty-note">No languages on file.</p>'; | |
| var r = c.redrob; | |
| var redrobKv = Object.keys(r).map(function (key) { | |
| var val = r[key]; | |
| if (val && typeof val === "object" && !Array.isArray(val)) val = JSON.stringify(val); | |
| return '<div><b>' + esc(key) + '</b>' + esc(String(val)) + '</div>'; | |
| }).join(""); | |
| return '<div class="detail-grid">' + | |
| '<div>' + | |
| '<div class="cred-block"><h4>Education</h4>' + edu + '</div>' + | |
| '<div class="cred-block"><h4>Certifications</h4>' + certs + '</div>' + | |
| '<div class="cred-block"><h4>Languages</h4>' + langs + '</div>' + | |
| '</div>' + | |
| '<div><div class="cred-block"><h4>Raw RedRob Platform Signals</h4><div class="kv-grid">' + redrobKv + '</div></div></div>' + | |
| '</div>'; | |
| } | |
| function reasoningTabHtml(c) { | |
| var badges = c.risk_badges.map(function (b) { return '<span class="badge badge-soft">' + esc(b) + '</span>'; }).join(""); | |
| return '<blockquote class="reasoning">“' + esc(c.reasoning) + '”</blockquote>' + | |
| (badges ? '<div class="reasoning-badges">' + badges + '</div>' : ''); | |
| } | |
| function buildDetailHtml(c) { | |
| return '<div class="detail-wrap">' + | |
| '<div class="tabs" data-cid="' + esc(c.candidate_id) + '">' + | |
| '<button class="tab-btn active" data-tab="overview">Overview</button>' + | |
| '<button class="tab-btn" data-tab="experience">Experience Timeline</button>' + | |
| '<button class="tab-btn" data-tab="competency">Competency Breakdown</button>' + | |
| '<button class="tab-btn" data-tab="risk">Risk & Gate Signals</button>' + | |
| '<button class="tab-btn" data-tab="credentials">Credentials</button>' + | |
| '<button class="tab-btn" data-tab="reasoning">Model Reasoning</button>' + | |
| '</div>' + | |
| '<div class="tab-panel active" data-tab="overview">' + overviewTabHtml(c) + '</div>' + | |
| '<div class="tab-panel" data-tab="experience">' + experienceTabHtml(c) + '</div>' + | |
| '<div class="tab-panel" data-tab="competency">' + competencyTabHtml(c) + '</div>' + | |
| '<div class="tab-panel" data-tab="risk">' + riskTabHtml(c) + '</div>' + | |
| '<div class="tab-panel" data-tab="credentials">' + credentialsTabHtml(c) + '</div>' + | |
| '<div class="tab-panel" data-tab="reasoning">' + reasoningTabHtml(c) + '</div>' + | |
| '</div>'; | |
| } | |
| function wireTabs(root, cid) { | |
| var buttons = root.querySelectorAll(".tab-btn"); | |
| var panels = root.querySelectorAll(".tab-panel"); | |
| buttons.forEach(function (btn) { | |
| btn.addEventListener("click", function (ev) { | |
| ev.stopPropagation(); // don't collapse the row when switching tabs | |
| buttons.forEach(function (b) { b.classList.remove("active"); }); | |
| panels.forEach(function (p) { p.classList.remove("active"); }); | |
| btn.classList.add("active"); | |
| root.querySelector('.tab-panel[data-tab="' + btn.dataset.tab + '"]').classList.add("active"); | |
| }); | |
| }); | |
| } | |
| // ---------------------------------------------------------------- | |
| // 5. SEARCH + RISK FILTER BADGES | |
| // ---------------------------------------------------------------- | |
| var CANDIDATES_BY_ID = {}; | |
| CANDIDATES.forEach(function (c) { CANDIDATES_BY_ID[c.candidate_id] = c; }); | |
| var activeFilters = { all: true }; | |
| var searchQuery = ""; | |
| var yoeMin = 6, yoeMax = 8; | |
| function matches(c) { | |
| if (searchQuery && c.search_blob.indexOf(searchQuery) === -1) return false; | |
| if (activeFilters.all) return true; | |
| var any = false; | |
| if (activeFilters.clear && c.flags.clear) any = true; | |
| if (activeFilters.title_chaser && c.flags.title_chaser) any = true; | |
| if (activeFilters.dabbler && c.flags.dabbler) any = true; | |
| if (activeFilters.ghost && c.flags.ghost) any = true; | |
| if (activeFilters.notice_over_30 && c.flags.notice_over_30) any = true; | |
| if (activeFilters.outside_india && c.flags.outside_india) any = true; | |
| if (activeFilters.yoe_band && c.yoe >= yoeMin && c.yoe <= yoeMax) any = true; | |
| return any; | |
| } | |
| function renderTable() { | |
| var tbody = document.getElementById("table-body"); | |
| var visible = CANDIDATES.filter(matches); | |
| tbody.innerHTML = ""; | |
| var frag = document.createDocumentFragment(); | |
| visible.forEach(function (c) { | |
| var existing = ROW_CACHE[c.candidate_id]; | |
| var pair = existing || buildRow(c); | |
| ROW_CACHE[c.candidate_id] = pair; | |
| frag.appendChild(pair[0]); | |
| frag.appendChild(pair[1]); | |
| }); | |
| tbody.appendChild(frag); | |
| document.getElementById("result-count").textContent = visible.length + " / " + CANDIDATES.length + " shown"; | |
| } | |
| var ROW_CACHE = {}; | |
| function wireControls() { | |
| var search = document.getElementById("search-input"); | |
| search.addEventListener("input", function () { | |
| searchQuery = search.value.trim().toLowerCase(); | |
| renderTable(); | |
| }); | |
| var yoeMinInput = document.getElementById("yoe-min"); | |
| var yoeMaxInput = document.getElementById("yoe-max"); | |
| function syncYoeBand() { | |
| yoeMin = parseFloat(yoeMinInput.value); | |
| yoeMax = parseFloat(yoeMaxInput.value); | |
| if (isNaN(yoeMin)) yoeMin = 0; | |
| if (isNaN(yoeMax)) yoeMax = 60; | |
| if (activeFilters.yoe_band) renderTable(); | |
| } | |
| yoeMinInput.addEventListener("input", syncYoeBand); | |
| yoeMaxInput.addEventListener("input", syncYoeBand); | |
| var chips = document.querySelectorAll(".chip"); | |
| chips.forEach(function (chip) { | |
| chip.addEventListener("click", function () { | |
| var f = chip.dataset.filter; | |
| if (f === "all") { | |
| activeFilters = { all: true }; | |
| } else { | |
| delete activeFilters.all; | |
| activeFilters[f] = !activeFilters[f]; | |
| if (!Object.keys(activeFilters).some(function (k) { return activeFilters[k]; })) { | |
| activeFilters = { all: true }; | |
| } | |
| } | |
| chips.forEach(function (c) { | |
| var key = c.dataset.filter; | |
| c.classList.toggle("active", !!activeFilters[key]); | |
| }); | |
| renderTable(); | |
| }); | |
| }); | |
| } | |
| function wireTheme() { | |
| var btn = document.getElementById("theme-toggle"); | |
| var root = document.documentElement; | |
| var saved = null; | |
| try { saved = localStorage.getItem("redstack-dashboard-theme"); } catch (e) { /* file:// storage may be blocked */ } | |
| if (saved) root.setAttribute("data-theme", saved); | |
| function syncLabel() { | |
| var t = root.getAttribute("data-theme"); | |
| btn.innerHTML = t === "dark" ? "☼ Light mode" : "☽ Dark mode"; | |
| } | |
| syncLabel(); | |
| btn.addEventListener("click", function () { | |
| var next = root.getAttribute("data-theme") === "dark" ? "light" : "dark"; | |
| root.setAttribute("data-theme", next); | |
| try { localStorage.setItem("redstack-dashboard-theme", next); } catch (e) { /* ignore */ } | |
| syncLabel(); | |
| }); | |
| } | |
| // ---------------------------------------------------------------- | |
| // 6. BOOT | |
| // ---------------------------------------------------------------- | |
| renderKpis(); | |
| renderGateChart(); | |
| wireControls(); | |
| wireTheme(); | |
| renderTable(); | |
| })(); | |
| </script> | |
| </body> | |
| </html> | |