diff --git a/.gitignore b/.gitignore
index 57b7d41..0588182 100644
--- a/.gitignore
+++ b/.gitignore
@@ -56,7 +56,7 @@ Desktop.ini
# Local build / test output (not tracked)
/spread-kits/
-server/coverage/
-server/builder_cov/
-server/$cov/
-server/internal/builder/cov/
+/server/coverage
+/server/builder_cov
+/server/internal/builder/cov
+/server/*cov*
diff --git a/server/web/src/pages/CruciblePage.css b/server/web/src/pages/CruciblePage.css
index 0af0195..9376f56 100644
--- a/server/web/src/pages/CruciblePage.css
+++ b/server/web/src/pages/CruciblePage.css
@@ -715,3 +715,157 @@
letter-spacing: 0.05em;
}
.crucible-btn-muted:hover { color: var(--text-primary); border-color: rgba(255,255,255,0.25); }
+
+/* ── Rich terminal renderers ──────────────────────────────────────────────── */
+
+.rich-line {
+ align-items: flex-start !important;
+}
+
+.ctl-rich {
+ flex: 1;
+ overflow-x: auto;
+}
+
+.rich-block {
+ margin: 0.2rem 0 0.4rem 0;
+ padding: 0.5rem 0.75rem;
+ border-left: 2px solid rgba(0, 245, 255, 0.3);
+ background: rgba(0, 0, 0, 0.35);
+ border-radius: 0 4px 4px 0;
+ font-family: var(--font-tech);
+ font-size: 0.76rem;
+ max-width: 720px;
+}
+
+.rich-ports { border-left-color: rgba(0, 245, 255, 0.5); }
+.rich-patch { border-left-color: rgba(255, 200, 0, 0.5); }
+.rich-posture { border-left-color: rgba(150, 120, 255, 0.5); }
+
+.rich-header {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ margin-bottom: 0.45rem;
+}
+
+.rich-label {
+ color: var(--neon-cyan);
+ font-weight: 700;
+ letter-spacing: 0.1em;
+ font-size: 0.72rem;
+}
+
+.rich-count {
+ color: var(--text-muted);
+ font-size: 0.68rem;
+}
+
+.rich-score {
+ font-weight: 700;
+ letter-spacing: 0.05em;
+ padding: 0.1rem 0.45rem;
+ border-radius: 3px;
+ font-size: 0.76rem;
+}
+.rich-score-ok { color: #00ff9d; background: rgba(0,255,100,0.1); }
+.rich-score-warn { color: #ffc800; background: rgba(255,200,0,0.1); }
+.rich-score-bad { color: #ff4444; background: rgba(255,50,0,0.12); }
+
+.rich-tag {
+ font-size: 0.68rem;
+ padding: 0.1rem 0.4rem;
+ border-radius: 3px;
+ font-weight: 700;
+ letter-spacing: 0.08em;
+}
+.rich-tag-warn { color: #ff4444; background: rgba(255, 50, 0, 0.15); border: 1px solid rgba(255,50,0,0.3); }
+
+.rich-empty {
+ color: var(--text-muted);
+ font-style: italic;
+ font-size: 0.72rem;
+ padding: 0.25rem 0;
+}
+
+/* Port/Patch/Posture tables */
+.rich-table {
+ border-collapse: collapse;
+ width: 100%;
+ font-size: 0.74rem;
+}
+
+.rich-table th {
+ color: var(--text-muted);
+ text-align: left;
+ padding: 0.1rem 0.6rem 0.15rem 0;
+ letter-spacing: 0.08em;
+ font-size: 0.66rem;
+ border-bottom: 1px solid rgba(255,255,255,0.07);
+ white-space: nowrap;
+}
+
+.rich-table td {
+ padding: 0.18rem 0.6rem 0.18rem 0;
+ white-space: nowrap;
+ color: var(--text-primary);
+}
+
+.rich-table tbody tr:hover { background: rgba(255,255,255,0.03); }
+
+.rich-row-ssh td { color: #00ff9d; }
+.rich-row-sys td:first-child { color: var(--neon-cyan); }
+
+.rich-port { font-weight: 700; min-width: 48px; color: var(--neon-cyan) !important; }
+.rich-addr { color: var(--text-muted) !important; min-width: 100px; }
+.rich-proto { color: rgba(180,160,255,0.85) !important; min-width: 50px; }
+.rich-proc { color: var(--text-primary); min-width: 100px; }
+.rich-pid { color: var(--text-muted) !important; font-size: 0.68rem; }
+
+/* Key-value rows */
+.rich-kv-row {
+ display: flex;
+ gap: 0.5rem;
+ padding: 0.12rem 0;
+ align-items: baseline;
+}
+
+.rich-key {
+ color: var(--text-muted);
+ min-width: 140px;
+ font-size: 0.71rem;
+ flex-shrink: 0;
+}
+
+.rich-val { color: var(--text-primary); font-size: 0.74rem; }
+.rich-val-ok { color: #00ff9d; }
+.rich-val-warn { color: #ffc800; }
+.rich-val-bad { color: #ff4444; }
+
+.rich-posture-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 0 1.5rem;
+}
+
+.rich-sub-label {
+ margin: 0.5rem 0 0.2rem;
+ color: var(--neon-cyan);
+ font-size: 0.66rem;
+ letter-spacing: 0.1em;
+ opacity: 0.7;
+}
+
+.rich-svc-status {
+ font-weight: 700;
+ font-size: 0.7rem;
+ letter-spacing: 0.05em;
+}
+
+.blink-slow {
+ animation: blink-slow 1.5s step-end infinite;
+}
+@keyframes blink-slow {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.3; }
+}
diff --git a/server/web/src/pages/CruciblePage.tsx b/server/web/src/pages/CruciblePage.tsx
index d6521c9..00b64d8 100644
--- a/server/web/src/pages/CruciblePage.tsx
+++ b/server/web/src/pages/CruciblePage.tsx
@@ -579,6 +579,137 @@ export default function CruciblePage() {
};
};
+ // ── Rich terminal renderers ────────────────────────────────────────────
+
+ const RichListenPortsTable = ({ d }: { d: RichListenPorts }) => (
+
+
+ LISTEN PORTS
+ {d.count} listener{d.count !== 1 ? 's' : ''}
+
+ {d.ports.length === 0 ? (
+
No listeners found
+ ) : (
+
+
+
+ | PORT |
+ ADDR |
+ PROTO |
+ PROCESS |
+ PID |
+
+
+
+ {d.ports.map((p, i) => (
+
+ | {p.port} |
+ {p.addr || '*'} |
+ {p.proto.toUpperCase()} |
+ {p.process || '—'} |
+ {p.pid ?? '—'} |
+
+ ))}
+
+
+ )}
+
+ );
+
+ const RichPatchStatusBlock = ({ d }: { d: RichPatchStatus }) => (
+
+
+ PATCH STATUS
+ {d.reboot_pending && REBOOT REQUIRED}
+
+
+ Pending Updates
+ 0 ? 'rich-val-warn' : 'rich-val-ok'}`}>
+ {d.pending_updates ?? 0}
+
+
+
+ Last Patch
+ 60 ? 'rich-val-bad' : (d.last_patch_days ?? 0) > 30 ? 'rich-val-warn' : 'rich-val-ok'}`}>
+ {d.last_patch ?? '—'}{d.last_patch_days != null ? ` (${d.last_patch_days}d ago)` : ''}
+
+
+
+ Reboot Pending
+
+ {d.reboot_pending ? 'YES' : 'NO'}
+
+
+
+ );
+
+ const fwIcon = (on?: boolean) => on ? ON : OFF;
+
+ const RichPostureSummaryBlock = ({ d }: { d: RichPostureSummary }) => (
+
+
+ POSTURE REPORT
+ {d.posture_score != null && (
+ = 70 ? 'rich-score-ok' : d.posture_score >= 40 ? 'rich-score-warn' : 'rich-score-bad'}`}>
+ {d.posture_score} / 100
+
+ )}
+
+
+
Defender
+ {d.defender_enabled ? 'ON' : 'OFF'}
+
+
Real-Time Prot
+ {d.defender_rtp ? 'ON' : 'OFF'}
+
+
Firewall Domain/Priv/Pub
+ {fwIcon(d.firewall_domain)} / {fwIcon(d.firewall_private)} / {fwIcon(d.firewall_public)}
+
+
AV Products
+ {d.av_products?.join(', ') || '—'}
+
+
SSH Listening
+ {d.ssh_listening ? 'YES' : 'NO'}
+
+
Agent Elevated
+ {d.agent_elevated ? 'YES (ADMIN)' : 'no'}
+
+ {d.last_patch_days != null && (
+
Last Patch
+ 60 ? 'rich-val-bad' : d.last_patch_days > 30 ? 'rich-val-warn' : 'rich-val-ok'}`}>
+ {d.last_patch_days}d ago{d.pending_updates ? ` · ${d.pending_updates} pending` : ''}
+ {d.reboot_pending ? ' · REBOOT!' : ''}
+
+
+ )}
+
+ {d.services && d.services.length > 0 && (
+ <>
+
SERVICES
+
+ | SERVICE | STATUS | START |
+
+ {d.services.map((s, i) => (
+
+ | {s.display_name || s.name} |
+ {s.status.toUpperCase()} |
+ {s.start_type} |
+
+ ))}
+
+
+ >
+ )}
+
+ );
+
+ const renderRichData = (d: RichTermData) => {
+ if (d.type === 'listen_ports') return ;
+ if (d.type === 'patch_status') return ;
+ if (d.type === 'posture') return ;
+ return null;
+ };
+
// ── Render ─────────────────────────────────────────────────────────────
return (
@@ -929,7 +1060,7 @@ export default function CruciblePage() {
return (
{line.agentName.slice(0, 12).padEnd(12)}
@@ -937,7 +1068,11 @@ export default function CruciblePage() {
{line.isCmd ? '▶' : '◀'}
- {line.text}
+ {line.richData ? (
+ {renderRichData(line.richData)}
+ ) : (
+ {line.text}
+ )}
{line.ts.toLocaleTimeString()}
);