singularity-forge/web/components/sf/diagnostics-panels.tsx
ace-pm 172753c3b2 refactor(forge): complete gsd → forge rebrand across native, logging, and build system
- Rename native Rust crates: gsd-engine → forge-engine, gsd-ast → forge-ast, gsd-grep → forge-grep
- Update all crate dependencies (Cargo.toml, .rs source) and N-API artifacts
- Mass rename log prefix [gsd] → [forge] across 81 files (scripts, src/, extensions, tests)
- Rename log prefix "gsd-db:" → "forge-db:" in template literals
- Update nix flake: add sf-run-native devShell with Rust toolchain for native addon builds
- Update CI workflow artifact names (build-native.yml)
- Verify only packages/native/* touched (no upstream pi-* packages renamed)

Rationale: Complete gsd-2 → singularity-forge rebrand (2026-04-15). Native addon is
sf-run-specific; all gsd-prefixed logging and crate names must align with new identity.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 14:11:45 +02:00

523 lines
23 KiB
TypeScript

"use client"
import { AlertTriangle, CheckCircle2, Info, LoaderCircle, RefreshCw, ShieldAlert, Wrench, XCircle } from "lucide-react"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import type {
DoctorIssue,
ForensicAnomaly,
ForensicReport,
DoctorReport,
SkillHealthReport,
SkillHealSuggestion,
} from "@/lib/diagnostics-types"
import { cn } from "@/lib/utils"
import {
formatCost,
useGSDWorkspaceActions,
useGSDWorkspaceState,
} from "@/lib/sf-workspace-store"
// ═══════════════════════════════════════════════════════════════════════
// SHARED
// ═══════════════════════════════════════════════════════════════════════
function SeverityIcon({ severity, className }: { severity: "info" | "warning" | "error" | "critical"; className?: string }) {
const base = cn("h-3.5 w-3.5 shrink-0", className)
switch (severity) {
case "error":
case "critical":
return <XCircle className={cn(base, "text-destructive")} />
case "warning":
return <AlertTriangle className={cn(base, "text-warning")} />
default:
return <Info className={cn(base, "text-info")} />
}
}
function severityBadgeVariant(s: string): "destructive" | "secondary" | "outline" {
if (s === "error" || s === "critical") return "destructive"
if (s === "warning") return "secondary"
return "outline"
}
function DiagHeader({
title,
subtitle,
status,
onRefresh,
refreshing,
}: {
title: string
subtitle?: string | null
status?: React.ReactNode
onRefresh: () => void
refreshing: boolean
}) {
return (
<div className="flex items-center justify-between gap-3 pb-4">
<div className="flex items-center gap-2.5">
<h3 className="text-[13px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{title}</h3>
{status}
{subtitle && <span className="text-[11px] text-muted-foreground">{subtitle}</span>}
</div>
<Button type="button" variant="ghost" size="sm" onClick={onRefresh} disabled={refreshing} className="h-7 gap-1.5 text-xs">
<RefreshCw className={cn("h-3 w-3", refreshing && "animate-spin")} />
Refresh
</Button>
</div>
)
}
function DiagError({ message }: { message: string }) {
return (
<div className="rounded-lg border border-destructive/20 bg-destructive/5 px-3 py-2.5 text-xs text-destructive">
{message}
</div>
)
}
function DiagLoading({ label }: { label: string }) {
return (
<div className="flex items-center gap-2 py-6 text-xs text-muted-foreground">
<LoaderCircle className="h-3.5 w-3.5 animate-spin" />
{label}
</div>
)
}
function DiagEmpty({ message }: { message: string }) {
return (
<div className="rounded-lg border border-border/50 bg-card/50 px-4 py-5 text-center text-xs text-muted-foreground">
{message}
</div>
)
}
function StatPill({ label, value, variant }: { label: string; value: number | string; variant?: "default" | "error" | "warning" | "info" }) {
return (
<div className={cn(
"flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-xs",
variant === "error" && "border-destructive/20 bg-destructive/5 text-destructive",
variant === "warning" && "border-warning/20 bg-warning/5 text-warning",
variant === "info" && "border-info/20 bg-info/5 text-info",
(!variant || variant === "default") && "border-border/50 bg-card/50 text-foreground/80",
)}>
<span className="text-muted-foreground">{label}</span>
<span className="font-medium tabular-nums">{value}</span>
</div>
)
}
// ═══════════════════════════════════════════════════════════════════════
// FORENSICS PANEL
// ═══════════════════════════════════════════════════════════════════════
function AnomalyRow({ anomaly }: { anomaly: ForensicAnomaly }) {
return (
<div className="rounded-lg border border-border/50 bg-card/50 px-3 py-2.5 space-y-1">
<div className="flex items-center gap-2">
<SeverityIcon severity={anomaly.severity} />
<Badge variant={severityBadgeVariant(anomaly.severity)} className="text-[10px] px-1.5 py-0">{anomaly.severity}</Badge>
<Badge variant="outline" className="text-[10px] px-1.5 py-0 font-mono">{anomaly.type}</Badge>
{anomaly.unitId && (
<span className="text-[10px] text-muted-foreground font-mono truncate">{anomaly.unitType}/{anomaly.unitId}</span>
)}
</div>
<p className="text-xs text-foreground">{anomaly.summary}</p>
{anomaly.details && anomaly.details !== anomaly.summary && (
<p className="text-[11px] text-muted-foreground leading-relaxed">{anomaly.details}</p>
)}
</div>
)
}
export function ForensicsPanel() {
const workspace = useGSDWorkspaceState()
const { loadForensicsDiagnostics } = useGSDWorkspaceActions()
const state = workspace.commandSurface.diagnostics.forensics
const data = state.data as ForensicReport | null
const busy = state.phase === "loading"
return (
<div className="space-y-4" data-testid="diagnostics-forensics">
<DiagHeader
title="Forensic Analysis"
subtitle={data ? new Date(data.timestamp).toLocaleString() : null}
status={data ? (
<span className={cn(
"inline-block h-1.5 w-1.5 rounded-full",
data.anomalies.length > 0 ? "bg-warning" : "bg-success",
)} />
) : null}
onRefresh={() => void loadForensicsDiagnostics()}
refreshing={busy}
/>
{state.error && <DiagError message={state.error} />}
{busy && !data && <DiagLoading label="Running forensic analysis…" />}
{data && (
<>
{/* Metrics summary */}
{data.metrics && (
<div className="flex flex-wrap gap-2">
<StatPill label="Units" value={data.metrics.totalUnits} />
<StatPill label="Cost" value={formatCost(data.metrics.totalCost)} />
<StatPill label="Duration" value={`${Math.round(data.metrics.totalDuration / 1000)}s`} />
<StatPill label="Traces" value={data.unitTraceCount} />
</div>
)}
{/* Crash lock */}
{data.crashLock ? (
<div className="rounded-lg border border-destructive/20 bg-destructive/5 px-3 py-2.5 space-y-1">
<div className="flex items-center gap-2">
<ShieldAlert className="h-3.5 w-3.5 text-destructive" />
<span className="text-xs font-medium text-destructive">Crash Lock Active</span>
</div>
<div className="grid grid-cols-2 gap-x-4 gap-y-0.5 text-[11px]">
<span className="text-muted-foreground">PID</span>
<span className="font-mono text-foreground/80">{data.crashLock.pid}</span>
<span className="text-muted-foreground">Started</span>
<span className="text-foreground/80">{new Date(data.crashLock.startedAt).toLocaleString()}</span>
<span className="text-muted-foreground">Unit</span>
<span className="font-mono text-foreground/80">{data.crashLock.unitType}/{data.crashLock.unitId}</span>
</div>
</div>
) : (
<div className="flex items-center gap-2 rounded-lg border border-border/50 bg-card/50 px-3 py-2 text-xs text-muted-foreground">
<CheckCircle2 className="h-3.5 w-3.5 text-success" />
No crash lock
</div>
)}
{/* Anomalies */}
{data.anomalies.length > 0 ? (
<div className="space-y-2">
<h4 className="text-xs font-medium text-muted-foreground">Anomalies ({data.anomalies.length})</h4>
{data.anomalies.map((a, i) => <AnomalyRow key={i} anomaly={a} />)}
</div>
) : (
<DiagEmpty message="No anomalies detected" />
)}
{/* Recent units */}
{data.recentUnits.length > 0 && (
<div className="space-y-2">
<h4 className="text-xs font-medium text-muted-foreground">Recent Units ({data.recentUnits.length})</h4>
<div className="overflow-x-auto rounded-lg border border-border/50">
<table className="w-full text-[11px]">
<thead>
<tr className="border-b border-border/50 bg-card/50">
<th className="px-2.5 py-1.5 text-left font-medium text-muted-foreground">Type</th>
<th className="px-2.5 py-1.5 text-left font-medium text-muted-foreground">ID</th>
<th className="px-2.5 py-1.5 text-left font-medium text-muted-foreground">Model</th>
<th className="px-2.5 py-1.5 text-right font-medium text-muted-foreground">Cost</th>
<th className="px-2.5 py-1.5 text-right font-medium text-muted-foreground">Duration</th>
</tr>
</thead>
<tbody>
{data.recentUnits.map((u, i) => (
<tr key={i} className="border-b border-border/50 last:border-0">
<td className="px-2.5 py-1.5 font-mono text-foreground/80">{u.type}</td>
<td className="px-2.5 py-1.5 font-mono text-foreground/80 truncate max-w-[120px]">{u.id}</td>
<td className="px-2.5 py-1.5 text-muted-foreground">{u.model}</td>
<td className="px-2.5 py-1.5 text-right tabular-nums text-foreground/80">{formatCost(u.cost)}</td>
<td className="px-2.5 py-1.5 text-right tabular-nums text-foreground/80">{Math.round(u.duration / 1000)}s</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</>
)}
</div>
)
}
// ═══════════════════════════════════════════════════════════════════════
// DOCTOR PANEL
// ═══════════════════════════════════════════════════════════════════════
function humanizeCode(code: string): string {
return code.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())
}
function IssueRow({ issue }: { issue: DoctorIssue }) {
return (
<div className="rounded-lg border border-border/50 bg-card/50 px-3 py-2.5 space-y-1">
<div className="flex items-center gap-2 flex-wrap">
<SeverityIcon severity={issue.severity} />
<Badge variant={severityBadgeVariant(issue.severity)} className="text-[10px] px-1.5 py-0">{issue.severity}</Badge>
<Badge variant="outline" className="text-[10px] px-1.5 py-0 font-mono">{humanizeCode(issue.code)}</Badge>
{issue.scope && <span className="text-[10px] text-muted-foreground font-mono">{issue.scope}</span>}
{issue.fixable && (
<Badge variant="outline" className="text-[10px] px-1.5 py-0 border-success/30 text-success">
<Wrench className="h-2.5 w-2.5 mr-0.5" />fixable
</Badge>
)}
</div>
<p className="text-xs text-foreground">{issue.message}</p>
{issue.file && <p className="text-[10px] font-mono text-muted-foreground truncate">{issue.file}</p>}
</div>
)
}
export function DoctorPanel() {
const workspace = useGSDWorkspaceState()
const { loadDoctorDiagnostics, applyDoctorFixes } = useGSDWorkspaceActions()
const state = workspace.commandSurface.diagnostics.doctor
const data = state.data as DoctorReport | null
const busy = state.phase === "loading"
const fixableCount = data?.summary.fixable ?? 0
return (
<div className="space-y-4" data-testid="diagnostics-doctor">
<DiagHeader
title="Doctor Health Check"
status={data ? (
<span className={cn(
"inline-block h-1.5 w-1.5 rounded-full",
data.ok ? "bg-success" : "bg-destructive",
)} />
) : null}
onRefresh={() => void loadDoctorDiagnostics()}
refreshing={busy}
/>
{state.error && <DiagError message={state.error} />}
{busy && !data && <DiagLoading label="Running health check…" />}
{data && (
<>
{/* Summary bar */}
<div className="flex flex-wrap gap-2">
<StatPill label="Total" value={data.summary.total} />
{data.summary.errors > 0 && <StatPill label="Errors" value={data.summary.errors} variant="error" />}
{data.summary.warnings > 0 && <StatPill label="Warnings" value={data.summary.warnings} variant="warning" />}
{data.summary.infos > 0 && <StatPill label="Info" value={data.summary.infos} variant="info" />}
{fixableCount > 0 && (
<StatPill label="Fixable" value={fixableCount} variant="info" />
)}
</div>
{/* Apply fixes button */}
{fixableCount > 0 && (
<div className="flex items-center gap-3">
<Button
type="button"
variant="default"
size="sm"
onClick={() => void applyDoctorFixes()}
disabled={state.fixPending}
className="h-7 gap-1.5 text-xs"
data-testid="doctor-apply-fixes"
>
{state.fixPending ? (
<LoaderCircle className="h-3 w-3 animate-spin" />
) : (
<Wrench className="h-3 w-3" />
)}
Apply Fixes ({fixableCount})
</Button>
{state.lastFixError && (
<span className="text-[11px] text-destructive">{state.lastFixError}</span>
)}
</div>
)}
{/* Fix results */}
{state.lastFixResult && state.lastFixResult.fixesApplied.length > 0 && (
<div className="rounded-lg border border-success/20 bg-success/5 px-3 py-2.5 space-y-1">
<div className="flex items-center gap-2">
<CheckCircle2 className="h-3.5 w-3.5 text-success" />
<span className="text-xs font-medium text-success">Fixes Applied</span>
</div>
<ul className="space-y-0.5 pl-5">
{state.lastFixResult.fixesApplied.map((fix, i) => (
<li key={i} className="text-[11px] text-foreground/80 list-disc">{fix}</li>
))}
</ul>
</div>
)}
{/* Issue list */}
{data.issues.length > 0 ? (
<div className="space-y-2">
<h4 className="text-xs font-medium text-muted-foreground">Issues ({data.issues.length})</h4>
{data.issues.map((issue, i) => <IssueRow key={i} issue={issue} />)}
</div>
) : (
<DiagEmpty message="No issues found — workspace is healthy" />
)}
</>
)}
</div>
)
}
// ═══════════════════════════════════════════════════════════════════════
// SKILL HEALTH PANEL
// ═══════════════════════════════════════════════════════════════════════
function trendArrow(trend: "stable" | "rising" | "declining"): string {
if (trend === "rising") return "↑"
if (trend === "declining") return "↓"
return "→"
}
function trendColor(trend: "stable" | "rising" | "declining"): string {
if (trend === "rising") return "text-warning"
if (trend === "declining") return "text-destructive"
return "text-muted-foreground"
}
function SuggestionRow({ suggestion }: { suggestion: SkillHealSuggestion }) {
return (
<div className="rounded-lg border border-border/50 bg-card/50 px-3 py-2.5 space-y-1">
<div className="flex items-center gap-2 flex-wrap">
<SeverityIcon severity={suggestion.severity} />
<Badge variant={severityBadgeVariant(suggestion.severity)} className="text-[10px] px-1.5 py-0">{suggestion.severity}</Badge>
<span className="text-[11px] font-medium text-foreground/80">{suggestion.skillName}</span>
<Badge variant="outline" className="text-[10px] px-1.5 py-0 font-mono">{suggestion.trigger.replace(/_/g, " ")}</Badge>
</div>
<p className="text-xs text-foreground">{suggestion.message}</p>
</div>
)
}
export function SkillHealthPanel() {
const workspace = useGSDWorkspaceState()
const { loadSkillHealthDiagnostics } = useGSDWorkspaceActions()
const state = workspace.commandSurface.diagnostics.skillHealth
const data = state.data as SkillHealthReport | null
const busy = state.phase === "loading"
return (
<div className="space-y-4" data-testid="diagnostics-skill-health">
<DiagHeader
title="Skill Health"
subtitle={data ? new Date(data.generatedAt).toLocaleString() : null}
status={data ? (
<span className={cn(
"inline-block h-1.5 w-1.5 rounded-full",
data.decliningSkills.length > 0 ? "bg-warning" : "bg-success",
)} />
) : null}
onRefresh={() => void loadSkillHealthDiagnostics()}
refreshing={busy}
/>
{state.error && <DiagError message={state.error} />}
{busy && !data && <DiagLoading label="Analyzing skill health…" />}
{data && (
<>
{/* Stats bar */}
<div className="flex flex-wrap gap-2">
<StatPill label="Skills" value={data.skills.length} />
{data.staleSkills.length > 0 && <StatPill label="Stale" value={data.staleSkills.length} variant="warning" />}
{data.decliningSkills.length > 0 && <StatPill label="Declining" value={data.decliningSkills.length} variant="error" />}
<StatPill label="Total units" value={data.totalUnitsWithSkills} />
</div>
{/* Skill table */}
{data.skills.length > 0 && (
<div className="space-y-2">
<h4 className="text-xs font-medium text-muted-foreground">Skills ({data.skills.length})</h4>
<div className="overflow-x-auto rounded-lg border border-border/50">
<table className="w-full text-[11px]">
<thead>
<tr className="border-b border-border/50 bg-card/50">
<th className="px-2.5 py-1.5 text-left font-medium text-muted-foreground">Skill</th>
<th className="px-2.5 py-1.5 text-right font-medium text-muted-foreground">Uses</th>
<th className="px-2.5 py-1.5 text-right font-medium text-muted-foreground">Success</th>
<th className="px-2.5 py-1.5 text-right font-medium text-muted-foreground">Tokens</th>
<th className="px-2.5 py-1.5 text-center font-medium text-muted-foreground">Trend</th>
<th className="px-2.5 py-1.5 text-right font-medium text-muted-foreground">Stale</th>
<th className="px-2.5 py-1.5 text-right font-medium text-muted-foreground">Cost</th>
</tr>
</thead>
<tbody>
{data.skills.map((skill) => (
<tr key={skill.name} className={cn(
"border-b border-border/50 last:border-0",
skill.flagged && "bg-destructive/3",
)}>
<td className="px-2.5 py-1.5 font-mono text-foreground/80">
<span className="flex items-center gap-1.5">
{skill.name}
{skill.flagged && <AlertTriangle className="h-3 w-3 text-warning shrink-0" />}
</span>
</td>
<td className="px-2.5 py-1.5 text-right tabular-nums text-foreground/80">{skill.totalUses}</td>
<td className={cn(
"px-2.5 py-1.5 text-right tabular-nums",
skill.successRate >= 0.9 ? "text-success" : skill.successRate >= 0.7 ? "text-warning" : "text-destructive",
)}>
{(skill.successRate * 100).toFixed(0)}%
</td>
<td className="px-2.5 py-1.5 text-right tabular-nums text-foreground/80">{Math.round(skill.avgTokens)}</td>
<td className={cn("px-2.5 py-1.5 text-center", trendColor(skill.tokenTrend))}>
{trendArrow(skill.tokenTrend)}
</td>
<td className={cn(
"px-2.5 py-1.5 text-right tabular-nums",
skill.staleDays > 30 ? "text-warning" : "text-foreground/80",
)}>
{skill.staleDays > 0 ? `${skill.staleDays}d` : "—"}
</td>
<td className="px-2.5 py-1.5 text-right tabular-nums text-foreground/80">{formatCost(skill.avgCost)}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* Stale skills */}
{data.staleSkills.length > 0 && (
<div className="space-y-1.5">
<h4 className="text-xs font-medium text-muted-foreground">Stale Skills</h4>
<div className="flex flex-wrap gap-1.5">
{data.staleSkills.map((name) => (
<Badge key={name} variant="secondary" className="text-[10px] font-mono">{name}</Badge>
))}
</div>
</div>
)}
{/* Declining skills */}
{data.decliningSkills.length > 0 && (
<div className="space-y-1.5">
<h4 className="text-xs font-medium text-muted-foreground">Declining Skills</h4>
<div className="flex flex-wrap gap-1.5">
{data.decliningSkills.map((name) => (
<Badge key={name} variant="destructive" className="text-[10px] font-mono">{name}</Badge>
))}
</div>
</div>
)}
{/* Suggestions */}
{data.suggestions.length > 0 && (
<div className="space-y-2">
<h4 className="text-xs font-medium text-muted-foreground">Suggestions ({data.suggestions.length})</h4>
{data.suggestions.map((s, i) => <SuggestionRow key={i} suggestion={s} />)}
</div>
)}
{data.skills.length === 0 && data.suggestions.length === 0 && (
<DiagEmpty message="No skill usage data available" />
)}
</>
)}
</div>
)
}