sentra / console /index.html
betterwithage's picture
Mobile retrofit: viewport-fit + PWA/iOS meta + responsive safety on landing pages
9f6a9c2 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, user-scalable=yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="theme-color" content="#0a0e14">
<title>Sentra Console | Cyber Resilience Command</title>
<meta name="description" content="Cyber Resilience Command platform — threat overview, incident command, 8 immune gates, asset risk, and recovery readiness."/>
<link rel="canonical" href="https://szlholdings-sentra.hf.space/console/"/>
<link rel="preconnect" href="https://fonts.googleapis.com"/>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"/>
<style>
/* ===== Base ===== */
:root {
--bg: #0f1117;
--surface: #161b27;
--surf2: #1c2236;
--border: #252d40;
--text: #e4e6ef;
--muted: #6b7a99;
--dim: #3d4660;
--gold: #c9b787;
--teal: #3ecfcf;
--red: #e05c5c;
--green: #5aac8e;
--yellow: #e0b84e;
--mono: 'JetBrains Mono', ui-monospace, monospace;
--sans: 'Inter', system-ui, sans-serif;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; background: var(--bg); color: var(--text); font-family: var(--sans); font-size: 14px; }
a { color: var(--teal); text-decoration: none; }
a:hover { text-decoration: underline; }
button { cursor: pointer; }
code, .mono { font-family: var(--mono); }
/* ===== Layout ===== */
.shell {
display: flex;
height: 100vh;
overflow: hidden;
}
/* ===== Sidebar ===== */
.sidebar {
width: 220px;
min-width: 220px;
background: var(--surface);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
overflow-y: auto;
overflow-x: hidden;
}
.sidebar-brand {
padding: 1.25rem 1rem 1rem;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
gap: 0.5rem;
}
.sidebar-brand-mark {
width: 1.5rem;
height: 1.5rem;
background: var(--gold);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
font-family: var(--mono);
font-size: 9px;
font-weight: 700;
color: #0a0a0a;
}
.sidebar-brand-name {
font-size: 13px;
font-weight: 600;
letter-spacing: -0.01em;
color: var(--text);
}
.sidebar-brand-tag {
font-family: var(--mono);
font-size: 9px;
color: var(--muted);
}
.sidebar-section {
padding: 1rem 0.75rem 0.5rem;
}
.sidebar-section-label {
font-family: var(--mono);
font-size: 9px;
letter-spacing: 0.1em;
color: var(--dim);
text-transform: uppercase;
padding: 0 0.25rem 0.5rem;
}
.nav-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.4rem 0.5rem;
border-radius: 6px;
font-size: 12.5px;
color: var(--muted);
transition: background 0.12s ease, color 0.12s ease;
cursor: pointer;
border: none;
background: none;
width: 100%;
text-align: left;
}
.nav-item:hover { background: var(--surf2); color: var(--text); }
.nav-item.active { background: rgba(201,183,135,0.08); color: var(--gold); }
.nav-dot {
width: 6px; height: 6px;
border-radius: 50%;
background: var(--dim);
flex-shrink: 0;
}
.nav-item.active .nav-dot { background: var(--gold); }
/* ===== Main pane ===== */
.main-pane {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.topbar {
height: 3rem;
border-bottom: 1px solid var(--border);
background: var(--surface);
display: flex;
align-items: center;
padding: 0 1.25rem;
gap: 1rem;
flex-shrink: 0;
}
.topbar-title {
font-size: 13px;
font-weight: 600;
flex: 1;
}
.topbar-badge {
font-family: var(--mono);
font-size: 9px;
padding: 0.2rem 0.5rem;
border-radius: 4px;
background: rgba(94,207,207,0.1);
color: var(--teal);
border: 1px solid rgba(94,207,207,0.2);
letter-spacing: 0.06em;
}
.topbar-link {
font-family: var(--mono);
font-size: 10px;
color: var(--muted);
text-decoration: none;
}
.topbar-link:hover { color: var(--gold); }
.content {
flex: 1;
overflow-y: auto;
padding: 1.5rem;
}
/* ===== Page: overview ===== */
.kpi-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 1px;
background: var(--border);
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
margin-bottom: 1.5rem;
}
.kpi-card {
background: var(--surface);
padding: 1.25rem 1.5rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.kpi-label {
font-family: var(--mono);
font-size: 9px;
letter-spacing: 0.1em;
color: var(--muted);
text-transform: uppercase;
}
.kpi-value {
font-size: 2rem;
font-weight: 700;
letter-spacing: -0.03em;
}
.kpi-delta {
font-family: var(--mono);
font-size: 10px;
color: var(--green);
}
.kpi-red { color: var(--red); }
.kpi-green { color: var(--green); }
.kpi-gold { color: var(--gold); }
.kpi-teal { color: var(--teal); }
/* ===== Section heading ===== */
.section-heading {
font-size: 13px;
font-weight: 600;
color: var(--text);
margin-bottom: 0.875rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.section-heading::after {
content: '';
flex: 1;
height: 1px;
background: var(--border);
}
/* ===== Gates page ===== */
.gates-list {
display: flex;
flex-direction: column;
gap: 1px;
background: var(--border);
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
margin-bottom: 1.5rem;
}
.gate-row {
background: var(--surface);
padding: 1rem 1.25rem;
display: grid;
grid-template-columns: 2rem 1fr auto;
align-items: center;
gap: 1rem;
cursor: pointer;
transition: background 0.12s ease;
}
.gate-row:hover { background: var(--surf2); }
.gate-row.expanded { background: var(--surf2); }
.gate-num-badge {
font-family: var(--mono);
font-size: 10px;
color: var(--gold);
font-weight: 600;
}
.gate-info h3 { font-size: 13px; font-weight: 600; margin-bottom: 0.2rem; }
.gate-info p { font-size: 11.5px; color: var(--muted); line-height: 1.45; }
.gate-tags {
display: flex;
gap: 0.35rem;
align-items: center;
}
.tag {
font-family: var(--mono);
font-size: 9px;
padding: 0.15rem 0.4rem;
border-radius: 3px;
background: rgba(201,183,135,0.08);
color: var(--gold);
white-space: nowrap;
}
.tag-teal {
background: rgba(62,207,207,0.08);
color: var(--teal);
}
.gate-detail {
display: none;
padding: 1rem 1.25rem 1.25rem;
background: var(--surf2);
border-top: 1px solid var(--border);
font-size: 12px;
color: var(--muted);
line-height: 1.6;
}
.gate-detail.open { display: block; }
.gate-test-btn {
display: inline-flex;
align-items: center;
gap: 0.3rem;
font-family: var(--mono);
font-size: 10px;
padding: 0.35rem 0.75rem;
background: var(--gold);
color: #0a0a0a;
border: none;
border-radius: 4px;
margin-top: 0.75rem;
cursor: pointer;
}
.gate-test-result {
margin-top: 0.625rem;
font-family: var(--mono);
font-size: 11px;
padding: 0.625rem;
background: rgba(0,0,0,0.4);
border-radius: 5px;
display: none;
}
.gate-test-result.visible { display: block; }
.result-allow { color: var(--green); }
.result-deny { color: var(--red); }
/* ===== Verdict tester ===== */
.verdict-box {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 1.5rem;
max-width: 640px;
}
.v-label {
font-family: var(--mono);
font-size: 9px;
letter-spacing: 0.1em;
color: var(--muted);
text-transform: uppercase;
margin-bottom: 0.4rem;
}
.v-textarea {
width: 100%;
background: var(--surf2);
border: 1px solid var(--border);
border-radius: 5px;
color: var(--text);
font-family: var(--mono);
font-size: 12px;
padding: 0.625rem 0.875rem;
outline: none;
resize: vertical;
min-height: 72px;
}
.v-textarea:focus { border-color: var(--gold); }
.v-row {
display: flex;
align-items: center;
gap: 0.875rem;
margin-top: 0.75rem;
}
.v-btn {
font-family: var(--mono);
font-size: 11px;
padding: 0.5rem 1.25rem;
background: var(--gold);
color: #0a0a0a;
border: none;
border-radius: 5px;
}
.v-btn:disabled { opacity: 0.5; }
.v-status {
font-family: var(--mono);
font-size: 10px;
color: var(--muted);
}
.v-output {
margin-top: 1rem;
background: rgba(0,0,0,0.5);
border: 1px solid var(--border);
border-radius: 6px;
padding: 0.875rem 1rem;
font-family: var(--mono);
font-size: 12px;
line-height: 1.7;
display: none;
}
.v-output.visible { display: block; }
/* ===== Audit log ===== */
.audit-table {
width: 100%;
border-collapse: collapse;
font-size: 11.5px;
margin-bottom: 1.5rem;
}
.audit-table th {
font-family: var(--mono);
font-size: 9px;
letter-spacing: 0.08em;
color: var(--muted);
text-transform: uppercase;
text-align: left;
padding: 0.5rem 0.875rem;
border-bottom: 1px solid var(--border);
}
.audit-table td {
padding: 0.5rem 0.875rem;
border-bottom: 1px solid rgba(255,255,255,0.03);
color: var(--text);
}
.audit-table tr:hover td { background: var(--surf2); }
.badge {
font-family: var(--mono);
font-size: 9px;
padding: 0.15rem 0.4rem;
border-radius: 3px;
}
.badge-allow { background: rgba(90,172,142,0.1); color: var(--green); }
.badge-deny { background: rgba(224,92,92,0.1); color: var(--red); }
/* ===== Threats ===== */
.threats-list {
display: flex;
flex-direction: column;
gap: 1px;
background: var(--border);
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
}
.threat-row {
background: var(--surface);
padding: 0.875rem 1.25rem;
display: grid;
grid-template-columns: 1fr auto auto;
align-items: center;
gap: 1rem;
}
.threat-sig { font-family: var(--mono); font-size: 12px; color: var(--red); }
.threat-cat { font-family: var(--mono); font-size: 10px; color: var(--muted); }
/* ===== Empty / loading ===== */
.loading {
font-family: var(--mono);
font-size: 11px;
color: var(--dim);
padding: 2rem 0;
text-align: center;
}
/* ===== Rosie widget ===== */
#rosie-widget {
position: fixed;
bottom: 1.25rem;
right: 1.25rem;
z-index: 1000;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 0.625rem;
}
#rosie-bubble {
background: var(--surface);
border: 1px solid rgba(201,183,135,0.2);
border-radius: 10px;
padding: 0.875rem 1rem;
width: 260px;
box-shadow: 0 8px 24px rgba(0,0,0,0.6);
display: none;
flex-direction: column;
gap: 0.5rem;
}
#rosie-bubble.open { display: flex; }
.rosie-title { font-family: var(--mono); font-size: 10px; color: var(--gold); letter-spacing: 0.1em; }
.rosie-msgs { font-family: var(--mono); font-size: 11px; color: var(--muted); line-height: 1.55; max-height: 120px; overflow-y: auto; }
.rmsg-u { color: var(--text); }
.rmsg-b { color: var(--gold); }
.rosie-inp { background: var(--surf2); border: 1px solid var(--border); border-radius: 5px; color: var(--text); font-family: var(--mono); font-size: 11px; padding: 0.4rem 0.625rem; outline: none; width: 100%; }
.rosie-inp:focus { border-color: var(--gold); }
.rosie-send-btn { font-family: var(--mono); font-size: 10px; background: var(--gold); color: #0a0a0a; border: none; border-radius: 4px; padding: 0.3rem 0.75rem; cursor: pointer; align-self: flex-end; }
#rosie-toggle { width: 2.75rem; height: 2.75rem; border-radius: 50%; background: var(--gold); border: none; font-size: 1.1rem; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 14px rgba(201,183,135,0.25); cursor: pointer; }
@media (max-width: 640px) {
.sidebar { display: none; }
}
</style>
</head>
<body>
<!-- SZL FLAGSHIP HERO v2 (ADDITIVE, prepended before #root) — Sign: Yachay · Perplexity Computer Agent
Anduril×Anthropic×a11oy aesthetic. Kanchay tokens reused (no new tokens). Open-source fonts.
Self-contained: no external CDN, no build step. Canvas wire-mesh animation (Live Wires lineage).
Does NOT touch #root (React SPA) or any /api route — pure prepend. MARKER: data-szl-hero-v2 -->
<section id="szl-flagship-hero" data-szl-hero-v2="sentra" aria-label="sentra command hero">
<style>
#szl-flagship-hero{position:relative;width:100%;min-height:560px;background:#0a0f1e;color:#f5f7fa;
font-family:"Inter","IBM Plex Sans",ui-sans-serif,system-ui,sans-serif;overflow:hidden;
border-bottom:1px solid #3c4757}
#szl-flagship-hero *{box-sizing:border-box}
#szl-hero-canvas{position:absolute;inset:0;width:100%;height:100%;display:block;z-index:0;opacity:.55}
#szl-hero-grid{position:absolute;inset:0;z-index:1;pointer-events:none;
background-image:linear-gradient(rgba(60,71,87,.18) 1px,transparent 1px),linear-gradient(90deg,rgba(60,71,87,.18) 1px,transparent 1px);
background-size:48px 48px;mask-image:radial-gradient(ellipse 75% 70% at 50% 38%,#000 55%,transparent 100%)}
#szl-hero-inner{position:relative;z-index:2;max-width:1180px;margin:0 auto;padding:30px 34px 26px}
.szl-hero-top{display:flex;align-items:center;gap:12px;font-family:"JetBrains Mono",ui-monospace,monospace;
font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:#76859b}
.szl-hero-top .dot{width:7px;height:7px;border-radius:50%;background:#1f9d57;box-shadow:0 0 0 0 rgba(31,157,87,.6);
animation:szlpulse 2.4s infinite}
@keyframes szlpulse{0%{box-shadow:0 0 0 0 rgba(31,157,87,.55)}70%{box-shadow:0 0 0 9px rgba(31,157,87,0)}100%{box-shadow:0 0 0 0 rgba(31,157,87,0)}}
.szl-hero-top .sep{color:#3c4757}
.szl-hero-top b{color:#d7b96b;font-weight:600}
#szl-flagship-hero h1{font-size:clamp(30px,5vw,52px);line-height:1.02;margin:18px 0 12px;font-weight:680;
letter-spacing:-.02em;max-width:18ch}
#szl-flagship-hero h1 .accent{color:#d7b96b}
.szl-hero-sub{font-size:clamp(14px,1.7vw,17px);line-height:1.5;color:#c9d2df;max-width:60ch;margin:0 0 22px}
.szl-metrics{display:grid;grid-template-columns:repeat(auto-fit,minmax(132px,1fr));gap:1px;
background:#3c4757;border:1px solid #3c4757;border-radius:10px;overflow:hidden;max-width:880px}
.szl-metric{background:#10151c;padding:13px 15px}
.szl-metric .v{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:21px;font-weight:600;color:#f5f7fa;line-height:1}
.szl-metric .v .u{font-size:12px;color:#76859b;margin-left:3px}
.szl-metric .k{font-size:10.5px;letter-spacing:.07em;text-transform:uppercase;color:#76859b;margin-top:7px}
.szl-metric.good .v{color:#5cc4bf}
.szl-hero-cta{display:flex;gap:10px;margin-top:22px;flex-wrap:wrap}
.szl-hero-cta a{font-size:13px;font-weight:600;text-decoration:none;padding:9px 16px;border-radius:8px;
font-family:"Inter",sans-serif;border:1px solid #3c4757;transition:.16s}
.szl-hero-cta a.primary{background:#d7b96b;color:#10151c;border-color:#d7b96b}
.szl-hero-cta a.primary:hover{background:#e4cf99}
.szl-hero-cta a.ghost{background:transparent;color:#c9d2df}
.szl-hero-cta a.ghost:hover{border-color:#5cc4bf;color:#5cc4bf}
.szl-hero-foot{font-family:"JetBrains Mono",monospace;font-size:10.5px;color:#525f73;margin-top:18px;letter-spacing:.04em}
@media(prefers-reduced-motion:reduce){#szl-hero-canvas{animation:none;opacity:.3}.szl-hero-top .dot{animation:none}}
@media(max-width:640px){#szl-flagship-hero{min-height:480px}}
</style>
<canvas id="szl-hero-canvas" aria-hidden="true"></canvas>
<div id="szl-hero-grid"></div>
<div id="szl-hero-inner">
<div class="szl-hero-top">
<span class="dot"></span><span>SZL HOLDINGS</span><span class="sep">/</span>
<span>SENTRA</span><span class="sep">/</span>
<span>DOCTRINE v11 · LOCKED</span><span class="sep">/</span>
<b>REPLAY bacf5443…</b>
</div>
<h1>Continuous <span class="accent">defensive assurance</span> for every agent.</h1>
<p class="szl-hero-sub">Sentra watches the watchers: adversarial resilience, alignment review and audit replay on a shared 13-axis doctrine. Defensive posture only — sensor fusion in the Lattice tradition, constitutional gating in the Anthropic tradition.</p>
<div class="szl-metrics"><div class="szl-metric"><div class="v">13<span class="u">axis</span></div><div class="k">doctrine</div></div><div class="szl-metric"><div class="v">163<span class="u">sorries</span></div><div class="k">audited</div></div><div class="szl-metric"><div class="v">14<span class="u">axioms</span></div><div class="k">pinned</div></div><div class="szl-metric good"><div class="v">100%<span class="u">gates</span></div><div class="k">GREEN</div></div><div class="szl-metric good"><div class="v">0<span class="u">RED</span></div><div class="k">routes</div></div></div>
<div class="szl-hero-cta"><a class="primary" href="#root">Open assurance deck</a><a class="ghost" href="/api/sentra/healthz">Health</a></div>
<div class="szl-hero-foot">FRONTIER: Breathing assurance pulse — live posture heartbeat across all monitored organs · governed loop GREEN · additive deploy · sign: Yachay</div>
</div>
<script>
(function(){
var prefersReduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var c = document.getElementById('szl-hero-canvas'); if(!c) return;
var ctx = c.getContext('2d'); var W,H,DPR,nodes=[],t=0;
var ACCENT='#d7b96b', TEAL='#5cc4bf', LINE='rgba(60,71,87,.55)';
function size(){DPR=Math.min(2,window.devicePixelRatio||1);var r=c.getBoundingClientRect();
W=c.width=Math.max(1,r.width*DPR);H=c.height=Math.max(1,r.height*DPR);build();}
function build(){nodes=[];var N=Math.round((W*H)/(DPR*DPR)/26000);N=Math.max(26,Math.min(64,N));
for(var i=0;i<N;i++){nodes.push({x:Math.random()*W,y:Math.random()*H,
vx:(Math.random()-.5)*0.18*DPR,vy:(Math.random()-.5)*0.18*DPR,
r:(1.1+Math.random()*1.8)*DPR,p:Math.random()*6.28});}}
function step(){t+=0.016;ctx.clearRect(0,0,W,H);
for(var i=0;i<nodes.length;i++){var n=nodes[i];n.x+=n.vx;n.y+=n.vy;
if(n.x<0||n.x>W)n.vx*=-1; if(n.y<0||n.y>H)n.vy*=-1;}
var MAX=150*DPR;
for(var i=0;i<nodes.length;i++){for(var j=i+1;j<nodes.length;j++){
var a=nodes[i],b=nodes[j];var dx=a.x-b.x,dy=a.y-b.y;var d=Math.sqrt(dx*dx+dy*dy);
if(d<MAX){var o=(1-d/MAX);ctx.strokeStyle='rgba(92,196,191,'+(o*0.28).toFixed(3)+')';
ctx.lineWidth=DPR*0.7;ctx.beginPath();ctx.moveTo(a.x,a.y);ctx.lineTo(b.x,b.y);ctx.stroke();}}}
for(var i=0;i<nodes.length;i++){var n=nodes[i];
var pulse=0.5+0.5*Math.sin(t*1.3+n.p);
ctx.fillStyle = (i%5===0)?ACCENT:TEAL;
ctx.globalAlpha=0.35+0.45*pulse;ctx.beginPath();ctx.arc(n.x,n.y,n.r*(0.8+0.5*pulse),0,6.2832);ctx.fill();}
ctx.globalAlpha=1;
if(!prefersReduce) requestAnimationFrame(step);}
size();window.addEventListener('resize',size);
if(prefersReduce){step();}else{requestAnimationFrame(step);}
})();
</script>
</section>
<div class="shell">
<!-- ===== SIDEBAR ===== -->
<nav class="sidebar" aria-label="Sentra console navigation">
<div class="sidebar-brand">
<div class="sidebar-brand-mark">S</div>
<div>
<div class="sidebar-brand-name">sentra</div>
<div class="sidebar-brand-tag">/ cyber resilience command</div>
</div>
</div>
<div class="sidebar-section">
<div class="sidebar-section-label">Overview</div>
<button class="nav-item active" onclick="showPage('overview')">
<span class="nav-dot"></span> Decision Center
</button>
<button class="nav-item" onclick="showPage('gates')">
<span class="nav-dot"></span> 8 Immune Gates
</button>
</div>
<div class="sidebar-section">
<div class="sidebar-section-label">Wire B</div>
<button class="nav-item" onclick="showPage('verdict')">
<span class="nav-dot"></span> Verdict Tester
</button>
<button class="nav-item" onclick="showPage('audit')">
<span class="nav-dot"></span> Audit Log
</button>
<button class="nav-item" onclick="showPage('threats')">
<span class="nav-dot"></span> Threat Corpus
</button>
</div>
<div class="sidebar-section">
<div class="sidebar-section-label">Platform</div>
<button class="nav-item" onclick="showPage('incidents')">
<span class="nav-dot"></span> Incidents
</button>
<button class="nav-item" onclick="showPage('assets')">
<span class="nav-dot"></span> Asset Risk
</button>
</div>
<div style="margin-top:auto;padding:1rem;border-top:1px solid var(--border);">
<a href="/" class="nav-item" style="text-decoration:none;color:var(--muted);">
<span class="nav-dot"></span> ← Landing
</a>
</div>
</nav>
<!-- ===== MAIN PANE ===== -->
<div class="main-pane">
<div class="topbar">
<span class="topbar-title" id="topbar-title">Decision Center</span>
<span class="topbar-badge">WIRE B LIVE</span>
<span class="topbar-badge" style="background:rgba(201,183,135,0.08);color:var(--gold);border-color:rgba(201,183,135,0.2);">8 GATES</span>
<a class="topbar-link" href="/api/sentra/docs" target="_blank">API docs →</a>
</div>
<div class="content" id="content">
<!-- Pages are injected here by JS -->
</div>
</div>
</div>
<!-- ===== ROSIE WIDGET ===== -->
<div id="rosie-widget" role="complementary" aria-label="Rosie assistant">
<div id="rosie-bubble" role="dialog" aria-modal="false" aria-label="Rosie assistant">
<div style="display:flex;align-items:center;justify-content:space-between;">
<span class="rosie-title">ROSIE · SENTRA</span>
<button onclick="toggleRosie()" style="background:none;border:none;color:var(--muted);cursor:pointer;font-size:13px;"></button>
</div>
<div class="rosie-msgs" id="rosie-msgs">
<div class="rmsg-b">Hi, I'm Rosie. Ask about gates, verdicts, Wire B, or threats.</div>
</div>
<input type="text" id="rosie-inp" class="rosie-inp" placeholder="Ask anything…" onkeydown="if(event.key==='Enter')sendRosie()"/>
<button class="rosie-send-btn" onclick="sendRosie()">Send →</button>
</div>
<button id="rosie-toggle" onclick="toggleRosie()" aria-label="Open Rosie">🛡️</button>
</div>
<script>
/* ===== Page state ===== */
let currentPage = 'overview';
let gatesData = [];
let auditData = [];
let threatsData = [];
/* ===== Navigation ===== */
function showPage(page) {
currentPage = page;
document.querySelectorAll('.nav-item').forEach(el => {
el.classList.toggle('active', el.getAttribute('onclick') === `showPage('${page}')`);
});
const titles = {
overview: 'Decision Center',
gates: '8 Immune Gates',
verdict: 'Verdict Tester',
audit: 'Audit Log',
threats: 'Threat Corpus',
incidents: 'Incidents',
assets: 'Asset Risk',
};
document.getElementById('topbar-title').textContent = titles[page] || page;
renderPage(page);
}
async function renderPage(page) {
const content = document.getElementById('content');
switch(page) {
case 'overview': content.innerHTML = await buildOverview(); break;
case 'gates': content.innerHTML = await buildGates(); attachGateHandlers(); break;
case 'verdict': content.innerHTML = buildVerdict(); break;
case 'audit': content.innerHTML = await buildAudit(); break;
case 'threats': content.innerHTML = await buildThreats(); break;
case 'incidents': content.innerHTML = buildIncidents(); break;
case 'assets': content.innerHTML = buildAssets(); break;
default: content.innerHTML = '<div class="loading">Page not found.</div>';
}
}
/* ===== Overview ===== */
async function buildOverview() {
let verdict_count = 0, deny_count = 0, gate_count = 0;
try {
const a = await fetch('/api/sentra/v1/audit-log?limit=200').then(r=>r.json());
verdict_count = a.total_buffered || 0;
deny_count = (a.entries||[]).filter(e=>e.decision==='deny').length;
} catch(e) {}
try {
const g = await fetch('/api/sentra/v1/gates').then(r=>r.json());
gate_count = g.total || 8;
gatesData = g.gates || [];
} catch(e) { gate_count = 8; }
return `
<div class="kpi-grid">
<div class="kpi-card">
<div class="kpi-label">Immune Gates</div>
<div class="kpi-value kpi-gold">${gate_count}</div>
<div class="kpi-delta">All operational</div>
</div>
<div class="kpi-card">
<div class="kpi-label">Verdicts (session)</div>
<div class="kpi-value kpi-teal">${verdict_count}</div>
<div class="kpi-delta">Wire B live</div>
</div>
<div class="kpi-card">
<div class="kpi-label">Denied</div>
<div class="kpi-value kpi-red">${deny_count}</div>
<div class="kpi-delta">Threat signatures</div>
</div>
<div class="kpi-card">
<div class="kpi-label">Recovery Posture</div>
<div class="kpi-value kpi-yellow">42%</div>
<div class="kpi-delta">OT assets compromised</div>
</div>
<div class="kpi-card">
<div class="kpi-label">Active Incidents</div>
<div class="kpi-value kpi-red">1</div>
<div class="kpi-delta">INC-2026-0891 critical</div>
</div>
<div class="kpi-card">
<div class="kpi-label">Financial Exposure</div>
<div class="kpi-value kpi-red" style="font-size:1.5rem;">$2.8M</div>
<div class="kpi-delta">Est. ransomware impact</div>
</div>
</div>
<div class="section-heading">Active Incident</div>
<div style="background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:1.25rem;margin-bottom:1.5rem;">
<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:1rem;">
<div>
<div style="font-size:12px;font-weight:600;margin-bottom:0.3rem;">INC-2026-0891 · Ransomware-Adjacent OT Payload Detected</div>
<div style="font-size:11.5px;color:var(--muted);line-height:1.5;max-width:560px;">
Encrypted payload detected on 3 OT assets (SCADA, PLC). Anomalous C2 beaconing to known malicious IPs. MITRE ATT&CK stage: Execution / C2.
</div>
</div>
<span class="badge badge-deny">CRITICAL</span>
</div>
</div>
<div class="section-heading">Quick Links</div>
<div style="display:flex;gap:0.75rem;flex-wrap:wrap;">
<button class="gate-test-btn" onclick="showPage('gates')">View 8 Gates →</button>
<button class="gate-test-btn" onclick="showPage('verdict')">Test Verdict →</button>
<button class="gate-test-btn" onclick="showPage('audit')">Audit Log →</button>
<button class="gate-test-btn" onclick="showPage('threats')">Threat Corpus →</button>
</div>`;
}
/* ===== Gates ===== */
async function buildGates() {
try {
const d = await fetch('/api/sentra/v1/gates').then(r=>r.json());
gatesData = d.gates || [];
} catch(e) {}
if (!gatesData.length) return '<div class="loading">Failed to load gates.</div>';
return `
<div class="section-heading">All 8 Immune Gates</div>
<p style="font-size:12px;color:var(--muted);margin-bottom:1.25rem;">Click any gate to expand details and run a per-gate test.</p>
<div class="gates-list" id="gates-list">
${gatesData.map((g,i)=>`
<div>
<div class="gate-row" id="gate-row-${g.id}" onclick="toggleGate('${g.id}')">
<span class="gate-num-badge">G${String(i+1).padStart(2,'0')}</span>
<div class="gate-info">
<h3>${g.label}</h3>
<p>${g.description.substring(0,110)}…</p>
</div>
<div class="gate-tags">
<span class="tag">${g.category}</span>
${g.dualUse ? '<span class="tag tag-teal">dual-use</span>' : ''}
</div>
</div>
<div class="gate-detail" id="gate-detail-${g.id}">
<p><strong style="color:var(--text);">ArtDomain:</strong> ${g.artDomain}</p>
<p><strong style="color:var(--text);">Permitted contexts:</strong> ${(g.permittedContexts||[]).join(', ')}</p>
<p><strong style="color:var(--text);">Sample input:</strong> <code>${JSON.stringify(g.sampleInput)}</code></p>
<p><strong style="color:var(--text);">Expected decision:</strong> <span class="${g.expectedDecision==='allow'?'result-allow':'result-deny'}">${g.expectedDecision.toUpperCase()}</span></p>
<button class="gate-test-btn" onclick="testGate('${g.id}')">Run gate test →</button>
<div class="gate-test-result" id="gate-result-${g.id}"></div>
</div>
</div>`).join('')}
</div>`;
}
function attachGateHandlers() { /* event delegation via onclick attrs */ }
function toggleGate(id) {
const detail = document.getElementById(`gate-detail-${id}`);
const row = document.getElementById(`gate-row-${id}`);
const open = detail.classList.toggle('open');
row.classList.toggle('expanded', open);
}
async function testGate(id) {
const el = document.getElementById(`gate-result-${id}`);
el.style.display = 'block';
el.textContent = 'Testing…';
try {
const r = await fetch(`/api/sentra/v1/gates/${id}/test`, {
method: 'POST', headers: {'Content-Type':'application/json'}, body: '{}'
});
const d = await r.json();
const passed = d.gate_passed;
el.className = 'gate-test-result visible';
el.innerHTML = `<span class="${d.verdict.decision==='allow'?'result-allow':'result-deny'}">
decision: ${d.verdict.decision.toUpperCase()}</span>
reason: ${d.verdict.reason}
lambda: ${d.verdict.lambda_value}
gate_passed: <span class="${passed?'result-allow':'result-deny'}">${passed}</span>`;
} catch(e) {
el.className = 'gate-test-result visible';
el.style.color = 'var(--red)';
el.textContent = 'Error: ' + e.message;
}
}
/* ===== Verdict tester ===== */
function buildVerdict() {
return `
<div class="section-heading">Verdict Tester — Wire B Live</div>
<p style="font-size:12px;color:var(--muted);margin-bottom:1.25rem;">
POST to <code>/api/sentra/v1/verdict</code> or <code>/api/sentra/v1/inspect</code>. Try both clean and malicious inputs.
</p>
<div class="verdict-box">
<div class="v-label">Action payload</div>
<textarea id="v-input" class="v-textarea" placeholder="Type any action…">DROP TABLE users; --</textarea>
<div class="v-row">
<button class="v-btn" onclick="runVerdict('/api/sentra/v1/verdict')">Verdict</button>
<button class="v-btn" onclick="runVerdict('/api/sentra/v1/inspect')" style="background:var(--surf2);color:var(--text);border:1px solid var(--border);">Inspect (full signals)</button>
<span class="v-status" id="v-status"></span>
</div>
<div class="v-output" id="v-output" aria-live="polite"></div>
</div>`;
}
async function runVerdict(endpoint) {
const action = document.getElementById('v-input').value.trim();
const status = document.getElementById('v-status');
const output = document.getElementById('v-output');
if (!action) return;
status.textContent = 'Evaluating…';
output.classList.remove('visible');
try {
const r = await fetch(endpoint, {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({ action, request_id: 'console-' + Date.now(), agent: 'sentra-console' })
});
const d = await r.json();
const dc = d.decision === 'allow' ? 'result-allow' : 'result-deny';
const sigs = d.signals && d.signals.length
? d.signals.map(s=>` <span style="color:var(--yellow)">⚠ ${s}</span>`).join('\n')
: ' (none)';
output.innerHTML = `<span class="${dc}">decision: ${d.decision.toUpperCase()}</span>
reason: ${d.reason}
lambda_value: ${d.lambda_value}
signals:
${sigs}`;
output.classList.add('visible');
status.textContent = '';
} catch(e) {
output.innerHTML = `<span style="color:var(--red)">Error: ${e.message}</span>`;
output.classList.add('visible');
status.textContent = '';
}
}
/* ===== Audit log ===== */
async function buildAudit() {
let entries = [];
try {
const d = await fetch('/api/sentra/v1/audit-log?limit=50').then(r=>r.json());
entries = d.entries || [];
auditData = entries;
} catch(e) {}
if (!entries.length) return '<div class="loading">No audit entries yet.</div>';
return `
<div class="section-heading">Recent Verdicts (last ${entries.length})</div>
<table class="audit-table">
<thead>
<tr>
<th>Request ID</th>
<th>Agent</th>
<th>Action (preview)</th>
<th>Decision</th>
<th>λ</th>
<th>Timestamp</th>
</tr>
</thead>
<tbody>
${entries.map(e=>`
<tr>
<td class="mono" style="font-size:10px;color:var(--muted);">${e.request_id}</td>
<td>${e.agent}</td>
<td class="mono" style="font-size:10px;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${escapeHtml(e.action_preview)}</td>
<td><span class="badge ${e.decision==='allow'?'badge-allow':'badge-deny'}">${e.decision}</span></td>
<td class="mono" style="font-size:10px;">${e.lambda_value}</td>
<td class="mono" style="font-size:10px;color:var(--muted);">${e.timestamp.substring(0,19).replace('T',' ')}</td>
</tr>`).join('')}
</tbody>
</table>`;
}
/* ===== Threats ===== */
async function buildThreats() {
let corpus = [];
try {
const d = await fetch('/api/sentra/v1/threats').then(r=>r.json());
corpus = d.corpus || [];
threatsData = corpus;
} catch(e) {}
return `
<div class="section-heading">Threat Signature Corpus — STIX 2.1</div>
<p style="font-size:12px;color:var(--muted);margin-bottom:1.25rem;">
${corpus.length} signatures · TAXII ingest enabled · Updated 2026-05-30
</p>
<div class="threats-list">
${corpus.map(t=>`
<div class="threat-row">
<div>
<div class="threat-sig">${escapeHtml(t.signature)}</div>
<div style="font-size:11px;color:var(--muted);font-family:var(--mono);margin-top:0.2rem;">${t.stix_pattern}</div>
</div>
<span class="threat-cat">${t.category}</span>
<span class="badge badge-deny">${t.severity}</span>
</div>`).join('')}
</div>`;
}
/* ===== Incidents ===== */
function buildIncidents() {
return `
<div class="section-heading">Active Incidents</div>
<div style="background:var(--surface);border:1px solid var(--border);border-radius:8px;overflow:hidden;">
<table class="audit-table" style="margin:0;">
<thead><tr><th>ID</th><th>Title</th><th>Severity</th><th>Status</th><th>MITRE Stage</th><th>Detected</th></tr></thead>
<tbody>
<tr>
<td class="mono" style="font-size:10px;">INC-2026-0891</td>
<td>Ransomware-Adjacent OT Payload Detected</td>
<td><span class="badge badge-deny">critical</span></td>
<td><span style="color:var(--red);font-size:11px;">active</span></td>
<td class="mono" style="font-size:10px;">Execution / C2</td>
<td class="mono" style="font-size:10px;color:var(--muted);">4h ago</td>
</tr>
</tbody>
</table>
</div>
<div class="section-heading" style="margin-top:1.5rem;">Control Drift</div>
<div style="background:var(--surface);border:1px solid var(--border);border-radius:8px;overflow:hidden;">
<table class="audit-table" style="margin:0;">
<thead><tr><th>Family</th><th>Control</th><th>Status</th><th>Evidence</th></tr></thead>
<tbody>
<tr>
<td>Respond</td>
<td>Incident Response Plan</td>
<td><span style="color:var(--yellow);font-size:11px;">drift_detected</span></td>
<td style="font-size:11px;color:var(--muted);">Isolation playbooks failed on legacy SCADA.</td>
</tr>
<tr>
<td>Recover</td>
<td>Backup Verification</td>
<td><span style="color:var(--yellow);font-size:11px;">drift_detected</span></td>
<td style="font-size:11px;color:var(--muted);">2 critical server backups failed integrity check.</td>
</tr>
</tbody>
</table>
</div>`;
}
/* ===== Assets ===== */
function buildAssets() {
const assets = [
{name:'SCADA Server', type:'OT', crit:'critical', exposure:88, backup:'stale', status:'compromised'},
{name:'HMI Workstation', type:'OT', crit:'high', exposure:65, backup:'current', status:'active'},
{name:'PLC Controller', type:'OT', crit:'critical', exposure:92, backup:'none', status:'compromised'},
{name:'Domain Controller', type:'IT', crit:'critical', exposure:45, backup:'stale', status:'active'},
];
return `
<div class="section-heading">Asset Risk Overview</div>
<div style="background:var(--surface);border:1px solid var(--border);border-radius:8px;overflow:hidden;">
<table class="audit-table" style="margin:0;">
<thead><tr><th>Asset</th><th>Type</th><th>Criticality</th><th>Exposure</th><th>Backup</th><th>Status</th></tr></thead>
<tbody>
${assets.map(a=>`
<tr>
<td style="font-weight:600;">${a.name}</td>
<td class="mono" style="font-size:10px;">${a.type}</td>
<td><span class="badge ${a.crit==='critical'?'badge-deny':'badge-allow'}">${a.crit}</span></td>
<td>
<div style="display:flex;align-items:center;gap:0.5rem;">
<div style="width:80px;height:6px;background:var(--surf2);border-radius:3px;">
<div style="width:${a.exposure}%;height:100%;background:${a.exposure>80?'var(--red)':a.exposure>60?'var(--yellow)':'var(--green)'};border-radius:3px;"></div>
</div>
<span class="mono" style="font-size:10px;">${a.exposure}</span>
</div>
</td>
<td><span style="color:${a.backup==='current'?'var(--green)':a.backup==='stale'?'var(--yellow)':'var(--red)'};font-size:11px;">${a.backup}</span></td>
<td><span style="color:${a.status==='active'?'var(--green)':'var(--red)'};font-size:11px;">${a.status}</span></td>
</tr>`).join('')}
</tbody>
</table>
</div>`;
}
/* ===== Rosie ===== */
function toggleRosie() {
document.getElementById('rosie-bubble').classList.toggle('open');
}
const ROSIE_KB = {
gate: 'sentra has 8 immune gates: signature-scan, size-guard, lambda-threshold, dual-use-detection, stix-taxii-ingest, traceparent-propagation, wire-b-contract, receipt-hash.',
verdict: 'POST /api/sentra/v1/verdict with {"action":"…"}. Returns {decision, reason, signals, lambda_value}.',
inspect: '/api/sentra/v1/inspect returns ALL signals fired — no short-circuit. Use for forensic analysis.',
threat: 'Threat signatures: DROP TABLE, rm -rf, <script, eval(, subprocess, ../../etc.',
wire: 'Wire B connects a11oy mesh-router to sentra. Every agent action is evaluated by the immune organ before admission.',
dual: 'Gate-04 dual-use-detection flags operations that are legitimate in threat-hunting context but weaponisable elsewhere.',
stix: 'Gate-05 STIX/TAXII ingest cross-references action indicators against active threat intelligence objects (IP, domain, hash).',
audit: 'The audit log (/api/sentra/v1/audit-log) records every verdict with request_id, agent, decision, signals, and timestamp.',
default: 'I can answer about the 8 gates, Wire B, verdicts, threats, dual-use, STIX/TAXII, or audit logs. What would you like to know?'
};
function sendRosie() {
const inp = document.getElementById('rosie-inp');
const msgs = document.getElementById('rosie-msgs');
const q = inp.value.trim().toLowerCase();
if (!q) return;
msgs.innerHTML += `<div class="rmsg-u">&gt; ${inp.value.trim()}</div>`;
let reply = ROSIE_KB.default;
for (const [k, v] of Object.entries(ROSIE_KB)) {
if (k !== 'default' && q.includes(k)) { reply = v; break; }
}
msgs.innerHTML += `<div class="rmsg-b">Rosie: ${reply}</div>`;
msgs.scrollTop = msgs.scrollHeight;
inp.value = '';
}
/* ===== Helpers ===== */
function escapeHtml(str) {
return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
/* ===== Boot ===== */
renderPage('overview');
</script>
</body>
</html>