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 PDF_VIEWER_JS from "./web/pdf_viewer.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" }, }); } // ---- PDF token system ---- // A token stores a resolved cache path so that Range requests // (which PDF.js makes many of) hit the local disk cache directly // without re-stat'ing or re-validating against NAS for each one. interface PdfToken { cachePath: string; sourcePath: string; fileSize: number; stale: boolean; createdAt: number; } const pdfTokens = new Map(); const TOKEN_TTL_MS = 30 * 60 * 1000; // 30 minutes function cleanupTokens() { const now = Date.now(); for (const [token, entry] of pdfTokens) { if (now - entry.createdAt > TOKEN_TTL_MS) pdfTokens.delete(token); } } // Periodic cleanup every 10 minutes setInterval(cleanupTokens, 10 * 60 * 1000); async function servePdfBytes( cachePath: string, fileSize: number, rangeHeader: string | null, elapsedStart: number, stale: boolean, ): Promise { 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(cachePath, { 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() - elapsedStart); console.log( `${PREFIX} PDF range served from local cache: bytes ${rangeStart}-${ rangeStart + bytesRead - 1 }/${fileSize} in ${elapsed}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", ...(stale ? { "x-bizmatch-cache-status": "stale" } : {}), }, }); } } const file = await Deno.open(cachePath, { read: true }); const elapsed = Math.round(performance.now() - elapsedStart); console.log( `${PREFIX} PDF full served from local cache (${fileSize}B) in ${elapsed}ms`, ); return new Response(file.readable, { headers: { "content-type": "application/pdf", "content-length": String(fileSize), "accept-ranges": "bytes", ...(stale ? { "x-bizmatch-cache-status": "stale" } : {}), }, }); } // ------- HTTP Server ------- 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); } } // ---- Client-side log forwarding ---- if (url.pathname === "/api/client-log" && request.method === "POST") { try { const body = await request.json() as { level?: string; message?: string; data?: unknown; }; const level = body.level || "info"; const prefix = `${PREFIX} Client`; if (level === "error") { console.error( `${prefix} [ERROR] ${body.message || ""}`, body.data || "", ); } else if (level === "warn") { console.warn(`${prefix} [WARN] ${body.message || ""}`, body.data || ""); } else { console.log(`${prefix} ${body.message || ""}`, body.data || ""); } } catch { /* ignore */ } return new Response(null, { status: 204 }); } // ---- PDF prepare endpoint ---- if (url.pathname === "/api/pdf/prepare" && request.method === "POST") { try { const body = await request.json() as { index?: number; requestId?: string; }; const index = body.index; const doc = documents[index!]; if (!Number.isInteger(index) || !doc) { return json({ error: "Unknown document index." }, 404); } const sourcePath = resolvePdfPath(settings.pdfBaseDirectory, doc); const relative = doc._letter + "/" + doc.file_name; const reqTag = body.requestId ? `PDF ${body.requestId}` : "PDF"; console.log(`${PREFIX} ${reqTag} prepare requested: ${relative}`); const startTime = performance.now(); const cacheResult = await cacheOrGetPdf(sourcePath); // Validate cached file let fileSize: number; try { fileSize = (await Deno.stat(cacheResult.path)).size; const f = await Deno.open(cacheResult.path, { read: true }); const head = new Uint8Array(5); let n = 0; while (n < 5) { const r = await f.read(head.subarray(n)); if (r === null) break; n += r; } f.close(); const header = new TextDecoder().decode(head.slice(0, n)); if (header !== "%PDF-") { console.warn( `${PREFIX} Cached PDF invalid header: "${header}", re-caching`, ); await Deno.remove(cacheResult.path).catch(() => {}); const fresh = await cacheOrGetPdf(sourcePath); fileSize = (await Deno.stat(fresh.path)).size; } } catch { throw new Error("Cached file is not readable."); } const token = crypto.randomUUID(); pdfTokens.set(token, { cachePath: cacheResult.path, sourcePath, fileSize, stale: cacheResult.stale, createdAt: Date.now(), }); const elapsed = Math.round(performance.now() - startTime); const status = cacheResult.stale ? "refresh" : "hit"; console.log( `${PREFIX} ${reqTag} disk cache ${status} in ${elapsed}ms, token created`, ); return json({ url: `/api/pdf/content/${token}`, size: fileSize, cacheStatus: status, }); } catch (error) { const message = errorMessage(error); console.error(`${PREFIX} PDF prepare failed: ${message}`); return json({ error: message }, 404); } } // ---- PDF content endpoint (range-aware, cache-only) ---- if ( url.pathname.startsWith("/api/pdf/content/") && request.method === "GET" ) { const token = url.pathname.slice("/api/pdf/content/".length); const entry = pdfTokens.get(token); if (!entry) { return json({ error: "Invalid or expired PDF token." }, 404); } const startTime = performance.now(); try { return await servePdfBytes( entry.cachePath, entry.fileSize, request.headers.get("range"), startTime, entry.stale, ); } catch (error) { const message = errorMessage(error); console.error(`${PREFIX} PDF content serve failed: ${message}`); return json({ error: message }, 500); } } // ---- HEAD request for pdf content ---- if ( url.pathname.startsWith("/api/pdf/content/") && request.method === "HEAD" ) { const token = url.pathname.slice("/api/pdf/content/".length); const entry = pdfTokens.get(token); if (!entry) { return new Response(null, { status: 404 }); } return new Response(null, { status: 200, headers: { "content-type": "application/pdf", "content-length": String(entry.fileSize), "accept-ranges": "bytes", }, }); } // ---- Static files ---- const staticFiles: 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" }, "/pdf_viewer.js": { body: PDF_VIEWER_JS, type: "text/javascript; charset=utf-8", }, "/styles.css": { body: STYLES_CSS, type: "text/css; charset=utf-8" }, }; const file = staticFiles[url.pathname]; if (file) { return new Response(file.body, { headers: { "content-type": file.type, "cache-control": "no-store" }, }); } return new Response("Not found", { status: 404 }); }); // ------- Startup logging ------- 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}`); // ------- Window management ------- const clamped = clampWindowSize(settings); console.log( `${PREFIX} Settings window size loaded: ${clamped.windowWidth}x${clamped.windowHeight}`, ); console.log( `${PREFIX} Creating main window with: ${clamped.windowWidth}x${clamped.windowHeight}`, ); // Deno 2.9 Desktop API: use BrowserWindow constructor with width/height, // then verify with getSize() and apply setSize() if needed. type _Win = { getSize(): [number, number]; setSize(w: number, h: number): void; addEventListener( type: string, listener: (event: CustomEvent) => void, ): void; 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, }); const [actualW, actualH] = win.getSize(); console.log( `${PREFIX} Main window actual size after construction: ${actualW}x${actualH}`, ); if ( actualW !== clamped.windowWidth || actualH !== clamped.windowHeight ) { console.log( `${PREFIX} Window size differs from requested, applying setSize(${clamped.windowWidth}, ${clamped.windowHeight})`, ); win.setSize(clamped.windowWidth, clamped.windowHeight); const [w2, h2] = win.getSize(); console.log(`${PREFIX} After setSize: ${w2}x${h2}`); } // Native resize tracking using Deno 2.9 BrowserWindow events let resizeTimer: ReturnType | undefined; try { win.addEventListener( "resize", ((e: CustomEvent) => { if (resizeTimer !== undefined) clearTimeout(resizeTimer); // Store immediately but debounce writes const w = e.detail?.width ?? 0; const h = e.detail?.height ?? 0; if (w >= 1100 && h >= 700) { settings.windowWidth = w; settings.windowHeight = h; } resizeTimer = setTimeout(() => { if (settings.windowWidth >= 1100 && settings.windowHeight >= 700) { console.log( `${PREFIX} Window resized: ${settings.windowWidth}x${settings.windowHeight}`, ); saveAppSettings(settings).catch((err) => console.error( `${PREFIX} Failed to save window size: ${errorMessage(err)}`, ) ); } }, 500); }) as (e: Event) => void, ); win.addEventListener( "close", (() => { if (resizeTimer !== undefined) clearTimeout(resizeTimer); const [fw, fh] = win.getSize(); if (fw >= 1100 && fh >= 700) { settings.windowWidth = fw; settings.windowHeight = fh; } try { const path = resolveSettingsPath(); Deno.mkdirSync(dirname(path), { recursive: true }); Deno.writeTextFileSync(path, JSON.stringify(settings, null, 2)); console.log( `${PREFIX} Window size saved on close: ${settings.windowWidth}x${settings.windowHeight}`, ); } catch (err) { console.error( `${PREFIX} Failed to save settings on close: ${errorMessage(err)}`, ); } }) as (e: Event) => void, ); } catch (err) { console.warn( `${PREFIX} Native window-event binding unavailable: ${errorMessage(err)}`, ); } win.show();