activity-simulation / index.html
Augusto Diaz
Space: dashboard only
5b7eaab
Raw
History Blame Contribute Delete
39.6 kB
<!doctype html>
<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()">&times;</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>