This commit is contained in:
valenti
2026-09-08 07:21:34 -04:00
commit a4b224da8e
169 changed files with 27483 additions and 0 deletions
+697
View File
@@ -0,0 +1,697 @@
"use client";
import React, { useMemo, useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import {
Code2,
Search,
AlertTriangle,
FileCode2,
CheckCircle2,
AlertOctagon,
AlertCircle,
Info,
ChevronRight,
ChevronDown,
GitBranch,
ArrowLeft,
Sparkles,
FileText,
Layers,
} from "lucide-react";
import { Card, CardBody, CardHeader, CardTitle } from "@/components/ui/Card";
import { Button } from "@/components/ui/Button";
import { Badge, ArtifactTypeBadge } from "@/components/ui/Badge";
import { EmptyState } from "@/components/ui/EmptyState";
import { artifacts as apiArtifacts, projects } from "@/lib/api";
import { cn, fmtDate, shortId, truncate } from "@/lib/utils";
import type { Artifact, ArtifactType, Project } from "@/lib/types";
type Scope = "all" | "source_file" | "smell";
type Severity = "all" | "low" | "medium" | "high" | "critical";
const SEVERITY_ORDER: Record<string, number> = { critical: 0, high: 1, medium: 2, low: 3 };
const SEVERITY_META: Record<Exclude<Severity, "all">, { icon: typeof AlertTriangle; tone: "red" | "amber" | "brand" | "neutral"; label: string }> = {
critical: { icon: AlertOctagon, tone: "red", label: "CRITICAL" },
high: { icon: AlertTriangle, tone: "amber", label: "HIGH" },
medium: { icon: AlertCircle, tone: "brand", label: "MEDIUM" },
low: { icon: Info, tone: "neutral", label: "LOW" },
};
const DEMO_ROWS: Artifact[] = [
{
id: "src-1", tenant_id: "t1", project_id: "p1",
type: "source_file", external_id: "SRC-DEMO-1",
title: "app/middleware/auth.py",
status: "active", source: "git",
content: "def authorize(request):\n return check_jwt(request.headers.get('authorization'))\n",
metadata: { language: "python", path: "app/middleware/auth.py", loc: 134, repo: "asf", branch: "main" },
created_at: "2025-11-01T09:00:00Z", updated_at: "2025-11-05T11:00:00Z",
},
{
id: "sm-1", tenant_id: "t1", project_id: "p1",
type: "ai_analysis", external_id: "CS-DEMO-1",
title: "SQL injection in user lookup (no ORM)",
status: "open", source: "bandit",
content: "Line 22 uses f-string on cursor.execute().",
metadata: { kind: "code_smell", language: "python", severity: "high", rule_id: "B608", engine: "bandit", lines: [20, 21, 22] },
created_at: "2025-11-08T09:00:00Z", updated_at: "2025-11-08T09:00:00Z",
},
];
function isSmell(a: Artifact): boolean {
const md = a.metadata ?? ({} as Record<string, unknown>);
return typeof md === "object" && (md as Record<string, unknown>)["kind"] === "code_smell";
}
function isSourceFile(a: Artifact): boolean {
return a.type === "source_file";
}
function getLanguage(a: Artifact): string {
const md = a.metadata ?? ({} as Record<string, unknown>);
const v = (md as Record<string, unknown>)["language"];
return typeof v === "string" ? v : "—";
}
function getSeverity(a: Artifact): "low" | "medium" | "high" | "critical" | null {
const md = a.metadata ?? ({} as Record<string, unknown>);
const v = (md as Record<string, unknown>)["severity"];
if (typeof v === "string" && (v === "low" || v === "medium" || v === "high" || v === "critical")) return v;
return null;
}
function getRule(a: Artifact): string | null {
const md = a.metadata ?? ({} as Record<string, unknown>);
const v = (md as Record<string, unknown>)["rule_id"];
return typeof v === "string" ? v : null;
}
function getEngine(a: Artifact): string | null {
const md = a.metadata ?? ({} as Record<string, unknown>);
const v = (md as Record<string, unknown>)["engine"];
return typeof v === "string" ? v : null;
}
function getPath(a: Artifact): string | null {
const md = a.metadata ?? ({} as Record<string, unknown>);
const v = (md as Record<string, unknown>)["path"];
return typeof v === "string" ? v : null;
}
function getLines(a: Artifact): number[] | null {
const md = a.metadata ?? ({} as Record<string, unknown>);
const v = (md as Record<string, unknown>)["lines"];
return Array.isArray(v) && v.every((x) => typeof x === "number") ? (v as number[]) : null;
}
function getLOC(a: Artifact): number | null {
const md = a.metadata ?? ({} as Record<string, unknown>);
const v = (md as Record<string, unknown>)["loc"];
return typeof v === "number" ? v : null;
}
function getRepo(a: Artifact): string | null {
const md = a.metadata ?? ({} as Record<string, unknown>);
const v = (md as Record<string, unknown>)["repo"];
return typeof v === "string" ? v : null;
}
function getBranch(a: Artifact): string | null {
const md = a.metadata ?? ({} as Record<string, unknown>);
const v = (md as Record<string, unknown>)["branch"];
return typeof v === "string" ? v : null;
}
function getConfidence(a: Artifact): number | null {
const md = a.metadata ?? ({} as Record<string, unknown>);
const v = (md as Record<string, unknown>)["confidence_pct"];
return typeof v === "number" ? v : null;
}
export default function CodePage() {
const [scope, setScope] = useState<Scope>("all");
const [projectFilter, setProjectFilter] = useState<string>("all");
const [languageFilter, setLanguageFilter] = useState<string>("all");
const [severityFilter, setSeverityFilter] = useState<Severity>("all");
const [statusFilter, setStatusFilter] = useState<string>("all");
const [q, setQ] = useState<string>("");
const [detailId, setDetailId] = useState<string>("");
const projectsQ = useQuery({
queryKey: ["code-projects"],
queryFn: () => projects.list({ limit: 50 }),
retry: false, refetchOnWindowFocus: false,
});
const apiResolved = projectsQ.isFetched && projectsQ.data !== undefined;
const projectRows: Project[] = apiResolved ? projectsQ.data! : [];
const projectNameById = useMemo(() => {
const m = new Map<string, string>();
for (const p of projectRows) m.set(String(p.id), p.name);
return m;
}, [projectRows]);
const sourceQ = useQuery({
queryKey: ["code-source", projectFilter, q.trim()],
queryFn: () =>
apiArtifacts.list({
type: "source_file" as ArtifactType,
project_id: projectFilter !== "all" ? projectFilter : undefined,
search: q.trim() || undefined,
limit: 200,
}),
retry: false, refetchOnWindowFocus: false,
});
const smellsQ = useQuery({
queryKey: ["code-smells", projectFilter, q.trim()],
queryFn: () =>
apiArtifacts.list({
type: "ai_analysis" as ArtifactType,
project_id: projectFilter !== "all" ? projectFilter : undefined,
search: q.trim() || undefined,
limit: 200,
}),
retry: false, refetchOnWindowFocus: false,
});
const sourceResolved = sourceQ.isFetched && sourceQ.data !== undefined;
const smellsResolved = smellsQ.isFetched && smellsQ.data !== undefined;
const isMock = !sourceResolved || !smellsResolved;
const rawRows: Artifact[] = useMemo(() => {
const allSources = sourceResolved ? sourceQ.data! : DEMO_ROWS.filter(isSourceFile);
const allAnalyses = smellsResolved ? smellsQ.data! : DEMO_ROWS.filter(isSmell);
const smellsOnly = allAnalyses.filter(isSmell);
const combined = [...allSources, ...smellsOnly];
// apply language + severity + status filters (server can't filter metadata)
return combined.filter((a) => {
if (scope === "source_file" && !isSourceFile(a)) return false;
if (scope === "smell" && !isSmell(a)) return false;
if (languageFilter !== "all" && getLanguage(a) !== languageFilter) return false;
if (severityFilter !== "all") {
if (!isSmell(a)) return false;
if (getSeverity(a) !== severityFilter) return false;
}
if (statusFilter !== "all" && (a.status ?? "") !== statusFilter) return false;
return true;
}).sort((b, a) => {
// severity first (only if smell), then updated_at desc
const sa = isSmell(a) ? (SEVERITY_ORDER[getSeverity(a) ?? "low"] ?? 9) : 99;
const sb = isSmell(b) ? (SEVERITY_ORDER[getSeverity(b) ?? "low"] ?? 9) : 99;
if (sa !== sb) return sa - sb;
return (a.updated_at ?? "").localeCompare(b.updated_at ?? "");
});
}, [sourceResolved, smellsResolved, sourceQ.data, smellsQ.data, scope, languageFilter, severityFilter, statusFilter]);
const languages = useMemo(() => {
const s = new Set<string>();
for (const r of rawRows) {
const l = getLanguage(r);
if (l && l !== "—") s.add(l);
}
return Array.from(s).sort();
}, [rawRows]);
const qc = useQueryClient();
const markFixedM = useMutation({
mutationFn: async ({ id, status }: { id: string; status: string }) =>
apiArtifacts.update(id, { status } as Partial<Artifact>),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["code-source"] });
qc.invalidateQueries({ queryKey: ["code-smells"] });
},
});
const stats = useMemo(() => {
const src = rawRows.filter(isSourceFile).length;
const sm = rawRows.filter(isSmell);
const smBySev = { low: 0, medium: 0, high: 0, critical: 0 } as Record<"low" | "medium" | "high" | "critical", number>;
for (const s of sm) {
const sv = getSeverity(s);
if (sv) smBySev[sv] += 1;
}
return { src, smells: sm.length, smBySev };
}, [rawRows]);
const effectiveDetail = useMemo(() => {
if (!detailId) return null;
return rawRows.find((r) => String(r.id) === String(detailId)) ?? null;
}, [detailId, rawRows]);
if (effectiveDetail) {
return (
<CodeDetailView
artifact={effectiveDetail}
projectName={projectNameById.get(String(effectiveDetail.project_id)) ?? String(effectiveDetail.project_id)}
onBack={() => setDetailId("")}
onMarkFixed={(status) =>
markFixedM.mutate({ id: String(effectiveDetail.id), status })
}
markFixedPending={markFixedM.isPending}
/>
);
}
return (
<div className="h-full flex flex-col min-h-0">
<header className="px-6 lg:px-8 pt-6 pb-4">
<div className="flex items-start gap-3 flex-wrap">
<div className="w-11 h-11 rounded-xl bg-gradient-to-br from-brand-500 to-violet-600 shadow-soft grid place-items-center text-white shrink-0">
<Code2 className="w-5 h-5" />
</div>
<div className="min-w-0 flex-1">
<h1 className="text-2xl font-semibold tracking-tight text-slate-900 flex items-center gap-2 flex-wrap">
Code Analysis
{isMock && <Badge tone="amber">Demo data</Badge>}
</h1>
<p className="mt-1 text-sm text-slate-500 max-w-3xl">
Source files indexed from your repositories + automated code smells detected by ASF AI Analyzer
(Ruff / ESLint / Bandit …). Click any row to see the detail with highlighted lines.
</p>
</div>
</div>
</header>
<div className="flex-1 min-h-0 px-6 lg:px-8 pb-6 flex flex-col gap-4 min-w-0">
<div className="grid grid-cols-2 md:grid-cols-5 gap-3">
<StatCard label="Source files" value={stats.src} tone="brand" icon={FileCode2} />
<StatCard label="Code smells" value={stats.smells} tone="amber" icon={AlertTriangle} />
<StatCard label="Critical" value={stats.smBySev.critical} tone="red" icon={AlertOctagon} />
<StatCard label="High" value={stats.smBySev.high} tone="amber" icon={AlertTriangle} />
<StatCard label="Med/Low" value={stats.smBySev.medium + stats.smBySev.low} tone="neutral" icon={Info} />
</div>
<Card className="flex-1 min-h-0 flex flex-col">
<CardHeader className="flex-wrap items-end gap-3">
<div className="min-w-0 flex-1">
<CardTitle className="!text-sm flex items-center gap-2">
<Layers className="w-4 h-4 text-slate-400" />
Inventory
<span className="asf-chip">{rawRows.length}</span>
</CardTitle>
</div>
<div className="flex flex-wrap gap-2 items-center">
<div className="relative w-full md:w-64">
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
<input
className="asf-input pl-9"
placeholder="Search paths, titles, rules…"
value={q}
onChange={(e) => setQ(e.target.value)}
/>
</div>
<select className="asf-input !w-auto" value={scope} onChange={(e) => setScope(e.target.value as Scope)}>
<option value="all">All code</option>
<option value="source_file">Source files only</option>
<option value="smell">Code smells only</option>
</select>
<select className="asf-input !w-auto" value={languageFilter} onChange={(e) => setLanguageFilter(e.target.value)}>
<option value="all">Any language</option>
{languages.map((l) => (
<option key={l} value={l}>{l.toUpperCase()}</option>
))}
</select>
<select className="asf-input !w-auto" value={severityFilter} onChange={(e) => setSeverityFilter(e.target.value as Severity)}>
<option value="all">Any severity</option>
<option value="critical">CRITICAL</option>
<option value="high">HIGH</option>
<option value="medium">MEDIUM</option>
<option value="low">LOW</option>
</select>
<select className="asf-input !w-auto" value={statusFilter} onChange={(e) => setStatusFilter(e.target.value)}>
<option value="all">Any status</option>
{Array.from(new Set(rawRows.map((r) => r.status ?? "unknown").filter(Boolean))).sort().map((s) => (
<option key={s} value={s}>{s}</option>
))}
</select>
<select className="asf-input !w-auto" value={projectFilter} onChange={(e) => setProjectFilter(e.target.value)}>
<option value="all">All projects</option>
{projectRows.map((p) => (
<option key={p.id} value={String(p.id)}>{p.name}</option>
))}
</select>
</div>
</CardHeader>
<CardBody className="p-0 flex flex-col min-h-0 overflow-hidden">
{rawRows.length === 0 ? (
<div className="flex-1 grid place-items-center p-10">
<EmptyState title="No matches" description="Try a different combination of filters, or index a repository to seed source files." icon={FileCode2} />
</div>
) : (
<div className="overflow-auto flex-1 min-h-0">
<table className="w-full text-sm">
<thead className="sticky top-0 z-10 bg-slate-50/95 backdrop-blur-sm border-b border-slate-200 text-[11px] uppercase tracking-wide text-slate-500">
<tr>
<th className="px-4 py-2.5 text-left w-16">Kind</th>
<th className="px-4 py-2.5 text-left">Artifact</th>
<th className="px-4 py-2.5 text-left w-28">Lang</th>
<th className="px-4 py-2.5 text-left w-28">Severity</th>
<th className="px-4 py-2.5 text-left w-28">Status</th>
<th className="px-4 py-2.5 text-left w-48">Project</th>
<th className="px-4 py-2.5 text-right w-28">Updated</th>
<th className="px-4 py-2.5 w-10" />
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{rawRows.map((a) => (
<tr
key={a.id}
onClick={() => setDetailId(String(a.id))}
className="group cursor-pointer hover:bg-slate-50/80 transition-colors"
>
<td className="px-4 py-3 align-top">
{isSourceFile(a) ? (
<Badge tone="blue" className="flex items-center gap-1 w-fit">
<FileCode2 className="w-3 h-3" />
<span>file</span>
</Badge>
) : (
<Badge tone="amber" className="flex items-center gap-1 w-fit">
<Sparkles className="w-3 h-3" />
<span>smell</span>
</Badge>
)}
</td>
<td className="px-4 py-3 align-top min-w-0">
<div className="flex items-center gap-2">
<span className="asf-monospace text-[11px] text-slate-500">
{a.external_id ?? shortId(a.id)}
</span>
{isSmell(a) && getRule(a) && (
<Badge tone="neutral">{getRule(a)}</Badge>
)}
{isSourceFile(a) && getLOC(a) != null && (
<span className="asf-chip">{getLOC(a)} LOC</span>
)}
</div>
<div className="mt-0.5 font-medium text-slate-800 truncate max-w-[520px]">
{a.title}
</div>
{getPath(a) && (
<div className="mt-0.5 text-xs text-slate-500 truncate font-mono max-w-[520px]">
{getPath(a)}
</div>
)}
{isSmell(a) && getEngine(a) && (
<div className="mt-1 text-[11px] text-slate-400">
scanner: <span className="text-slate-600 font-medium">{getEngine(a)}</span>
</div>
)}
</td>
<td className="px-4 py-3 align-top">
<Badge tone="brand" className="uppercase">{getLanguage(a)}</Badge>
</td>
<td className="px-4 py-3 align-top">
{isSmell(a) && getSeverity(a) ? (
<SeverityBadge s={getSeverity(a)!} />
) : (
<span className="text-slate-300 text-xs">—</span>
)}
</td>
<td className="px-4 py-3 align-top">
{a.status ? <Badge tone={a.status === "open" ? "amber" : a.status === "fixed" ? "green" : a.status === "active" ? "blue" : "brand"}>{a.status}</Badge> : <span className="text-slate-300 text-xs">—</span>}
</td>
<td className="px-4 py-3 align-top text-slate-600 text-xs truncate max-w-[200px]">
{projectNameById.get(String(a.project_id)) ?? shortId(a.project_id)}
</td>
<td className="px-4 py-3 align-top text-right text-xs text-slate-500 whitespace-nowrap">
{fmtDate(a.updated_at)}
</td>
<td className="px-4 py-3 align-top text-right">
<ChevronRight className="w-4 h-4 text-slate-300 group-hover:text-brand-500 inline-block" />
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</CardBody>
</Card>
</div>
</div>
);
}
function SeverityBadge({ s }: { s: "low" | "medium" | "high" | "critical" }) {
const m = SEVERITY_META[s];
const Icon = m.icon;
return (
<Badge tone={m.tone} className="inline-flex items-center gap-1">
<Icon className="w-3 h-3" />
{m.label}
</Badge>
);
}
function StatCard({
label, value, tone, icon: Icon,
}: {
label: string; value: number; tone: "brand" | "amber" | "red" | "neutral" | "green" | "blue";
icon: React.ComponentType<{ className?: string }>;
}) {
const toneBg: Record<string, string> = {
brand: "from-brand-500 to-brand-600",
amber: "from-amber-400 to-amber-500",
red: "from-rose-500 to-red-600",
neutral: "from-slate-400 to-slate-500",
green: "from-emerald-400 to-emerald-600",
blue: "from-sky-400 to-blue-600",
};
return (
<Card className="!shadow-none overflow-hidden">
<CardBody className="p-4 flex items-center gap-3">
<div className={cn("w-11 h-11 rounded-xl bg-gradient-to-br shadow-sm grid place-items-center text-white shrink-0", toneBg[tone])}>
<Icon className="w-5 h-5" />
</div>
<div className="min-w-0 flex-1">
<div className="text-xs uppercase tracking-wide text-slate-500 font-medium">{label}</div>
<div className="text-2xl font-semibold text-slate-900 leading-tight mt-0.5">{value}</div>
</div>
</CardBody>
</Card>
);
}
function CodeDetailView({
artifact, projectName, onBack, onMarkFixed, markFixedPending,
}: {
artifact: Artifact;
projectName: string;
onBack: () => void;
onMarkFixed?: (status: string) => void;
markFixedPending?: boolean;
}) {
const lang = getLanguage(artifact);
const sev = getSeverity(artifact);
const path = getPath(artifact);
const rule = getRule(artifact);
const engine = getEngine(artifact);
const repo = getRepo(artifact);
const branch = getBranch(artifact);
const lines = getLines(artifact);
const confidence = getConfidence(artifact);
const smell = isSmell(artifact);
const source = isSourceFile(artifact);
const linesToHighlight = useMemo(() => {
const set = new Set<number>();
if (lines) {
for (const n of lines) set.add(n);
}
return set;
}, [lines]);
const contentLines = useMemo(() => {
const raw = artifact.content ?? "";
return raw.split("\n");
}, [artifact.content]);
return (
<div className="h-full flex flex-col min-h-0">
<header className="px-6 lg:px-8 pt-6 pb-4">
<Button variant="ghost" size="sm" className="mb-3 -ml-2" onClick={onBack}>
<ArrowLeft className="w-4 h-4 mr-1" /> Back to inventory
</Button>
<div className="flex items-start gap-3 flex-wrap">
<div className={cn(
"w-11 h-11 rounded-xl bg-gradient-to-br shadow-soft grid place-items-center text-white shrink-0",
smell
? "from-amber-400 to-rose-500"
: "from-brand-500 to-violet-600",
)}>
{smell ? <AlertTriangle className="w-5 h-5" /> : <FileCode2 className="w-5 h-5" />}
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2 flex-wrap">
<ArtifactTypeBadge type={artifact.type as ArtifactType} />
{smell ? (
<Badge tone="amber">code_smell</Badge>
) : (
<Badge tone="blue">source_file</Badge>
)}
{sev && <SeverityBadge s={sev} />}
{rule && <Badge tone="neutral">rule {rule}</Badge>}
{lang && <Badge tone="brand" className="uppercase">{lang}</Badge>}
{artifact.status && (
<Badge tone={artifact.status === "open" ? "amber" : artifact.status === "fixed" ? "green" : artifact.status === "active" ? "blue" : "brand"}>
{artifact.status}
</Badge>
)}
<span className="asf-monospace text-xs text-slate-500">
{artifact.external_id ?? shortId(artifact.id)}
</span>
</div>
<h1 className="mt-2 text-xl font-semibold text-slate-900 break-words">{artifact.title}</h1>
{path && (
<div className="mt-1 text-xs text-slate-500 font-mono break-all">{path}</div>
)}
<div className="mt-1 text-xs text-slate-500 flex flex-wrap items-center gap-x-3 gap-y-1">
<span>Project: <span className="text-slate-700 font-medium">{projectName}</span></span>
{engine && <span>Scanner: <span className="text-slate-700 font-medium">{engine}</span></span>}
{repo && (
<span className="inline-flex items-center gap-1">
<GitBranch className="w-3 h-3 text-slate-400" />
<span className="text-slate-700 font-medium">{truncate(repo, 40)}{branch ? ` @ ${branch}` : ""}</span>
</span>
)}
{confidence != null && <span>Confidence: <span className="text-slate-700 font-medium">{confidence}%</span></span>}
<span>Updated: <span className="text-slate-700 font-medium">{fmtDate(artifact.updated_at)}</span></span>
{lines && <span>Affected lines: <span className="text-slate-700 font-medium">{lines.join(", ")}</span></span>}
{source && getLOC(artifact) != null && <span>Lines of code: <span className="text-slate-700 font-medium">{getLOC(artifact)!}</span></span>}
</div>
</div>
{smell && onMarkFixed && (
<div className="flex items-center gap-2 shrink-0 ml-auto">
{artifact.status === "fixed" ? (
<Badge tone="green" className="inline-flex items-center gap-1 !px-3 !py-1.5">
<CheckCircle2 className="w-3.5 h-3.5" /> Resolved
</Badge>
) : (
<Button
size="sm"
variant="primary"
disabled={markFixedPending}
onClick={() => onMarkFixed("fixed")}
className="!bg-emerald-500 hover:!bg-emerald-600"
>
<CheckCircle2 className="w-3.5 h-3.5 mr-1.5" />
{markFixedPending ? "Saving..." : "Mark as fixed"}
</Button>
)}
</div>
)}
</div>
</header>
<div className="flex-1 min-h-0 px-6 lg:px-8 pb-6 grid grid-cols-1 lg:grid-cols-12 gap-4">
<Card className="lg:col-span-7 flex flex-col min-h-0">
<CardHeader>
<CardTitle className="!text-sm flex items-center gap-2">
<FileText className="w-4 h-4 text-slate-400" />
{smell ? "Finding details" : "Source preview"}
</CardTitle>
</CardHeader>
<CardBody className="p-0 flex flex-col min-h-0 overflow-hidden">
{contentLines.length === 1 && contentLines[0] === "" ? (
<div className="p-10">
<EmptyState title="No content body available" description="This artifact is metadata-only. The blob content may still be fetchable from the Git integration." icon={FileCode2} />
</div>
) : (
<pre className="flex-1 min-h-0 overflow-auto text-[12.5px] leading-6 bg-slate-900 text-slate-100 p-4 font-mono rounded-b-xl">
{contentLines.map((ln, i) => {
const n = i + 1;
const hi = linesToHighlight.has(n);
return (
<div
key={n}
className={cn(
"flex -mx-4 px-4",
hi && "bg-rose-500/15 border-l-2 border-rose-400",
)}
>
<span className={cn(
"w-10 shrink-0 text-right pr-3 select-none",
hi ? "text-rose-200" : "text-slate-600",
)}>{n}</span>
<span className="flex-1 min-w-0 break-all whitespace-pre-wrap">{ln || " "}</span>
</div>
);
})}
</pre>
)}
</CardBody>
</Card>
<Card className="lg:col-span-5 flex flex-col min-h-0">
<CardHeader>
<CardTitle className="!text-sm flex items-center gap-2">
{smell ? <AlertTriangle className="w-4 h-4 text-amber-500" /> : <CheckCircle2 className="w-4 h-4 text-emerald-500" />}
{smell ? "Remediation guidance" : "File index info"}
</CardTitle>
</CardHeader>
<CardBody className="space-y-4 text-sm">
{smell ? (
<>
<SuggestionBlock
title="Root cause"
body={
`Rule ${rule ?? "unknown"} reported by ${engine ?? "scanner"}. ` +
(lines?.length ? `Focus attention on ${lines.length} affected line${lines.length > 1 ? "s" : ""}.` : "Inspect the highlighted rows in the source pane.")
}
tone="amber"
/>
<SuggestionBlock
title="Suggested fix"
body={
sev === "critical" || sev === "high"
? "Open a TASK linked to this finding, assign an owner, and block the PR/deployment pipeline until merged. Prefer writing a regression test that proves the issue."
: "Fix in the next planned sprint; add a suppression comment only if you've filed a follow-up TASK with justification."
}
tone={sev === "critical" || sev === "high" ? "red" : "brand"}
/>
<div className="grid grid-cols-2 gap-2 text-xs">
<InfoCell label="Version" value={artifact.version != null ? `v${artifact.version}` : "—"} />
<InfoCell label="Source" value={artifact.source ?? "—"} />
<InfoCell label="Created" value={fmtDate(artifact.created_at)} />
<InfoCell label="Updated" value={fmtDate(artifact.updated_at)} />
</div>
</>
) : (
<>
<SuggestionBlock
title="Traceability"
body="This file is linked to the ASF artifact graph. Open Context Explorer to inspect which PRs, commits, tasks, and deployments reference it."
tone="brand"
/>
<div className="grid grid-cols-2 gap-2 text-xs">
<InfoCell label="LOC" value={getLOC(artifact) != null ? String(getLOC(artifact)!) : "—"} />
<InfoCell label="Language" value={lang ? lang.toUpperCase() : "—"} />
<InfoCell label="Created" value={fmtDate(artifact.created_at)} />
<InfoCell label="Updated" value={fmtDate(artifact.updated_at)} />
</div>
</>
)}
</CardBody>
</Card>
</div>
</div>
);
}
function SuggestionBlock({
title, body, tone,
}: { title: string; body: string; tone: "brand" | "amber" | "red" | "green" | "blue" }) {
const toneCls: Record<string, string> = {
brand: "bg-brand-50 border-brand-100 text-brand-900",
amber: "bg-amber-50 border-amber-100 text-amber-900",
red: "bg-rose-50 border-rose-100 text-rose-900",
green: "bg-emerald-50 border-emerald-100 text-emerald-900",
blue: "bg-sky-50 border-sky-100 text-sky-900",
};
return (
<div className={cn("p-3 rounded-lg border", toneCls[tone])}>
<div className="text-xs font-semibold uppercase tracking-wide mb-1 opacity-80">{title}</div>
<p className="text-sm leading-snug">{body}</p>
</div>
);
}
function InfoCell({ label, value }: { label: string; value: string }) {
return (
<div className="rounded-md border border-slate-100 bg-slate-50/60 px-2.5 py-1.5">
<div className="uppercase tracking-wide text-[9px] text-slate-400">{label}</div>
<div className="asf-monospace font-semibold text-slate-700 truncate">{value}</div>
</div>
);
}