import { readFile } from "node:fs/promises"; import path from "node:path"; import { REPORTS_ROOT } from "@/lib/paths"; import { listTakes, readVerdicts } from "@/lib/report/takes.mjs"; import { siteWorkspaces, tildify } from "./sources.mjs"; import { untildify } from "./links.mjs"; import { workspaceFile, workspaceFiles } from "./workspace.mjs"; // The page side of lib/articles/workspace.mjs and the video projects' takes. export type WorkspaceListing = { /** The workspace's name under REPORTS_ROOT (what /api/sites/workspace takes). */ name: string; dir: string; files: { rel: string; kind: "draft" | "out" | "doc"; bytes: number; mtimeMs: number }[]; }; export async function listingOf(dir: string): Promise { const files = (await workspaceFiles(dir)).map(({ rel, kind, bytes, mtimeMs }: { rel: string; kind: WorkspaceListing["files"][number]["kind"]; bytes: number; mtimeMs: number }) => ({ rel, kind, bytes, mtimeMs })); return { name: path.basename(dir), dir: tildify(dir), files }; } /** Every workspace a site's articles were written in. */ export async function siteWorkspaceListings(siteId: string, reportIds: string[]): Promise { const dirs: string[] = await siteWorkspaces(siteId, reportIds, { reportsRoot: REPORTS_ROOT }); return Promise.all(dirs.map(listingOf)); } /** One article's workspace (its source's), or null. */ export async function articleWorkspaceListing(workspace: string | undefined | null): Promise { if (!workspace) return null; const abs = untildify(workspace); if (path.dirname(abs) !== path.resolve(REPORTS_ROOT)) return null; return listingOf(abs); } export type OpenedFile = | { ws: string; rel: string; kind: "md"; text: string } | { ws: string; rel: string; kind: "json"; value: unknown; text: string } | { ws: string; rel: string; kind: "html"; url: string } | { ws: string; rel: string; kind: "error"; message: string }; const MAX = 2 * 1024 * 1024; /** A workspace file opened for the page, or an error saying why not. */ export async function openWorkspaceFile(ws: string, rel: string): Promise { const f = await workspaceFile(ws, rel); if (!f) return { ws, rel, kind: "error", message: "not a listed workspace file" }; if (rel.endsWith(".html")) { return { ws, rel, kind: "html", url: `/api/sites/workspace?ws=${encodeURIComponent(ws)}&rel=${encodeURIComponent(rel)}` }; } if (f.bytes > MAX) return { ws, rel, kind: "error", message: `${f.bytes} bytes; too big to show` }; const text = await readFile(/* turbopackIgnore: true */ f.real, "utf8"); if (rel.endsWith(".json")) { try { return { ws, rel, kind: "json", value: JSON.parse(text), text }; } catch (err) { return { ws, rel, kind: "error", message: `not JSON: ${(err as Error).message}` }; } } return { ws, rel, kind: "md", text }; } export type TakeTally = { takes: number; like: number; maybe: number; no: number; skipped: number }; /** How many takes a video project has, and how they were judged. */ export async function takeTally(dir: string): Promise { const [t, v] = await Promise.all([listTakes(dir), readVerdicts(dir)]); const rows = Object.values(v) as { verdict: string | null }[]; return { takes: t.takes.length, like: rows.filter((r) => r.verdict === "like").length, maybe: rows.filter((r) => r.verdict === "maybe").length, no: rows.filter((r) => r.verdict === "no").length, skipped: t.skipped.length, }; }