Expand test coverage across server, agent, and web; fix bugs found during audit.
Adds hundreds of unit/integration/e2e tests, fixes WS bcrypt auth, config merge, fleet analytics, agent schedule/log tail, and documents stale PROBLEMS items. Updates PROBLEMS.md, README, and test scripts; ignores local spread-kits and coverage dirs.
This commit is contained in:
@@ -18,8 +18,53 @@ interface TermLine {
|
||||
text: string;
|
||||
ts: Date;
|
||||
success?: boolean;
|
||||
// Structured data for rich terminal renderers
|
||||
richData?: RichTermData;
|
||||
}
|
||||
|
||||
// ── Rich terminal data types ────────────────────────────────────────────────
|
||||
|
||||
interface RichListenPort {
|
||||
port: number;
|
||||
addr: string;
|
||||
proto: string;
|
||||
process?: string;
|
||||
pid?: number;
|
||||
}
|
||||
|
||||
interface RichListenPorts {
|
||||
type: 'listen_ports';
|
||||
ports: RichListenPort[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
interface RichPatchStatus {
|
||||
type: 'patch_status';
|
||||
pending_updates?: number;
|
||||
last_patch?: string;
|
||||
last_patch_days?: number;
|
||||
reboot_pending?: boolean;
|
||||
}
|
||||
|
||||
interface RichPostureSummary {
|
||||
type: 'posture';
|
||||
posture_score?: number;
|
||||
defender_enabled?: boolean;
|
||||
defender_rtp?: boolean;
|
||||
av_products?: string[];
|
||||
firewall_domain?: boolean;
|
||||
firewall_private?: boolean;
|
||||
firewall_public?: boolean;
|
||||
ssh_listening?: boolean;
|
||||
agent_elevated?: boolean;
|
||||
last_patch_days?: number;
|
||||
pending_updates?: number;
|
||||
reboot_pending?: boolean;
|
||||
services?: Array<{ name: string; display_name?: string; status: string; start_type: string }>;
|
||||
}
|
||||
|
||||
type RichTermData = RichListenPorts | RichPatchStatus | RichPostureSummary;
|
||||
|
||||
interface NodeGroup {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -34,35 +79,35 @@ const AGENT_COLORS = [
|
||||
'#7209b7', '#3a86ff', '#06d6a0', '#ffd60a',
|
||||
];
|
||||
|
||||
function agentColor(agentId: string, allIds: string[]): string {
|
||||
export function agentColor(agentId: string, allIds: string[]): string {
|
||||
const idx = allIds.indexOf(agentId);
|
||||
return AGENT_COLORS[idx % AGENT_COLORS.length] ?? '#00f5ff';
|
||||
}
|
||||
|
||||
function sshBadge(agent: Agent) {
|
||||
export function sshBadge(agent: Agent) {
|
||||
if (agent.ssh_available === true) return { label: 'SSH ON', cls: 'ssh-on' };
|
||||
if (agent.ssh_available === false) return { label: 'SSH OFF', cls: 'ssh-off' };
|
||||
return { label: 'SSH ?', cls: 'ssh-unk' };
|
||||
}
|
||||
|
||||
function postureBadge(score?: number) {
|
||||
export function postureBadge(score?: number) {
|
||||
if (score === undefined) return { label: 'POSTURE ?', cls: 'posture-unk' };
|
||||
if (score >= 80) return { label: `POSTURE ${score}`, cls: 'posture-good' };
|
||||
if (score >= 40) return { label: `POSTURE ${score}`, cls: 'posture-warn' };
|
||||
return { label: `POSTURE ${score}`, cls: 'posture-bad' };
|
||||
}
|
||||
|
||||
function patchLabel(days?: number) {
|
||||
export function patchLabel(days?: number) {
|
||||
if (days === undefined) return null;
|
||||
return { label: `PATCH ${days}d`, cls: days <= 30 ? 'patch-ok' : 'patch-stale' };
|
||||
}
|
||||
|
||||
function portsBadge(count?: number): { label: string; cls: string } | null {
|
||||
export function portsBadge(count?: number): { label: string; cls: string } | null {
|
||||
if (count === undefined) return null;
|
||||
return { label: `PORTS ${count}`, cls: count > 20 ? 'ports-many' : 'ports-ok' };
|
||||
}
|
||||
|
||||
function postureTooltip(agent: Agent): string {
|
||||
export function postureTooltip(agent: Agent): string {
|
||||
const lines: string[] = [];
|
||||
const yn = (v?: boolean) => v === true ? '✓' : v === false ? '✗' : '?';
|
||||
const na = (v: unknown) => v !== undefined && v !== null ? String(v) : '?';
|
||||
@@ -118,7 +163,7 @@ function postureTooltip(agent: Agent): string {
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function pendingBadge(agent: Agent): { label: string; cls: string } | null {
|
||||
export function pendingBadge(agent: Agent): { label: string; cls: string } | null {
|
||||
const u = agent.pending_updates;
|
||||
if (u === undefined) return null;
|
||||
if (u < 0) return { label: 'UPD ?', cls: 'upd-unk' };
|
||||
@@ -135,7 +180,7 @@ function rebootBadge(agent: Agent): { label: string; cls: string } | null {
|
||||
|
||||
// ── Resource pressure badges ───────────────────────────────────────────────
|
||||
|
||||
function thermalBadge(agent: Agent): { label: string; cls: string } | null {
|
||||
export function thermalBadge(agent: Agent): { label: string; cls: string } | null {
|
||||
const t = agent.gpu_temp_c ?? agent.cpu_temp_c;
|
||||
if (t === undefined) return null;
|
||||
if (t > 80) return { label: `${t}°`, cls: 'therm-hot' };
|
||||
@@ -289,44 +334,68 @@ export default function CruciblePage() {
|
||||
if (!aid) continue;
|
||||
|
||||
const msg = r.message ?? '';
|
||||
// Always update badges from probe / heartbeat command responses
|
||||
|
||||
// ── SSH badge updates ───────────────────────────────────────────────
|
||||
if (msg.includes('SSH_PROBE:ONLINE')) {
|
||||
setSshOverride((prev) => ({ ...prev, [aid]: true }));
|
||||
} else if (msg.includes('SSH_PROBE:OFFLINE')) {
|
||||
setSshOverride((prev) => ({ ...prev, [aid]: false }));
|
||||
}
|
||||
if (r.action === 'posture' || msg.includes('{')) {
|
||||
|
||||
// ── Parse structured JSON for known actions ────────────────────────
|
||||
let richData: RichTermData | undefined;
|
||||
const jsonStart = msg.indexOf('{');
|
||||
|
||||
if (jsonStart >= 0) {
|
||||
try {
|
||||
const start = msg.indexOf('{');
|
||||
if (start >= 0) {
|
||||
const p = JSON.parse(msg.slice(start)) as { posture_score?: number; last_patch_days?: number; ssh_listening?: boolean };
|
||||
if (typeof p.posture_score === 'number') {
|
||||
const parsed = JSON.parse(msg.slice(jsonStart));
|
||||
|
||||
if (r.action === 'listen_ports' && Array.isArray(parsed.ports)) {
|
||||
richData = { type: 'listen_ports', ports: parsed.ports, count: parsed.count ?? parsed.ports.length };
|
||||
} else if (r.action === 'patch_status') {
|
||||
richData = { type: 'patch_status', ...parsed };
|
||||
} else if (r.action === 'posture' && typeof parsed.posture_score === 'number') {
|
||||
richData = { type: 'posture', ...parsed };
|
||||
// Update badge state
|
||||
setPostureOverride((prev) => ({
|
||||
...prev,
|
||||
[aid]: { score: parsed.posture_score, patchDays: parsed.last_patch_days },
|
||||
}));
|
||||
if (parsed.ssh_listening === true) setSshOverride((prev) => ({ ...prev, [aid]: true }));
|
||||
if (parsed.ssh_listening === false) setSshOverride((prev) => ({ ...prev, [aid]: false }));
|
||||
} else {
|
||||
// Generic JSON with posture fields (legacy path)
|
||||
if (typeof parsed.posture_score === 'number') {
|
||||
setPostureOverride((prev) => ({
|
||||
...prev,
|
||||
[aid]: { score: p.posture_score!, patchDays: p.last_patch_days },
|
||||
[aid]: { score: parsed.posture_score, patchDays: parsed.last_patch_days },
|
||||
}));
|
||||
}
|
||||
if (p.ssh_listening === true) setSshOverride((prev) => ({ ...prev, [aid]: true }));
|
||||
if (p.ssh_listening === false) setSshOverride((prev) => ({ ...prev, [aid]: false }));
|
||||
if (parsed.ssh_listening === true) setSshOverride((prev) => ({ ...prev, [aid]: true }));
|
||||
if (parsed.ssh_listening === false) setSshOverride((prev) => ({ ...prev, [aid]: false }));
|
||||
}
|
||||
} catch { /* ignore malformed JSON */ }
|
||||
} catch { /* malformed JSON — fall through to plain text */ }
|
||||
}
|
||||
|
||||
if (selectedIds.size > 0 && !selectedIds.has(aid)) continue;
|
||||
const agent = agents.find((a) => a.id === aid);
|
||||
const name = agent?.name ?? aid.slice(0, 8);
|
||||
|
||||
const msgLines = msg.split('\n').filter(Boolean);
|
||||
for (const line of msgLines) {
|
||||
if (richData) {
|
||||
// Single rich-rendered line (table/block replaces raw JSON)
|
||||
lines.push({
|
||||
id: mkId(),
|
||||
agentId: aid,
|
||||
agentName: name,
|
||||
isCmd: false,
|
||||
text: line,
|
||||
ts: new Date(),
|
||||
success: r.success,
|
||||
id: mkId(), agentId: aid, agentName: name,
|
||||
isCmd: false, text: '', ts: new Date(),
|
||||
success: r.success, richData,
|
||||
});
|
||||
} else {
|
||||
const msgLines = msg.split('\n').filter(Boolean);
|
||||
for (const line of msgLines) {
|
||||
lines.push({
|
||||
id: mkId(), agentId: aid, agentName: name,
|
||||
isCmd: false, text: line, ts: new Date(), success: r.success,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (lines.length > 0) {
|
||||
|
||||
Reference in New Issue
Block a user