/** * Rendering approach: all agent/user text is escaped as plain text. * No markdown/HTML pass-through is allowed in this renderer. */ import type { CombinedReview } from "../review-types.js"; import type { Report } from "../store/report-types.js"; import { escapeAttr, escapeHtml } from "./escape.js"; import { buildBrandingCss, REPORT_STYLESHEET, type ReportBranding } from "./html-styles.js"; export interface ReportRecord extends Report { metadata: Record; } export interface ReportRenderOptions { theme?: "dark" | "light" | "auto"; includeChrome?: boolean; } interface ReportSectionsPayload { summary?: string; system?: SectionBuckets; perAgent?: Array; dataCoverage?: string[]; } interface SectionBuckets { wins?: string[]; highlights?: string[]; lowlights?: string[]; proposals?: string[]; deepDives?: string[]; } interface AgentSection extends SectionBuckets { agentId: string; agentName?: string; } function asObj(v: unknown): Record { return v && typeof v === "object" && !Array.isArray(v) ? v as Record : {}; } function asString(v: unknown): string | undefined { return typeof v === "string" && v.trim() ? v : undefined; } function asStringArray(v: unknown): string[] { return Array.isArray(v) ? v.filter((x): x is string => typeof x === "string").map((x) => x.trim()).filter(Boolean) : []; } function extract(record: ReportRecord): { sections: ReportSectionsPayload; order: string[]; enabled: Set; branding: ReportBranding } { const metadata = asObj(record.metadata); const sections = asObj(metadata.sections); const settings = asObj(metadata.settings); const branding = asObj(settings.branding); const order = asStringArray(settings.sectionOrder); const enabled = new Set(asStringArray(settings.enabledSections)); return { sections: { summary: asString(sections.summary), system: asObj(sections.system) as SectionBuckets, perAgent: Array.isArray(sections.perAgent) ? sections.perAgent.map((item) => { const agent = asObj(item); return { agentId: asString(agent.agentId) ?? "unknown", agentName: asString(agent.agentName), wins: asStringArray(agent.wins), highlights: asStringArray(agent.highlights), lowlights: asStringArray(agent.lowlights), proposals: asStringArray(agent.proposals), deepDives: asStringArray(agent.deepDives), }; }) : [], dataCoverage: asStringArray(sections.dataCoverage), }, order, enabled, branding: { accentColor: asString(branding.accentColor), logoDataUri: asString(branding.logoDataUri), logoTextColor: asString(branding.logoTextColor), }, }; } function listSection(title: string, items: string[] | undefined, marker: string): string { if (!items || items.length === 0) return ""; return `

${escapeHtml(title)}

    ${items.map((item) => `
  • ${escapeHtml(item)}
  • `).join("")}
`; } function cadenceLabel(cadence: string): string { return cadence.charAt(0).toUpperCase() + cadence.slice(1); } export function renderReportHtml(record: ReportRecord, options: ReportRenderOptions = {}): string { const { sections, order, enabled, branding } = extract(record); const dataTheme = options.theme && options.theme !== "auto" ? options.theme : "dark"; const includeChrome = options.includeChrome !== false; const title = asString(record.title) ?? "Fusion Activity Report"; const summary = sections.summary ? `

Executive Summary

${escapeHtml(sections.summary)}

` : ""; const system = sections.system ?? {}; const systemMap: Record = { wins: listSection("Wins", system.wins, "system-wins"), highlights: listSection("Highlights", system.highlights, "system-highlights"), lowlights: listSection("Lowlights", system.lowlights, "system-lowlights"), proposals: listSection("Proposals", system.proposals, "system-proposals"), "deep-dives": listSection("Deep dives", system.deepDives, "system-deep-dives"), }; const orderedKeys = order.length > 0 ? [...order, ...Object.keys(systemMap).filter((k) => !order.includes(k))] : Object.keys(systemMap); const systemSections = orderedKeys .filter((key) => key in systemMap) .filter((key) => enabled.size === 0 || enabled.has(key)) .map((key) => systemMap[key]) .join(""); const perAgent = sections.perAgent ?? []; const perAgentHtml = (enabled.size === 0 || enabled.has("per-agent")) ? `

Per-agent sections

${perAgent.map((agent) => `

${escapeHtml(agent.agentName ?? agent.agentId)}

${listSection("Wins", agent.wins, "agent-wins")}${listSection("Highlights", agent.highlights, "agent-highlights")}${listSection("Lowlights", agent.lowlights, "agent-lowlights")}${listSection("Proposals", agent.proposals, "agent-proposals")}${listSection("Deep dives", agent.deepDives, "agent-deep-dives")}
`).join("")}
` : ""; const coverage = sections.dataCoverage ?? ["Task board", "Agent activity", "Missions", "Run audit", "Workflow/test/build", "Manual notes"]; const coverageSection = ``; const review = (record.combinedReview as CombinedReview | null) ?? null; const reviewSection = review ? `` : ""; const header = `

${escapeHtml(title)}

${escapeHtml(cadenceLabel(record.cadence))}${escapeHtml(record.periodStart)} → ${escapeHtml(record.periodEnd)}Generated ${escapeHtml(record.generationCompletedAt ?? record.updatedAt)}${escapeHtml(record.status)}
${branding.logoDataUri ? `

Logo

` : ""}
`; const article = `
${header}${summary}${systemSections ? `

System-wide rollup

${systemSections}
` : ""}${perAgentHtml}${coverageSection}${reviewSection}
`; if (!includeChrome) return article; return `${escapeHtml(title)}${article}`; }