import { dirname } from "@std/path"; import { validateDocuments } from "./src/data.ts"; import { resolvePdfPath } from "./src/paths.ts"; import type { BuyerDocument } from "./src/types.ts"; import { type AppSettings, clampWindowSize, DEFAULTS, readLegacyConfig, resolveCacheDir, resolveSettingsPath, saveSettings as saveAppSettings, } from "./src/settings.ts"; import { cacheOrGetPdf } from "./src/pdf_cache.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 PREFIX = "[BizMatch QC]"; let settings: AppSettings = await initializeSettings(); let documents: BuyerDocument[] = []; let loadError = ""; let dataSource: "sample" | "file" = "sample"; try { const loaded = await loadDocumentsForMode(); documents = loaded.documents; dataSource = loaded.source; } catch (error) { loadError = errorMessage(error); console.error(`${PREFIX} Initial data load failed: ${loadError}`); documents = validateDocuments(SAMPLE_DOCUMENTS); dataSource = "sample"; } async function initializeSettings(): Promise { const path = resolveSettingsPath(); console.log(`${PREFIX} Settings path: ${path}`); let result: AppSettings; let fileExisted = false; try { const content = await Deno.readTextFile(path); result = JSON.parse(content) as unknown as AppSettings; fileExisted = true; } catch { result = { ...DEFAULTS }; } const validated = { ...DEFAULTS }; if ( fileExisted && result && typeof result === "object" && !Array.isArray(result) ) { const obj = result as unknown as Record; if (typeof obj.jsonPath === "string") validated.jsonPath = obj.jsonPath; if (typeof obj.pdfBaseDirectory === "string") { validated.pdfBaseDirectory = obj.pdfBaseDirectory; } if (typeof obj.useAnonymousData === "boolean") { validated.useAnonymousData = obj.useAnonymousData; } if ( typeof obj.windowWidth === "number" && !isNaN(obj.windowWidth) && obj.windowWidth >= 1100 ) { validated.windowWidth = obj.windowWidth; } if ( typeof obj.windowHeight === "number" && !isNaN(obj.windowHeight) && obj.windowHeight >= 700 ) { validated.windowHeight = obj.windowHeight; } console.log(`${PREFIX} Settings loaded successfully.`); } else { console.log(`${PREFIX} No existing settings file found. Using defaults.`); } if (!fileExisted) { const legacy = await readLegacyConfig(); if (legacy) { validated.jsonPath = validated.jsonPath || legacy.jsonPath; validated.pdfBaseDirectory = validated.pdfBaseDirectory || legacy.pdfBaseDirectory; await saveAppSettings(validated); } } return validated; } function errorMessage(error: unknown): string { return error instanceof Error ? error.message : String(error); } async function loadDocumentsFromFile( jsonPath: string, ): Promise<{ documents: BuyerDocument[]; source: "file" }> { 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 loadDocumentsForMode(): Promise< { documents: BuyerDocument[]; source: "sample" | "file" } > { if (settings.useAnonymousData) { return { documents: validateDocuments(SAMPLE_DOCUMENTS), source: "sample" }; } if (!settings.jsonPath.trim()) { return { documents: validateDocuments(SAMPLE_DOCUMENTS), source: "sample" }; } return await loadDocumentsFromFile(settings.jsonPath); } async function applySettings( next: { jsonPath?: string; pdfBaseDirectory?: string; useAnonymousData?: boolean; }, ): Promise { const newSettings: AppSettings = { ...settings, jsonPath: next.jsonPath !== undefined ? next.jsonPath.trim() : settings.jsonPath, pdfBaseDirectory: next.pdfBaseDirectory !== undefined ? next.pdfBaseDirectory.trim() : settings.pdfBaseDirectory, useAnonymousData: next.useAnonymousData !== undefined ? next.useAnonymousData : settings.useAnonymousData, }; if (newSettings.useAnonymousData) { const docs = validateDocuments(SAMPLE_DOCUMENTS); settings = newSettings; documents = docs; dataSource = "sample"; loadError = ""; await saveAppSettings(settings); console.log( `${PREFIX} Switched to anonymized sample data (${docs.length} documents).`, ); return; } if (!newSettings.jsonPath.trim()) { settings = newSettings; documents = validateDocuments(SAMPLE_DOCUMENTS); dataSource = "sample"; loadError = ""; await saveAppSettings(settings); console.log(`${PREFIX} No JSON path configured; using sample data.`); return; } const loaded = await loadDocumentsFromFile(newSettings.jsonPath); if (newSettings.pdfBaseDirectory) { try { const stat = await Deno.stat(newSettings.pdfBaseDirectory); if (!stat.isDirectory) { throw new Error("Path exists but is not a directory."); } } catch (error) { throw new Error( `Cannot access PDF base directory "${newSettings.pdfBaseDirectory}": ${ errorMessage(error) }`, ); } } await saveAppSettings(newSettings); settings = newSettings; documents = loaded.documents; dataSource = loaded.source; loadError = ""; console.log( `${PREFIX} Loaded ${documents.length} documents from ${settings.jsonPath}.`, ); if (settings.pdfBaseDirectory) { console.log(`${PREFIX} PDF base directory: ${settings.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" }, }); } function syncSaveSettings() { try { const path = resolveSettingsPath(); Deno.mkdirSync(dirname(path), { recursive: true }); Deno.writeTextFileSync(path, JSON.stringify(settings, null, 2)); console.log( `${PREFIX} Window size saved: ${settings.windowWidth}x${settings.windowHeight}`, ); } catch (err) { console.error( `${PREFIX} Failed to save settings on close: ${errorMessage(err)}`, ); } } Deno.serve(async (request) => { const url = new URL(request.url); if (url.pathname === "/api/state" && request.method === "GET") { const configForClient = { jsonPath: settings.jsonPath, pdfBaseDirectory: settings.pdfBaseDirectory, useAnonymousData: settings.useAnonymousData, }; return json({ config: configForClient, documents, loadError, dataSource }); } if (url.pathname === "/api/config" && request.method === "POST") { try { interface ConfigBody { jsonPath?: string; pdfBaseDirectory?: string; useAnonymousData?: boolean; } const body = await request.json() as ConfigBody; await applySettings({ jsonPath: body.jsonPath ?? settings.jsonPath, pdfBaseDirectory: body.pdfBaseDirectory ?? settings.pdfBaseDirectory, useAnonymousData: body.useAnonymousData ?? settings.useAnonymousData, }); return json({ ok: true, count: documents.length, dataSource }); } catch (error) { const message = errorMessage(error); loadError = message; console.error(`${PREFIX} Settings rejected: ${message}`); return json({ error: message }, 400); } } if (url.pathname === "/api/window-size" && request.method === "POST") { try { const body = await request.json() as { width?: number; height?: number }; if (typeof body.width === "number" && typeof body.height === "number") { settings.windowWidth = Math.max(1100, Math.round(body.width)); settings.windowHeight = Math.max(700, Math.round(body.height)); await saveAppSettings(settings); console.log( `${PREFIX} Window size saved (fallback): ${settings.windowWidth}x${settings.windowHeight}`, ); } return json({ ok: true }); } catch { return json({ ok: true }); } } 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 sourcePath = resolvePdfPath(settings.pdfBaseDirectory, doc); const startTotal = performance.now(); const cacheResult = await cacheOrGetPdf(sourcePath); const cacheLookupMs = Math.round(performance.now() - startTotal); let fileSize: number; try { fileSize = (await Deno.stat(cacheResult.path)).size; } catch { throw new Error(`Cached file is not readable: "${cacheResult.path}"`); } const rangeHeader = request.headers.get("range"); if (rangeHeader) { const match = rangeHeader.match(/^bytes=(\d+)-(\d*)$/); if (match) { const rangeStart = parseInt(match[1], 10); const rangeEnd = match[2] ? parseInt(match[2], 10) : fileSize - 1; const length = rangeEnd - rangeStart + 1; if ( rangeStart < 0 || rangeEnd >= fileSize || rangeStart > rangeEnd ) { return new Response(null, { status: 416, headers: { "content-range": `bytes */${fileSize}`, }, }); } const file = await Deno.open(cacheResult.path, { read: true }); await file.seek(rangeStart, Deno.SeekMode.Start); const buf = new Uint8Array(length); let bytesRead = 0; while (bytesRead < length) { const n = await file.read(buf.subarray(bytesRead)); if (n === null) break; bytesRead += n; } file.close(); const elapsed = Math.round(performance.now() - startTotal); console.log( `${PREFIX} PDF served (range ${rangeStart}-${rangeEnd}/${fileSize}): ${elapsed}ms, cache_lookup=${cacheLookupMs}ms`, ); return new Response(buf.slice(0, bytesRead), { status: 206, headers: { "content-type": "application/pdf", "content-length": String(bytesRead), "content-range": `bytes ${rangeStart}-${ rangeStart + bytesRead - 1 }/${fileSize}`, "accept-ranges": "bytes", "content-disposition": `inline; filename*=UTF-8''${ encodeURIComponent(doc.file_name) }`, ...(cacheResult.stale ? { "x-bizmatch-cache-status": "stale" } : {}), }, }); } } const file = await Deno.open(cacheResult.path, { read: true }); const elapsed = Math.round(performance.now() - startTotal); console.log( `${PREFIX} PDF served (full ${fileSize}B): ${elapsed}ms, cache_lookup=${cacheLookupMs}ms`, ); return new Response(file.readable, { headers: { "content-type": "application/pdf", "content-length": String(fileSize), "accept-ranges": "bytes", "content-disposition": `inline; filename*=UTF-8''${ encodeURIComponent(doc.file_name) }`, ...(cacheResult.stale ? { "x-bizmatch-cache-status": "stale" } : {}), }, }); } catch (error) { const message = errorMessage(error); console.error( `${PREFIX} PDF open failed for "${doc.file_name}": ${message}`, ); return json({ error: message }, 404); } } return staticFile(url.pathname); }); console.log(`${PREFIX} Listening on http://127.0.0.1:42469/`); console.log(`${PREFIX} Settings path: ${resolveSettingsPath()}`); console.log(`${PREFIX} PDF cache directory: ${resolveCacheDir()}`); console.log( `${PREFIX} Data mode: ${ settings.useAnonymousData ? "anonymized sample" : (settings.jsonPath ? `file (${settings.jsonPath})` : "embedded sample") }`, ); console.log(`${PREFIX} Documents loaded: ${documents.length}`); if (loadError) console.error(`${PREFIX} ${loadError}`); const clamped = clampWindowSize(settings); console.log( `${PREFIX} Restoring window size: ${clamped.windowWidth}x${clamped.windowHeight}`, ); // Deno Desktop provides Deno.BrowserWindow at runtime but the standard // Deno 2.9 type checker does not include desktop type declarations. // The runtime `deno desktop` command also types this as a generic // BrowserWindow without width/height properties. // We use a local type assertion to narrow to the exact API we use. // Remove this cast when official desktop types become available. type _Win = { readonly width: number; readonly height: number; onresize: ((event: Event) => void) | null; onclose: ((event: Event) => void) | null; show(): void; }; const _WinCtor = (Deno as unknown as { BrowserWindow: new ( opts?: { title?: string; width?: number; height?: number }, ) => _Win; }).BrowserWindow; const win = new _WinCtor({ title: "BizMatch QC", width: clamped.windowWidth, height: clamped.windowHeight, }); console.log( `${PREFIX} Window initialized: ${clamped.windowWidth}x${clamped.windowHeight}`, ); // Native resize tracking let resizeTimer: ReturnType | undefined; try { win.onresize = () => { if (resizeTimer !== undefined) clearTimeout(resizeTimer); resizeTimer = setTimeout(() => { const w = win.width; const h = win.height; if (w > 0 && h > 0) { settings.windowWidth = w; settings.windowHeight = h; console.log(`${PREFIX} Window resized: ${w}x${h}`); saveAppSettings(settings).catch((err) => console.error( `${PREFIX} Failed to save window size: ${errorMessage(err)}`, ) ); } }, 500); }; win.onclose = () => { if (resizeTimer !== undefined) clearTimeout(resizeTimer); const w = win.width; const h = win.height; if (w > 0 && h > 0) { settings.windowWidth = w; settings.windowHeight = h; syncSaveSettings(); } }; } catch (err) { console.warn( `${PREFIX} Native window-event binding unavailable: ${errorMessage(err)}`, ); console.warn(`${PREFIX} Falling back to web-page resize tracking.`); } win.show();