"use client"; import { FormEvent, useEffect, useMemo, useRef, useState } from "react"; import { inspectionToMarkdown } from "@/lib/inspect/report"; import { analyzeHeaders, type HeaderMap } from "@/lib/inspect/headers"; import { statusForHttp } from "@/lib/inspect/http-status"; import type { Finding, InspectionResult } from "@/lib/inspect/types"; import { ReportActions } from "./components/report/report-actions"; import { DnsEvidence, HttpEvidence, TlsEvidence } from "./components/report/evidence-views"; import { CloseIcon, Gauge, type FindingFilter, type Tab, collectorLabel, collectorMeta, compact, downloadText, finiteNumber, formatMxRecords, findingIcon, formatInspectedAt, isRecord, severityLabel, tabs, terminalHttpsResult, toneForScore, } from "./components/report/report-utils"; function headerRows(http: Record) { const terminal = terminalHttpsResult(http); return terminal && isRecord(terminal.headers) ? terminal.headers as HeaderMap : null; } export function ReportView({ result, source, isLoading = false, requestedHost, requestError, onReinspect, onInspectAnother }: { result: InspectionResult; source: "demo" | "live"; isLoading?: boolean; requestedHost: string | null; requestError: { requestedHost: string; message: string } | null; onReinspect: (host: string) => void; onInspectAnother: (host: string) => void; }) { const isDemo = source === "demo"; const [tab, setTab] = useState("overview"); const [selectedFindingId, setSelectedFindingId] = useState(null); const [showAllFindings, setShowAllFindings] = useState(false); const [showAllAddresses, setShowAllAddresses] = useState(false); const [showAllIssues, setShowAllIssues] = useState(false); const [filter, setFilter] = useState("all"); const [wrapRaw, setWrapRaw] = useState(false); const [highlightedEvidence, setHighlightedEvidence] = useState(null); const [inspectAnotherOpen, setInspectAnotherOpen] = useState(false); const [nextHost, setNextHost] = useState(""); const [statusMessage, setStatusMessage] = useState(""); const [manualCopy, setManualCopy] = useState<{ label: string; value: string } | null>(null); const manualCopyRef = useRef(null); const dialogRef = useRef(null); const closeRef = useRef(null); const originatingFindingRef = useRef(null); const findings = useMemo(() => { const severityRank: Record = { critical: 0, warning: 1, info: 2, good: 3 }; return [...result.findings].sort((left, right) => severityRank[left.severity] - severityRank[right.severity] || left.scoreImpact - right.scoreImpact || left.id.localeCompare(right.id)); }, [result.findings]); const filteredFindings = useMemo(() => filter === "all" ? findings : findings.filter((finding) => finding.severity === filter), [findings, filter]); const displayFindings = showAllFindings ? filteredFindings : filteredFindings.slice(0, 4); const severityCounts = useMemo(() => (["critical", "warning", "info"] as const) .map((severity) => ({ severity, count: findings.filter((finding) => finding.severity === severity).length })) .filter(({ count }) => count > 0), [findings]); const selectedFinding = findings.find((finding) => finding.id === selectedFindingId) ?? null; const dns = result.dns as Record; const tls = result.tls as Record; const http = result.http as Record; const headers = headerRows(http); const httpsChain = Array.isArray(http.httpsChain) ? http.httpsChain.filter(isRecord) : []; const terminal = terminalHttpsResult(http); const dnsCollectorStatus = collectorMeta(dns).status; const tlsCollectorStatus = collectorMeta(tls).status; const httpCollectorStatus = collectorMeta(http).status; const tlsVerified = tls.authorized === true && tls.hostnameValid === true; const tlsStatus = tls.error ? "TLS probe incomplete" : tlsVerified ? "Verified certificate" : "Certificate needs attention"; const httpTerminalStatus = statusForHttp(terminal?.status); const httpStatus = httpCollectorStatus === "ok" ? httpTerminalStatus : { label: httpCollectorStatus === "failed" ? "HTTP probe failed" : "HTTP evidence partial", tone: httpCollectorStatus === "failed" ? "red" as const : "muted" as const }; const scoreTone = (category: "overall" | "dns" | "tls" | "http", value: number) => { const complete = category === "overall" ? result.completeness.status === "complete" : ({ dns: dnsCollectorStatus, tls: tlsCollectorStatus, http: httpCollectorStatus }[category] === "ok"); return complete ? toneForScore(value) : "muted" as const; }; const addresses = Array.isArray(dns.addresses) ? dns.addresses.map((address) => String(address)).filter(Boolean) : []; const responses = [http.http, ...httpsChain].filter(isRecord).filter((hop) => finiteNumber(hop.status)); const redirectCount = responses.filter((hop) => Number(hop.status) >= 300 && Number(hop.status) < 400).length; useEffect(() => { if (manualCopy) manualCopyRef.current?.focus(); }, [manualCopy]); useEffect(() => { if (!selectedFinding) return; const previousOverflow = document.body.style.overflow; const backgroundElements = [...document.querySelectorAll(".app-topbar, .cert-main > *:not(.drawer-backdrop)")]; const previousInert = backgroundElements.map((element) => element.hasAttribute("inert")); backgroundElements.forEach((element) => element.setAttribute("inert", "")); document.body.style.overflow = "hidden"; const focusTarget = window.setTimeout(() => closeRef.current?.focus(), 0); const dialog = dialogRef.current; const closeOnKey = (event: KeyboardEvent) => { if (event.key === "Escape") { event.preventDefault(); setSelectedFindingId(null); return; } if (event.key !== "Tab" || !dialog) return; const focusable = [...dialog.querySelectorAll("button, [href], input, select, textarea, [tabindex]:not([tabindex='-1'])")].filter((element) => !element.hasAttribute("disabled")); if (!focusable.length) return; const first = focusable[0]; const last = focusable[focusable.length - 1]; if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); } else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); } }; dialog?.addEventListener("keydown", closeOnKey); return () => { window.clearTimeout(focusTarget); dialog?.removeEventListener("keydown", closeOnKey); backgroundElements.forEach((element, index) => { if (!previousInert[index]) element.removeAttribute("inert"); }); document.body.style.overflow = previousOverflow; if (originatingFindingRef.current?.isConnected) originatingFindingRef.current.focus(); else document.getElementById("raw-tab")?.focus(); }; }, [selectedFinding]); function announce(message: string) { setStatusMessage(message); } async function copyText(label: string, value: string) { try { if (navigator.clipboard?.writeText) { await navigator.clipboard.writeText(value); announce(label + " copied"); return; } } catch { /* Try the selection-based browser fallback below. */ } const field = document.createElement("textarea"); const previousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null; field.value = value; field.readOnly = true; field.style.position = "fixed"; field.style.opacity = "0"; document.body.appendChild(field); field.select(); let copied = false; try { copied = document.execCommand("copy"); } catch { /* Manual copy remains available. */ } field.remove(); previousFocus?.focus(); if (copied) { announce(label + " copied"); } else { setManualCopy({ label, value }); announce(label + " ready to copy manually"); } } async function share() { const text = inspectionToMarkdown(result); try { if (navigator.share) { await navigator.share({ title: "CertScope — " + result.host, text }); announce("Report shared"); } else await copyText("Report", text); } catch (error) { if (error instanceof DOMException && error.name === "AbortError") announce("Share cancelled"); else await copyText("Report", text); } } function exportJson() { downloadText(result.host + "-certscope.json", JSON.stringify(result, null, 2), "application/json"); announce("JSON export downloaded"); } function exportMarkdown() { downloadText(result.host + "-certscope.md", inspectionToMarkdown(result), "text/markdown"); announce("Markdown export downloaded"); } function switchTab(next: Tab) { setTab(next); } function submitAnotherHost(event: FormEvent) { event.preventDefault(); if (!nextHost.trim()) return; onInspectAnother(nextHost.trim()); } function handleTabKey(event: React.KeyboardEvent, index: number) { const direction = event.key === "ArrowRight" ? 1 : event.key === "ArrowLeft" ? -1 : 0; if (event.key === "Home") { event.preventDefault(); switchTab(tabs[0]); document.getElementById(tabs[0] + "-tab")?.focus(); return; } if (event.key === "End") { event.preventDefault(); const last = tabs.at(-1)!; switchTab(last); document.getElementById(last + "-tab")?.focus(); return; } if (!direction) return; event.preventDefault(); const nextIndex = (index + direction + tabs.length) % tabs.length; switchTab(tabs[nextIndex]); document.getElementById(tabs[nextIndex] + "-tab")?.focus(); } function evidenceTargetFor(finding: Finding) { const pointer = finding.evidence.join(" "); if (pointer.includes("tls.daysRemaining") || pointer.includes("tls.validTo") || pointer.includes("tls.validFrom")) return { tab: "tls" as const, id: "tls-certificate" }; if (pointer.includes("tls.")) return { tab: "tls" as const, id: "tls-certificate" }; if (pointer.includes("dns.dmarc") || pointer.includes("dns.spf")) return { tab: "dns" as const, id: "dns-mail-posture" }; if (pointer.includes("dns.")) return { tab: "dns" as const, id: "dns-records" }; if (pointer.includes("headers") || pointer.includes("header")) return { tab: "http" as const, id: "http-header-posture" }; if (pointer.includes("httpsChain") || pointer.includes("http.http")) return { tab: "http" as const, id: "http-redirect-chain" }; return { tab: "raw" as const, id: "raw-evidence" }; } function viewEvidence(finding: Finding) { const target = evidenceTargetFor(finding); setSelectedFindingId(null); switchTab(target.tab); window.setTimeout(() => { const element = document.getElementById(target.id); element?.scrollIntoView({ behavior: "smooth", block: "center" }); element?.focus(); setHighlightedEvidence(target.id); window.setTimeout(() => setHighlightedEvidence((current) => current === target.id ? null : current), 2_000); }, 0); } return <>

CertScope inspection report for {result.host}

{isDemo ? "Deterministic demo report" : "Live inspection report"}

{result.host}

Coverage
{result.completeness.status === "complete" ? "Complete" : result.completeness.status === "partial" ? "Partial" : "Incomplete"} {result.completeness.complete}/{result.completeness.total} collectors
Addresses
{addresses.length ? <>{(showAllAddresses ? addresses : addresses.slice(0, 4)).join(" · ")}{addresses.length > 4 && } : "Unavailable"}
Last inspected
{formatInspectedAt(result.inspectedAt)}
{inspectAnotherOpen &&
setNextHost(event.target.value)} placeholder="example.com" autoComplete="off" spellCheck={false} />
}
{(() => { const tone = scoreTone("overall", result.score.overall); return
Overall posture{result.score.overall}/ 100{result.completeness.status === "complete" ? `(DNS ${result.score.categories.dns} + TLS ${result.score.categories.tls} + HTTP ${result.score.categories.http}) ÷ 3 = ${result.score.overall} (rounded)` : "Review incomplete evidence before acting"}
; })()}
{[["DNS", result.score.categories.dns, result.completeness.collectors.dns === "ok" ? "DNS evidence complete" : "DNS evidence partial"], ["TLS", result.score.categories.tls, tlsStatus], ["HTTP", result.score.categories.http, httpStatus.label]].map(([label, value, description]) => { const tone = scoreTone(String(label).toLowerCase() as "dns" | "tls" | "http", Number(value)); return
{String(label)}{Number(value)}/100{String(description)}
; })}
{result.completeness.status === "complete" ? "Complete inspection" : result.completeness.status === "partial" ? "Partial inspection" : "Inspection incomplete"}{result.completeness.complete}/{result.completeness.total} collectors complete{isDemo ? "Deterministic demo" : "Live inspection"}{isLoading && requestedHost && {requestedHost === result.host ? "Re-inspecting" : "Inspecting"} {requestedHost}…}
{requestError &&
Previous report remains displayed{requestError.message}The {isDemo ? "deterministic demo" : "live"} report for {result.host} is still available while {requestError.requestedHost} could not be inspected.
} {result.completeness.issues.length > 0 &&

Inspection incomplete{(showAllIssues ? result.completeness.issues : result.completeness.issues.slice(0, 3)).join(" · ")}{result.completeness.issues.length > 3 && } Remaining evidence is still valid.

}
{ announce("Re-inspection started"); onReinspect(result.host); }} onInspectAnother={() => setInspectAnotherOpen((open) => !open)} onShare={() => void share()} onExportJson={exportJson} onExportMarkdown={exportMarkdown} />

{statusMessage}

{manualCopy &&
{manualCopy.label} ready to copy

Use Ctrl+C (or Copy on your device) to copy the selected text.