Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>ActivitySimDetect Dashboard</title> | |
| <style> | |
| :root{ --white:#FFFFFF; --black:#000000; --blue:#1E59F0; --light-blue:#F7F7F7; --dark-gray:#7C7D7E; --gray:#E1E4E6; --gray-2:#F9FAFB; --light-gray:#F7F7F7; --dark-blue:#404E90; --red:#D92D20; --green:#13A964; --radius:14px; } | |
| html,body{height:100%} | |
| body{margin:0;font-family:Inter,system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif;background:linear-gradient(135deg,#F9FAFB 0%, #E1E4E6 100%);color:#0b1020;display:flex;flex-direction:column} | |
| .header{padding:24px 28px;display:flex;justify-content:space-between;align-items:center;background:linear-gradient(135deg,#1E59F0 0%, #404E90 100%);color:var(--white)} | |
| .header .brand{font-weight:700;letter-spacing:.4px} | |
| .container{padding:24px;display:grid;grid-template-columns:repeat(4,minmax(280px,1fr));gap:20px;flex:1;max-width:100%;margin:0 auto} | |
| /* Place Risk History in right-most column on wide screens */ | |
| @media (min-width: 1200px){ | |
| .history{grid-column:4} | |
| } | |
| @media (max-width: 1199px){ | |
| .container{grid-template-columns:repeat(3,minmax(280px,1fr))} | |
| .history{grid-column:auto} | |
| } | |
| @media (max-width: 900px){ | |
| .container{grid-template-columns:repeat(2,minmax(260px,1fr))} | |
| } | |
| @media (max-width: 600px){ | |
| .container{grid-template-columns:1fr} | |
| } | |
| .card{background:var(--white);border-radius:var(--radius);box-shadow:0 10px 30px rgba(16,24,40,.08);border:1px solid #EEF2F6} | |
| .card h3{margin:16px 16px 0 16px} | |
| .card .content{padding:16px} | |
| .pill{display:inline-block;padding:6px 10px;border-radius:999px;font-size:12px;border:1px solid #EEF2F6} | |
| .pill.low{background:#ECFDF3;color:#027A48;border-color:#ABEFC6} | |
| .pill.med{background:#FFF6ED;color:#B54708;border-color:#FEDF89} | |
| .pill.high{background:#FEF3F2;color:#B42318;border-color:#FECDCA} | |
| .muted{color:#667085} | |
| .footer{padding:24px;text-align:center;color:#667085} | |
| .bar{height:18px;border-radius:999px;background:linear-gradient(90deg,#13A964,#F59E0B,#D92D20)} | |
| .scroll{overflow:auto} | |
| .btn{appearance:none;border:0;border-radius:10px;padding:10px 14px;font-weight:600;cursor:pointer;box-shadow:0 2px 6px rgba(16,24,40,.08);transition:transform .06s ease,box-shadow .2s ease} | |
| .btn:active{transform:translateY(1px)} | |
| .btn-seed{background:#13A964;color:#FFFFFF} | |
| .btn-clear{background:#B54708;color:#FFFFFF} | |
| .btn-evict{background:#1E59F0;color:#FFFFFF} | |
| .btn:hover{box-shadow:0 6px 16px rgba(16,24,40,.12)} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="header"> | |
| <div class="brand">Activity Simulation Detection</div> | |
| </div> | |
| <div class="container"> | |
| <div class="card"> | |
| <h3>Machine Risk</h3> | |
| <div class="content"> | |
| <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px"> | |
| <div> | |
| <div class="muted">machineId</div> | |
| <div id="machineId">demo-machine</div> | |
| </div> | |
| <span id="riskLevel" class="pill low">Low</span> | |
| </div> | |
| <div class="bar"><div id="riskBar" style="height:100%;width:12%;border-radius:999px;background:#13A964"></div></div> | |
| <div class="muted" style="margin-top:4px">Risk score: <span id="riskScore">0.12</span></div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <h3>Evidence</h3> | |
| <div class="content"> | |
| <pre id="evidence" style="white-space:pre-wrap;margin:0" class="muted">No data yet.</pre> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <h3>Device Trust (Top Observed)</h3> | |
| <div class="content"> | |
| <div id="devices" class="muted">Loading…</div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <h3>Heuristics</h3> | |
| <div class="content"> | |
| <div style="margin-bottom:10px"> | |
| <div class="muted" style="display:flex;justify-content:space-between"> | |
| <span>Inter-event Entropy</span> | |
| <span id="entropyVal">–</span> | |
| </div> | |
| <div class="bar"><div id="entropyBar" style="height:100%;width:0%;border-radius:999px;background:#13A964"></div></div> | |
| </div> | |
| <div style="margin-bottom:10px"> | |
| <div class="muted" style="display:flex;justify-content:space-between"> | |
| <span>Interval Regularity</span> | |
| <span id="intervalVal">–</span> | |
| </div> | |
| <div class="bar"><div id="intervalBar" style="height:100%;width:0%;border-radius:999px;background:#F59E0B"></div></div> | |
| </div> | |
| <div> | |
| <div class="muted" style="display:flex;justify-content:space-between"> | |
| <span>Path Straightness</span> | |
| <span id="straightVal">–</span> | |
| </div> | |
| <div class="bar"><div id="straightBar" style="height:100%;width:0%;border-radius:999px;background:#D92D20"></div></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <h3>ML Model (Level 2)</h3> | |
| <div class="content"> | |
| <div style="margin-bottom:10px"> | |
| <div class="muted" style="display:flex;justify-content:space-between"> | |
| <span>ML Anomaly Score</span> | |
| <span id="mlScoreVal">–</span> | |
| </div> | |
| <div class="bar"><div id="mlScoreBar" style="height:100%;width:0%;border-radius:999px;background:#13A964"></div></div> | |
| </div> | |
| <div style="margin-bottom:10px"> | |
| <div class="muted" style="display:flex;justify-content:space-between"> | |
| <span>Model Confidence</span> | |
| <span id="mlConfidenceVal">–</span> | |
| </div> | |
| <div class="bar"><div id="mlConfidenceBar" style="height:100%;width:0%;border-radius:999px;background:#1E59F0"></div></div> | |
| </div> | |
| <div class="muted" style="font-size:12px;margin-top:8px">Model Status: <span id="modelStatus">Loading…</span></div> | |
| <div style="margin-top:12px"><button id="refreshModel">Refresh Model</button></div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <h3>Machines</h3> | |
| <div class="content"> | |
| <div id="machines" class="muted">Loading…</div> | |
| </div> | |
| </div> | |
| <div class="card history"> | |
| <h3>Risk History</h3> | |
| <div class="content scroll" id="historyContainer"> | |
| <div id="history" class="muted">Loading…</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="footer" style="margin-top:auto"> | |
| <button id="seedBtn" class="btn btn-seed" style="margin-right:8px">Seed Demo</button> | |
| <button id="clearBtn" class="btn btn-clear" style="margin-right:8px">Clear</button> | |
| <button id="evictBtn" class="btn btn-evict">Evict Stale (120m)</button> | |
| <div style="margin-top:12px">Augusto Diaz 2025</div> | |
| </div> | |
| <script> | |
| const API = 'http://localhost:5208' | |
| async function loadRisk(){ | |
| const machineId = 'demo-machine' | |
| document.getElementById('machineId').textContent = machineId | |
| try{ | |
| const resp = await fetch(API + '/risk/' + machineId) | |
| if(!resp.ok){ throw new Error('No risk yet') } | |
| const data = await resp.json() | |
| const score = data.riskScore ?? 0 | |
| document.getElementById('riskScore').textContent = score.toFixed(2) | |
| document.getElementById('riskBar').style.width = Math.round(score*100) + '%' | |
| const levelEl = document.getElementById('riskLevel') | |
| levelEl.textContent = data.riskLevel | |
| levelEl.classList.remove('low','med','high') | |
| if(score >= .75) levelEl.classList.add('high') | |
| else if(score >= .4) levelEl.classList.add('med') | |
| else levelEl.classList.add('low') | |
| document.getElementById('evidence').textContent = JSON.stringify(data.evidence,null,2) | |
| // Heuristic visuals (expect evidence.features = { interEventEntropy, fixedIntervalScore, pathStraightness, speedVariance, contextCoupling }) | |
| const feats = data.evidence && data.evidence.features || {} | |
| const entropy = typeof feats.interEventEntropy === 'number' ? feats.interEventEntropy : null | |
| const fixed = typeof feats.fixedIntervalScore === 'number' ? feats.fixedIntervalScore : null | |
| const straight = typeof feats.pathStraightness === 'number' ? feats.pathStraightness : null | |
| const speedVar = typeof feats.speedVariance === 'number' ? feats.speedVariance : null | |
| const context = typeof feats.contextCoupling === 'number' ? feats.contextCoupling : null | |
| // Level 2 ML Model visuals | |
| const mlScore = typeof data.evidence.onnxScore === 'number' ? data.evidence.onnxScore : null | |
| const heuristicScore = typeof data.evidence.heuristicRisk === 'number' ? data.evidence.heuristicRisk : null | |
| if(entropy !== null){ | |
| document.getElementById('entropyVal').textContent = entropy.toFixed(2) | |
| // Normalize entropy for display (0-3 range -> 0-1) | |
| const normalizedEntropy = Math.max(0, Math.min(1, entropy / 3)) | |
| document.getElementById('entropyBar').style.width = normalizedEntropy * 100 + '%' | |
| // Color coding: low entropy = red (suspicious), high entropy = green (normal) | |
| const entropyBar = document.getElementById('entropyBar') | |
| if(entropy < 1.0) entropyBar.style.background = '#D92D20' // Red for low entropy | |
| else if(entropy < 2.0) entropyBar.style.background = '#F59E0B' // Yellow for medium | |
| else entropyBar.style.background = '#13A964' // Green for high entropy | |
| } | |
| if(fixed !== null){ | |
| document.getElementById('intervalVal').textContent = fixed.toFixed(2) | |
| document.getElementById('intervalBar').style.width = Math.max(0, Math.min(1, fixed)) * 100 + '%' | |
| // Color coding: high regularity = red (suspicious), low regularity = green (normal) | |
| const intervalBar = document.getElementById('intervalBar') | |
| if(fixed > 0.7) intervalBar.style.background = '#D92D20' // Red for high regularity | |
| else if(fixed > 0.3) intervalBar.style.background = '#F59E0B' // Yellow for medium | |
| else intervalBar.style.background = '#13A964' // Green for low regularity | |
| } | |
| if(straight !== null){ | |
| document.getElementById('straightVal').textContent = straight.toFixed(2) | |
| document.getElementById('straightBar').style.width = Math.max(0, Math.min(1, straight)) * 100 + '%' | |
| // Color coding: high straightness = red (suspicious), low straightness = green (normal) | |
| const straightBar = document.getElementById('straightBar') | |
| if(straight > 0.6) straightBar.style.background = '#D92D20' // Red for very straight | |
| else if(straight > 0.3) straightBar.style.background = '#F59E0B' // Yellow for medium | |
| else straightBar.style.background = '#13A964' // Green for curved | |
| } | |
| // ML Model visualization | |
| if(mlScore !== null){ | |
| document.getElementById('mlScoreVal').textContent = mlScore.toFixed(3) | |
| document.getElementById('mlScoreBar').style.width = Math.max(0, Math.min(1, mlScore)) * 100 + '%' | |
| // Color coding: high ML score = red (anomaly), low ML score = green (normal) | |
| const mlScoreBar = document.getElementById('mlScoreBar') | |
| if(mlScore > 0.7) mlScoreBar.style.background = '#D92D20' // Red for high anomaly | |
| else if(mlScore > 0.3) mlScoreBar.style.background = '#F59E0B' // Yellow for medium | |
| else mlScoreBar.style.background = '#13A964' // Green for low anomaly | |
| } | |
| // Model confidence (based on how much ML vs heuristic contributes) | |
| if(mlScore !== null && heuristicScore !== null){ | |
| const confidence = Math.abs(mlScore - heuristicScore) < 0.2 ? 0.9 : 0.6 // High confidence if scores agree | |
| document.getElementById('mlConfidenceVal').textContent = confidence.toFixed(2) | |
| document.getElementById('mlConfidenceBar').style.width = confidence * 100 + '%' | |
| // Color coding: high confidence = blue, low confidence = orange | |
| const confidenceBar = document.getElementById('mlConfidenceBar') | |
| if(confidence > 0.8) confidenceBar.style.background = '#1E59F0' // Blue for high confidence | |
| else if(confidence > 0.6) confidenceBar.style.background = '#F59E0B' // Orange for medium | |
| else confidenceBar.style.background = '#D92D20' // Red for low confidence | |
| } | |
| }catch(e){ document.getElementById('evidence').textContent = 'Waiting for data…' } | |
| } | |
| setInterval(loadRisk, 2000); loadRisk() | |
| async function loadDevices(){ | |
| try{ | |
| const resp = await fetch(API + '/trust/devices') | |
| if(!resp.ok) throw new Error() | |
| const items = await resp.json() | |
| const html = items.map(x => `<div style="display:flex;justify-content:space-between;margin:6px 0"><span>${x.vid}/${x.pid}</span><span>${x.count}</span></div>`).join('') | |
| document.getElementById('devices').innerHTML = html || 'No inventory yet.' | |
| }catch{ document.getElementById('devices').textContent = 'No inventory yet.' } | |
| } | |
| setInterval(loadDevices, 3000); loadDevices() | |
| // Seed/Clear buttons | |
| document.getElementById('seedBtn').onclick = async () => { | |
| try{ await fetch(API + '/demo/seed', { method:'POST' }); loadRisk(); loadDevices(); } catch{} | |
| } | |
| document.getElementById('clearBtn').onclick = async () => { | |
| try{ await fetch(API + '/demo/clear', { method:'POST' }); loadRisk(); loadDevices(); } catch{} | |
| } | |
| // Model meta | |
| async function loadModel(){ | |
| try{ | |
| const resp = await fetch(API + '/model/meta') | |
| if(!resp.ok) throw new Error() | |
| const meta = await resp.json() | |
| // Update model status | |
| let status = 'Unknown' | |
| if(meta.modelPresent && meta.oneClassSvmPresent){ | |
| status = 'Level 2 Active (Isolation Forest + One-Class SVM)' | |
| } else if(meta.modelPresent){ | |
| status = 'Level 2 Partial (Isolation Forest only)' | |
| } else { | |
| status = 'Level 1 Only (Heuristics)' | |
| } | |
| document.getElementById('modelStatus').textContent = status | |
| // Show detailed model info in evidence section for debugging | |
| if(meta.level && meta.level.includes('Level 2')){ | |
| document.getElementById('evidence').textContent = | |
| JSON.stringify({...meta, note: 'Level 2 ML models are active and contributing to risk assessment'}, null, 2) | |
| } | |
| }catch{ | |
| document.getElementById('modelStatus').textContent = 'Error loading model info' | |
| } | |
| } | |
| document.getElementById('refreshModel').onclick = loadModel | |
| setInterval(loadModel, 10000); loadModel() | |
| // Machines list | |
| async function loadMachines(){ | |
| try{ | |
| const resp = await fetch(API + '/machines') | |
| if(!resp.ok) throw new Error() | |
| const items = await resp.json() | |
| const html = items.map(x => { | |
| const hb = x.lastHeartbeat ? new Date(x.lastHeartbeat).toLocaleTimeString() : '—' | |
| const ft = x.lastFeatures ? new Date(x.lastFeatures).toLocaleTimeString() : '—' | |
| return `<div class="row" data-id="${x.machineId}" style="display:flex;justify-content:space-between;margin:6px 0;cursor:pointer"><span>${x.machineId}</span><span class="muted">hb:${hb} · ft:${ft}</span></div>` | |
| }).join('') | |
| document.getElementById('machines').innerHTML = html || 'No machines yet.' | |
| document.querySelectorAll('#machines .row').forEach(el => el.addEventListener('click', () => selectMachine(el.dataset.id))) | |
| }catch{ document.getElementById('machines').textContent = 'No machines yet.' } | |
| } | |
| setInterval(loadMachines, 5000); loadMachines() | |
| // Risk history | |
| async function loadHistory(){ | |
| const machineId = 'demo-machine' | |
| try{ | |
| const resp = await fetch(`${API}/risk/${machineId}/history?take=20`) | |
| if(!resp.ok) throw new Error() | |
| const items = await resp.json() | |
| const html = items.map(x => { | |
| const t = new Date(x.timestampUtc).toLocaleTimeString() | |
| const s = (x.riskScore ?? 0).toFixed(2) | |
| return `<div style="display:flex;justify-content:space-between;margin:6px 0"><span>${t}</span><span>${s}</span></div>` | |
| }).join('') | |
| document.getElementById('history').innerHTML = html || 'No history yet.' | |
| // Match heuristics height by measuring that card's content height | |
| const heuristicsContent = document.querySelector('.card h3 + .content') | |
| const targetH = heuristicsContent ? heuristicsContent.clientHeight : 180 | |
| document.getElementById('historyContainer').style.height = targetH + 'px' | |
| }catch{ document.getElementById('history').textContent = 'No history yet.' } | |
| } | |
| setInterval(loadHistory, 4000); loadHistory() | |
| // Select machine | |
| function selectMachine(id){ | |
| document.getElementById('machineId').textContent = id | |
| loadRisk(); loadHistory(); | |
| } | |
| // Evict stale | |
| document.getElementById('evictBtn').onclick = async () => { | |
| try{ | |
| await fetch(API + '/admin/evict', { method:'POST' }) | |
| loadMachines(); loadDevices(); | |
| }catch{} | |
| } | |
| </script> | |
| </body> | |
| </html> | |