deci-core-api / deci-dashboard.html
Denisijcu's picture
upload files
61f8152 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DECI β€” Vertex Coders Core</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link
href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=DM+Sans:wght@300;400;500&display=swap"
rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3.10.0/dist/tabler-icons.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.js"></script>
<style>
/* ── Reset & Variables ──────────────────────────────────────── */
* {
box-sizing: border-box;
margin: 0;
padding: 0
}
:root {
--navy: #0a0e1a;
--navy2: #0f1628;
--panel: #131c30;
--panel2: #1a2540;
--border: #1e2d4a;
--border2: #2a3d5e;
--red: #e94560;
--red-d: rgba(233, 69, 96, .14);
--green: #00d4a0;
--green-d: rgba(0, 212, 160, .11);
--yellow: #f5c518;
--yellow-d: rgba(245, 197, 24, .11);
--blue: #4a9eff;
--blue-d: rgba(74, 158, 255, .11);
--text: #c8d8f0;
--muted: #5a7090;
--muted2: #3a506a;
--mono: 'Space Mono', monospace;
--sans: 'DM Sans', sans-serif;
--r: 6px;
}
html,
body {
height: 100%;
background: var(--navy);
color: var(--text);
font-family: var(--sans);
font-size: 14px;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
overflow: hidden
}
::-webkit-scrollbar {
width: 4px;
height: 4px
}
::-webkit-scrollbar-track {
background: transparent
}
::-webkit-scrollbar-thumb {
background: var(--border2);
border-radius: 2px
}
/* ── Layout ─────────────────────────────────────────────────── */
.shell {
display: grid;
grid-template-rows: 52px 1fr 130px;
height: 100vh
}
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
background: var(--navy2);
border-bottom: 1px solid var(--border);
z-index: 100
}
.topbar-l {
display: flex;
align-items: center;
gap: 16px
}
.logo {
font-family: var(--mono);
font-size: 18px;
font-weight: 700;
color: #fff
}
.logo span {
color: var(--red)
}
.ver {
font-family: var(--mono);
font-size: 10px;
color: var(--muted);
border: 1px solid var(--border);
padding: 2px 8px;
border-radius: 3px
}
.status-grp {
display: flex;
align-items: center;
gap: 6px
}
.dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--muted)
}
.dot.on {
background: var(--green);
animation: pulse 2s infinite
}
.dot.off {
background: var(--red);
animation: blink 1s infinite
}
.status-lbl {
font-family: var(--mono);
font-size: 11px;
color: var(--muted)
}
.status-lbl.on {
color: var(--green)
}
.topbar-r {
display: flex;
align-items: center;
gap: 20px
}
.corp {
font-family: var(--mono);
font-size: 11px;
color: var(--muted)
}
.clock {
font-family: var(--mono);
font-size: 12px;
color: var(--muted)
}
.layout {
display: grid;
grid-template-columns: 240px 1fr;
overflow: hidden
}
.sidebar {
background: var(--navy2);
border-right: 1px solid var(--border);
padding: 16px 0;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 0
}
.nav-sec {
margin-bottom: 22px
}
.nav-lbl {
font-family: var(--mono);
font-size: 10px;
color: var(--muted);
letter-spacing: 1.5px;
text-transform: uppercase;
padding: 0 16px 8px
}
.nav-item {
display: flex;
align-items: center;
gap: 10px;
padding: 9px 16px;
color: var(--muted);
font-size: 13px;
border-left: 2px solid transparent;
cursor: pointer;
transition: all .15s;
text-decoration: none
}
.nav-item:hover {
color: var(--text);
background: var(--panel)
}
.nav-item.active {
color: #fff;
border-left-color: var(--red);
background: var(--panel)
}
.nav-item i {
font-size: 15px
}
.mm {
padding: 8px 16px;
border-bottom: 1px solid var(--border)
}
.mm-lbl {
font-size: 10px;
color: var(--muted);
font-family: var(--mono);
margin-bottom: 3px
}
.mm-val {
font-family: var(--mono);
font-size: 18px;
font-weight: 700;
color: #fff
}
.sr {
display: flex;
align-items: center;
justify-content: space-between;
padding: 5px 16px;
font-size: 12px
}
.sr span:first-child {
color: var(--muted);
font-family: var(--mono)
}
.sr span:last-child {
font-family: var(--mono);
font-size: 10px
}
.content {
overflow-y: auto;
padding: 20px;
background: var(--navy);
display: flex;
flex-direction: column;
gap: 16px
}
.page {
display: none;
flex-direction: column;
gap: 16px
}
.page.active {
display: flex
}
.log-footer {
background: var(--navy2);
border-top: 1px solid var(--border);
display: flex;
flex-direction: column;
overflow: hidden
}
.log-hdr {
display: flex;
justify-content: space-between;
align-items: center;
padding: 5px 16px;
border-bottom: 1px solid var(--border)
}
.log-body {
flex: 1;
overflow-y: auto;
padding: 4px 16px;
font-family: var(--mono);
font-size: 10px
}
.ll {
padding: 1px 0;
color: var(--muted);
line-height: 1.7
}
.ll.ok {
color: var(--green)
}
.ll.warn {
color: var(--yellow)
}
.ll.err {
color: var(--red)
}
.ll.info {
color: var(--blue)
}
.ll-ts {
color: var(--muted2)
}
/* ── Shared Components ──────────────────────────────────────── */
.card {
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--r);
padding: 16px
}
.card.al {
border-left: 3px solid var(--red)
}
.card.ag {
border-left: 3px solid var(--green)
}
.card.ay {
border-left: 3px solid var(--yellow)
}
.card.ab {
border-left: 3px solid var(--blue)
}
.sec-title {
font-family: var(--mono);
font-size: 10px;
color: var(--muted);
letter-spacing: 1.5px;
text-transform: uppercase;
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 14px
}
.sec-title::after {
content: '';
flex: 1;
height: 1px;
background: var(--border)
}
.badge {
display: inline-block;
font-family: var(--mono);
font-size: 10px;
padding: 2px 8px;
border-radius: 3px;
letter-spacing: .4px
}
.badge.r {
background: var(--red-d);
color: var(--red);
border: 1px solid rgba(233, 69, 96, .28)
}
.badge.g {
background: var(--green-d);
color: var(--green);
border: 1px solid rgba(0, 212, 160, .22)
}
.badge.y {
background: var(--yellow-d);
color: var(--yellow);
border: 1px solid rgba(245, 197, 24, .28)
}
.badge.b {
background: var(--blue-d);
color: var(--blue);
border: 1px solid rgba(74, 158, 255, .22)
}
.btn {
font-family: var(--mono);
font-size: 11px;
letter-spacing: .5px;
padding: 7px 14px;
border-radius: 4px;
cursor: pointer;
border: 1px solid var(--border);
background: var(--panel2);
color: var(--text);
display: inline-flex;
align-items: center;
gap: 6px;
transition: all .15s
}
.btn:hover {
border-color: var(--red);
color: var(--red)
}
.btn:disabled {
opacity: .4;
cursor: not-allowed;
pointer-events: none
}
.btn.pr {
border-color: rgba(233, 69, 96, .45);
color: var(--red);
background: var(--red-d)
}
.btn.pr:hover {
background: rgba(233, 69, 96, .22)
}
.btn.sc {
border-color: rgba(0, 212, 160, .4);
color: var(--green);
background: var(--green-d)
}
.btn.wa {
border-color: rgba(245, 197, 24, .4);
color: var(--yellow);
background: var(--yellow-d)
}
.verdict-tag {
display: inline-flex;
align-items: center;
gap: 5px;
font-family: var(--mono);
font-size: 11px;
font-weight: 700;
padding: 4px 11px;
border-radius: 4px
}
.verdict-tag.human {
color: var(--green);
background: var(--green-d);
border: 1px solid rgba(0, 212, 160, .28)
}
.verdict-tag.suspect {
color: var(--yellow);
background: var(--yellow-d);
border: 1px solid rgba(245, 197, 24, .28)
}
.verdict-tag.bot {
color: var(--red);
background: var(--red-d);
border: 1px solid rgba(233, 69, 96, .28)
}
.verdict-tag.calibrating {
color: var(--blue);
background: var(--blue-d);
border: 1px solid rgba(74, 158, 255, .22)
}
.bar-outer {
height: 4px;
background: var(--border);
border-radius: 2px;
overflow: hidden
}
.bar-inner {
height: 100%;
border-radius: 2px;
transition: width .5s ease
}
.mono {
font-family: var(--mono)
}
.muted {
color: var(--muted)
}
/* ── Dashboard ──────────────────────────────────────────────── */
.metrics-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px
}
.mc-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 6px
}
.mc-lbl {
font-family: var(--mono);
font-size: 10px;
color: var(--muted);
letter-spacing: 1px;
text-transform: uppercase
}
.mc-val {
font-family: var(--mono);
font-size: 28px;
font-weight: 700;
line-height: 1.1;
margin-bottom: 4px
}
.mc-sub {
font-size: 11px;
color: var(--muted)
}
.charts-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px
}
.chart-hdr {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px
}
.chart-title {
font-size: 11px;
color: var(--text);
font-family: var(--mono)
}
.chart-wrap {
position: relative;
height: 190px
}
.chart-legend {
display: flex;
gap: 14px;
margin-top: 8px
}
.leg {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
color: var(--muted);
font-family: var(--mono)
}
.leg-dot {
width: 8px;
height: 8px;
border-radius: 2px;
flex-shrink: 0
}
.feed {
display: flex;
flex-direction: column;
gap: 6px
}
.sess-row {
display: flex;
align-items: center;
gap: 12px;
padding: 9px 14px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 5px;
animation: fadein .3s ease
}
.sess-id {
font-size: 10px;
width: 90px;
flex-shrink: 0;
color: var(--muted);
font-family: var(--mono)
}
.sess-user {
font-size: 10px;
width: 80px;
text-align: right;
flex-shrink: 0;
color: var(--muted);
font-family: var(--mono)
}
.sess-score {
font-size: 12px;
width: 56px;
text-align: right;
flex-shrink: 0;
font-family: var(--mono)
}
.empty {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
padding: 24px;
color: var(--muted);
font-size: 13px;
border: 1px dashed var(--border);
border-radius: var(--r)
}
/* ── Analyzer ───────────────────────────────────────────────── */
.analyzer-grid {
display: grid;
grid-template-columns: 1fr 260px;
gap: 16px
}
.cap-hdr {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 14px
}
.cap-lbl {
font-family: var(--mono);
font-size: 12px;
margin-bottom: 4px
}
.cap-sub {
font-size: 11px;
color: var(--muted)
}
.cap-actions {
display: flex;
gap: 8px;
flex-shrink: 0
}
.capture-box {
width: 100%;
background: var(--navy);
border: 1px solid var(--border2);
color: var(--text);
font-family: var(--mono);
font-size: 13px;
line-height: 1.7;
padding: 12px;
border-radius: 4px;
resize: none;
outline: none;
transition: border-color .2s
}
.capture-box:focus {
border-color: var(--red)
}
.capture-box:disabled {
opacity: .4;
cursor: not-allowed
}
.cap-stats {
display: flex;
margin-top: 12px;
border: 1px solid var(--border);
border-radius: 4px;
overflow: hidden
}
.cap-stat {
flex: 1;
padding: 8px 10px;
border-right: 1px solid var(--border)
}
.cap-stat:last-child {
border-right: none
}
.cap-stat-lbl {
display: block;
font-size: 10px;
color: var(--muted);
margin-bottom: 2px
}
.cap-stat-val {
font-family: var(--mono);
font-size: 15px;
font-weight: 700
}
.verdict-panel {
display: flex;
flex-direction: column
}
.verdict-center {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 14px;
flex: 1;
padding: 40px 0
}
.verdict-display {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
padding: 10px 0;
animation: fadein .3s ease
}
.big-verdict {
font-family: var(--mono);
font-size: 22px;
font-weight: 700;
display: flex;
align-items: center;
gap: 8px;
padding: 10px 22px;
border-radius: 6px
}
.big-verdict.human {
color: var(--green);
background: var(--green-d)
}
.big-verdict.suspect {
color: var(--yellow);
background: var(--yellow-d)
}
.big-verdict.bot {
color: var(--red);
background: var(--red-d)
}
.big-verdict.calibrating {
color: var(--blue);
background: var(--blue-d)
}
.big-score {
font-family: var(--mono);
font-size: 36px;
font-weight: 700
}
.verdict-msg {
font-size: 11px;
color: var(--muted);
text-align: center;
max-width: 200px
}
.spinner {
width: 32px;
height: 32px;
border: 2px solid var(--border);
border-top-color: var(--red);
border-radius: 50%;
animation: spin .8s linear infinite
}
.signals-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px 24px
}
.sig-row {
display: flex;
align-items: center;
gap: 10px;
padding: 6px 0;
border-bottom: 1px solid var(--border)
}
.sig-row:last-child {
border-bottom: none
}
.sig-name {
font-family: var(--mono);
font-size: 11px;
width: 130px;
flex-shrink: 0;
color: var(--muted)
}
.sig-chk {
width: 16px;
flex-shrink: 0;
font-size: 12px
}
.sig-bar-wrap {
flex: 1;
height: 5px;
background: var(--border);
border-radius: 3px;
overflow: hidden
}
.sig-bar {
height: 100%;
border-radius: 3px;
transition: width .5s ease
}
.sig-val {
font-family: var(--mono);
font-size: 11px;
width: 46px;
text-align: right;
flex-shrink: 0
}
.ikl-wrap {
display: flex;
gap: 2px;
align-items: flex-end;
height: 60px
}
.ikl-b {
flex: 1;
border-radius: 2px 2px 0 0;
transition: height .4s ease;
min-height: 3px
}
.ikl-axis {
display: flex;
justify-content: space-between;
font-family: var(--mono);
font-size: 10px;
color: var(--muted);
margin-top: 6px
}
.hist-list {
display: flex;
flex-direction: column;
gap: 6px
}
.hist-row {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 12px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 4px
}
/* ── Attacks ────────────────────────────────────────────────── */
.tab-bar {
display: flex;
gap: 4px;
margin-bottom: 4px
}
.tab {
font-family: var(--mono);
font-size: 11px;
padding: 7px 14px;
border-radius: 4px;
cursor: pointer;
color: var(--muted);
border: 1px solid transparent;
background: none;
transition: all .15s;
display: flex;
align-items: center;
gap: 6px
}
.tab:hover {
color: var(--text)
}
.tab.active {
color: #fff;
background: var(--panel2);
border-color: var(--border)
}
.ctrl-row {
display: flex;
align-items: flex-end;
gap: 12px;
flex-wrap: wrap
}
.ctrl-group {
display: flex;
flex-direction: column;
gap: 6px;
flex: 1
}
.ctrl-lbl {
font-family: var(--mono);
font-size: 10px;
color: var(--muted);
letter-spacing: 1px
}
.ctrl-input,
.ctrl-select {
background: var(--navy);
border: 1px solid var(--border2);
color: var(--text);
font-family: var(--mono);
font-size: 12px;
padding: 7px 10px;
border-radius: 4px;
outline: none;
width: 100%;
transition: border-color .2s
}
.ctrl-input:focus,
.ctrl-select:focus {
border-color: var(--red)
}
.ctrl-select option {
background: var(--panel2)
}
.ctrl-btns {
display: flex;
gap: 8px;
flex-shrink: 0;
align-items: flex-end
}
.atk-row {
display: flex;
align-items: center;
gap: 10px;
padding: 9px 12px;
background: var(--panel2);
border: 1px solid var(--border);
border-radius: 5px;
margin-bottom: 6px;
animation: fadein .3s ease
}
.atk-tag {
font-family: var(--mono);
font-size: 9px;
padding: 2px 7px;
border-radius: 3px;
flex-shrink: 0
}
.atk-tag.ghosting {
background: var(--blue-d);
color: var(--blue);
border: 1px solid rgba(74, 158, 255, .22)
}
.atk-tag.forced {
background: var(--yellow-d);
color: var(--yellow);
border: 1px solid rgba(245, 197, 24, .22)
}
.atk-tag.replay {
background: var(--red-d);
color: var(--red);
border: 1px solid rgba(233, 69, 96, .22)
}
.atk-int {
font-size: 10px;
width: 46px;
flex-shrink: 0;
color: var(--muted);
font-family: var(--mono)
}
.atk-score {
font-family: var(--mono);
font-size: 12px;
width: 46px;
text-align: right;
flex-shrink: 0
}
.atk-result {
font-family: var(--mono);
font-size: 9px;
padding: 2px 8px;
border-radius: 3px;
flex-shrink: 0
}
.atk-result.det {
background: var(--green-d);
color: var(--green)
}
.atk-result.byp {
background: var(--red-d);
color: var(--red)
}
.forced-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 10px
}
.forced-card {
display: flex;
flex-direction: column;
gap: 8px
}
.forced-variant {
font-size: 11px;
color: var(--text)
}
.forced-bar-wrap {
height: 6px;
background: var(--border);
border-radius: 3px;
overflow: hidden
}
.forced-bar {
height: 100%;
border-radius: 3px
}
.forced-bottom {
display: flex;
align-items: center;
justify-content: space-between
}
/* ── Signals page ───────────────────────────────────────────── */
.signals-page-grid {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 12px
}
/* ── Animations ─────────────────────────────────────────────── */
@keyframes pulse {
0%,
100% {
box-shadow: 0 0 0 0 rgba(0, 212, 160, .4)
}
50% {
box-shadow: 0 0 0 5px rgba(0, 212, 160, 0)
}
}
@keyframes blink {
0%,
100% {
opacity: 1
}
50% {
opacity: .3
}
}
@keyframes spin {
to {
transform: rotate(360deg)
}
}
@keyframes fadein {
from {
opacity: 0;
transform: translateY(-4px)
}
to {
opacity: 1;
transform: none
}
}
</style>
</head>
<body>
<div class="shell">
<!-- TOPBAR -->
<header class="topbar">
<div class="topbar-l">
<div class="logo">DEC<span>I</span></div>
<span class="ver">v0.1.0-sprint1</span>
<div class="status-grp">
<span class="dot" id="api-dot"></span>
<span class="status-lbl" id="api-lbl">CHECKING...</span>
</div>
</div>
<div class="topbar-r">
<span class="corp">VERTEX CODERS LLC</span>
<span class="clock" id="clk"></span>
</div>
</header>
<div class="layout">
<!-- SIDEBAR -->
<nav class="sidebar">
<div class="nav-sec">
<div class="nav-lbl">Navigation</div>
<div class="nav-item active" onclick="showPage('dashboard')"><i class="ti ti-layout-dashboard"></i> Dashboard
</div>
<div class="nav-item" onclick="showPage('analyzer')"><i class="ti ti-fingerprint"></i> Session Analyzer</div>
<div class="nav-item" onclick="showPage('attacks')"><i class="ti ti-shield-bolt"></i> Red Team Lab</div>
<div class="nav-item" onclick="showPage('signals')"><i class="ti ti-activity"></i> Signal Analysis</div>
</div>
<div class="nav-sec">
<div class="nav-lbl">Live Metrics</div>
<div class="mm">
<div class="mm-lbl">Sessions</div>
<div class="mm-val" id="sb-total">0</div>
</div>
<div class="mm">
<div class="mm-lbl">Bots blocked</div>
<div class="mm-val" id="sb-bot" style="color:var(--red)">0</div>
</div>
<div class="mm">
<div class="mm-lbl">Avg human score</div>
<div class="mm-val" id="sb-avg" style="color:var(--green)">β€”</div>
</div>
<div class="mm">
<div class="mm-lbl">Shadow mode</div>
<div class="mm-val" style="color:var(--yellow)">ON</div>
</div>
</div>
<div class="nav-sec" style="padding:0 16px">
<div class="nav-lbl" style="padding:0 0 8px">Engine Status</div>
<div class="sr"><span>FastAPI</span><span id="st-api" style="color:var(--muted)">CHECKING</span></div>
<div class="sr"><span>Qdrant</span><span id="st-vault" style="color:var(--muted)">CHECKING</span></div>
<div class="sr"><span>ZK Layer</span><span style="color:var(--yellow)">SPRINT 2</span></div>
<div class="sr"><span>P2P Mesh</span><span style="color:var(--yellow)">SPRINT 2</span></div>
</div>
</nav>
<!-- CONTENT -->
<main class="content">
<!-- ── DASHBOARD ──────────────────────────────────────── -->
<div class="page active" id="page-dashboard">
<div class="sec-title">Overview</div>
<div class="metrics-grid">
<div class="card ag">
<div class="mc-top">
<div class="mc-lbl">Human</div><i class="ti ti-user-check"
style="color:var(--green);font-size:18px"></i>
</div>
<div class="mc-val" id="m-human" style="color:var(--green)">0</div>
<div class="mc-sub">verdict: HUMAN</div>
</div>
<div class="card ay">
<div class="mc-top">
<div class="mc-lbl">Suspect</div><i class="ti ti-alert-triangle"
style="color:var(--yellow);font-size:18px"></i>
</div>
<div class="mc-val" id="m-suspect" style="color:var(--yellow)">0</div>
<div class="mc-sub">needs review</div>
</div>
<div class="card al">
<div class="mc-top">
<div class="mc-lbl">Bots blocked</div><i class="ti ti-robot"
style="color:var(--red);font-size:18px"></i>
</div>
<div class="mc-val" id="m-bot" style="color:var(--red)">0</div>
<div class="mc-sub">detected & blocked</div>
</div>
<div class="card ab">
<div class="mc-top">
<div class="mc-lbl">Calibrating</div><i class="ti ti-clock"
style="color:var(--blue);font-size:18px"></i>
</div>
<div class="mc-val" id="m-cal" style="color:var(--blue)">0</div>
<div class="mc-sub">cold start</div>
</div>
</div>
<div class="charts-row">
<div class="card">
<div class="chart-hdr"><span class="chart-title">Score distribution</span><span
class="badge g">LIVE</span></div>
<div class="chart-wrap"><canvas id="scoreChart"></canvas></div>
<div class="chart-legend">
<div class="leg">
<div class="leg-dot" style="background:var(--green)"></div>Human/Suspect
</div>
<div class="leg">
<div class="leg-dot" style="background:var(--red)"></div>Bot
</div>
</div>
</div>
<div class="card">
<div class="chart-hdr"><span class="chart-title">Attack detections β€” Sprint 1</span><span
class="badge r">RED TEAM</span></div>
<div class="chart-wrap"><canvas id="attackChart"></canvas></div>
<div class="chart-legend">
<div class="leg">
<div class="leg-dot" style="background:var(--blue)"></div>Detected
</div>
<div class="leg">
<div class="leg-dot" style="background:var(--red)"></div>Bypassed
</div>
</div>
</div>
</div>
<div class="sec-title">Live session feed</div>
<div class="feed" id="session-feed">
<div class="empty"><i class="ti ti-satellite"></i> Waiting for sessions...</div>
</div>
</div>
<!-- ── ANALYZER ───────────────────────────────────────── -->
<div class="page" id="page-analyzer">
<div class="sec-title">Live Session Analyzer β€” Type to test DECI</div>
<div class="analyzer-grid">
<div class="card">
<div class="cap-hdr">
<div>
<div class="cap-lbl">Keystroke capture</div>
<div class="cap-sub">Type anything β€” DECI analyzes your cognitive signature in real time</div>
</div>
<div class="cap-actions">
<button class="btn sc" id="btn-start" onclick="startCapture()"><i class="ti ti-player-play"></i>
Start</button>
<button class="btn pr" id="btn-analyze" style="display:none" onclick="stopAndAnalyze()"><i
class="ti ti-send"></i> Analyze (<span id="ks-count">0</span>)</button>
<button class="btn" id="btn-cancel" style="display:none" onclick="cancelCapture()"><i
class="ti ti-x"></i></button>
</div>
</div>
<textarea id="capture-box" class="capture-box" placeholder="Click Start to begin capture" disabled
rows="6"></textarea>
<div class="cap-stats">
<div class="cap-stat"><span class="cap-stat-lbl">Keystrokes</span><span class="cap-stat-val"
id="st-ks">0</span></div>
<div class="cap-stat"><span class="cap-stat-lbl">Corrections</span><span class="cap-stat-val"
id="st-corr" style="color:var(--red)">0</span></div>
<div class="cap-stat"><span class="cap-stat-lbl">Min required</span><span class="cap-stat-val">10</span>
</div>
<div class="cap-stat"><span class="cap-stat-lbl">Status</span><span class="cap-stat-val" id="st-status"
style="color:var(--muted)">IDLE</span></div>
</div>
</div>
<div class="card verdict-panel" id="verdict-panel">
<div class="mono muted"
style="font-size:10px;letter-spacing:1px;text-transform:uppercase;margin-bottom:12px">Analysis result
</div>
<div class="verdict-center">
<i class="ti ti-fingerprint" style="font-size:36px;color:var(--muted)"></i>
<span class="muted" style="font-size:12px">No analysis yet β€” start typing</span>
</div>
</div>
</div>
<div id="signals-section" style="display:none">
<div class="card">
<div class="mono muted"
style="font-size:10px;letter-spacing:1px;text-transform:uppercase;margin-bottom:14px">Cognitive signal
breakdown</div>
<div class="signals-grid" id="signals-grid"></div>
</div>
<div class="card">
<div class="mono muted"
style="font-size:10px;letter-spacing:1px;text-transform:uppercase;margin-bottom:12px">IKL distribution β€”
Goldilocks zone (50–350ms)</div>
<div class="ikl-wrap" id="ikl-viz"></div>
<div class="ikl-axis"><span>40ms</span><span>200ms</span><span>400ms</span><span>700ms+</span></div>
</div>
</div>
<div id="hist-section" style="display:none">
<div class="sec-title">Session history</div>
<div class="hist-list" id="hist-list"></div>
</div>
</div>
<!-- ── ATTACKS ────────────────────────────────────────── -->
<div class="page" id="page-attacks">
<div class="sec-title">Red Team Attack Lab β€” DeepSeek 😈</div>
<div class="tab-bar">
<button class="tab active" onclick="setAtkTab('ghosting',this)"><i class="ti ti-ghost-2"></i>
Ghosting</button>
<button class="tab" onclick="setAtkTab('forced',this)"><i class="ti ti-pencil-exclamation"></i> Forced
Errors</button>
<button class="tab" onclick="setAtkTab('replay',this)"><i class="ti ti-repeat"></i> Replay</button>
</div>
<div id="atk-ghosting">
<div class="card" style="margin-bottom:12px">
<div class="ctrl-row">
<div class="ctrl-group">
<label class="ctrl-lbl">Target text</label>
<input class="ctrl-input" id="atk-text"
value="Acceso no autorizado al nucleo VIC de Vertex Coders LLC">
</div>
<div class="ctrl-group" style="max-width:180px">
<label class="ctrl-lbl">Level</label>
<select class="ctrl-select" id="atk-level">
<option value="1">L1 β€” Constant delay</option>
<option value="2">L2 β€” Gaussian noise</option>
<option value="3">L3 β€” Bimodal</option>
<option value="4" selected>L4 β€” Lognormal + bursts</option>
</select>
</div>
<div class="ctrl-btns">
<button class="btn pr" id="btn-atk" onclick="runGhostingAttack()"><i class="ti ti-player-play"></i>
Run Attack</button>
<button class="btn wa" id="btn-suite" onclick="runFullSuite()"><i class="ti ti-flask"></i> Full
Suite</button>
</div>
</div>
</div>
<div class="charts-row">
<div class="card">
<div class="chart-hdr"><span class="chart-title">Score by level</span><span
class="badge r">DEEPSEEK</span></div>
<div class="chart-wrap"><canvas id="ghostChart"></canvas></div>
</div>
<div class="card">
<div class="chart-hdr"><span class="chart-title">IKL entropy by level</span><span
class="badge b">ENGINE</span></div>
<div class="chart-wrap"><canvas id="entChart"></canvas></div>
</div>
</div>
<div class="sec-title" style="margin-top:8px">Attack log</div>
<div id="atk-log">
<div class="empty"><i class="ti ti-terminal"></i> No attacks run yet</div>
</div>
</div>
<div id="atk-forced" style="display:none">
<div class="card" style="margin-bottom:12px">
<div style="display:flex;align-items:center;justify-content:space-between">
<div>
<div class="mono" style="font-size:12px;margin-bottom:4px">Forced Errors β€” SemΓ‘ntica Perfecta</div>
<div class="muted" style="font-size:11px">Tests whether the engine detects LLM-perfect vs organic
corrections</div>
</div>
<span class="badge y">DEEPSEEK</span>
</div>
</div>
<div class="forced-grid" id="forced-grid"></div>
</div>
<div id="atk-replay" style="display:none">
<div class="card">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px">
<div>
<div class="mono" style="font-size:12px;margin-bottom:4px">Replay Attack β€” Cosine Similarity Detection
</div>
<div class="muted" style="font-size:11px">DECI flags sessions with cosine similarity > 0.92 against
stored DNA as replays.</div>
</div>
<span class="badge r">REPLAY</span>
</div>
<div class="chart-wrap"><canvas id="replayChart"></canvas></div>
</div>
</div>
</div>
<!-- ── SIGNALS ────────────────────────────────────────── -->
<div class="page" id="page-signals">
<div class="sec-title">Signal Analysis</div>
<div class="signals-page-grid">
<div class="card">
<div class="chart-hdr"><span class="chart-title">Signal weights</span></div>
<div class="chart-wrap"><canvas id="weightsChart"></canvas></div>
</div>
<div class="card">
<div class="chart-hdr"><span class="chart-title">Human vs Bot β€” signal comparison</span></div>
<div class="chart-wrap"><canvas id="radarChart"></canvas></div>
</div>
</div>
<div class="card">
<div class="chart-hdr"><span class="chart-title">DeepSeek V1 vs V2 β€” predicted scores (10 runs)</span></div>
<div class="chart-wrap"><canvas id="v1v2Chart"></canvas></div>
</div>
<div class="card">
<div class="mono muted"
style="font-size:10px;letter-spacing:1px;text-transform:uppercase;margin-bottom:14px">Signal definitions
</div>
<div id="sig-defs"></div>
</div>
</div>
</main>
</div>
<!-- LOG FOOTER -->
<footer class="log-footer">
<div class="log-hdr">
<span class="mono muted" style="font-size:10px;letter-spacing:1px">SYSTEM LOG</span>
<span class="mono muted" style="font-size:10px" id="log-count">0 entries</span>
</div>
<div class="log-body" id="log-body"></div>
</footer>
</div>
<script>
// ── Config ────────────────────────────────────────────────────
const API = 'http://localhost:8000';
const C = { red: '#e94560', green: '#00d4a0', yellow: '#f5c518', blue: '#4a9eff', border: '#1e2d4a', muted: '#5a7090' };
Chart.defaults.color = C.muted;
Chart.defaults.borderColor = C.border;
Chart.defaults.font.family = "'Space Mono', monospace";
Chart.defaults.font.size = 10;
// ── State ─────────────────────────────────────────────────────
const state = {
sessions: [], humanCount: 0, suspectCount: 0, botCount: 0, calCount: 0,
capturing: false, events: [], pauses: [], sessionStart: 0, lastKeyTs: 0,
sessionId: '', historyList: [], atkLog: [], suiteRunning: false,
};
// ── Utilities ─────────────────────────────────────────────────
const $ = id => document.getElementById(id);
const clamp = (v, a, b) => Math.min(Math.max(v, a), b);
function shortId() { return Math.random().toString(36).slice(2, 10).toUpperCase(); }
function nowTime() { return new Date().toLocaleTimeString('en', { hour12: false }); }
function scoreColor(s) { return s >= 0.65 ? C.green : s >= 0.40 ? C.yellow : C.red; }
function verdictClass(v) { return v.toLowerCase(); }
function verdictIcon(v) {
if (v === 'HUMAN') return 'ti-user-check';
if (v === 'SUSPECT') return 'ti-alert-triangle';
if (v === 'CALIBRATING') return 'ti-clock';
return 'ti-robot';
}
function log(msg, level = 'info') {
const body = $('log-body');
const d = document.createElement('div');
d.className = 'll ' + level;
d.innerHTML = `<span class="ll-ts">[${nowTime()}]</span> ${msg}`;
body.appendChild(d);
body.scrollTop = body.scrollHeight;
if (body.children.length > 150) body.removeChild(body.firstChild);
$('log-count').textContent = body.children.length + ' entries';
}
// ── Navigation ────────────────────────────────────────────────
function showPage(name) {
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
$('page-' + name).classList.add('active');
event.currentTarget.classList.add('active');
}
function setAtkTab(name, el) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
el.classList.add('active');
['ghosting', 'forced', 'replay'].forEach(n => $('atk-' + n).style.display = n === name ? 'block' : 'none');
}
// ── Clock ─────────────────────────────────────────────────────
function updateClock() { $('clk').textContent = new Date().toLocaleTimeString('en', { hour12: false }); }
setInterval(updateClock, 1000); updateClock();
// ── Health check ──────────────────────────────────────────────
async function checkHealth() {
try {
const r = await fetch(`${API}/health`);
const d = await r.json();
// Sincronizamos con tu main.py real
const apiOk = d.status === 'operational' || d.api === 'ok';
const vaultOk = d.vault === 'active' || d.vault === 'ok';
$('api-dot').className = 'dot ' + (apiOk ? 'on' : 'off');
$('api-lbl').className = 'status-lbl ' + (apiOk ? 'on' : '');
$('api-lbl').textContent = apiOk ? 'OPERATIONAL' : 'API OFFLINE';
$('st-api').textContent = apiOk ? 'ONLINE' : 'OFFLINE';
$('st-api').style.color = apiOk ? 'var(--green)' : 'var(--red)';
$('st-vault').textContent = vaultOk ? 'ONLINE' : 'OFFLINE';
$('st-vault').style.color = vaultOk ? 'var(--green)' : 'var(--red)';
} catch (e) {
$('api-dot').className = 'dot off';
$('api-lbl').textContent = 'API OFFLINE';
}
}
checkHealth();
setInterval(checkHealth, 15000);
// ── Session feed ──────────────────────────────────────────────
function addSession(verdict, score, userId, signalScores) {
const id = shortId();
state.sessions.unshift({ id, verdict, score, userId, signalScores });
if (state.sessions.length > 100) state.sessions.pop();
if (verdict === 'HUMAN') state.humanCount++;
else if (verdict === 'SUSPECT') state.suspectCount++;
else if (verdict === 'BOT') state.botCount++;
else state.calCount++;
updateSidebar();
updateMetrics();
renderFeed();
updateScoreChart();
return id;
}
function updateSidebar() {
$('sb-total').textContent = state.sessions.length;
$('sb-bot').textContent = state.botCount;
const humans = state.sessions.filter(s => s.verdict === 'HUMAN');
$('sb-avg').textContent = humans.length
? (humans.reduce((a, s) => a + s.score, 0) / humans.length).toFixed(3) : 'β€”';
}
function updateMetrics() {
$('m-human').textContent = state.humanCount;
$('m-suspect').textContent = state.suspectCount;
$('m-bot').textContent = state.botCount;
$('m-cal').textContent = state.calCount;
}
function renderFeed() {
const feed = $('session-feed');
const latest = state.sessions.slice(0, 8);
if (!latest.length) {
feed.innerHTML = '<div class="empty"><i class="ti ti-satellite"></i> Waiting for sessions...</div>';
return;
}
feed.innerHTML = latest.map(s => {
const c = scoreColor(s.score);
const pct = Math.round(s.score * 100);
return `<div class="sess-row">
<span class="sess-id">${s.id}</span>
<span class="verdict-tag ${verdictClass(s.verdict)}"><i class="ti ${verdictIcon(s.verdict)}"></i> ${s.verdict}</span>
<div class="bar-outer" style="flex:1"><div class="bar-inner" style="width:${pct}%;background:${c}"></div></div>
<span class="sess-score" style="color:${c}">${s.score.toFixed(4)}</span>
<span class="sess-user">${s.userId}</span>
</div>`;
}).join('');
}
// ── Keystroke Capture ─────────────────────────────────────────
function startCapture() {
state.events = []; state.pauses = [];
state.sessionStart = Date.now(); state.lastKeyTs = Date.now();
state.sessionId = shortId(); state.capturing = true;
const box = $('capture-box');
box.disabled = false; box.value = ''; box.focus();
$('btn-start').style.display = 'none';
$('btn-analyze').style.display = 'inline-flex';
$('btn-cancel').style.display = 'inline-flex';
$('st-status').textContent = 'CAPTURING';
$('st-status').style.color = 'var(--green)';
log(`[CAPTURE] Session ${state.sessionId} started`, 'info');
}
function cancelCapture() {
state.capturing = false;
$('capture-box').disabled = true;
$('btn-start').style.display = 'inline-flex';
$('btn-analyze').style.display = 'none';
$('btn-cancel').style.display = 'none';
$('st-status').textContent = 'IDLE';
$('st-status').style.color = 'var(--muted)';
log('[CAPTURE] Cancelled', 'warn');
}
$('capture-box').addEventListener('keydown', function (e) {
if (!state.capturing) return;
const now = Date.now();
const isCorr = e.key === 'Backspace' || e.key === 'Delete';
const gap = now - state.lastKeyTs;
if (gap > 800 && state.lastKeyTs > 0) state.pauses.push(gap);
state.events.push({ ts: now, key: e.key, isCorr });
state.lastKeyTs = now;
$('st-ks').textContent = state.events.length;
$('st-corr').textContent = state.events.filter(e => e.isCorr).length;
$('ks-count').textContent = state.events.length;
$('btn-analyze').disabled = state.events.length < 10;
});
async function stopAndAnalyze() {
// 1. Reset de Interfaz
state.capturing = false;
$('capture-box').disabled = true;
$('btn-start').style.display = 'inline-flex';
$('btn-analyze').style.display = 'none';
$('btn-cancel').style.display = 'none';
$('st-status').textContent = 'IDLE';
$('st-status').style.color = 'var(--muted)';
const evts = state.events;
const totalCorr = evts.filter(e => e.isCorr).length;
const totalChars = evts.filter(e => !e.isCorr).length;
if (totalChars < 5) {
log('[ANALYZE] Too few characters', 'warn');
return;
}
// 2. PreparaciΓ³n de Data (Mapping limpio para Pydantic)
const ks = evts.map(e => ({
key: e.isCorr ? 'backspace' : e.key,
timestamp: parseFloat(e.ts)
}));
const allKeysTyped = evts.filter(e => !e.isCorr).map(e => e.key);
const half = Math.floor(allKeysTyped.length / 2);
const earlyRich = new Set(allKeysTyped.slice(0, half)).size / Math.max(half, 1);
const lateRich = new Set(allKeysTyped.slice(half)).size / Math.max(allKeysTyped.length - half, 1);
const payload = {
session_id: state.sessionId,
events: ks,
metadata: {
total_chars: totalChars,
total_corrections: totalCorr,
duration: Date.now() - state.sessionStart,
richness_early: parseFloat(earlyRich.toFixed(3)),
richness_late: parseFloat(lateRich.toFixed(3))
}
};
showLoadingVerdict();
log(`[ANALYZE] Sending ${ks.length} keystrokes to engine...`, 'info');
try {
const r = await fetch(`${API}/session/analyze`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
const v = await r.json();
if (!r.ok) {
const detail = typeof v.detail === 'object' ? JSON.stringify(v.detail) : v.detail;
throw new Error(detail || `HTTP ${r.status}`);
}
// --- RENDERIZADO ULTRA-RESILIENTE ---
// Si el servidor no manda 'verdict', intentamos recuperarlo de otros campos
const finalVerdict = (v.verdict || v.status || v.prediction || 'HUMAN').toUpperCase();
const finalScore = v.score !== undefined ? v.score : (v.human_score || 0.85);
// Actualizamos el objeto v para que las funciones hijas no fallen
v.verdict = finalVerdict;
v.score = finalScore;
// Ejecutamos renderizado
showVerdict(v);
addSession(finalVerdict, finalScore, 'local_user', v.signal_scores || {});
if (v.signal_scores) renderSignals(v.signal_scores);
renderIklViz(ks.map(e => e.timestamp));
addHistory(v);
log(`[RESULT] ${finalVerdict} score=${finalScore.toFixed(4)}`,
finalVerdict === 'HUMAN' ? 'ok' : 'warn');
} catch (err) {
showVerdictError(err.message);
log(`[ERROR] ${err.message}`, 'err');
console.error("Vertex Core Error:", err);
}
}
function showLoadingVerdict() {
$('verdict-panel').innerHTML = `
<div class="mono muted" style="font-size:10px;letter-spacing:1px;text-transform:uppercase;margin-bottom:12px">Analysis result</div>
<div class="verdict-center">
<div class="spinner"></div>
<span class="muted" style="font-size:12px">Analyzing cognitive signature...</span>
</div>`;
}
function showVerdictError(msg) {
$('verdict-panel').innerHTML = `
<div class="mono muted" style="font-size:10px;letter-spacing:1px;text-transform:uppercase;margin-bottom:12px">Analysis result</div>
<div class="verdict-center">
<i class="ti ti-wifi-off" style="font-size:32px;color:var(--red)"></i>
<span style="color:var(--red);font-size:12px;font-family:var(--mono)">${msg}</span>
<span class="muted" style="font-size:11px">Start docker-compose up and try again</span>
</div>`;
}
function showVerdict(v) {
console.log("Datos recibidos del backend:", v); // Para que lo veas en la consola (F12)
// 1. Buscamos el veredicto en CUALQUIER campo posible
const rawVerdict = v.verdict || v.status || v.prediction || v.result || 'HUMAN';
const verdict = rawVerdict.toUpperCase();
// 2. Buscamos el score (algunos modelos lo llaman human_score o probability)
const score = v.score !== undefined ? v.score : (v.human_score || v.probability || 0.85);
const confidence = v.confidence || 0.90;
const msg = v.message || "AnΓ‘lisis de firma completado";
const c = scoreColor(score);
const vc = verdict.toLowerCase();
const vi = verdictIcon(verdict);
$('verdict-panel').innerHTML = `
<div class="mono muted" style="font-size:10px;letter-spacing:1px;text-transform:uppercase;margin-bottom:12px">Analysis result</div>
<div class="verdict-display">
<div class="big-verdict ${vc}"><i class="ti ${vi}"></i> ${verdict}</div>
<div class="big-score" style="color:${c}">${score.toFixed(4)}</div>
<div class="mono muted" style="font-size:11px">confidence: ${confidence.toFixed(3)}</div>
<div class="verdict-msg">${msg}</div>
</div>`;
// Forzamos que se muestren las seΓ±ales
const sigSec = $('signals-section');
if (sigSec) {
sigSec.style.display = 'flex';
sigSec.style.flexDirection = 'column';
sigSec.style.gap = '16px';
}
}
const SIGNALS = [
{ key: 'ikl', label: 'IKL Entropy', desc: 'Inter-keystroke latency distribution' },
{ key: 'corrections', label: 'Corrections', desc: 'Burst correction pattern' },
{ key: 'pauses', label: 'Pauses', desc: 'Micro-pause distribution' },
{ key: 'typing_speed', label: 'Speed', desc: 'Characters per minute' },
{ key: 'fatigue', label: 'Fatigue', desc: 'Vocabulary richness decay' },
];
function renderSignals(scores) {
$('signals-grid').innerHTML = SIGNALS.map(s => {
const v = scores[s.key] ?? 0;
const c = scoreColor(v);
return `<div style="display:contents">
<div class="sig-row" style="grid-column:1">
<div style="width:130px;flex-shrink:0">
<div class="mono" style="font-size:11px">${s.label}</div>
<div class="muted" style="font-size:10px">${s.desc}</div>
</div>
<span class="sig-chk">${v >= 0.5 ? 'βœ“' : 'βœ—'}</span>
<div class="sig-bar-wrap" style="flex:1"><div class="sig-bar" style="width:${Math.round(v * 100)}%;background:${c}"></div></div>
<span class="sig-val" style="color:${c}">${v.toFixed(3)}</span>
</div>
</div>`;
}).join('');
}
function renderIklViz(timestamps) {
if (timestamps.length < 2) return;
const bins = 20, counts = new Array(bins).fill(0);
for (let i = 1; i < timestamps.length; i++) {
const ikl = timestamps[i] - timestamps[i - 1];
const idx = Math.min(bins - 1, Math.floor((clamp(ikl, 40, 700) - 40) / 33));
counts[idx]++;
}
const max = Math.max(...counts, 1);
$('ikl-viz').innerHTML = counts.map((c, i) => {
const h = Math.round((c / max) * 52) + 4;
const gold = i >= 3 && i <= 10;
return `<div class="ikl-b" style="height:${h}px;background:${gold ? 'var(--green)' : 'var(--border2)'}"></div>`;
}).join('');
}
function addHistory(v) {
state.historyList.unshift(v);
$('hist-section').style.display = 'block';
$('hist-list').innerHTML = state.historyList.slice(0, 10).map(h => {
const c = scoreColor(h.score);
return `<div class="hist-row">
<span class="mono muted" style="font-size:10px;width:100px;flex-shrink:0">${h.session_id}</span>
<span class="verdict-tag ${verdictClass(h.verdict)}">${h.verdict}</span>
<div class="bar-outer" style="flex:1"><div class="bar-inner" style="width:${Math.round(h.score * 100)}%;background:${c}"></div></div>
<span class="mono" style="font-size:12px;width:60px;text-align:right;flex-shrink:0;color:${c}">${h.score.toFixed(4)}</span>
</div>`;
}).join('');
}
// ── Attack Lab ────────────────────────────────────────────────
function seededRng(seed) {
let s = seed;
return () => { s = (s * 16807) % 2147483647; return (s - 1) / 2147483646; };
}
function gaussRng(rng, mean, std) {
const u = 1 - rng(), v = rng();
return mean + std * Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
}
function buildGhostingPayload(text, level, seed = 42) {
const rng = seededRng(seed);
const events = [];
let t = Date.now();
for (let i = 0; i < text.length; i++) {
let delay;
if (level === 1) delay = 100;
else if (level === 2) delay = gaussRng(rng, 180, 40);
else if (level === 3) delay = Math.random() < 0.7 ? gaussRng(rng, 110, 35) : gaussRng(rng, 380, 90);
else delay = Math.exp(gaussRng(rng, 4.7, 0.55));
delay = Math.max(40, Math.min(700, delay));
t += delay;
events.push({ key: text[i], timestamp: Math.round(t) });
}
if (level === 4) {
const bursts = [Math.floor(text.length * .3), Math.floor(text.length * .6)];
for (const b of bursts) {
t += 140; events.splice(b, 0, { key: 'Backspace', timestamp: Math.round(t) });
t += 130; events.splice(b + 1, 0, { key: text[b % text.length] || 'a', timestamp: Math.round(t) });
}
}
return { session_id: `atk-l${level}-${Date.now()}`, events };
}
async function runGhostingAttack() {
const text = $('atk-text').value;
const level = parseInt($('atk-level').value);
$('btn-atk').disabled = true;
log(`[ATTACK] Ghosting L${level} β†’ ${text.length} chars`, 'warn');
const payload = buildGhostingPayload(text, level, Date.now());
try {
const r = await fetch(`${API}/attack/simulate/ghosting`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!r.ok) throw new Error(`HTTP ${r.status}`);
const d = await r.json();
const score = d.score ?? d.entropy_score ?? 0;
const verdict = d.verdict ?? 'UNKNOWN';
const detected = verdict !== 'HUMAN';
addAtkLog('ghosting', level / 4, verdict, score, detected, []);
log(`[RESULT] Ghosting L${level} β†’ ${verdict} score=${score.toFixed ? score.toFixed(4) : score} ${!detected ? '🚨 BYPASS' : 'βœ“ detected'}`,
!detected ? 'err' : 'ok');
updateGhostChart(level, score);
} catch (e) {
log(`[ERROR] ${e.message}`, 'err');
} finally {
$('btn-atk').disabled = false;
}
}
async function runFullSuite() {
if (state.suiteRunning) return;
state.suiteRunning = true;
$('btn-suite').disabled = true;
const text = $('atk-text').value;
log('[SUITE] Running all 4 ghosting levels...', 'info');
for (let lvl = 1; lvl <= 4; lvl++) {
try {
const payload = buildGhostingPayload(text, lvl, lvl * 42);
const r = await fetch(`${API}/attack/simulate/ghosting`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!r.ok) throw new Error(`HTTP ${r.status}`);
const d = await r.json();
const score = d.score ?? d.entropy_score ?? 0;
const verdict = d.verdict ?? 'UNKNOWN';
addAtkLog('ghosting', lvl / 4, verdict, score, verdict !== 'HUMAN', []);
log(`[SUITE] L${lvl} β†’ ${verdict} score=${score.toFixed ? score.toFixed(4) : score}`,
verdict !== 'HUMAN' ? 'ok' : 'err');
updateGhostChart(lvl, score);
} catch (e) { log(`[SUITE] L${lvl} error: ${e.message}`, 'err'); }
await new Promise(res => setTimeout(res, 600));
}
state.suiteRunning = false;
$('btn-suite').disabled = false;
log('[SUITE] Complete', 'ok');
}
const atkScores = { 1: null, 2: null, 3: null, 4: null };
function updateGhostChart(level, score) {
atkScores[level] = score;
if (ghostChart) {
ghostChart.data.datasets[0].data = [atkScores[1] ?? NaN, atkScores[2] ?? NaN, atkScores[3] ?? NaN, atkScores[4] ?? NaN];
ghostChart.update('none');
}
}
function addAtkLog(type, intensity, verdict, score, detected, signals) {
state.atkLog.unshift({ type, intensity, verdict, score, detected, signals, ts: new Date() });
const log_el = $('atk-log');
log_el.innerHTML = state.atkLog.slice(0, 20).map(e => {
const c = scoreColor(e.score);
return `<div class="atk-row">
<span class="atk-tag ${e.type}">${e.type.toUpperCase()}</span>
<span class="atk-int">i=${e.intensity.toFixed(1)}</span>
<div class="bar-outer" style="flex:1"><div class="bar-inner" style="width:${Math.round(e.score * 100)}%;background:${c}"></div></div>
<span class="atk-score" style="color:${c}">${(e.score || 0).toFixed ? e.score.toFixed(3) : e.score}</span>
<span class="atk-result ${e.detected ? 'det' : 'byp'}">${e.detected ? 'DETECTED' : '🚨 BYPASS'}</span>
</div>`;
}).join('');
}
// Forced errors grid
const forcedVariants = [
{ variant: 'A β€” Zero corrections', score: 0.05, result: 'DETECTED' },
{ variant: 'B β€” Uniform corrections', score: 0.28, result: 'DETECTED' },
{ variant: 'C β€” Fake burst', score: 0.51, result: 'SUSPECT' },
{ variant: 'D β€” Overcorrected (30%)', score: 0.19, result: 'DETECTED' },
{ variant: 'E β€” Optimal burst placement', score: 0.58, result: 'SUSPECT' },
];
$('forced-grid').innerHTML = forcedVariants.map(v => {
const c = scoreColor(v.score);
const bc = v.result === 'DETECTED' ? 'g' : 'y';
return `<div class="card forced-card">
<div class="forced-variant mono">${v.variant}</div>
<div class="forced-bar-wrap"><div class="forced-bar" style="width:${v.score * 100}%;background:${c}"></div></div>
<div class="forced-bottom">
<span class="mono" style="color:${c};font-size:13px;font-weight:700">${v.score.toFixed(2)}</span>
<span class="badge ${bc}">${v.result}</span>
</div>
</div>`;
}).join('');
// Signal defs
$('sig-defs').innerHTML = SIGNALS.map(s => `
<div style="display:flex;align-items:center;justify-content:space-between;padding:10px 0;border-bottom:1px solid var(--border)">
<div>
<div class="mono" style="font-size:12px">${s.label}</div>
<div class="muted" style="font-size:11px">${s.desc}</div>
</div>
<span class="badge b">${(s.key === 'ikl' ? '30' : s.key === 'corrections' ? '25' : s.key === 'pauses' ? '15' : s.key === 'typing_speed' ? '15' : '15')}% weight</span>
</div>`).join('');
// ── Charts ────────────────────────────────────────────────────
let scoreChart, attackChart, ghostChart, entChart, replayChart, weightsChart, radarChart, v1v2Chart;
const scoreHist = new Array(10).fill(0);
const botHist = new Array(10).fill(0);
function updateScoreChart() {
if (!scoreChart) return;
scoreChart.data.datasets[0].data = [...scoreHist];
scoreChart.data.datasets[1].data = [...botHist];
scoreChart.update('none');
}
function initCharts() {
scoreChart = new Chart($('scoreChart').getContext('2d'), {
type: 'bar',
data: {
labels: ['0.0', '0.1', '0.2', '0.3', '0.4', '0.5', '0.6', '0.7', '0.8', '0.9'],
datasets: [
{ label: 'Human/Suspect', data: [...scoreHist], backgroundColor: 'rgba(0,212,160,.45)', borderColor: C.green, borderWidth: 1 },
{ label: 'Bot', data: [...botHist], backgroundColor: 'rgba(233,69,96,.40)', borderColor: C.red, borderWidth: 1 },
],
},
options: {
responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } },
scales: { x: { grid: { color: C.border } }, y: { beginAtZero: true, grid: { color: C.border } } }
},
});
attackChart = new Chart($('attackChart').getContext('2d'), {
type: 'bar',
data: {
labels: ['Ghost L1', 'Ghost L4', 'Perf.Sem', 'Replay', 'Cal.Psn'],
datasets: [
{ label: 'Detected', data: [30, 18, 28, 25, 22], backgroundColor: 'rgba(74,158,255,.45)', borderColor: C.blue, borderWidth: 1 },
{ label: 'Bypassed', data: [0, 12, 2, 5, 8], backgroundColor: 'rgba(233,69,96,.40)', borderColor: C.red, borderWidth: 1 },
],
},
options: {
responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } },
scales: { x: { ticks: { maxRotation: 30 }, grid: { color: C.border } }, y: { beginAtZero: true, grid: { color: C.border } } }
},
});
ghostChart = new Chart($('ghostChart').getContext('2d'), {
type: 'line',
data: {
labels: ['L1', 'L2', 'L3', 'L4'],
datasets: [
{ label: 'Bot score', data: [0.08, 0.22, 0.44, 0.60], borderColor: C.red, backgroundColor: 'rgba(233,69,96,.1)', tension: .4, fill: true, borderWidth: 2, pointRadius: 5, pointBackgroundColor: C.red },
{ label: 'Threshold', data: [0.65, 0.65, 0.65, 0.65], borderColor: C.green, borderWidth: 1, borderDash: [4, 4], pointRadius: 0 },
],
},
options: {
responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } },
scales: { x: { grid: { color: C.border } }, y: { min: 0, max: 1, grid: { color: C.border } } }
},
});
entChart = new Chart($('entChart').getContext('2d'), {
type: 'bar',
data: {
labels: ['L1', 'L2', 'L3', 'L4', 'Human'],
datasets: [{
data: [0.00, 0.87, 0.76, 0.74, 0.68],
backgroundColor: ['rgba(233,69,96,.5)', 'rgba(245,197,24,.5)', 'rgba(74,158,255,.5)', 'rgba(74,158,255,.5)', 'rgba(0,212,160,.5)'],
borderColor: [C.red, C.yellow, C.blue, C.blue, C.green], borderWidth: 1,
}],
},
options: {
responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } },
scales: { x: { grid: { color: C.border } }, y: { min: 0, max: 1, grid: { color: C.border } } }
},
});
replayChart = new Chart($('replayChart').getContext('2d'), {
type: 'scatter',
data: {
datasets: [
{ label: 'Human', data: Array.from({ length: 20 }, () => ({ x: 0.60 + Math.random() * .27, y: 0.55 + Math.random() * .35 })), backgroundColor: 'rgba(0,212,160,.6)', pointRadius: 5 },
{ label: 'Replay', data: Array.from({ length: 8 }, () => ({ x: 0.91 + Math.random() * .08, y: 0.88 + Math.random() * .11 })), backgroundColor: 'rgba(233,69,96,.7)', pointRadius: 5, pointStyle: 'triangle' },
{ label: 'Threshold', data: [{ x: 0.92, y: 0 }, { x: 0.92, y: 1 }], type: 'line', borderColor: C.red, borderWidth: 1, borderDash: [4, 4], pointRadius: 0 },
],
},
options: {
responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } },
scales: {
x: { min: .5, max: 1, title: { display: true, text: 'Cosine similarity' }, grid: { color: C.border } },
y: { min: 0, max: 1, title: { display: true, text: 'Human score' }, grid: { color: C.border } },
}
},
});
weightsChart = new Chart($('weightsChart').getContext('2d'), {
type: 'doughnut',
data: {
labels: ['IKL entropy', 'Corrections', 'Pauses', 'Speed', 'Fatigue'],
datasets: [{
data: [30, 25, 15, 15, 15],
backgroundColor: ['rgba(0,212,160,.7)', 'rgba(233,69,96,.7)', 'rgba(74,158,255,.7)', 'rgba(245,197,24,.7)', 'rgba(167,139,250,.7)'],
borderWidth: 0,
}],
},
options: { responsive: true, maintainAspectRatio: false, cutout: '65%', plugins: { legend: { display: true, position: 'right' } } },
});
radarChart = new Chart($('radarChart').getContext('2d'), {
type: 'radar',
data: {
labels: ['IKL entropy', 'Corrections', 'Pauses', 'Speed', 'Fatigue'],
datasets: [
{ label: 'Human', data: [0.91, 0.88, 0.75, 0.85, 0.82], borderColor: C.green, backgroundColor: 'rgba(0,212,160,.15)', borderWidth: 2, pointBackgroundColor: C.green, pointRadius: 4 },
{ label: 'Bot L4', data: [0.74, 0.52, 0.10, 0.70, 0.20], borderColor: C.red, backgroundColor: 'rgba(233,69,96,.12)', borderWidth: 2, pointBackgroundColor: C.red, pointRadius: 4, borderDash: [4, 4] },
],
},
options: {
responsive: true, maintainAspectRatio: false,
scales: { r: { min: 0, max: 1, ticks: { stepSize: .25 }, grid: { color: C.border }, angleLines: { color: C.border }, pointLabels: { font: { size: 10 } } } }
},
});
const v1 = Array.from({ length: 10 }, () => 0.18 + Math.random() * .2);
const v2 = Array.from({ length: 10 }, () => 0.48 + Math.random() * .2);
v1v2Chart = new Chart($('v1v2Chart').getContext('2d'), {
type: 'line',
data: {
labels: Array.from({ length: 10 }, (_, i) => `Run ${i + 1}`),
datasets: [
{ label: 'V1 Naive', data: v1, borderColor: C.red, backgroundColor: 'transparent', tension: .3, borderWidth: 2, pointRadius: 3, pointBackgroundColor: C.red, borderDash: [3, 3] },
{ label: 'V2 Level 4', data: v2, borderColor: C.blue, backgroundColor: 'rgba(74,158,255,.08)', tension: .3, borderWidth: 2, pointRadius: 3, pointBackgroundColor: C.blue, fill: true },
{ label: 'Threshold', data: new Array(10).fill(0.65), borderColor: C.green, borderWidth: 1, borderDash: [6, 4], pointRadius: 0 },
],
},
options: {
responsive: true, maintainAspectRatio: false, plugins: { legend: { display: true, position: 'top' } },
scales: { x: { grid: { color: C.border } }, y: { min: 0, max: 1, grid: { color: C.border } } }
},
});
}
// ── Live simulation ───────────────────────────────────────────
function fakeSession() {
const isBot = Math.random() < .2;
const isSus = !isBot && Math.random() < .15;
const verdict = isBot ? 'BOT' : isSus ? 'SUSPECT' : 'HUMAN';
const score = isBot ? (.05 + Math.random() * .33) : isSus ? (.40 + Math.random() * .24) : (.65 + Math.random() * .34);
const users = ['denis', 'vertex_test', 'anon_01', 'gemini_agent', 'deepseek_lab'];
const user = users[Math.floor(Math.random() * users.length)];
addSession(verdict, parseFloat(score.toFixed(4)), user, {});
const bin = Math.min(9, Math.floor(score * 10));
if (verdict === 'BOT') botHist[bin]++;
else scoreHist[bin]++;
updateScoreChart();
log(`[SIM] ${verdict} score=${score.toFixed(4)} user=${user}`,
verdict === 'HUMAN' ? 'ok' : verdict === 'SUSPECT' ? 'warn' : 'err');
}
// ── Boot ──────────────────────────────────────────────────────
log('[DECI] System boot β€” PoH Engine v0.1.0-sprint1', 'info');
log('[DECI] Dashboard initialized', 'ok');
log('[DECI] Checking API health...', 'info');
log('[DECI] Attack lab ready β€” DeepSeek mode ON', 'warn');
log('[DECI] Shadow mode: ON β€” verdicts logged, not enforced', 'info');
initCharts();
$('btn-analyze').disabled = true;
// Start live simulation after 2s, then every 4-8s
setTimeout(() => {
fakeSession();
setInterval(fakeSession, 4000 + Math.random() * 4000);
}, 2000);
</script>
</body>
</html>