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 { | |
| /* Light mode colors */ | |
| --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: 20px; | |
| /* Theme variables */ | |
| --bg-primary: #F9FAFB; | |
| --bg-secondary: #FFFFFF; | |
| --bg-tertiary: #F7F7F7; | |
| --text-primary: #000000; | |
| --text-secondary: #7C7D7E; | |
| --text-accent: #404E90; | |
| --accent-blue: #1E59F0; | |
| --accent-red: #D92D20; | |
| --accent-green: #13A964; | |
| --border-color: #E1E4E6; | |
| --shadow-light: rgba(0, 0, 0, 0.1); | |
| --shadow-medium: rgba(0, 0, 0, 0.15); | |
| --shadow-heavy: rgba(0, 0, 0, 0.25); | |
| --glass-bg: rgba(255, 255, 255, 0.8); | |
| --glass-border: rgba(255, 255, 255, 0.2); | |
| } | |
| [data-theme="dark"] { | |
| /* Dark mode colors */ | |
| --bg-primary: #0F172A; | |
| --bg-secondary: #1E293B; | |
| --bg-tertiary: #334155; | |
| --text-primary: #FFFFFF; | |
| --text-secondary: #94A3B8; | |
| --text-accent: #60A5FA; | |
| --accent-blue: #3B82F6; | |
| --accent-red: #EF4444; | |
| --accent-green: #10B981; | |
| --border-color: #475569; | |
| --shadow-light: rgba(0, 0, 0, 0.3); | |
| --shadow-medium: rgba(0, 0, 0, 0.4); | |
| --shadow-heavy: rgba(0, 0, 0, 0.6); | |
| --glass-bg: rgba(30, 41, 59, 0.8); | |
| --glass-border: rgba(255, 255, 255, 0.1); | |
| } | |
| html, body { | |
| height: 100%; | |
| transition: all 0.3s ease; | |
| } | |
| body { | |
| margin: 0; | |
| font-family: Inter, system-ui, Segoe UI, Roboto, Helvetica, Arial, sans-serif; | |
| background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-tertiary) 100%); | |
| color: var(--text-primary); | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .header { | |
| padding: 12px 16px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| background: linear-gradient(135deg, var(--accent-blue) 0%, var(--text-accent) 100%); | |
| color: var(--white); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .header::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.1), transparent); | |
| animation: shimmer 3s infinite; | |
| } | |
| @keyframes shimmer { | |
| 0% { transform: translateX(-100%); } | |
| 100% { transform: translateX(100%); } | |
| } | |
| .header .brand { | |
| font-weight: 700; | |
| letter-spacing: 0.4px; | |
| position: relative; | |
| z-index: 1; | |
| } | |
| .header-controls { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| position: relative; | |
| z-index: 1; | |
| padding: 4px 8px; | |
| background: rgba(255, 255, 255, 0.1); | |
| border-radius: 12px; | |
| backdrop-filter: blur(10px); | |
| } | |
| .demo-buttons { | |
| display: flex; | |
| gap: 4px; | |
| } | |
| .demo-buttons .btn { | |
| padding: 4px 8px; | |
| font-size: 0.75rem; | |
| border-radius: 6px; | |
| } | |
| .theme-switcher { | |
| background: rgba(255, 255, 255, 0.1); | |
| backdrop-filter: blur(10px); | |
| border: 1px solid rgba(255, 255, 255, 0.2); | |
| border-radius: 12px; | |
| padding: 4px 6px; | |
| transition: all 0.3s ease; | |
| position: relative; | |
| z-index: 1; | |
| } | |
| .slider-container { | |
| display: flex; | |
| align-items: center; | |
| gap: 4px; | |
| } | |
| .slider-label { | |
| font-size: 10px; | |
| opacity: 0.6; | |
| transition: opacity 0.3s ease; | |
| } | |
| .slider { | |
| position: relative; | |
| display: inline-block; | |
| width: 32px; | |
| height: 16px; | |
| } | |
| .slider input { | |
| opacity: 0; | |
| width: 0; | |
| height: 0; | |
| } | |
| .slider-round { | |
| position: absolute; | |
| cursor: pointer; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: #E1E4E6; | |
| transition: 0.3s; | |
| border-radius: 16px; | |
| box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1); | |
| } | |
| .slider-round:before { | |
| position: absolute; | |
| content: ""; | |
| height: 12px; | |
| width: 12px; | |
| left: 2px; | |
| bottom: 2px; | |
| background: #7C7D7E; | |
| transition: 0.3s; | |
| border-radius: 50%; | |
| box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); | |
| } | |
| input:checked + .slider-round { | |
| background: #404E90; | |
| } | |
| input:checked + .slider-round:before { | |
| transform: translateX(16px); | |
| background: #F9FAFB; | |
| } | |
| .theme-switcher:hover { | |
| transform: scale(1.02); | |
| box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | |
| } | |
| .container { | |
| padding: 16px; | |
| display: grid; | |
| grid-template-columns: repeat(4, minmax(260px, 1fr)); | |
| gap: 12px; | |
| flex: 1; | |
| max-width: 100%; | |
| margin: 0 auto; | |
| } | |
| @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(--glass-bg); | |
| backdrop-filter: blur(20px); | |
| border-radius: var(--radius); | |
| box-shadow: 0 8px 32px var(--shadow-light); | |
| border: 1px solid var(--glass-border); | |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| position: relative; | |
| overflow: hidden; | |
| min-height: 196px; | |
| } | |
| .card::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| height: 1px; | |
| background: linear-gradient(90deg, transparent, var(--accent-blue), transparent); | |
| } | |
| .card:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 12px 40px var(--shadow-medium); | |
| } | |
| .card h3 { | |
| margin: 12px 12px 0 12px; | |
| color: var(--text-primary); | |
| } | |
| .card .content { | |
| padding: 12px; | |
| color: var(--text-primary); | |
| } | |
| .pill { | |
| display: inline-block; | |
| padding: 8px 16px; | |
| border-radius: 50px; | |
| font-size: 12px; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| backdrop-filter: blur(10px); | |
| } | |
| .pill.low { | |
| background: linear-gradient(135deg, rgba(16, 185, 129, 0.2), rgba(16, 185, 129, 0.1)); | |
| color: var(--accent-green); | |
| border: 1px solid rgba(16, 185, 129, 0.3); | |
| } | |
| .pill.med { | |
| background: linear-gradient(135deg, rgba(245, 158, 11, 0.2), rgba(245, 158, 11, 0.1)); | |
| color: #F59E0B; | |
| border: 1px solid rgba(245, 158, 11, 0.3); | |
| } | |
| .pill.high { | |
| background: linear-gradient(135deg, rgba(239, 68, 68, 0.2), rgba(239, 68, 68, 0.1)); | |
| color: var(--accent-red); | |
| border: 1px solid rgba(239, 68, 68, 0.3); | |
| } | |
| .muted { | |
| color: var(--text-secondary); | |
| } | |
| .footer { | |
| padding: 24px; | |
| text-align: center; | |
| color: var(--text-secondary); | |
| } | |
| .bar { | |
| height: 20px; | |
| border-radius: 50px; | |
| background: var(--bg-tertiary); | |
| overflow: hidden; | |
| margin: 12px 0; | |
| box-shadow: inset 0 2px 4px var(--shadow-light); | |
| } | |
| .bar div { | |
| height: 100%; | |
| border-radius: 50px; | |
| transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); | |
| position: relative; | |
| } | |
| .bar div::after { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent); | |
| animation: shimmer 5s infinite; | |
| } | |
| .scroll { | |
| overflow: auto; | |
| } | |
| .scroll::-webkit-scrollbar { | |
| width: 8px; | |
| } | |
| .scroll::-webkit-scrollbar-track { | |
| background: var(--bg-tertiary); | |
| border-radius: 4px; | |
| } | |
| .scroll::-webkit-scrollbar-thumb { | |
| background: linear-gradient(135deg, var(--accent-blue), var(--text-accent)); | |
| border-radius: 4px; | |
| } | |
| .scroll::-webkit-scrollbar-thumb:hover { | |
| background: linear-gradient(135deg, var(--text-accent), var(--accent-blue)); | |
| } | |
| .btn { | |
| appearance: none; | |
| border: 0; | |
| border-radius: 12px; | |
| padding: 12px 20px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| position: relative; | |
| overflow: hidden; | |
| box-shadow: 0 4px 12px var(--shadow-light); | |
| } | |
| .btn::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: -100%; | |
| width: 100%; | |
| height: 100%; | |
| background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); | |
| transition: left 0.5s; | |
| } | |
| .btn:hover::before { | |
| left: 100%; | |
| } | |
| .btn:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 6px 20px var(--shadow-medium); | |
| } | |
| .btn:active { | |
| transform: translateY(1px); | |
| } | |
| .btn-seed { | |
| background: linear-gradient(135deg, var(--accent-green) 0%, #059669 100%); | |
| color: #FFFFFF; | |
| box-shadow: 0 4px 12px rgba(19, 169, 100, 0.3); | |
| } | |
| .btn-clear { | |
| background: linear-gradient(135deg, #B54708 0%, #92400E 100%); | |
| color: #FFFFFF; | |
| box-shadow: 0 4px 12px rgba(181, 71, 8, 0.3); | |
| } | |
| .btn-evict { | |
| background: linear-gradient(135deg, #8B5CF6 0%, #7C3AED 100%); | |
| color: #FFFFFF; | |
| box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3); | |
| } | |
| .btn-evict:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 6px 20px rgba(139, 92, 246, 0.4); | |
| } | |
| /* Smooth transitions to prevent blinking */ | |
| .card, .bar, .pill, #evidence, #devices, #machines, #history, #summaryPercentage, #summaryText { | |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| .bar div { | |
| transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| /* Help system styles */ | |
| .help-btn { | |
| background: linear-gradient(135deg, var(--text-secondary) 0%, var(--text-accent) 100%); | |
| color: white; | |
| border: none; | |
| border-radius: 50%; | |
| width: 24px; | |
| height: 24px; | |
| font-size: 12px; | |
| cursor: pointer; | |
| margin-left: 8px; | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: all 0.3s ease; | |
| box-shadow: 0 2px 8px var(--shadow-light); | |
| } | |
| .help-btn:hover { | |
| transform: scale(1.1); | |
| box-shadow: 0 4px 12px var(--shadow-medium); | |
| } | |
| .help-modal { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(0, 0, 0, 0.6); | |
| backdrop-filter: blur(8px); | |
| display: none; | |
| z-index: 1000; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .help-content { | |
| background: var(--glass-bg); | |
| backdrop-filter: blur(20px); | |
| border-radius: 20px; | |
| padding: 32px; | |
| max-width: 600px; | |
| max-height: 80vh; | |
| overflow-y: auto; | |
| box-shadow: 0 20px 60px var(--shadow-heavy); | |
| border: 1px solid var(--glass-border); | |
| } | |
| .help-content h3 { | |
| margin-top: 0; | |
| color: var(--accent-blue); | |
| } | |
| .help-content h4 { | |
| margin: 16px 0 8px 0; | |
| color: var(--text-accent); | |
| } | |
| .help-content p { | |
| margin: 8px 0; | |
| line-height: 1.6; | |
| } | |
| .help-content ul { | |
| margin: 8px 0; | |
| padding-left: 20px; | |
| } | |
| .help-content li { | |
| margin: 4px 0; | |
| } | |
| .close-btn { | |
| float: right; | |
| background: none; | |
| border: none; | |
| font-size: 24px; | |
| cursor: pointer; | |
| color: var(--text-secondary); | |
| transition: color 0.3s ease; | |
| } | |
| .close-btn:hover { | |
| color: var(--accent-blue); | |
| } | |
| pre { | |
| background: var(--bg-tertiary); | |
| border: 1px solid var(--border-color); | |
| border-radius: 12px; | |
| padding: 16px; | |
| font-size: 0.75rem; | |
| overflow-x: auto; | |
| white-space: pre-wrap; | |
| word-break: break-word; | |
| backdrop-filter: blur(10px); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="header"> | |
| <div class="brand">Activity Simulation Detection</div> | |
| <div class="header-controls"> | |
| <div class="demo-buttons"> | |
| <button id="seedBtn" class="btn btn-seed">Seed Demo</button> | |
| <button id="clearBtn" class="btn btn-clear">Clear</button> | |
| <button id="evictBtn" class="btn btn-evict">Evict Stale</button> | |
| </div> | |
| <div class="theme-switcher" title="Toggle Dark/Light Mode"> | |
| <div class="slider-container"> | |
| <span class="slider-label">☀️</span> | |
| <label class="slider"> | |
| <input type="checkbox" id="theme-toggle" onchange="toggleTheme()"> | |
| <span class="slider-round"></span> | |
| </label> | |
| <span class="slider-label">🌙</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="container"> | |
| <div class="card"> | |
| <h3>Machine Risk <button class="help-btn" onclick="showHelp('risk')">?</button></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 <button class="help-btn" onclick="showHelp('evidence')">?</button></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) <button class="help-btn" onclick="showHelp('devices')">?</button></h3> | |
| <div class="content"> | |
| <div id="devices" class="muted">Loading…</div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <h3>Heuristics <button class="help-btn" onclick="showHelp('heuristics')">?</button></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) <button class="help-btn" onclick="showHelp('mlmodel')">?</button></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> | |
| </div> | |
| <div class="card"> | |
| <h3>Machines <button class="help-btn" onclick="showHelp('machines')">?</button></h3> | |
| <div class="content"> | |
| <div id="machines" class="muted">Loading…</div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <h3>Summary <button class="help-btn" onclick="showHelp('summary')">?</button></h3> | |
| <div class="content"> | |
| <div style="text-align: center; padding: 20px 0;"> | |
| <div id="summaryPercentage" style="font-size: 3em; font-weight: bold; color: #1E59F0; margin-bottom: 10px;">0%</div> | |
| <div id="summaryText" style="font-size: 1.1em; color: #667085; line-height: 1.4; max-width: 280px; margin: 0 auto;">Analyzing user behavior...</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="card history"> | |
| <h3>Risk History <button class="help-btn" onclick="showHelp('history')">?</button></h3> | |
| <div class="content scroll" id="historyContainer"> | |
| <div id="history" class="muted">Loading…</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Help Modal --> | |
| <div id="helpModal" class="help-modal"> | |
| <div class="help-content"> | |
| <button class="close-btn" onclick="hideHelp()">×</button> | |
| <div id="helpContent"></div> | |
| </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 | |
| } | |
| // Update Summary card | |
| updateSummary(score, data.riskLevel) | |
| }catch(e){ document.getElementById('evidence').textContent = 'Waiting for data…' } | |
| } | |
| // Use a more stable update mechanism to prevent refreshing | |
| let isUpdating = false; | |
| async function stableLoadRisk() { | |
| if (isUpdating) return; | |
| isUpdating = true; | |
| try { | |
| await loadRisk(); | |
| } finally { | |
| isUpdating = false; | |
| } | |
| } | |
| setInterval(stableLoadRisk, 5000); stableLoadRisk() | |
| 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, 8000); 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' | |
| } | |
| } | |
| setInterval(loadModel, 20000); 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, 10000); 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, 8000); 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{} | |
| } | |
| // Summary update function with smooth transitions | |
| function updateSummary(riskScore, riskLevel) { | |
| const percentage = Math.round(riskScore * 100) | |
| const percentageEl = document.getElementById('summaryPercentage') | |
| const textEl = document.getElementById('summaryText') | |
| // Update percentage with color coding and smooth transition | |
| percentageEl.style.transition = 'color 0.3s ease' | |
| percentageEl.textContent = percentage + '%' | |
| if (riskScore >= 0.75) { | |
| percentageEl.style.color = '#D92D20' // Red for high risk | |
| } else if (riskScore >= 0.4) { | |
| percentageEl.style.color = '#F59E0B' // Yellow for medium risk | |
| } else { | |
| percentageEl.style.color = '#13A964' // Green for low risk | |
| } | |
| // Select appropriate text based on risk level | |
| const summaryTexts = { | |
| low: [ | |
| "User appears to be interacting naturally with the system.", | |
| "No signs of automated activity detected.", | |
| "Behavior patterns indicate genuine human interaction.", | |
| "User activity shows typical human characteristics.", | |
| "Low probability of automation or simulation." | |
| ], | |
| medium: [ | |
| "Some patterns suggest possible automation or assistance.", | |
| "Mixed signals detected - some human-like, some automated.", | |
| "User behavior shows some unusual patterns.", | |
| "Moderate chance of automated activity.", | |
| "Activity patterns are partially suspicious." | |
| ], | |
| high: [ | |
| "Strong indicators of automated or simulated activity.", | |
| "High probability of bot or automation usage.", | |
| "User behavior appears to be artificially generated.", | |
| "Likely using tools to simulate human interaction.", | |
| "Clear signs of non-human activity patterns." | |
| ] | |
| } | |
| // Select text based on risk level | |
| let textArray | |
| if (riskScore >= 0.75) { | |
| textArray = summaryTexts.high | |
| } else if (riskScore >= 0.4) { | |
| textArray = summaryTexts.medium | |
| } else { | |
| textArray = summaryTexts.low | |
| } | |
| // Select a random text from the appropriate array | |
| const randomIndex = Math.floor(Math.random() * textArray.length) | |
| const newText = textArray[randomIndex] | |
| // Smooth text transition with fade out/in | |
| if (textEl.textContent !== newText) { | |
| textEl.style.transition = 'opacity 0.2s ease' | |
| textEl.style.opacity = '0' | |
| setTimeout(() => { | |
| textEl.textContent = newText | |
| textEl.style.opacity = '1' | |
| }, 200) | |
| } | |
| } | |
| // Help system | |
| const helpContent = { | |
| risk: { | |
| title: "Machine Risk Assessment", | |
| content: ` | |
| <h4>Risk Score (0.0 - 1.0)</h4> | |
| <p>The overall risk score combines multiple detection methods:</p> | |
| <ul> | |
| <li><strong>0.0 - 0.4:</strong> Low Risk (Green) - Normal human behavior</li> | |
| <li><strong>0.4 - 0.75:</strong> Medium Risk (Yellow) - Suspicious patterns</li> | |
| <li><strong>0.75 - 1.0:</strong> High Risk (Red) - Likely automation</li> | |
| </ul> | |
| <h4>Risk Level</h4> | |
| <p>Color-coded indicator based on the risk score threshold.</p> | |
| <h4>Machine ID</h4> | |
| <p>Unique identifier for the monitored machine/device.</p> | |
| ` | |
| }, | |
| evidence: { | |
| title: "Detection Evidence", | |
| content: ` | |
| <h4>Raw Evidence Data</h4> | |
| <p>This section shows the complete evidence object used for risk calculation, including:</p> | |
| <ul> | |
| <li><strong>heuristicRisk:</strong> Level 1 analysis score (0-1)</li> | |
| <li><strong>onnxScore:</strong> Level 2 ML model score (0-1)</li> | |
| <li><strong>features:</strong> Detailed behavioral metrics</li> | |
| <li><strong>deviceTrust:</strong> Hardware fingerprint analysis</li> | |
| </ul> | |
| <p>This data is used by the system to make the final risk assessment.</p> | |
| ` | |
| }, | |
| devices: { | |
| title: "Device Trust Analysis", | |
| content: ` | |
| <h4>Hardware Fingerprinting</h4> | |
| <p>Shows the most frequently observed input devices:</p> | |
| <ul> | |
| <li><strong>VID/PID:</strong> Vendor ID / Product ID of USB devices</li> | |
| <li><strong>Count:</strong> Number of times this device was detected</li> | |
| </ul> | |
| <h4>Trust Indicators</h4> | |
| <p>Common trusted devices include:</p> | |
| <ul> | |
| <li><strong>Logitech:</strong> VID 046d (mice, keyboards, webcams)</li> | |
| <li><strong>Microsoft:</strong> VID 045e (mice, keyboards)</li> | |
| <li><strong>Apple:</strong> VID 05ac (Magic Mouse, Trackpad)</li> | |
| </ul> | |
| <p>Suspicious patterns: Generic devices, virtual devices, or rapid device changes.</p> | |
| ` | |
| }, | |
| heuristics: { | |
| title: "Level 1 Heuristic Analysis", | |
| content: ` | |
| <h4>Inter-event Entropy</h4> | |
| <p>Measures randomness in timing between events:</p> | |
| <ul> | |
| <li><strong>High (Green):</strong> Random, human-like timing</li> | |
| <li><strong>Low (Red):</strong> Regular, machine-like timing</li> | |
| </ul> | |
| <h4>Interval Regularity</h4> | |
| <p>Detects fixed-interval patterns:</p> | |
| <ul> | |
| <li><strong>Low (Green):</strong> Variable intervals, natural</li> | |
| <li><strong>High (Red):</strong> Fixed intervals, suspicious</li> | |
| </ul> | |
| <h4>Path Straightness</h4> | |
| <p>Analyzes mouse movement patterns:</p> | |
| <ul> | |
| <li><strong>Curved (Green):</strong> Natural mouse movements</li> | |
| <li><strong>Straight (Red):</strong> Linear, artificial movements</li> | |
| </ul> | |
| ` | |
| }, | |
| mlmodel: { | |
| title: "Level 2 Machine Learning Model", | |
| content: ` | |
| <h4>ML Anomaly Score</h4> | |
| <p>Advanced ML model output using Isolation Forest and One-Class SVM:</p> | |
| <ul> | |
| <li><strong>Low (Green):</strong> Normal behavior pattern</li> | |
| <li><strong>High (Red):</strong> Anomalous behavior detected</li> | |
| </ul> | |
| <h4>Model Confidence</h4> | |
| <p>Indicates how confident the model is in its prediction:</p> | |
| <ul> | |
| <li><strong>High (Blue):</strong> ML and heuristic scores agree</li> | |
| <li><strong>Low (Red):</strong> Conflicting signals</li> | |
| </ul> | |
| <h4>Model Status</h4> | |
| <p>Shows which detection levels are active:</p> | |
| <ul> | |
| <li><strong>Level 1:</strong> Heuristics only</li> | |
| <li><strong>Level 2:</strong> Heuristics + ML models</li> | |
| </ul> | |
| ` | |
| }, | |
| machines: { | |
| title: "Monitored Machines", | |
| content: ` | |
| <h4>Active Machines</h4> | |
| <p>List of all machines currently being monitored:</p> | |
| <ul> | |
| <li><strong>Machine ID:</strong> Unique identifier</li> | |
| <li><strong>hb:</strong> Last heartbeat timestamp</li> | |
| <li><strong>ft:</strong> Last feature data timestamp</li> | |
| </ul> | |
| <h4>Machine Selection</h4> | |
| <p>Click on any machine to view its specific risk assessment and history.</p> | |
| <h4>Status Indicators</h4> | |
| <p>Recent timestamps indicate active monitoring. Stale timestamps may indicate disconnected machines.</p> | |
| ` | |
| }, | |
| history: { | |
| title: "Risk History Timeline", | |
| content: ` | |
| <h4>Historical Risk Scores</h4> | |
| <p>Shows the last 20 risk assessments for the selected machine:</p> | |
| <ul> | |
| <li><strong>Time:</strong> When the assessment was made</li> | |
| <li><strong>Score:</strong> Risk score at that moment</li> | |
| </ul> | |
| <h4>Pattern Analysis</h4> | |
| <p>Look for patterns in the history:</p> | |
| <ul> | |
| <li><strong>Consistent Low:</strong> Normal user behavior</li> | |
| <li><strong>Spikes:</strong> Temporary suspicious activity</li> | |
| <li><strong>Consistent High:</strong> Likely automated behavior</li> | |
| </ul> | |
| <h4>Real-time Updates</h4> | |
| <p>History updates automatically every 6 seconds with new risk assessments.</p> | |
| ` | |
| }, | |
| summary: { | |
| title: "Activity Simulation Summary", | |
| content: ` | |
| <h4>Percentage Indicator</h4> | |
| <p>Shows the probability that the user is using automation or simulation tools:</p> | |
| <ul> | |
| <li><strong>0-40% (Green):</strong> Low probability - Natural human behavior</li> | |
| <li><strong>40-75% (Yellow):</strong> Medium probability - Some suspicious patterns</li> | |
| <li><strong>75-100% (Red):</strong> High probability - Likely automated activity</li> | |
| </ul> | |
| <h4>User-Friendly Explanation</h4> | |
| <p>The text below the percentage provides a simple, non-technical explanation of what the system has detected. The text rotates randomly to provide variety while maintaining the same meaning.</p> | |
| <h4>Real-time Assessment</h4> | |
| <p>This summary updates automatically every 3 seconds based on the latest behavioral analysis, combining both heuristic and machine learning detection methods.</p> | |
| ` | |
| } | |
| } | |
| function showHelp(type) { | |
| const modal = document.getElementById('helpModal') | |
| const content = document.getElementById('helpContent') | |
| if (helpContent[type]) { | |
| content.innerHTML = ` | |
| <h3>${helpContent[type].title}</h3> | |
| ${helpContent[type].content} | |
| ` | |
| modal.style.display = 'flex' | |
| } | |
| } | |
| function hideHelp() { | |
| document.getElementById('helpModal').style.display = 'none' | |
| } | |
| // Close modal when clicking outside | |
| document.getElementById('helpModal').onclick = (e) => { | |
| if (e.target.id === 'helpModal') { | |
| hideHelp() | |
| } | |
| } | |
| // Theme switcher | |
| function toggleTheme() { | |
| const body = document.body | |
| const themeToggle = document.getElementById('theme-toggle') | |
| if (themeToggle.checked) { | |
| body.setAttribute('data-theme', 'dark') | |
| localStorage.setItem('theme', 'dark') | |
| } else { | |
| body.removeAttribute('data-theme') | |
| localStorage.setItem('theme', 'light') | |
| } | |
| } | |
| // Load saved theme on page load | |
| document.addEventListener('DOMContentLoaded', () => { | |
| const savedTheme = localStorage.getItem('theme') | |
| const themeToggle = document.getElementById('theme-toggle') | |
| if (savedTheme === 'dark') { | |
| document.body.setAttribute('data-theme', 'dark') | |
| themeToggle.checked = true | |
| } else { | |
| themeToggle.checked = false | |
| } | |
| }) | |
| </script> | |
| </body> | |
| </html> | |