-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdashboard.html
More file actions
239 lines (220 loc) · 13.1 KB
/
Copy pathdashboard.html
File metadata and controls
239 lines (220 loc) · 13.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>CardOpsAI · Tier-0 Risk Command Center</title>
<style>
:root{
--bg:#050810;--panel:#0c1222;--panel2:#111827;--border:#1e293b;
--platinum:#e2e8f0;--gold:#c9a227;--gold-dim:#8a7020;
--accent:#3b82f6;--good:#10b981;--bad:#ef4444;--warn:#f59e0b;--muted:#64748b;
}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:"Segoe UI",system-ui,sans-serif;background:var(--bg);color:var(--platinum);min-height:100vh;
background-image:radial-gradient(ellipse 80% 50% at 50% -20%,#1e3a5f44,transparent),
linear-gradient(180deg,#050810 0%,#0a0f1a 100%);}
.topbar{display:flex;align-items:center;justify-content:space-between;padding:1rem 2rem;
border-bottom:1px solid var(--border);background:#050810ee;backdrop-filter:blur(12px);position:sticky;top:0;z-index:100}
.brand{display:flex;align-items:center;gap:1rem}
.logo{width:44px;height:44px;border-radius:10px;background:linear-gradient(135deg,var(--gold),var(--gold-dim));
display:flex;align-items:center;justify-content:center;font-weight:800;color:#050810;font-size:1.1rem}
.brand h1{font-size:1.15rem;font-weight:600;letter-spacing:.04em}
.brand small{display:block;color:var(--muted);font-size:.7rem;letter-spacing:.12em;text-transform:uppercase}
.badge-tier{padding:.35rem .75rem;border:1px solid var(--gold);color:var(--gold);border-radius:999px;
font-size:.65rem;font-weight:700;letter-spacing:.15em}
.live-dot{width:8px;height:8px;border-radius:50%;background:var(--good);display:inline-block;margin-right:6px;
animation:pulse 2s infinite}
.live-dot.off{background:var(--muted);animation:none}
@keyframes pulse{50%{opacity:.4}}
.grid{display:grid;grid-template-columns:repeat(12,1fr);gap:1rem;padding:1.25rem 2rem 2.5rem;max-width:1600px;margin:0 auto}
.card{background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:1.25rem;
box-shadow:0 4px 24px #00000040}
.card h2{font-size:.75rem;text-transform:uppercase;letter-spacing:.14em;color:var(--muted);margin-bottom:1rem;font-weight:600}
.kpi-row{display:grid;grid-template-columns:repeat(4,1fr);gap:1rem;grid-column:span 12}
.kpi{background:var(--panel2);border:1px solid var(--border);border-radius:10px;padding:1rem 1.25rem}
.kpi label{font-size:.65rem;color:var(--muted);text-transform:uppercase;letter-spacing:.1em}
.kpi .val{font-size:1.75rem;font-weight:700;margin-top:.25rem;background:linear-gradient(90deg,var(--platinum),var(--gold));
-webkit-background-clip:text;-webkit-text-fill-color:transparent}
.kpi .sub{font-size:.75rem;color:var(--muted);margin-top:.25rem}
.span4{grid-column:span 4}.span6{grid-column:span 6}.span8{grid-column:span 8}.span12{grid-column:span 12}
input,select,button{width:100%;padding:.65rem .85rem;border-radius:8px;border:1px solid var(--border);
background:var(--panel2);color:var(--platinum);margin-top:.35rem;font-size:.85rem}
button{background:linear-gradient(135deg,var(--gold),var(--gold-dim));border:none;color:#050810;
font-weight:700;cursor:pointer;transition:transform .15s,filter .15s}
button:hover{filter:brightness(1.08);transform:translateY(-1px)}
button.secondary{background:var(--panel2);color:var(--platinum);border:1px solid var(--border)}
.score-display{font-size:2.5rem;font-weight:800;margin:.75rem 0;color:var(--gold)}
.factors{max-height:180px;overflow:auto;font-size:.8rem}
.factor{display:flex;justify-content:space-between;padding:.4rem 0;border-bottom:1px solid #1e293b55}
.fired{color:var(--warn)}.ok{color:var(--good)}
.chain{display:flex;gap:.5rem;overflow-x:auto;padding-bottom:.5rem}
.block{min-width:110px;padding:.65rem;border-radius:8px;border:1px solid var(--border);background:var(--panel2);font-size:.7rem}
.block.broken{border-color:var(--bad);background:#ef444422}
canvas{width:100%;height:200px;background:var(--panel2);border-radius:8px;border:1px solid var(--border)}
svg{width:100%;height:220px;background:var(--panel2);border-radius:8px;border:1px solid var(--border)}
.api-banner{grid-column:span 12;padding:.75rem 1rem;border-radius:8px;font-size:.85rem;display:flex;align-items:center;gap:.75rem}
.api-banner.live{background:#10b98118;border:1px solid #10b98155;color:var(--good)}
.api-banner.demo{background:#f59e0b18;border:1px solid #f59e0b55;color:var(--warn)}
@media(max-width:1100px){.kpi-row{grid-template-columns:1fr 1fr}.span4,.span6,.span8{grid-column:span 12}}
</style>
</head>
<body>
<header class="topbar">
<div class="brand">
<div class="logo">CO</div>
<div>
<h1>CardOpsAI</h1>
<small>Tier-0 Risk Command Center · Fortune 500 Grade</small>
</div>
</div>
<div style="display:flex;align-items:center;gap:1rem">
<span class="badge-tier">TIER-0</span>
<span id="apiStatus"><span class="live-dot off"></span>Connecting…</span>
</div>
</header>
<main class="grid">
<div id="banner" class="api-banner demo"><span class="live-dot off"></span> Demo mode — start API: <code>python cardops_api.py</code></div>
<div class="kpi-row">
<div class="kpi"><label>Queue Depth</label><div class="val" id="kpiQueue">—</div><div class="sub">pending decisions</div></div>
<div class="kpi"><label>P1 Alerts</label><div class="val" id="kpiP1">—</div><div class="sub">critical triage</div></div>
<div class="kpi"><label>Ledger</label><div class="val" id="kpiLedger">—</div><div class="sub">hash chain integrity</div></div>
<div class="kpi"><label>Budget Left</label><div class="val" id="kpiBudget">—</div><div class="sub">daily alert capacity</div></div>
</div>
<section class="card span4">
<h2>Transaction Scoring</h2>
<label>Tenant ID</label><input id="tenantId" type="number" value="1"/>
<label>Transaction ID</label><input id="txId" type="number" value="1"/>
<label>Amount (demo fallback)</label><input id="amount" type="number" value="125000"/>
<button id="btnScore" style="margin-top:.85rem">Score · Hybrid ML</button>
<div class="score-display" id="scoreOut">—</div>
<div id="decisionOut" style="font-size:.85rem;color:var(--muted)"></div>
<div class="factors" id="rulesOut"></div>
</section>
<section class="card span8">
<h2>Hash Chain · Tamper Evidence</h2>
<div class="chain" id="chain"></div>
<button class="secondary" id="tamperBtn" style="margin-top:.75rem;width:auto;display:inline-block;padding:.5rem 1rem">Simulate Tamper</button>
<p id="chainStatus" style="margin-top:.75rem;font-size:.8rem;color:var(--good)">Chain integrity: OK</p>
</section>
<section class="card span6">
<h2>Monte Carlo · Capital Stress</h2>
<canvas id="mcChart" width="640" height="200"></canvas>
<p id="mcLegend" style="font-size:.75rem;color:var(--muted);margin-top:.5rem"></p>
</section>
<section class="card span6">
<h2>Fraud Ring Intelligence</h2>
<svg id="ringSvg" viewBox="0 0 640 220"></svg>
</section>
<section class="card span12">
<h2>Expected-Loss Triage Queue</h2>
<div id="triageTable" style="font-size:.8rem;overflow:auto"></div>
</section>
</main>
<script>
const API = localStorage.getItem('cardops_api') || 'http://localhost:8000';
let live = false;
let tenantId = 1;
async function api(path, opts) {
const r = await fetch(API + path, opts);
if (!r.ok) throw new Error(await r.text());
const ct = r.headers.get('content-type') || '';
if (ct.includes('json')) return r.json();
return r.text();
}
function setLive(on) {
live = on;
const b = document.getElementById('banner');
const s = document.getElementById('apiStatus');
b.className = 'api-banner ' + (on ? 'live' : 'demo');
b.innerHTML = on
? '<span class="live-dot"></span> Live · PostgreSQL Risk OS connected at ' + API
: '<span class="live-dot off"></span> Demo mode — run <code>docker compose up -d</code> then <code>python cardops_api.py</code>';
s.innerHTML = (on ? '<span class="live-dot"></span>LIVE' : '<span class="live-dot off"></span>DEMO');
}
async function refreshKPIs() {
tenantId = +document.getElementById('tenantId').value || 1;
if (!live) { demoKPIs(); return; }
try {
const h = await api('/api/v1/system/health?tenant_id=' + tenantId);
document.getElementById('kpiQueue').textContent = h.pending_queue ?? '—';
document.getElementById('kpiP1').textContent = h.p1_alerts ?? '—';
document.getElementById('kpiLedger').textContent = h.ledger_ok === 'true' ? 'SECURE' : 'BREACH';
const b = await api('/api/v1/budget?tenant_id=' + tenantId);
document.getElementById('kpiBudget').textContent = b.remaining_budget ?? '—';
const t = await api('/api/v1/triage?tenant_id=' + tenantId + '&limit=10');
document.getElementById('triageTable').innerHTML = t.alerts.length
? '<table style="width:100%;border-collapse:collapse"><tr><th align=left>TX</th><th>Loss</th><th>Score</th><th>Band</th></tr>'
+ t.alerts.map(a=>'<tr><td>'+a.transaction_id+'</td><td>'+(a.expected_loss||0).toFixed(2)+'</td><td>'+(a.risk_score||0)+'</td><td>'+a.priority_band+'</td></tr>').join('')
+ '</table>'
: '<em>No alerts in budget window</em>';
} catch(e) { console.warn(e); }
}
function demoKPIs() {
document.getElementById('kpiQueue').textContent = '47';
document.getElementById('kpiP1').textContent = '12';
document.getElementById('kpiLedger').textContent = 'SECURE';
document.getElementById('kpiBudget').textContent = '73';
}
async function scoreTx() {
tenantId = +document.getElementById('tenantId').value || 1;
const txId = +document.getElementById('txId').value || 1;
if (live) {
try {
const r = await api('/api/v1/score/' + txId + '?tenant_id=' + tenantId + '&hybrid=true', {method:'POST'});
renderScore(r);
return;
} catch(e) { console.warn(e); }
}
demoScore();
}
function renderScore(r) {
const score = r.hybrid_score ?? r.score ?? 0;
document.getElementById('scoreOut').textContent = (+score).toFixed(2);
document.getElementById('decisionOut').textContent = 'Action: ' + (r.action||r.decision||'—') + ' · Expected loss: ' + (r.expected_loss||0);
const factors = r.factors || [];
document.getElementById('rulesOut').innerHTML = factors.map(d =>
'<div class="factor"><span>'+d.factor+'</span><span class="'+(d.fired?'fired':'ok')+'">'+(d.fired?'FIRED':'ok')+'</span></div>'
).join('');
}
function demoScore() {
const amt = +document.getElementById('amount').value;
const score = Math.min(100, (amt/5000)*30 + 25);
renderScore({score, action: score>80?'BLOCK':score>40?'REVIEW':'APPROVE', expected_loss: score/100*amt,
factors:[{factor:'high_amount',fired:amt>100000},{factor:'velocity',fired:false}]});
}
function buildChain(n=6){let chain=[],prev='GENESIS';for(let i=1;i<=n;i++){const d=i%3===0?'declined':'approved';
const h=Array.from({length:16},(_,j)=>((prev.charCodeAt(j%prev.length)+i*7)%16).toString(16)).join('');
chain.push({id:i,prev,current:h,decision:d,broken:false});prev=h;}
window._chain=chain;renderChain();}
function renderChain(){const el=document.getElementById('chain');
el.innerHTML=window._chain.map(b=>'<div class="block '+(b.broken?'broken':'')+'">#'+b.id+' · '+b.decision+'<br><code style="color:var(--muted)">'+b.current.slice(0,12)+'…</code></div>').join('<span style="align-self:center;color:var(--muted)">→</span>');
document.getElementById('chainStatus').textContent=window._chain.some(b=>b.broken)?'Chain BREACH detected':'Chain integrity: OK';
document.getElementById('chainStatus').style.color=window._chain.some(b=>b.broken)?'var(--bad)':'var(--good)';}
function drawMC(){
const c=document.getElementById('mcChart'),ctx=c.getContext('2d'),losses=[];
for(let i=0;i<800;i++){const u1=Math.max(Math.random(),1e-9),u2=Math.random();
const z=Math.sqrt(-2*Math.log(u1))*Math.cos(2*Math.PI*u2);losses.push(Math.max(0,250000+80000*z));}
losses.sort((a,b)=>a-b);const v95=losses[Math.floor(losses.length*.95)],v99=losses[Math.floor(losses.length*.99)];
const bins=24,mx=losses[losses.length-1],hist=Array(bins).fill(0);
losses.forEach(v=>hist[Math.min(bins-1,Math.floor(v/mx*bins))]++);
const mh=Math.max(...hist);ctx.clearRect(0,0,c.width,c.height);
hist.forEach((h,i)=>{const ht=(h/mh)*(c.height-20);ctx.fillStyle='#c9a22788';ctx.fillRect(i*(c.width/bins)+1,c.height-ht-8,c.width/bins-2,ht);});
document.getElementById('mcLegend').textContent='VaR95='+v95.toFixed(0)+' · VaR99='+v99.toFixed(0)+' · Reserve='+(v95*1.2).toFixed(0);
}
function drawRings(){
const svg=document.getElementById('ringSvg'),nodes=[{id:1,x:180,y:110},{id:2,x:300,y:50},{id:3,x:420,y:110},{id:4,x:300,y:170}];
let h='';[[1,2],[2,3],[3,4],[4,1]].forEach(([a,b])=>{h+=`<line x1="${nodes[a-1].x}" y1="${nodes[a-1].y}" x2="${nodes[b-1].x}" y2="${nodes[b-1].y}" stroke="#c9a227" stroke-width="2"/>`;});
nodes.forEach(n=>{h+=`<circle cx="${n.x}" cy="${n.y}" r="14" fill="#c9a22733" stroke="#c9a227" stroke-width="2"/><text x="${n.x}" y="${n.y+4}" text-anchor="middle" fill="#e2e8f0" font-size="10">M${n.id}</text>`;});
svg.innerHTML=h;
}
document.getElementById('btnScore').onclick=scoreTx;
document.getElementById('tamperBtn').onclick=()=>{window._chain[2].broken=true;window._chain[3].broken=true;renderChain();};
(async()=>{
try{await api('/health');setLive(true);}catch{setLive(false);}
buildChain();drawMC();drawRings();demoKPIs();await refreshKPIs();
setInterval(refreshKPIs,15000);
})();
</script>
</body>
</html>