import { useQuery } from "@tanstack/react-query"; import { ArrowLeft, ArrowUpRight, Check, ChevronDown, Download, ExternalLink, Github, LoaderCircle, Shield, } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import { Link, useLocation, useParams, useSearchParams } from "react-router-dom"; import { EmptyState } from "../../components/EmptyState"; import { ReportBody } from "../../components/ReportBody"; import { ScanDurationNotice } from "../../components/ScanDurationNotice"; import { SeverityBar } from "../../components/SeverityBar"; import { SeverityChip } from "../../components/SeverityChip"; import { ScanningSpinner, StatusBadge } from "../../components/StatusBadge"; import { api, apiUrl } from "../../shared/api/client"; import { useMe } from "../auth/useAuth"; import { OwnerFindings } from "./OwnerFindings"; import { VersionBar, type ViewJob } from "./VersionBar"; import { formatDate, formatStars, shortSha, totalFindings } from "../../shared/lib/format"; export function ProjectPage() { const [openKey, setOpenKey] = useState(null); const { owner = "", repo = "" } = useParams(); const [params, setParams] = useSearchParams(); const tabParam = params.get("tab"); const location = useLocation(); const justSubmitted = Boolean( (location.state as { justSubmitted?: boolean } | null)?.justSubmitted, ); const projectQ = useQuery({ queryKey: ["public", "project", owner, repo], queryFn: () => api.getProject(owner, repo), enabled: Boolean(owner && repo), refetchInterval: (q) => { const s = q.state.data?.latest_scan?.state; if (s === "queued" || s === "dispatching" || s === "scanning") return 5000; return false; }, }); const project = projectQ.data; const meQ = useMe(); // owner 探测:登录后尝试拉取全量 findings;200=有权限(显示 Manage findings tab),401/403=公众视图 const ownerQ = useQuery({ queryKey: ["owner-findings", project?.id], queryFn: () => api.ownerFindings(project!.id), enabled: Boolean(project?.id) && meQ.data?.authenticated === true, retry: false, staleTime: 30_000, }); const isOwner = ownerQ.isSuccess; // 版本查看状态(fish No.1253:切换操作上移到头部) const [viewJob, setViewJob] = useState(null); // Details 与 Manage findings 合并为 Findings(fish No.1252);tab=details 兼容旧链接 const tab = tabParam === "findings" || tabParam === "details" ? "findings" : "overview"; const cweMax = useMemo(() => { const list = project?.cwe_distribution ?? []; return Math.max(1, ...list.map((c) => c.count)); }, [project]); if (projectQ.isLoading) { return ; } if (projectQ.isError || !project) { return (
Back to projects } />
); } const state = project.latest_scan?.state; const scanning = state === "scanning" || state === "dispatching"; const soFar = project.latest_scan?.findings_so_far ?? 0; const findingsTotal = totalFindings(project.severity_counts); if (state === "queued" || scanning) { return ( ); } return (
Projects {justSubmitted && (
Added to the scan queue.
)} {/* Header */}

{project.owner_login} / {project.name}

{project.description && (

{project.description}

)}
{project.language && ( {project.language} )} · ★ {formatStars(project.stars)} · Added {formatDate(project.created_at)}
{isOwner && (
)}
{/* Tabs */}
setParams({})}> Overview setParams({ tab: "findings" })}> Findings
{tab === "overview" && (
{project.latest_scan?.finished_at && ( {formatDate(project.latest_scan.finished_at)} )} {project.latest_scan?.commit_sha && ( {shortSha(project.latest_scan.commit_sha)} )}

{state === "completed" ? "Scanned" : scanning ? "Scanning" : "To be scanned"} by{" "} VulnHunter AI engine {project.default_branch ? ` · default branch (${project.default_branch})` : null}

{scanning && (
0 ? `Scan in progress — ${soFar} finding${soFar === 1 ? "" : "s"} so far` : "Scan in progress — stats will appear when complete." } />
)} {state !== "completed" && !scanning && (

This project is in the scan queue.

)} {state === "completed" && ( <>

Scan results

Findings overview

{findingsTotal === 0 ? (

No findings were confirmed in this scan. A clean scan does not prove a project is free of vulnerabilities.

) : ( <>

{findingsTotal} finding{findingsTotal === 1 ? "" : "s"} found

)}
{project.cwe_distribution.length > 0 && (

Top CWE categories

    {project.cwe_distribution.slice(0, 5).map((c) => (
  • {c.cwe}
    {c.count}
  • ))}
)}

Detailed findings are visible to signed-in repository maintainers. Maintainers review the full reports and choose what to disclose publicly.

{project.disclosed_findings.length > 0 && (

Disclosed findings

{project.disclosed_findings.map((f) => ( ))}
Sev Title CWE Status
{f.title} {f.cwe ?? "—"} Disclosed
)} )}
)} {tab === "findings" && !isOwner && (
{project.disclosed_findings.length === 0 ? (

No disclosures yet

Full finding details are visible to repository maintainers after sign-in. Disclosed findings appear here with the full report content, and on the Overview tab.

) : ( project.disclosed_findings.map((f) => { const open = openKey === f.id; return (
setOpenKey(open ? null : f.id)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setOpenKey(open ? null : f.id); } }} aria-expanded={open} className="flex w-full cursor-pointer items-start gap-3 px-5 py-4 text-left transition-colors hover:bg-surface-sunken/50 focus-ring sm:px-6" >
{f.cwe && ( {f.cwe} )} Disclosed

{f.title}

{f.finding_key}

e.stopPropagation()} title="Download full report (markdown)" > .md
{open && (
{f.report ? ( ) : (

Structured report is not available for this disclosure (older summary-only disclose). Use the download button if a fidelity pack was attached later.

)}
)}
); }) )}
)} {tab === "findings" && isOwner && (
)}
); } function ScanProgressPage({ owner, repo, htmlUrl, branch, state, findingsSoFar = 0, justSubmitted = false, }: { owner: string; repo: string; htmlUrl?: string; branch?: string | null; state: "loading" | "queued" | "dispatching" | "scanning"; findingsSoFar?: number; justSubmitted?: boolean; }) { useEffect(() => { document.body.classList.add("openvuln-running"); return () => document.body.classList.remove("openvuln-running"); }, []); const queued = state === "queued"; const loading = state === "loading"; const dispatching = state === "dispatching"; const currentStage = queued ? 0 : dispatching ? 1 : 2; const stages = ["Queued", "Preparing", "Scanning", "Results"]; const statusLabel = loading ? "Loading scan status" : queued ? "Waiting in scan queue" : dispatching ? "Preparing the scan" : "AI security analysis in progress"; const detail = loading ? "Retrieving the latest status…" : queued ? "The repository is queued and will start automatically when a scanner is available." : dispatching ? "OpenVuln is packaging the repository and handing it to an available scanner." : findingsSoFar > 0 ? `${findingsSoFar} confirmed finding${findingsSoFar === 1 ? "" : "s"} so far. Analysis is still running.` : "VulnHunter is analyzing the repository. Results will appear after the scan completes."; return (
OpenVuln {htmlUrl && ( GitHub )}

{justSubmitted ? "Repository added" : "Live scan status"}

{statusLabel}

{owner} / {repo} {branch && ( <> · {branch} )}

{statusLabel}

{detail}

{!loading && (
Current stage {currentStage + 1} of {stages.length}
    {stages.map((stageLabel, index) => { const complete = index < currentStage; const current = index === currentStage; return (
  1. {complete ? : index + 1} {stageLabel}
  2. ); })}

This shows status stages, not elapsed-time progress. Refreshes every 5 seconds.

)}
); } function TabButton({ active, onClick, children, }: { active: boolean; onClick: () => void; children: React.ReactNode; }) { return ( ); }