- 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>
51 lines
2.4 KiB
JavaScript
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, "/");
|
|
}
|