`;
- const names: Record = { csp:"Content-Security-Policy", hsts:"Strict-Transport-Security", xFrameOptions:"X-Frame-Options", xContentTypeOptions:"X-Content-Type-Options", referrerPolicy:"Referrer-Policy", permissionsPolicy:"Permissions-Policy" };
- for (const [key, label] of Object.entries(names)) { const val = (h as any)[key] as string; html += `
`;
+export type RepoDetailTab = "overview" | "nist" | "ai" | "branches" | "trends";
- // Governance artifacts table shows TWO columns per policy: IN REPO
- // (scanner's file-on-disk state) and SERVED (live-URL state from the last
- // Check Production click). These are distinct compliance signals — a policy
- // file can exist in the repo but not be served at its configured URL, and
- // vice versa.
- html += `
GOVERNANCE ARTIFACTS
`;
- html += `
`;
- html += `
ARTIFACT
IN REPO
SERVED
`;
- const labels: Record = { privacyPolicy:"Privacy Policy", termsOfService:"Terms of Service", securityTxt:"security.txt", vulnerabilityDisclosure:"Vuln Disclosure", incidentResponsePlan:"Incident Response Plan" };
+/**
+ * Render the right pane of the v2 two-pane shell. `tab` selects which body
+ * to show under the detail head — overview keeps the 3-col panel grid,
+ * the others embed the existing NIST / AI / branches / trends views so
+ * navigation is a single full-page load rather than HTMX.
+ */
+export function renderRepoDetail(
+ manifest: Manifest,
+ summary: RepoSummary,
+ served: ServedState = {},
+ opts: {
+ tab?: RepoDetailTab;
+ functionScores?: FunctionScore[];
+ branchSummaries?: RepoSummary[];
+ history?: HistoryEntry[];
+ } = {},
+): string {
+ const [owner, name] = manifest.repo.split("/") as [string, string];
+ const safeId = (owner + "-" + name).replace(/\./g, "-");
+ const h = manifest.securityHeaders;
+ const ac = manifest.accessControls;
+ const dc = manifest.dataCollection;
+ const tp = manifest.thirdPartyServices;
+ const ai = manifest.aiSystems || [];
+ const aiCount = ai.length;
+ const aiScore = summary.aiScore;
+ const hasSiteUrl = !!summary.siteUrl;
+
+ // --- panels ---
+
+ const dataBody = dc.length === 0
+ ? `
Served state last checked ${timeAgo(served.policyServedCheckedAt)}. CHECK PRODUCTION refreshes it.
`
+ : `
Served state never checked. Click CHECK PRODUCTION above to populate — only URLs declared in policy_urls: are verified.
`);
+
+ const panelsHtml = [
+ v2Panel(`DATA COLLECTION`, dataBody, { count: dc.length }),
+ v2Panel(`TRANSPORT`, transportBody),
+ v2Panel(`DEPENDENCIES`, depsBody),
+ v2Panel(`ACCESS CONTROLS`, accessBody),
+ v2Panel(aiCount > 0 ? `AI SYSTEMS` : `AI SYSTEMS · NONE`, aiBody, aiCount > 0 ? { count: aiCount } : {}),
+ v2Panel(`THIRD-PARTY`, tpBody, tp.length > 0 ? { count: tp.length } : {}),
+ v2Panel(`GOVERNANCE ARTIFACTS`, artifactsBody, { span3: true }),
+ ].join("\n");
+
+ // --- detail head (title + meta + action buttons + score trio + tabs) ---
+
+ // Keep v1 class names (export-combo, branch-combo, check-prod-btn) for
+ // backward compatibility with the existing JS functions — they still work
+ // the same way, just styled at new positions.
+ const exportDropdownHtml = `
+
+
+
+
MACHINE-READABLE
+
JSON (full state)
+
SARIF (code scanning)
+
OSCAL (assessment)
+
CSV
+
NIST CSF controls
+
EU AI Act articles
+
Risk register
+
Vulnerabilities
+
+
`;
- // Freshness footer for the SERVED column.
- if (served.policyServedCheckedAt) {
- html += `
Served status last checked ${timeAgo(served.policyServedCheckedAt)}. Click CHECK PRODUCTION above to refresh. IN REPO reflects the scanner's view of the file on disk at the last scan; SERVED reflects an HTTP GET against the URL declared in .grc/config.yml under policy_urls:.
`;
- } else {
- html += `
SERVED status has never been checked for this repo. Click CHECK PRODUCTION above to populate it. Only policies with a URL declared under policy_urls: in .grc/config.yml will be checked; unlisted ones stay as NOT CONFIGURED.
`;
+ const checkProdBtn = hasSiteUrl
+ ? ``
+ : "";
+
+ const activeTab: RepoDetailTab = opts.tab ?? "overview";
+ const tabLabels: Array<[RepoDetailTab, string]> = [
+ ["overview", "OVERVIEW"],
+ ["nist", "NIST"],
+ ["ai", "AI"],
+ ["branches", "BRANCHES"],
+ ["trends", "TRENDS"],
+ ];
+ const repoHref = `/?repo=${encodeURIComponent(manifest.repo)}`;
+ const tabHref = (tab: RepoDetailTab, label: string) =>
+ `${label}`;
+
+ // Body varies by tab. Non-overview tabs embed the existing specialized
+ // views — they were originally HTMX fragments but render fine as plain
+ // content inside the panel body.
+ let bodyHtml: string;
+ switch (activeTab) {
+ case "nist":
+ bodyHtml = opts.functionScores
+ ? renderNistView(summary, opts.functionScores)
+ : `