init
This commit is contained in:
188
main.ts
Normal file
188
main.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
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<Config> {
|
||||
try {
|
||||
return {
|
||||
jsonPath: "",
|
||||
pdfBaseDirectory: "",
|
||||
...JSON.parse(await Deno.readTextFile(CONFIG_PATH)),
|
||||
};
|
||||
} catch {
|
||||
return { jsonPath: "", pdfBaseDirectory: "" };
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDocuments(jsonPath: string): Promise<LoadResult> {
|
||||
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<void> {
|
||||
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<string, { body: string; type: string }> = {
|
||||
"/": { 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();
|
||||
Reference in New Issue
Block a user