metatroncubeswdev 82814f3757 White-label workspace and getting-started checklist
- Settings → Workspace: agency name, logo, brand colour, support email, website
- Logo replaces Quorla's in the app (with 'Powered by Quorla'), sign-in page, client approval page and PDF reports;
  brand colour drives buttons, links and highlights; emails and reports use the agency name
- Getting started checklist on the Clients page for admins, checked against the real state
  (workspace, Meta, first client, team, platforms, email, library, first post, approval link); dismissible

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 10:28:23 -04:00

49 lines
2.2 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 } 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");
return { path: rel, meta, html, 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, "/");
}