Add Crucible rich terminal renderers and ignore local coverage dirs.
Listen ports table UI and styles; gitignore for spread-kits and cov artifact files.
This commit is contained in:
@@ -579,6 +579,137 @@ export default function CruciblePage() {
|
||||
};
|
||||
};
|
||||
|
||||
// ── Rich terminal renderers ────────────────────────────────────────────
|
||||
|
||||
const RichListenPortsTable = ({ d }: { d: RichListenPorts }) => (
|
||||
<div className="rich-block rich-ports">
|
||||
<div className="rich-header">
|
||||
<span className="rich-label">LISTEN PORTS</span>
|
||||
<span className="rich-count">{d.count} listener{d.count !== 1 ? 's' : ''}</span>
|
||||
</div>
|
||||
{d.ports.length === 0 ? (
|
||||
<div className="rich-empty">No listeners found</div>
|
||||
) : (
|
||||
<table className="rich-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>PORT</th>
|
||||
<th>ADDR</th>
|
||||
<th>PROTO</th>
|
||||
<th>PROCESS</th>
|
||||
<th>PID</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{d.ports.map((p, i) => (
|
||||
<tr key={i} className={p.port === 22 ? 'rich-row-ssh' : p.port < 1024 ? 'rich-row-sys' : ''}>
|
||||
<td className="rich-port">{p.port}</td>
|
||||
<td className="rich-addr">{p.addr || '*'}</td>
|
||||
<td className="rich-proto">{p.proto.toUpperCase()}</td>
|
||||
<td className="rich-proc">{p.process || '—'}</td>
|
||||
<td className="rich-pid">{p.pid ?? '—'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const RichPatchStatusBlock = ({ d }: { d: RichPatchStatus }) => (
|
||||
<div className="rich-block rich-patch">
|
||||
<div className="rich-header">
|
||||
<span className="rich-label">PATCH STATUS</span>
|
||||
{d.reboot_pending && <span className="rich-tag rich-tag-warn blink-slow">REBOOT REQUIRED</span>}
|
||||
</div>
|
||||
<div className="rich-kv-row">
|
||||
<span className="rich-key">Pending Updates</span>
|
||||
<span className={`rich-val ${(d.pending_updates ?? 0) > 0 ? 'rich-val-warn' : 'rich-val-ok'}`}>
|
||||
{d.pending_updates ?? 0}
|
||||
</span>
|
||||
</div>
|
||||
<div className="rich-kv-row">
|
||||
<span className="rich-key">Last Patch</span>
|
||||
<span className={`rich-val ${(d.last_patch_days ?? 0) > 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)` : ''}
|
||||
</span>
|
||||
</div>
|
||||
<div className="rich-kv-row">
|
||||
<span className="rich-key">Reboot Pending</span>
|
||||
<span className={`rich-val ${d.reboot_pending ? 'rich-val-warn' : 'rich-val-ok'}`}>
|
||||
{d.reboot_pending ? 'YES' : 'NO'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const fwIcon = (on?: boolean) => on ? <span className="rich-val-ok">ON</span> : <span className="rich-val-bad">OFF</span>;
|
||||
|
||||
const RichPostureSummaryBlock = ({ d }: { d: RichPostureSummary }) => (
|
||||
<div className="rich-block rich-posture">
|
||||
<div className="rich-header">
|
||||
<span className="rich-label">POSTURE REPORT</span>
|
||||
{d.posture_score != null && (
|
||||
<span className={`rich-score ${d.posture_score >= 70 ? 'rich-score-ok' : d.posture_score >= 40 ? 'rich-score-warn' : 'rich-score-bad'}`}>
|
||||
{d.posture_score} / 100
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="rich-posture-grid">
|
||||
<div className="rich-kv-row"><span className="rich-key">Defender</span>
|
||||
<span className={`rich-val ${d.defender_enabled ? 'rich-val-ok' : 'rich-val-bad'}`}>{d.defender_enabled ? 'ON' : 'OFF'}</span>
|
||||
</div>
|
||||
<div className="rich-kv-row"><span className="rich-key">Real-Time Prot</span>
|
||||
<span className={`rich-val ${d.defender_rtp ? 'rich-val-ok' : 'rich-val-warn'}`}>{d.defender_rtp ? 'ON' : 'OFF'}</span>
|
||||
</div>
|
||||
<div className="rich-kv-row"><span className="rich-key">Firewall Domain/Priv/Pub</span>
|
||||
<span className="rich-val">{fwIcon(d.firewall_domain)} / {fwIcon(d.firewall_private)} / {fwIcon(d.firewall_public)}</span>
|
||||
</div>
|
||||
<div className="rich-kv-row"><span className="rich-key">AV Products</span>
|
||||
<span className="rich-val">{d.av_products?.join(', ') || '—'}</span>
|
||||
</div>
|
||||
<div className="rich-kv-row"><span className="rich-key">SSH Listening</span>
|
||||
<span className={`rich-val ${d.ssh_listening ? 'rich-val-ok' : 'rich-val-bad'}`}>{d.ssh_listening ? 'YES' : 'NO'}</span>
|
||||
</div>
|
||||
<div className="rich-kv-row"><span className="rich-key">Agent Elevated</span>
|
||||
<span className={`rich-val ${d.agent_elevated ? 'rich-val-warn' : 'rich-val-ok'}`}>{d.agent_elevated ? 'YES (ADMIN)' : 'no'}</span>
|
||||
</div>
|
||||
{d.last_patch_days != null && (
|
||||
<div className="rich-kv-row"><span className="rich-key">Last Patch</span>
|
||||
<span className={`rich-val ${d.last_patch_days > 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!' : ''}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{d.services && d.services.length > 0 && (
|
||||
<>
|
||||
<div className="rich-sub-label">SERVICES</div>
|
||||
<table className="rich-table">
|
||||
<thead><tr><th>SERVICE</th><th>STATUS</th><th>START</th></tr></thead>
|
||||
<tbody>
|
||||
{d.services.map((s, i) => (
|
||||
<tr key={i}>
|
||||
<td className="rich-proc">{s.display_name || s.name}</td>
|
||||
<td className={`rich-svc-status ${s.status === 'running' ? 'rich-val-ok' : 'rich-val-bad'}`}>{s.status.toUpperCase()}</td>
|
||||
<td className="rich-addr">{s.start_type}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderRichData = (d: RichTermData) => {
|
||||
if (d.type === 'listen_ports') return <RichListenPortsTable d={d} />;
|
||||
if (d.type === 'patch_status') return <RichPatchStatusBlock d={d} />;
|
||||
if (d.type === 'posture') return <RichPostureSummaryBlock d={d} />;
|
||||
return null;
|
||||
};
|
||||
|
||||
// ── Render ─────────────────────────────────────────────────────────────
|
||||
|
||||
return (
|
||||
@@ -929,7 +1060,7 @@ export default function CruciblePage() {
|
||||
return (
|
||||
<div
|
||||
key={line.id}
|
||||
className={`crucible-term-line ${line.isCmd ? 'cmd-line' : 'out-line'} ${line.success === false ? 'err-line' : ''}`}
|
||||
className={`crucible-term-line ${line.isCmd ? 'cmd-line' : 'out-line'} ${line.success === false ? 'err-line' : ''} ${line.richData ? 'rich-line' : ''}`}
|
||||
>
|
||||
<span className="ctl-agent" style={{ color }}>
|
||||
{line.agentName.slice(0, 12).padEnd(12)}
|
||||
@@ -937,7 +1068,11 @@ export default function CruciblePage() {
|
||||
<span className="ctl-arrow" style={{ color }}>
|
||||
{line.isCmd ? '▶' : '◀'}
|
||||
</span>
|
||||
<span className="ctl-text">{line.text}</span>
|
||||
{line.richData ? (
|
||||
<span className="ctl-text ctl-rich">{renderRichData(line.richData)}</span>
|
||||
) : (
|
||||
<span className="ctl-text">{line.text}</span>
|
||||
)}
|
||||
<span className="ctl-ts">{line.ts.toLocaleTimeString()}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user