training/dashboard: add 'stack' scene showing real imports

Slots in between intro and collect: a side-by-side display of
pyproject.toml's annotated dependency list and the import header of
receiver/app.py. The point is to surface the project's
stdlib-first / annotated-deps stance early in the deck without
making the audience open a terminal.

Lightweight syntax highlighting via a small char-by-char Python
tokenizer (no third-party highlight.js). The TOML case uses regex.
GitHub-dark color palette.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Max Gorog 2026-05-07 21:44:44 -05:00
parent e5931df8ad
commit cd133a2dd8
3 changed files with 223 additions and 3 deletions

View file

@ -196,6 +196,46 @@ html, body {
.phase-legend .swatch { display: inline-block; width: 10px; height: 10px;
border-radius: 2px; margin-right: 6px; }
/* ─── Code cards (stack scene) ─────────────────────────────────────── */
.code-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
gap: clamp(12px, 1.4vw, 22px);
align-items: start;
}
.code-card {
background: var(--bg-elev2);
border: 1px solid var(--line);
border-radius: 4px;
overflow: hidden;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: clamp(11px, 0.92vw, 14px);
line-height: 1.6;
}
.code-card-header {
padding: 7px 14px;
background: var(--bg-elev);
border-bottom: 1px solid var(--line);
color: var(--fg-dim);
font-size: 11px;
letter-spacing: 0.06em;
text-transform: lowercase;
}
.code-card pre.code {
margin: 0;
padding: 14px 18px;
white-space: pre;
overflow-x: auto;
color: var(--fg);
max-height: clamp(260px, 50vh, 560px);
}
.code-card .kw { color: #ff7b72; }
.code-card .str { color: #a5d6ff; }
.code-card .com { color: #6e7681; font-style: italic; }
.code-card .fn { color: #d2a8ff; }
.code-card .ty { color: #ffa657; }
.code-card .num { color: #79c0ff; }
/* ─── Database explorer ────────────────────────────────────────────── */
.db-header {
display: flex; align-items: baseline; gap: 16px; flex-wrap: wrap;

View file

@ -240,6 +240,148 @@
// 4. Widgets
// ────────────────────────────────────────────────────────────────
// ── Stack scene · pyproject.toml + receiver/app.py header ─────
// Static content. The point is to surface the "stdlib-first,
// every dep annotated" stance to the audience without making them
// open a terminal.
(function () {
const PYPROJECT = `[project]
name = "cis490"
description = "CIS490 behavioral malware detection — dataset, transport, training"
requires-python = ">=3.11"
dependencies = [
"starlette>=0.36",
"uvicorn[standard]>=0.27",
"msgpack>=1.0", # MSF RPC wire format for the Tier-3 exploit driver
"pycdlib>=1.14", # build NoCloud cidata ISOs in pure Python
]
[dependency-groups]
dev = [
"pytest>=8",
"pytest-asyncio>=0.23",
"httpx>=0.27",
"paramiko>=3", # SSH client for in-guest control on images that support it
]
`;
const RECEIVER = `from __future__ import annotations
import json
import logging
import secrets
import time
from pathlib import Path
from typing import Awaitable, Callable
from starlette.applications import Starlette
from starlette.requests import Request
from starlette.responses import JSONResponse, Response
from starlette.routing import Route
from .store import EpisodeStore, is_valid_id
from .version_gate import VersionGate
log = logging.getLogger("cis490.receiver")
`;
const PY_KEYWORDS = new Set([
'from', 'import', 'def', 'class', 'return', 'async', 'await',
'if', 'else', 'elif', 'for', 'while', 'in', 'as', 'with',
'lambda', 'None', 'True', 'False', 'raise', 'try', 'except',
'finally', 'yield', 'global', 'nonlocal', 'pass', 'break',
'continue', 'not', 'and', 'or', 'is', 'self',
]);
function escapeHtml(s) {
return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
// Find the first `#` that's not inside a string literal.
function findCommentStart(line) {
let inString = false;
for (let i = 0; i < line.length; i++) {
const ch = line[i];
if (inString) {
if (ch === '\\') { i++; continue; }
if (ch === inString) inString = false;
} else if (ch === '"' || ch === "'") {
inString = ch;
} else if (ch === '#') {
return i;
}
}
return -1;
}
// Char-by-char Python tokenizer. Handles strings, identifiers,
// numbers — enough for an imports block. Keyword regex would
// fire inside strings; we tokenize properly to avoid that.
function tokenizePython(s) {
let out = '';
let i = 0;
while (i < s.length) {
const ch = s[i];
if (ch === '"' || ch === "'") {
const quote = ch;
let j = i + 1;
while (j < s.length && s[j] !== quote) {
if (s[j] === '\\') j++;
j++;
}
out += `<span class="str">${s.slice(i, j + 1)}</span>`;
i = j + 1;
} else if (/[A-Za-z_]/.test(ch)) {
let j = i;
while (j < s.length && /[A-Za-z0-9_]/.test(s[j])) j++;
const word = s.slice(i, j);
if (PY_KEYWORDS.has(word)) out += `<span class="kw">${word}</span>`;
else out += word;
i = j;
} else if (/\d/.test(ch)) {
let j = i;
while (j < s.length && /[\d.]/.test(s[j])) j++;
out += `<span class="num">${s.slice(i, j)}</span>`;
i = j;
} else {
out += ch;
i++;
}
}
return out;
}
function highlightPython(code) {
return escapeHtml(code).split('\n').map(line => {
const idx = findCommentStart(line);
const codePart = idx >= 0 ? line.slice(0, idx) : line;
const comment = idx >= 0 ? line.slice(idx) : '';
return tokenizePython(codePart) +
(comment ? `<span class="com">${comment}</span>` : '');
}).join('\n');
}
function highlightToml(code) {
return escapeHtml(code).split('\n').map(line => {
const idx = findCommentStart(line);
const codePart = idx >= 0 ? line.slice(0, idx) : line;
const comment = idx >= 0 ? line.slice(idx) : '';
// Order matters: section headers, then key=, then strings,
// then numbers. Each replace operates on the result of the
// previous so we don't double-wrap.
const coded = codePart
.replace(/(\[[^\]\n]+\])/g, '<span class="ty">$1</span>')
.replace(/^([A-Za-z_][\w-]*)(\s*=)/, '<span class="fn">$1</span>$2')
.replace(/("[^"\n]*"|'[^'\n]*')/g, '<span class="str">$1</span>')
.replace(/\b(\d+(?:\.\d+)?)\b/g, '<span class="num">$1</span>');
return coded +
(comment ? `<span class="com">${comment}</span>` : '');
}).join('\n');
}
document.getElementById('code-pyproject').innerHTML = highlightToml(PYPROJECT);
document.getElementById('code-receiver').innerHTML = highlightPython(RECEIVER);
})();
// ── Ingest counter + 60-second sparkline ──────────────────────
// Real-data widget: populated by snapshot + episode events. No
// demo gating — when demo is off, it just shows real activity.

View file

@ -4,7 +4,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>CIS490 — live</title>
<link rel="stylesheet" href="/static/dashboard.css?v=8176c951">
<link rel="stylesheet" href="/static/dashboard.css?v=c1a8f8e7">
</head>
<body>
<header class="topbar">
@ -31,7 +31,24 @@
</div>
</div>
<!-- 2. collect -->
<!-- 2. stack — Python stack & libraries used in the project -->
<div class="stage-view" data-view="stack">
<div class="metric-stack metric-stack-wide">
<div class="metric-eyebrow">the stack · stdlib-first, four runtime deps, every choice annotated</div>
<div class="code-grid">
<div class="code-card">
<div class="code-card-header">pyproject.toml</div>
<pre class="code" id="code-pyproject"></pre>
</div>
<div class="code-card">
<div class="code-card-header">receiver/app.py · file header</div>
<pre class="code" id="code-receiver"></pre>
</div>
</div>
</div>
</div>
<!-- 3. collect -->
<div class="stage-view" data-view="collect">
<div class="metric-stack">
<div class="metric-eyebrow">episodes ingested</div>
@ -169,6 +186,27 @@
</div>
</section>
<section class="scene" data-stage="stack">
<div class="prose">
<h2>The stack</h2>
<p>Four runtime dependencies. <strong>Starlette</strong> +
<strong>uvicorn</strong> give us the async HTTP / WebSocket
surface; <strong>msgpack</strong> talks to Metasploit's RPC over
TLS for the Tier-3 exploit driver; <strong>pycdlib</strong> mints
NoCloud cidata ISOs in pure Python so we don't shell out to
<code>genisoimage</code>.</p>
<p>Everything else is the standard library — <code>asyncio</code>,
<code>pathlib</code>, <code>json</code>, <code>logging</code>,
<code>secrets</code>. Every module opens with <code>from
__future__ import annotations</code> and types its function
signatures, so ten months from now the docstring tells you what
it does and the signature tells you what it eats.</p>
<p>Every dep in <code>pyproject.toml</code> carries a one-line
comment explaining why it's there. If a future maintainer can't
justify a dep in ten words, it doesn't go in.</p>
</div>
</section>
<section class="scene" data-stage="collect">
<div class="prose">
<h2>Collecting the dataset</h2>
@ -296,6 +334,6 @@
</article>
</div>
<script src="/static/dashboard.js?v=9d42eb5f"></script>
<script src="/static/dashboard.js?v=f70e1018"></script>
</body>
</html>