import { dirname, fromFileUrl, join } from "jsr:@std/path"; import { validateDocuments } from "./src/data.ts"; import { resolvePdfPath } from "./src/paths.ts"; import type { BuyerDocument } from "./src/types.ts"; import INDEX_HTML from "./web/index.html" with { type: "text" }; import APP_JS from "./web/app.js" with { type: "text" }; import STYLES_CSS from "./web/styles.css" with { type: "text" }; import SAMPLE_DOCUMENTS from "./sample-data/buyers_vision_anonymous.json" with { type: "json" }; const ROOT = dirname(fromFileUrl(import.meta.url)); const CONFIG_PATH = join(Deno.cwd(), ".bizmatch-qc.json"); type Config = { jsonPath: string; pdfBaseDirectory: string }; type LoadResult = { documents: BuyerDocument[]; source: "sample" | "file" }; let config: Config = await readConfig(); let documents: BuyerDocument[] = []; let loadError = ""; let dataSource: "sample" | "file" = "sample"; try { const loaded = await loadDocuments(config.jsonPath); documents = loaded.documents; dataSource = loaded.source; } catch (error) { loadError = errorMessage(error); console.error(`[BizMatch QC] Initial data load failed: ${loadError}`); documents = validateDocuments(SAMPLE_DOCUMENTS); dataSource = "sample"; } function errorMessage(error: unknown): string { return error instanceof Error ? error.message : String(error); } async function readConfig(): Promise { try { return { jsonPath: "", pdfBaseDirectory: "", ...JSON.parse(await Deno.readTextFile(CONFIG_PATH)), }; } catch { return { jsonPath: "", pdfBaseDirectory: "" }; } } async function loadDocuments(jsonPath: string): Promise { if (!jsonPath.trim()) { return { documents: validateDocuments(SAMPLE_DOCUMENTS), source: "sample" }; } let raw: string; try { raw = await Deno.readTextFile(jsonPath); } catch (error) { throw new Error(`Cannot read JSON file "${jsonPath}": ${errorMessage(error)}`); } let parsed: unknown; try { parsed = JSON.parse(raw); } catch (error) { throw new Error(`Invalid JSON in "${jsonPath}": ${errorMessage(error)}`); } try { return { documents: validateDocuments(parsed), source: "file" }; } catch (error) { throw new Error(`JSON validation failed for "${jsonPath}": ${errorMessage(error)}`); } } async function saveConfig(next: Config): Promise { const normalized: Config = { jsonPath: next.jsonPath.trim(), pdfBaseDirectory: next.pdfBaseDirectory.trim(), }; // Load first. Never replace valid in-memory data with an empty list on failure. const loaded = await loadDocuments(normalized.jsonPath); if (normalized.pdfBaseDirectory) { try { const stat = await Deno.stat(normalized.pdfBaseDirectory); if (!stat.isDirectory) throw new Error("Path exists but is not a directory."); } catch (error) { throw new Error(`Cannot access PDF base directory "${normalized.pdfBaseDirectory}": ${errorMessage(error)}`); } } await Deno.writeTextFile(CONFIG_PATH, JSON.stringify(normalized, null, 2)); config = normalized; documents = loaded.documents; dataSource = loaded.source; loadError = ""; console.log(`[BizMatch QC] Loaded ${documents.length} documents from ${dataSource === "sample" ? "embedded sample data" : normalized.jsonPath}.`); if (normalized.pdfBaseDirectory) { console.log(`[BizMatch QC] PDF base directory: ${normalized.pdfBaseDirectory}`); } } function json(data: unknown, status = 200) { return Response.json(data, { status, headers: { "cache-control": "no-store" }, }); } function staticFile(pathname: string): Response { const files: Record = { "/": { body: INDEX_HTML, type: "text/html; charset=utf-8" }, "/index.html": { body: INDEX_HTML, type: "text/html; charset=utf-8" }, "/app.js": { body: APP_JS, type: "text/javascript; charset=utf-8" }, "/styles.css": { body: STYLES_CSS, type: "text/css; charset=utf-8" }, }; const file = files[pathname]; if (!file) return new Response("Not found", { status: 404 }); return new Response(file.body, { headers: { "content-type": file.type, "cache-control": "no-store" }, }); } Deno.serve(async (request) => { const url = new URL(request.url); if (url.pathname === "/api/state" && request.method === "GET") { return json({ config, documents, loadError, dataSource }); } if (url.pathname === "/api/config" && request.method === "POST") { try { const body = await request.json() as Config; await saveConfig({ jsonPath: body.jsonPath ?? "", pdfBaseDirectory: body.pdfBaseDirectory ?? "", }); return json({ ok: true, count: documents.length, dataSource }); } catch (error) { const message = errorMessage(error); loadError = message; console.error(`[BizMatch QC] Settings rejected: ${message}`); return json({ error: message }, 400); } } if (url.pathname === "/api/pdf" && request.method === "GET") { const index = Number(url.searchParams.get("index")); const doc = documents[index]; if (!Number.isInteger(index) || !doc) { return json({ error: "Unknown document index." }, 404); } try { const path = resolvePdfPath(config.pdfBaseDirectory, doc); const stat = await Deno.stat(path); if (!stat.isFile) throw new Error("Resolved path is not a file."); const file = await Deno.open(path, { read: true }); return new Response(file.readable, { headers: { "content-type": "application/pdf", "content-length": String(stat.size), "content-disposition": `inline; filename*=UTF-8''${encodeURIComponent(doc.file_name)}`, "cache-control": "no-store", }, }); } catch (error) { const message = errorMessage(error); console.error(`[BizMatch QC] PDF open failed for "${doc.file_name}": ${message}`); return json({ error: message }, 404); } } return staticFile(url.pathname); }); console.log(`[BizMatch QC] Listening on http://127.0.0.1:42469/`); console.log(`[BizMatch QC] Config file: ${CONFIG_PATH}`); console.log(`[BizMatch QC] Current data source: ${dataSource === "sample" ? "embedded sample data" : config.jsonPath}`); console.log(`[BizMatch QC] Documents loaded: ${documents.length}`); if (loadError) console.error(`[BizMatch QC] ${loadError}`); const win = new Deno.BrowserWindow({ title: "BizMatch QC", width: 1500, height: 950, }); win.show();