import type { Metadata } from "next"; import Link from "next/link"; import { notFound } from "next/navigation"; import { getPaths } from "yt-dlp-transcript-common/lib/paths"; import { formatBytes } from "yt-dlp-transcript-common/lib/format"; import { isCitedSite } from "yt-dlp-transcript-common/lib/site"; import { readReportMediaIndex } from "yt-dlp-transcript-common/publish/reportMedia"; import { PrepareReportMediaButton } from "../../components/PrepareReportMediaButton"; import { FetchMissingEvidenceButton } from "../../components/FetchMissingEvidenceButton"; import { ExportReportsButton } from "../../components/ExportReportsButton"; import { REPORT_EXPORT_FILENAMES, REPORT_EXPORT_FORMATS, } from "yt-dlp-transcript-common/lib/report/views"; import { ReportRowActions } from "../../components/ReportRowActions"; import { publishRefusal, reportMediaSummary, type ReportMediaSummary, type ReportRow, } from "../../lib/reportList"; import { latestSiteReportsJob, readSiteReportExports, readSiteReportRows, type ReportExportsRow, } from "../../lib/reportListServer"; import { getSiteCached } from "../lib/siteCache"; export const dynamic = "force-dynamic"; // → "Reports — — Sites" through the layout's title template. export const metadata: Metadata = { title: "Reports" }; // A SITE'S REPORTS: every report directory under `sites//reports/`, // whether the site publishes it (site.json `reports`, in order) or it is a // draft, and what is wrong with it — the report's own checker, so this lists // what prepare and the build would refuse. Publish, unpublish and reorder write // that one key (lib/reportsActions.ts). The evidence media the published // reports cite is prepared here too, before a build, and the reports exported // as files (HTML, PDF, Markdown, an evidence pack) for the build to publish; // each export of a changed report.json commits a revision to the report's own // history, and each report shows its revision and the last change. // // The documents themselves are written elsewhere (a converter, or by hand); // this tab never edits a report.json. export default async function SiteReportsPage({ params, }: { params: Promise<{ siteId: string }>; }) { const { siteId } = await params; const site = getSiteCached(siteId); if (!site) notFound(); const paths = getPaths(); const [rows, mediaIndex, lastJob, exportRows, lastExportJob] = await Promise.all([ readSiteReportRows(paths, site), readReportMediaIndex(paths, siteId), latestSiteReportsJob(paths, siteId, "reports-prepare"), readSiteReportExports(paths, site), latestSiteReportsJob(paths, siteId, "reports-export"), ]); const publishedCount = rows.filter((r) => r.position !== null).length; const cited = isCitedSite(site); return (

A report is a report.json in its own directory under this site's reports/. The site publishes the reports listed here as published, in this order; any other report directory is a draft. Only a report with no problems can be published.

Publishes:{" "} {cited ? "Reports only — search is off" : "Full archive — search is on"}{" "} (changed on{" "} Settings )

{cited && publishedCount === 0 && (

Search is off and no report is published: the site is an empty report index.

)}

Reports{" "} {publishedCount} published, {rows.length - publishedCount} draft {rows.length - publishedCount === 1 ? "" : "s"}

{rows.length === 0 ? (

No reports yet. Put a report at{" "} sites/{siteId}/reports/<reportId>/report.json.

) : (
    {rows.map((row) => ( ))}
)}

Evidence media

Fetch missing evidence downloads the window of every cited span whose media is not on disk, one paced job per platform; Preview lists them, and the spans no window can fill (a deleted video, a channel off this site), without fetching. Prepare then cuts a clip of every cited span and copies every cited post capture, from the media on disk, ready for the site's next build — it downloads nothing, and lists a citation whose media is still missing as a problem.

Last prepare job:{" "} {lastJob ? ( {lastJob.id} ) : ( "none among recent jobs" )} {lastJob && <> ({lastJob.status})}

{mediaIndex ? ( ) : (

Not prepared yet.

)}

Exports

Writes each published report as files a reader can save and host again — one self-contained HTML page, a PDF of it, Markdown, and an evidence pack (the page with its clips, stills and screenshots) — for the site's next build to publish beside the report. Preparing the evidence media exports too. A file over 24 MiB stays on this machine. Exporting a changed report also commits a new revision to the report's history, which the site publishes.

Last export job:{" "} {lastExportJob ? ( {lastExportJob.id} ) : ( "none among recent jobs" )} {lastExportJob && <> ({lastExportJob.status})}

{exportRows.length === 0 ? (

No published report to export.

) : (
    {exportRows.map((row) => ( ))}
)}
); } const STATUS_TEXT: Record = { ok: "No problems", problems: "Has problems", invalid: "Not a valid report", missing: "Missing", }; function ReportListItem({ siteId, row, publishedCount, }: { siteId: string; row: ReportRow; publishedCount: number; }) { const dates = [ row.publishedDate ? `published ${row.publishedDate}` : null, row.updatedDate ? `updated ${row.updatedDate}` : null, ].filter(Boolean); return (
  • {row.position !== null && ( {row.position + 1}. )} {row.title ?? row.id} {row.subtitle && ( {row.subtitle} )} {row.id} {row.kind && <> · {row.kind === "factcheck" ? "fact-check" : "sweep"}} {dates.length > 0 && <> · {dates.join(", ")}}
    {row.position !== null ? "Published" : "Draft"}
    {(row.status === "ok" || row.status === "problems") && (

    {row.sections} section{row.sections === 1 ? "" : "s"} · {row.claims} claim {row.claims === 1 ? "" : "s"} · {row.citations} citation {row.citations === 1 ? "" : "s"} {row.citationsByKind.length > 0 && ( {" "} ({row.citationsByKind.map((k) => `${k.count} ${k.kind}`).join(", ")}) )}

    )} {row.tally && row.verdicts && row.tally.length > 0 && (
      {row.tally.map((t) => (
    • {row.verdicts![t.verdict].label} {t.count}
    • ))}
    )} {row.problems.length === 0 ? ( {STATUS_TEXT[row.status]} ) : (
    {STATUS_TEXT[row.status]} — {row.problems.length} problem {row.problems.length === 1 ? "" : "s"}
      {row.problems.map((p, i) => (
    • {p.path && {p.path}} {p.path && ": "} {p.message}
    • ))}
    )}
  • ); } function MediaSummary({ summary }: { summary: ReportMediaSummary }) { return (

    Prepared {new Date(summary.preparedAt).toLocaleString()}: {summary.moments} moment {summary.moments === 1 ? "" : "s"} {summary.momentsByKind.length > 0 && ( <> ({summary.momentsByKind.map((k) => `${k.count} ${k.kind}`).join(", ")}) )} , {formatBytes(summary.totalBytes)}.

    {summary.problems.length === 0 ? (

    No problems.

    ) : (
    {summary.problems.length} problem{summary.problems.length === 1 ? "" : "s"}:{" "} {summary.problemsByKind.map((k) => `${k.count} ${k.kind}`).join(", ")}
      {summary.problems.map((p, i) => (
    • {p.kind}{" "} {p.moment ? ( {p.moment} ) : ( <> {p.report ?? "?"} {p.path && {p.path}} )} {p.citations && p.citations.length > 0 && ( (cited by {p.citations.join(", ")}) )} : {p.message}
    • ))}
    )}
    ); } function ExportSummary({ row }: { row: ReportExportsRow }) { const { manifest, publishable } = row; return (
  • {row.reportId}{" "} {manifest ? ( exported {new Date(manifest.exportedAt).toLocaleString()} ) : ( not exported yet )}

    {manifest?.revision && (

    Revision {manifest.revision.revision} · {manifest.revision.date.slice(0, 10)} ·{" "} {manifest.revision.commit.slice(0, 12)} {manifest.revision.summary.length > 0 && ( {" "} — {manifest.revision.summary.join("; ")} )}

    )} {manifest && (
      {REPORT_EXPORT_FORMATS.filter((f) => manifest.files[f]).map((f) => (
    • {REPORT_EXPORT_FILENAMES[f]}{" "} {formatBytes(manifest.files[f]!.bytes)}{" "} {publishable.files[f] ? ( published on build ) : ( local only )}
    • ))}
    )} {[...(manifest?.notes ?? []), ...publishable.notes].map((note, i) => (

    {note}

    ))}
  • ); }