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:
parent
e5931df8ad
commit
cd133a2dd8
3 changed files with 223 additions and 3 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue