Adds the full Next.js app (app/, components/, lib/, scripts/, assets/, docs/, Dockerfile, compose, configs) to version control. Security: - CIPC BizPortal customer-code logins (Login 1 + Login 3) now read CIPC_LOGIN1_CODE / CIPC_LOGIN1_PASSWORD / CIPC_LOGIN3_CODE / CIPC_LOGIN3_PASSWORD from .env.local instead of source. - BizPortal personal login in apply-bizportal route reads BIZPORTAL_ID_NO / BIZPORTAL_PASSWORD. - Added .env.example as the committed template. .gitignore excludes .env*, node_modules, .next, logs, sqlite, Playwright storage_state.json (live CIPC cookies), scratch dirs, client PDFs and debug screenshots.
99 lines
3.4 KiB
TypeScript
99 lines
3.4 KiB
TypeScript
// lib/pdf-render.ts
|
|
// SERVER-ONLY — bridges the Workflow app to the patched minimax-pdf skill.
|
|
//
|
|
// Why: minimax-pdf produces print-quality branded PDFs (Playfair Display, navy
|
|
// accent rules, alternating row tints). The skill lives in the ysterbaard
|
|
// profile at ~/.hermes/profiles/ysterbaard/skills/minimax-pdf — installed with
|
|
// auto h2 keepWithNext (no orphan headings) + an explicit `keep_together`
|
|
// block type.
|
|
//
|
|
// Fallback order:
|
|
// 1. Profile skill path (preferred, has David's edits).
|
|
// 2. Default skill path (~/.hermes/skills/minimax-pdf, symlinked in this env).
|
|
// 3. Throw with a clear "skill not installed" error.
|
|
|
|
import "server-only";
|
|
import { spawn } from "child_process";
|
|
import { promises as fs } from "fs";
|
|
import os from "os";
|
|
import path from "path";
|
|
|
|
const HOME = os.homedir();
|
|
const SKILL_PATHS = [
|
|
path.join(HOME, ".hermes/profiles/ysterbaard/skills/minimax-pdf"),
|
|
path.join(HOME, ".hermes/skills/minimax-pdf"),
|
|
];
|
|
|
|
export interface RenderPdfOptions {
|
|
/** Title shown on the cover page. */
|
|
title: string;
|
|
/** Subtitle / company-or-director name shown on the cover. */
|
|
author: string;
|
|
/** Date string for the cover (e.g. "30 August 2026"). */
|
|
date: string;
|
|
/** Hex color, e.g. "#1C3A5E". Deep navy = legal/compliance. */
|
|
accent: string;
|
|
/** Document type — see skill (report/proposal/etc). "report" is the default. */
|
|
type?: "report" | "proposal" | "general" | "minimal";
|
|
/** minimax-pdf content.json: a flat array of blocks. */
|
|
blocks: unknown[];
|
|
/** Absolute path for the rendered PDF. */
|
|
outPath: string;
|
|
}
|
|
|
|
function pickSkill(): string {
|
|
for (const p of SKILL_PATHS) {
|
|
try {
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
require("fs").accessSync(path.join(p, "scripts/make.sh"));
|
|
return p;
|
|
} catch { /* try next */ }
|
|
}
|
|
throw new Error(
|
|
`minimax-pdf skill not installed at any of: ${SKILL_PATHS.join(", ")}. ` +
|
|
"Copy the skill into the ysterbaard profile and re-deploy."
|
|
);
|
|
}
|
|
|
|
function run(cmd: string, args: string[], cwd: string): Promise<{ ok: boolean; stderr: string }> {
|
|
return new Promise((resolve) => {
|
|
const child = spawn(cmd, args, { cwd });
|
|
let stderr = "";
|
|
child.stderr.on("data", (b) => (stderr += b.toString()));
|
|
child.on("error", (err) => resolve({ ok: false, stderr: err.message }));
|
|
child.on("close", (code) => resolve({ ok: code === 0, stderr }));
|
|
});
|
|
}
|
|
|
|
export async function renderPdf(opts: RenderPdfOptions): Promise<Buffer> {
|
|
const skillRoot = pickSkill();
|
|
const makeSh = path.join(skillRoot, "scripts/make.sh");
|
|
|
|
// Write content.json to a temp file in the skill's working dir
|
|
// (make.sh reads it relative to cwd).
|
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "wf-report-"));
|
|
const contentPath = path.join(tmpDir, "content.json");
|
|
await fs.writeFile(contentPath, JSON.stringify(opts.blocks, null, 2));
|
|
|
|
try {
|
|
const result = await run("bash", [
|
|
makeSh, "run",
|
|
"--title", opts.title,
|
|
"--type", opts.type || "report",
|
|
"--author", opts.author,
|
|
"--date", opts.date,
|
|
"--accent", opts.accent,
|
|
"--content", contentPath,
|
|
"--out", opts.outPath,
|
|
], tmpDir);
|
|
|
|
if (!result.ok) {
|
|
throw new Error(`minimax-pdf render failed: ${result.stderr.slice(0, 800)}`);
|
|
}
|
|
|
|
return await fs.readFile(opts.outPath);
|
|
} finally {
|
|
await fs.unlink(contentPath).catch(() => {});
|
|
await fs.rmdir(tmpDir).catch(() => {});
|
|
}
|
|
}
|