metatroncubeswdev df3dbb63c4 Visual reports and client Screenshots tab
- Reports: mc: blocks (score, KPIs, reach, funnel, followers, formats,
  rhythm, top posts, actions, screenshots, callouts) drawn as SVG from
  stored account data, frozen per report; same page in PDF and preview
- Audit/weekly-report runs refresh account numbers first
- Screenshots tab: monthly shot list, uploads, examples; audit reads them

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 00:48:32 -04:00

51 lines
2.4 KiB
JavaScript

// Client documents: render markdown for the in-app viewer, export/serve branded PDFs.
import fs from "node:fs";
import path from "node:path";
import { marked } from "marked";
import { safeSlug } from "./clients.js";
import { toPdf, buildHtml } from "../../tools/report-pdf/build.mjs";
import { reportOptions } from "./workspace.js";
/** Resolve a document path inside one client's folder (blocks ../ escapes). */
export function resolveDoc(slug, rel) {
const dir = safeSlug(slug);
const full = path.resolve(dir, String(rel || ""));
if (!full.startsWith(dir + path.sep) || !/\.(md|pdf)$/.test(full) || !fs.existsSync(full)) {
throw Object.assign(new Error("Document not found."), { status: 404 });
}
return full;
}
function splitFrontMatter(md) {
const m = md.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
if (!m) return { meta: {}, body: md };
const meta = {};
const kpis = [];
for (const line of m[1].split(/\r?\n/)) {
const kpi = line.match(/^\s+-\s+(.*)$/);
if (kpi) { const [value, label = "", note = ""] = kpi[1].split("|").map((s) => s.trim()); kpis.push({ value, label, note }); continue; }
const kv = line.match(/^(\w+):\s*(.*)$/);
if (kv && kv[1] !== "kpis") meta[kv[1]] = kv[2].trim();
}
return { meta: { ...meta, kpis }, body: md.slice(m[0].length) };
}
export function renderDoc(slug, rel) {
const full = resolveDoc(slug, rel);
if (!full.endsWith(".md")) throw Object.assign(new Error("Only markdown documents can be previewed."), { status: 400 });
const { meta, body } = splitFrontMatter(fs.readFileSync(full, "utf8"));
// Content is written by our own team/AI runs; links open in a new tab.
const html = marked.parse(body, { gfm: true, breaks: true }).replace(/<a href=/g, '<a target="_blank" rel="noopener" href=');
const pdf = full.replace(/\.md$/, ".pdf");
// Reports with visual blocks (```mc:…) preview as the same designed page the PDF uses.
const page = /^```mc:/m.test(body) ? buildHtml(full, { ...reportOptions(), screen: true }) : undefined;
return { path: rel, meta, html, page, updatedAt: fs.statSync(full).mtime.toISOString(), hasPdf: fs.existsSync(pdf) };
}
export function exportPdf(slug, rel) {
const full = resolveDoc(slug, rel);
if (!full.endsWith(".md")) throw Object.assign(new Error("Pick a markdown document."), { status: 400 });
const written = toPdf(full, reportOptions());
return path.relative(safeSlug(slug), written).replace(/\\/g, "/");
}