// server.js import fs from "fs"; import path from "path"; import crypto from "crypto"; import fetch from "node-fetch"; import express from "express"; import bodyParser from "body-parser"; import PDFDocument from "pdfkit"; import sharp from "sharp"; import moment from "moment-timezone"; import cors from "cors"; // Configuration (all from environment variables) const BASE_URL = process.env.BASE_URL || "https://rakshitjan-cps-b2c.hf.space"; const ADMIN_EMAIL = process.env.ADMIN_EMAIL || "admin123@example.com"; const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || "SecurePass123"; const AES_KEY_STR = process.env.AES_KEY_STR || "7E892875A52C59A3B588306B13C31FBD"; const AES_IV_STR = process.env.AES_IV_STR || "XYE45DKJ0967GFAZ"; const GOOGLE_MAPS_API_KEY = process.env.GOOGLE_MAPS_API_KEY || "AIzaSyBpna0F0J-c9QNBh8lSnfT2Z2uzD_vS9Ag"; // For geocoding const GEOAPIFY_API_KEY = process.env.GEOAPIFY_API_KEY || "ceaea8a8a8f14f46b73221384eb4b5f0" ; // For static maps const LOGO_PATH = process.env.LOGO_PATH || "logo.png"; const statelist = JSON.parse(fs.readFileSync("state.json", "utf8")); const citylist = JSON.parse(fs.readFileSync("city.json", "utf8")); const districtlist = JSON.parse(fs.readFileSync("district.json", "utf8")); const organizationlist = JSON.parse(fs.readFileSync("organization.json", "utf8")); const genderlist = JSON.parse(fs.readFileSync("gender.json", "utf8")); const qualificationlist = JSON.parse(fs.readFileSync("qualification.json", "utf8")); const ownershiplist = JSON.parse(fs.readFileSync("ownership.json", "utf8")); const app = express(); app.use(bodyParser.json({ limit: "50mb" })); // Allow ALL origins app.use(cors()); // Utility: convert cm to PDF points (1 pt = 1/72 in; 1 cm = 28.3464567 pt) const CM = 28.3464567; const A4_WIDTH = 595.2755905511812; // points (210mm) const A4_HEIGHT = 841.8897637795277; // points (297mm) const MARGIN = 1.2 * CM; // AES encryption like Python's Crypto.Cipher AES-CBC w/ PKCS7 padding function aesEncrypt(plaintext, keyStr, ivStr) { // Validate inputs if (!plaintext) throw new Error("Plaintext is required for encryption"); if (!keyStr) throw new Error("AES key is required"); if (!ivStr) throw new Error("AES IV is required"); const key = Buffer.from(keyStr, "utf8"); const iv = Buffer.from(ivStr, "utf8"); // Validate key and IV lengths if (key.length !== 16 && key.length !== 24 && key.length !== 32) { throw new Error(`Invalid AES key length: ${key.length} bytes. Must be 16, 24, or 32 bytes.`); } // Node's createCipheriv uses PKCS#7 padding by default (called PKCS#5 in OpenSSL) const cipher = crypto.createCipheriv(`aes-${key.length * 8}-cbc`, key, iv); const encrypted = Buffer.concat([ cipher.update(Buffer.from(plaintext, "utf8")), cipher.final() ]); return encrypted.toString("base64"); } // HTTP session mimic (we reuse fetch) let token = null; async function adminLogin() { try { // encrypt email same as python const encryptedEmail = aesEncrypt(ADMIN_EMAIL, AES_KEY_STR, AES_IV_STR); const payload = { email: encryptedEmail, password: ADMIN_PASSWORD }; console.log(`Logging in to ${BASE_URL} with admin email...`); const res = await fetch(`${BASE_URL}/api/admin/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), timeout: 30000 }); const data = await res.json(); if (res.status !== 200 || !data.token) { console.error(`Login failed. Status: ${res.status}, Response:`, data); throw new Error(`Login failed: ${JSON.stringify(data)}`); } token = data.token; console.log("Admin login successful"); return token; } catch (error) { console.error("Admin login error:", error.message); throw error; } } async function getCase(caseId) { if (!token) await adminLogin(); console.log(`Fetching case ${caseId}...`); const res = await fetch(`${BASE_URL}/api/cases/${caseId}`, { headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` }, timeout: 30000 }); if (!res.ok) { console.error(`Failed to get case ${caseId}: ${res.status} ${res.statusText}`); throw new Error(`Failed get_case: ${res.status}`); } return await res.json(); } async function getFiles(caseId) { if (!token) await adminLogin(); console.log(`Fetching files for case ${caseId}...`); const res = await fetch(`${BASE_URL}/api/cases/${caseId}/files`, { headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` }, timeout: 60000 }); if (!res.ok) { console.error(`Failed to get files for case ${caseId}: ${res.status} ${res.statusText}`); throw new Error(`Failed get_files: ${res.status}`); } const json = await res.json(); console.log(`Retrieved ${json.files?.length || 0} files for case ${caseId}`); return json.files || []; } async function downloadFile(fileId) { if (!token) await adminLogin(); console.log(`Downloading file ${fileId}...`); const res = await fetch(`${BASE_URL}/api/files/${fileId}`, { headers: { "Authorization": `Bearer ${token}` }, timeout: 60000 }); if (!res.ok) { console.error(`File download failed for ${fileId}: ${res.status} ${res.statusText}`); throw new Error(`File download failed for ${fileId}: ${res.status}`); } const buf = await res.arrayBuffer(); return Buffer.from(buf); } // Google Maps Geocoding API helpers (for forward/reverse geocoding) async function forwardGeocode(address) { if (!address) { console.log("No address provided for geocoding"); return [null, null]; } if (!GOOGLE_MAPS_API_KEY) { console.error("GOOGLE_MAPS_API_KEY is not set"); return [null, null]; } console.log(`Geocoding address: ${address.substring(0, 50)}...`); try { const url = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(address)}&key=${GOOGLE_MAPS_API_KEY}`; const res = await fetch(url, { timeout: 20000 }); if (!res.ok) { console.error(`Geocoding API error: ${res.status} ${res.statusText}`); return [null, null]; } const json = await res.json(); if (json.status === "OK" && json.results && json.results[0] && json.results[0].geometry && json.results[0].geometry.location) { const loc = json.results[0].geometry.location; console.log(`Geocoding successful: ${loc.lat}, ${loc.lng}`); return [loc.lat, loc.lng]; } else { console.warn(`Geocoding failed: ${json.status} - ${json.error_message || 'No results'}`); return [null, null]; } } catch (error) { console.error("Geocoding error:", error.message); return [null, null]; } } async function reverseGeocode(lat, lon) { if (lat == null || lon == null) { console.log("No coordinates provided for reverse geocoding"); return "Unknown Address"; } if (!GOOGLE_MAPS_API_KEY) { console.error("GOOGLE_MAPS_API_KEY is not set"); return "Unknown Address"; } console.log(`Reverse geocoding coordinates: ${lat}, ${lon}`); try { const url = `https://maps.googleapis.com/maps/api/geocode/json?latlng=${lat},${lon}&key=${GOOGLE_MAPS_API_KEY}`; const res = await fetch(url, { timeout: 20000 }); if (!res.ok) { console.error(`Reverse geocoding API error: ${res.status} ${res.statusText}`); return "Unknown Address"; } const json = await res.json(); if (json.status === "OK" && json.results && json.results[0] && json.results[0].formatted_address) { const address = json.results[0].formatted_address; console.log(`Reverse geocoding successful: ${address.substring(0, 50)}...`); return address; } else { console.warn(`Reverse geocoding failed: ${json.status} - ${json.error_message || 'No results'}`); return "Unknown Address"; } } catch (error) { console.error("Reverse geocoding error:", error.message); return "Unknown Address"; } } // Geoapify Static Maps API (for static maps with markers and path) async function staticMapWithPath(captureLat, captureLng, caseLat = null, caseLng = null, zoom = 17, size = "1800x600") { if (captureLat == null || captureLng == null) { console.log("No capture coordinates provided for static map"); return null; } if (!GEOAPIFY_API_KEY) { console.error("GEOAPIFY_API_KEY is not set"); return null; } console.log(`Generating static map for: ${captureLat}, ${captureLng}`); try { let parts = [ `center=lonlat:${captureLng},${captureLat}`, `zoom=${zoom}`, `size=${size}`, `scaleFactor=2`, `marker=lonlat:${captureLng},${captureLat};color:%23ff0000;size:large` ]; if (caseLat != null && caseLng != null) { parts.push(`marker=lonlat:${caseLng},${caseLat};color:%23007bff;size:large`); parts.push(`path=color:%23ff0000;weight:4|lonlat:${caseLng},${caseLat}|lonlat:${captureLng},${captureLat}`); } const url = `https://maps.geoapify.com/v1/staticmap?${parts.join("&")}&apiKey=${GEOAPIFY_API_KEY}`; const res = await fetch(url, { timeout: 30000 }); if (!res.ok) { console.error(`Static map API error: ${res.status} ${res.statusText}`); return null; } const buf = Buffer.from(await res.arrayBuffer()); console.log("Static map generated successfully"); return buf; } catch (error) { console.error("Static map generation error:", error.message); return null; } } function haversineKm(lat1, lon1, lat2, lon2) { if ([lat1, lon1, lat2, lon2].some(v => v == null)) return null; const R = 6371.0; const phi1 = lat1 * Math.PI / 180; const phi2 = lat2 * Math.PI / 180; const dphi = (lat2 - lat1) * Math.PI / 180; const dlambda = (lon2 - lon1) * Math.PI / 180; const a = Math.sin(dphi / 2) * Math.sin(dphi / 2) + Math.cos(phi1) * Math.cos(phi2) * Math.sin(dlambda / 2) * Math.sin(dlambda / 2); return 2 * R * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); } // date formatting to IST similar to fmt_ist in python function fmtIST(dtStr) { if (!dtStr) return "N/A"; try { // Accept Z or offset const m = moment(dtStr); const ist = m.tz("Asia/Kolkata").format("DD/MM/YYYY hh:mm:ss A") + " IST"; return ist; } catch { return dtStr; } } function exportedOnStr() { return moment().tz("Asia/Kolkata").format("MMMM DD, YYYY"); } // Mapping display names const DISPLAY_NAME = { "aadhaar_photo": "Photo of Aadhar", "customer_photo": "Customer's Photo", "residence_photo": "Residence Photo", "business_photo": "Business Address Photo" }; function displayType(fileType) { return DISPLAY_NAME[fileType] || fileType || "Upload"; } // create thumbnail similar to Python make_thumb async function makeThumb(imgBuf, maxW = 860, maxH = 640) { try { // produce an RGB JPEG/PNG buffer sized with thumbnail const img = sharp(imgBuf).rotate().resize({ width: maxW, height: maxH, fit: "inside" }).flatten({ background: { r: 255, g: 255, b: 255 } }); const png = await img.png().toBuffer(); // create bordered image by extending canvas const bordered = await sharp(png) .extend({ top: 1, bottom: 1, left: 1, right: 1, background: { r: 170, g: 170, b: 170 } }) .png() .toBuffer(); return bordered; } catch (error) { console.error("Thumbnail creation error:", error.message); // Return original buffer if thumbnail creation fails return imgBuf; } } // text wrapping helper that attempts to match Python's draw_wrapped_label_value. // We measure text widths using doc.widthOfString and output lines manually at exact coordinates. function drawWrappedLabelValue(doc, x, y, label, text, maxWidthCm = 16.0) { const maxW = maxWidthCm * CM; const fontSize = 10; doc.font("Helvetica").fontSize(fontSize); const labelStr = `${label}: `; const labelW = doc.widthOfString(labelStr); const words = (text || "N/A").toString().split(/\s+/); let first = true; let line = ""; let width = 0; let lines = []; for (let w of words) { const w_w = doc.widthOfString(w + " "); if (first) { if (labelW + width + w_w < maxW) { line += w + " "; width += w_w; } else { lines.push(labelStr + line.trim()); first = false; line = w + " "; width = w_w; } } else { if (width + w_w < maxW) { line += w + " "; width += w_w; } else { // indent roughly by label_w/6 characters -> compute pixels const indentText = " ".repeat(Math.max(0, Math.floor(labelW / (fontSize * 0.6)))); lines.push(indentText + line.trim()); line = w + " "; width = w_w; } } } if (first) { lines.push(labelStr + (line.trim())); } else { if (line) { const indentText = " ".repeat(Math.max(0, Math.floor(labelW / (fontSize * 0.6)))); lines.push(indentText + line.trim()); } } for (let ln of lines) { doc.text(ln, x, y, { lineBreak: false }); y += 0.45 * CM; } return y; } // Draw header/footer helpers (matching positions & fonts from python) function drawHeader(doc, title) { // header blue rectangle at top: height 2.4 cm doc.save(); doc.rect(0, 0, A4_WIDTH, 2.4 * CM).fill("#0F61A8"); // logo let titleX = MARGIN; if (fs.existsSync(LOGO_PATH)) { try { doc.image(LOGO_PATH, MARGIN, 0.2 * CM, { width: 2.4 * CM, height: 2.0 * CM }); titleX = MARGIN + 2.6 * CM; } catch (e) { console.warn(`Could not load logo from ${LOGO_PATH}:`, e.message); } } doc.fillColor("white").font("Helvetica-Bold").fontSize(15).text(title, titleX, 0.9 * CM, { continued: false }); doc.font("Helvetica").fontSize(10); const exp = `Exported on ${exportedOnStr()}`; // draw right-aligned const w = doc.widthOfString(exp); doc.fillColor("white").text(exp, A4_WIDTH - MARGIN - w, 1.0 * CM); doc.fillColor("black"); doc.restore(); } function drawFooter(doc, page) { doc.font("Helvetica").fontSize(9).text(`Page ${page}`, A4_WIDTH - MARGIN - 50, A4_HEIGHT - 1.0 * CM); } // Build PDF function (mirrors build_pdf in Python) async function buildPdf(caseObj, files, outPath) { return new Promise(async (resolve, reject) => { try { console.log(`Building PDF for case ${caseObj.case_id} with ${files.length} files...`); const doc = new PDFDocument({ size: "A4", margin: 0 }); const ws = fs.createWriteStream(outPath); doc.pipe(ws); let page = 1; // prepare address and geocode const ad = ((caseObj.demographic_details || {}).address_details) || {}; const caseAddr = `${ad.residential_address || ""}, ${ad.city || ""}, ${ad.state || ""} ${ad.pincode || ""}`.trim(); console.log(`Case address: ${caseAddr}`); const [caseLat, caseLng] = await forwardGeocode(caseAddr); if (caseLat && caseLng) { console.log(`Case coordinates: ${caseLat}, ${caseLng}`); } const title = `Name : ${caseObj.case_applicant_name || ""}`; drawHeader(doc, title); let y = 3.4 * CM; // Customer Details section const customerstate = statelist.find(state => state.id === caseObj.residenceDetail.stateId); const customerdistrict = districtlist.find(district => district.id === caseObj.residenceDetail.districtId); const customercity = citylist.find(city => city.id === caseObj.residenceDetail.cityId); const customeraddress=caseObj.residenceDetail.address1 +", "+ caseObj.residenceDetail.address2 +", "+ caseObj.residenceDetail.address3 +", "+customercity.city_name +", "+ customerdistrict.district_name +", "+ customerstate.state_name +", "+ caseObj.residenceDetail.pincode; y = sectionTitle(doc, "Customer Details", MARGIN, y); labelValue(doc, MARGIN, y, "Customer Name", caseObj.case_applicant_name); y += 0.6 * CM; labelValue(doc, MARGIN, y, "Customer ID", caseObj.case_id); y += 0.6 * CM; labelValue(doc, MARGIN, y, "Phone Number", caseObj.case_applicant_contact); y += 0.6 * CM; //const emailId = (caseObj.applicantDetail.email || {}).contact_information && (caseObj.demographic_details.contact_information.email_id); labelValue(doc, MARGIN, y, "Email", caseObj.applicantDetail.email); y += 0.6 * CM; y = drawWrappedLabelValue(doc, MARGIN, y, "Customer Address", customeraddress, 18.0); y += 0.6 * CM; // Inspection Summary y = sectionTitle(doc, "Inspection Summary", MARGIN, y); doc.font("Helvetica").fontSize(10); doc.text(`Request Started: ${fmtIST(caseObj.created_at)}`, MARGIN, y); y += 0.5 * CM; doc.text(`Total Uploads: ${files.length} uploads`, MARGIN, y); y += 0.5 * CM; doc.text(`Status: ${caseObj.status}`, MARGIN, y); y += 0.5 * CM; doc.text(`Priority: ${caseObj.priority}`, MARGIN, y); y += 0.7 * CM; // render static map if caseLat present (attempt) if (caseLat) { console.log("Generating overview map..."); const mapBuf = await staticMapWithPath(caseLat, caseLng, caseLat, caseLng, 12); if (mapBuf) { // approximate map size used in python: map_h = H/2 + 2*cm const mapH = A4_HEIGHT / 2 + 2 * CM; const mapW = A4_WIDTH - 2 * MARGIN; doc.image(mapBuf, MARGIN, y, { width: mapW, height: mapH }); y += mapH + 0.8 * CM; } } drawFooter(doc, page); doc.addPage(); page += 1; // Page 2: thumbnails drawHeader(doc, title); y = 3.4 * CM; doc.font("Helvetica-Bold").fontSize(14).text("Image Thumbnails", MARGIN, y); y += 0.8 * CM; const x0 = MARGIN; const y0 = y; const colW = (A4_WIDTH - 2 * MARGIN) / 3; const rowH = 6.1 * CM; for (let i = 0; i < Math.min(6, files.length); ++i) { const f = files[i]; console.log(`Creating thumbnail for upload ${i+1} (${f.file_type})...`); const imgBuf = await makeThumb(f._bin); const col = i % 3; const row = Math.floor(i / 3); const xi = x0 + col * colW + 0.2 * CM; const yi = y0 + row * rowH + 4.6 * CM; // draw image within width colW-0.6cm and height 4 cm try { doc.image(imgBuf, xi, yi, { width: colW - 0.6 * CM, height: 4.0 * CM, align: "center", valign: "center" }); } catch (e) { console.warn(`Could not draw thumbnail ${i+1}:`, e.message); } doc.font("Helvetica").fontSize(9).text(`Upload ${i + 1} — ${displayType(f.file_type)}`, xi, yi - 0.3 * CM); } drawFooter(doc, page); doc.addPage(); page += 1; // Page 3: Demographic, Business, Financial drawHeader(doc, title); y = 3.4 * CM; y = sectionTitle(doc, "Demographic Details", MARGIN, y); const demo = caseObj.applicantDetail || {}; const ci = demo.contact_information || {}; const pd = demo.personal_details || {}; const gender = genderlist.find(gender => gender.id === demo.genderId); const qualification = qualificationlist.find(qualification => qualification.id === demo.qualification); labelValue(doc, MARGIN, y, "Aadhaar Photo Match", demo.aadhaar_photo_match); y += 0.5 * CM; labelValue(doc, MARGIN, y, "Email ID", demo.email); y += 0.5 * CM; labelValue(doc, MARGIN, y, "Mobile Number", demo.mobile); y += 0.5 * CM; labelValue(doc, MARGIN, y, "Gender", gender.gender_name); y += 0.5 * CM; labelValue(doc, MARGIN, y, "Education", qualification.qualification_name); y += 0.5 * CM; labelValue(doc, MARGIN, y, "Family Members", demo.numOfDependent); y += 1.0 * CM; y = sectionTitle(doc, "Business Details", MARGIN, y); const bd = caseObj.businessDetail || {}; const ei = bd.enterprise_information || {}; const bld = bd.business_location_details || {}; const ba = bd.business_address || {}; const state = statelist.find(state => state.id === bd.stateId); const district = districtlist.find(district => district.id === bd.districtId); const city = citylist.find(city => city.id === bd.cityId); const organization = organizationlist.find(organization => organization.id === bd.organizationType); //const city = citylist.find(city => city.id === bd.cityId); labelValue(doc, MARGIN, y, "Enterprise Name", bd.businessName); y += 0.5 * CM; labelValue(doc, MARGIN, y, "Organization Type", organization.organization_name); y += 0.5 * CM; labelValue(doc, MARGIN, y, "Business Location Type", bd.businessLocation); y += 0.5 * CM; labelValue(doc, MARGIN, y, "Address", `${bd.address1} ${bd.address2} ${bd.landmark }`); y += 0.5 * CM; labelValue(doc, MARGIN, y, "City/District/State/Pincode", `${city.city_name || ""}/ ${district.district_name || ""}/ ${state.state_name || ""} - ${bd.pincode || ""}`); y += 1.0 * CM; y = sectionTitle(doc, "Financial Details", MARGIN, y); const fin = caseObj.financial_details || {}; const bfi = fin.business_financial_information || {}; const ffi = fin.family_financial_information || {}; function fmtInr(v) { try { if (v == null || v === "" || isNaN(v)) return "N/A"; const amount = parseInt(v); // Format with Indian numbering system and ₹ symbol return `₹ ${amount.toLocaleString('en-IN')}`; } catch { return "N/A"; } } labelValue(doc, MARGIN, y, "Monthly Income", fmtInr(caseObj.incomeExpenditure.monthlyIncome)); y += 0.5 * CM; labelValue(doc, MARGIN, y, "Monthly Expense", fmtInr(caseObj.familyExpenses.totalExp)); y += 0.5 * CM; labelValue(doc, MARGIN, y, "Family Income", fmtInr(caseObj.incomeExpenditure.totalIncome)); y += 0.5 * CM; drawFooter(doc, page); doc.addPage(); page += 1; // For each file, a detailed page for (let idx = 0; idx < files.length; ++idx) { const f = files[idx]; console.log(`Processing detailed page for upload ${idx+1}...`); drawHeader(doc, title); y = 3.4 * CM; doc.font("Helvetica-Bold").fontSize(14).text(`Upload ${idx + 1} — ${displayType(f.file_type)}`, MARGIN, y); y += 0.8 * CM; // integrity_dashboard image insertion (if present) try { const integrityPath = "integrity_dashboard.png"; if (fs.existsSync(integrityPath)) { const imageWidth = 16 * CM; const imageHeight = 2.5 * CM; const centerX = (A4_WIDTH - imageWidth) / 2; doc.image(integrityPath, centerX, y, { width: imageWidth, height: imageHeight }); y = y + imageHeight + 0.8 * CM; } else { y += 0.5 * CM; } } catch (e) { y += 0.5 * CM; } const gl = f.geo_location || {}; const lat = gl.lat; const lng = gl.lng; // draw the file image on left console.log(`Creating thumbnail for detailed view ${idx+1}...`); const imgBuf = await makeThumb(f._bin); try { doc.image(imgBuf, MARGIN, y, { width: 8.6 * CM, height: 9.0 * CM }); } catch (e) { console.warn(`Could not draw main image ${idx+1}:`, e.message); } // draw static map for the single upload using Geoapify if (lat != null && lng != null) { console.log(`Generating static map for upload ${idx+1}...`); const mimgBuf = await staticMapWithPath(lat, lng, caseLat, caseLng, 17); if (mimgBuf) { try { doc.image(mimgBuf, 11.0 * CM, y, { width: 8.0 * CM, height: 9.0 * CM }); } catch (e) { console.warn(`Could not draw static map ${idx+1}:`, e.message); } } } let yb = y + 10.4 * CM; // Use Google Maps for reverse geocoding const rev = await reverseGeocode(lat, lng); yb = drawWrappedLabelValue(doc, MARGIN, yb, "Location Address", rev, 18.0); const distKm = (caseLat != null && lat != null) ? haversineKm(caseLat, caseLng, lat, lng) : null; const distM = distKm ? Math.round(distKm * 1000) : null; doc.font("Helvetica").fontSize(10); doc.text(distM ? `Distance from case location: ${distM} meters` : "Distance from case location: N/A", MARGIN, yb); yb += 0.7 * CM; const upAt = fmtIST(f.uploaded_at); doc.text(`Uploaded At: ${upAt}`, MARGIN, yb); yb += 1.0 * CM; doc.font("Helvetica-Bold").fontSize(12).text("Metadata & Sensor Information", MARGIN, yb); yb += 0.6 * CM; doc.font("Helvetica").fontSize(10).text(`Server Time: ${upAt}`, MARGIN, yb); yb += 0.6 * CM; doc.text(`Device Time: ${upAt}`, MARGIN, yb); yb += 0.6 * CM; doc.text(distM ? `Accuracy: ${distM} meters` : "Distance from case location: N/A", MARGIN, yb); yb += 0.6 * CM; if (lat != null) { doc.text(`Geo: Lat ${lat.toFixed(6)}, Lng ${lng.toFixed(6)}`, MARGIN, yb); } else { doc.text("Geo: N/A", MARGIN, yb); } yb += 0.6 * CM; drawFooter(doc, page); doc.addPage(); page += 1; } // Timeline page (mirrors python) console.log("Creating timeline page..."); drawHeader(doc, title); y = 3.4 * CM; doc.font("Helvetica-Bold").fontSize(14).text("Inspection Timeline", MARGIN, y); y += 0.8 * CM; // draw a rounded rect with a pale bg similar to python's setFillColorRGB doc.save(); doc.roundedRect(MARGIN, y, A4_WIDTH - 2 * MARGIN, 1.1 * CM, 0.3 * CM).fillOpacity(1).fill("#E0F7E6"); doc.fillColor("#1F743F").font("Helvetica-Bold").fontSize(12).text("Ready for Review", MARGIN + 0.6 * CM, y + 0.4 * CM); doc.fillColor("black"); doc.restore(); y += 2.0 * CM; const events = []; function evt(text, ts, sub = null) { if (ts) events.push({ text, ts, sub }); } evt("Case Created", caseObj.created_at); evt("Assigned to Agent", caseObj.assigned_at); evt("Accepted by Agent", caseObj.accepted_at); for (let f of files) { const sub = await reverseGeocode((f.geo_location || {}).lat, (f.geo_location || {}).lng); evt(`Upload — ${displayType(f.file_type)}`, f.uploaded_at, sub); } evt("Case Updated", caseObj.updated_at); evt("Case Completed", caseObj.completed_at); function parseTs(s) { try { return new Date(s); } catch { return new Date(0); } } events.sort((a, b) => parseTs(a.ts) - parseTs(b.ts)); for (let ev of events) { doc.font("Helvetica").fontSize(10).text(`• ${fmtIST(ev.ts)}`, MARGIN, y); y += 0.45 * CM; doc.font("Helvetica-Bold").fontSize(11).text(ev.text, MARGIN + 0.6 * CM, y); y += 0.45 * CM; if (ev.sub) { doc.font("Helvetica").fontSize(9).fillColor("gray"); y = drawWrappedLabelValue(doc, MARGIN + 0.6 * CM, y, "Uploaded from", ev.sub, 17.0); doc.fillColor("black"); y += 0.3 * CM; } y += 0.3 * CM; if (y > A4_HEIGHT - 3 * CM) { drawFooter(doc, page); doc.addPage(); page += 1; drawHeader(doc, title); y = 3.4 * CM; doc.font("Helvetica-Bold").fontSize(14).text("Inspection Timeline (Cont.)", MARGIN, y); y += 1.0 * CM; } } drawFooter(doc, page); doc.addPage(); // finalize doc.end(); ws.on("finish", () => { console.log(`PDF created successfully at ${outPath}`); resolve(); }); ws.on("error", (e) => { console.error("PDF write stream error:", e); reject(e); }); } catch (e) { console.error("PDF build error:", e); reject(e); } }); } // small helpers to draw section title and label:value function sectionTitle(doc, text, x, y) { doc.font("Helvetica-Bold").fontSize(14).text(text, x, y); return y + 0.9 * CM; } function labelValue(doc, x, y, label, value) { doc.font("Helvetica-Bold").fontSize(10).text(`${label}:`, x, y); doc.font("Helvetica").fontSize(10).text((value == null || value === "" || (Array.isArray(value) && value.length === 0)) ? "N/A" : value.toString(), x + 4.8 * CM, y); } // FastAPI-equivalent endpoints app.get("/health", (req, res) => { console.log("Health check"); res.json({ status: "healthy", env_loaded: !!ADMIN_EMAIL && !!ADMIN_PASSWORD && !!AES_KEY_STR && !!AES_IV_STR }); }); app.post("/generate-report", async (req, res) => { console.log("Generate report request received"); try { const caseId = (req.body || {}).case_id; if (!caseId) { console.error("Missing case_id in request"); return res.status(400).json({ error: "case_id required" }); } console.log(`Generating report for case: ${caseId}`); await adminLogin(); const caseObj = await getCase(caseId); const files = await getFiles(caseId); console.log(caseObj) console.log(`Downloading ${files.length} files...`); // download binaries for (let i = 0; i < files.length; i++) { console.log(`Downloading file ${i+1}/${files.length}...`); files[i]._bin = await downloadFile(files[i].file_id); } // ensure output dir const outDir = "/app/output"; if (!fs.existsSync(outDir)) { console.log(`Creating output directory: ${outDir}`); fs.mkdirSync(outDir, { recursive: true }); } const outputFilename = `Report_${caseId}.pdf`; const outputPath = path.join(outDir, outputFilename); console.log(`Building PDF at: ${outputPath}`); await buildPdf(caseObj, files, outputPath); const pdfContent = fs.readFileSync(outputPath); // cleanup try { fs.unlinkSync(outputPath); console.log(`Cleaned up temporary file: ${outputPath}`); } catch { } console.log(`Sending PDF response: ${outputFilename} (${pdfContent.length} bytes)`); res.setHeader("Content-Type", "application/pdf"); res.setHeader("Content-Disposition", `attachment; filename="${outputFilename}"`); res.send(pdfContent); console.log("PDF sent successfully"); } catch (e) { console.error("Error generating PDF:", e); res.status(500).json({ error: `Error generating PDF: ${e.message || String(e)}`, stack: process.env.NODE_ENV === 'development' ? e.stack : undefined }); } }); app.get("/", (req, res) => { console.log("Root endpoint accessed"); res.json({ message: "Case Report Generator API", status: "running", endpoints: { health: "GET /health", generate: "POST /generate-report" } }); }); const PORT = process.env.PORT || 7860; app.listen(PORT, "0.0.0.0", () => { console.log(`Server listening on port ${PORT}`); console.log(`Environment check:`); }); // // server.js // import fs from "fs"; // import path from "path"; // import crypto from "crypto"; // import fetch from "node-fetch"; // import express from "express"; // import bodyParser from "body-parser"; // import PDFDocument from "pdfkit"; // import sharp from "sharp"; // import moment from "moment-timezone"; // import cors from "cors"; // // Configuration (all from environment variables) // const BASE_URL = process.env.BASE_URL || "https://rakshitjan-cps-b2c.hf.space"; // const ADMIN_EMAIL = process.env.ADMIN_EMAIL || "admin123@example.com"; // const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || "SecurePass123"; // const AES_KEY_STR = process.env.AES_KEY_STR || "7E892875A52C59A3B588306B13C31FBD"; // const AES_IV_STR = process.env.AES_IV_STR || "XYE45DKJ0967GFAZ"; // const GOOGLE_MAPS_API_KEY = process.env.GOOGLE_MAPS_API_KEY || "AIzaSyBpna0F0J-c9QNBh8lSnfT2Z2uzD_vS9Ag"; // For geocoding // const GEOAPIFY_API_KEY = process.env.GEOAPIFY_API_KEY || "ceaea8a8a8f14f46b73221384eb4b5f0" ; // For static maps // const LOGO_PATH = process.env.LOGO_PATH || "logo.png"; // // PDF compression settings // const PDF_COMPRESSION_ENABLED = process.env.PDF_COMPRESSION_ENABLED !== "false"; // const IMAGE_COMPRESSION_QUALITY = parseInt(process.env.IMAGE_COMPRESSION_QUALITY) || 65; // const MAX_IMAGE_DIMENSION = parseInt(process.env.MAX_IMAGE_DIMENSION) || 1200; // const THUMBNAIL_QUALITY = parseInt(process.env.THUMBNAIL_QUALITY) || 55; // const app = express(); // app.use(bodyParser.json({ limit: "50mb" })); // // Allow ALL origins // app.use(cors()); // // Utility: convert cm to PDF points (1 pt = 1/72 in; 1 cm = 28.3464567 pt) // const CM = 28.3464567; // const A4_WIDTH = 595.2755905511812; // points (210mm) // const A4_HEIGHT = 841.8897637795277; // points (297mm) // const MARGIN = 1.2 * CM; // // AES encryption like Python's Crypto.Cipher AES-CBC w/ PKCS7 padding // function aesEncrypt(plaintext, keyStr, ivStr) { // // Validate inputs // if (!plaintext) throw new Error("Plaintext is required for encryption"); // if (!keyStr) throw new Error("AES key is required"); // if (!ivStr) throw new Error("AES IV is required"); // const key = Buffer.from(keyStr, "utf8"); // const iv = Buffer.from(ivStr, "utf8"); // // Validate key and IV lengths // if (key.length !== 16 && key.length !== 24 && key.length !== 32) { // throw new Error(`Invalid AES key length: ${key.length} bytes. Must be 16, 24, or 32 bytes.`); // } // // Node's createCipheriv uses PKCS#7 padding by default (called PKCS#5 in OpenSSL) // const cipher = crypto.createCipheriv(`aes-${key.length * 8}-cbc`, key, iv); // const encrypted = Buffer.concat([ // cipher.update(Buffer.from(plaintext, "utf8")), // cipher.final() // ]); // return encrypted.toString("base64"); // } // // HTTP session mimic (we reuse fetch) // let token = null; // async function adminLogin() { // try { // // encrypt email same as python // const encryptedEmail = aesEncrypt(ADMIN_EMAIL, AES_KEY_STR, AES_IV_STR); // const payload = { // email: encryptedEmail, // password: ADMIN_PASSWORD // }; // console.log(`Logging in to ${BASE_URL} with admin email...`); // const res = await fetch(`${BASE_URL}/api/admin/login`, { // method: "POST", // headers: { "Content-Type": "application/json" }, // body: JSON.stringify(payload), // timeout: 30000 // }); // const data = await res.json(); // if (res.status !== 200 || !data.token) { // console.error(`Login failed. Status: ${res.status}, Response:`, data); // throw new Error(`Login failed: ${JSON.stringify(data)}`); // } // token = data.token; // console.log("Admin login successful"); // return token; // } catch (error) { // console.error("Admin login error:", error.message); // throw error; // } // } // async function getCase(caseId) { // if (!token) await adminLogin(); // console.log(`Fetching case ${caseId}...`); // const res = await fetch(`${BASE_URL}/api/cases/${caseId}`, { // headers: { // "Content-Type": "application/json", // "Authorization": `Bearer ${token}` // }, // timeout: 30000 // }); // if (!res.ok) { // console.error(`Failed to get case ${caseId}: ${res.status} ${res.statusText}`); // throw new Error(`Failed get_case: ${res.status}`); // } // return await res.json(); // } // async function getFiles(caseId) { // if (!token) await adminLogin(); // console.log(`Fetching files for case ${caseId}...`); // const res = await fetch(`${BASE_URL}/api/cases/${caseId}/files`, { // headers: { // "Content-Type": "application/json", // "Authorization": `Bearer ${token}` // }, // timeout: 60000 // }); // if (!res.ok) { // console.error(`Failed to get files for case ${caseId}: ${res.status} ${res.statusText}`); // throw new Error(`Failed get_files: ${res.status}`); // } // const json = await res.json(); // console.log(`Retrieved ${json.files?.length || 0} files for case ${caseId}`); // return json.files || []; // } // async function downloadFile(fileId) { // if (!token) await adminLogin(); // console.log(`Downloading file ${fileId}...`); // const res = await fetch(`${BASE_URL}/api/files/${fileId}`, { // headers: { "Authorization": `Bearer ${token}` }, // timeout: 60000 // }); // if (!res.ok) { // console.error(`File download failed for ${fileId}: ${res.status} ${res.statusText}`); // throw new Error(`File download failed for ${fileId}: ${res.status}`); // } // const buf = await res.arrayBuffer(); // return Buffer.from(buf); // } // // Google Maps Geocoding API helpers (for forward/reverse geocoding) // async function forwardGeocode(address) { // if (!address) { // console.log("No address provided for geocoding"); // return [null, null]; // } // if (!GOOGLE_MAPS_API_KEY) { // console.error("GOOGLE_MAPS_API_KEY is not set"); // return [null, null]; // } // console.log(`Geocoding address: ${address.substring(0, 50)}...`); // try { // const url = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(address)}&key=${GOOGLE_MAPS_API_KEY}`; // const res = await fetch(url, { timeout: 20000 }); // if (!res.ok) { // console.error(`Geocoding API error: ${res.status} ${res.statusText}`); // return [null, null]; // } // const json = await res.json(); // if (json.status === "OK" && json.results && json.results[0] && json.results[0].geometry && json.results[0].geometry.location) { // const loc = json.results[0].geometry.location; // console.log(`Geocoding successful: ${loc.lat}, ${loc.lng}`); // return [loc.lat, loc.lng]; // } else { // console.warn(`Geocoding failed: ${json.status} - ${json.error_message || 'No results'}`); // return [null, null]; // } // } catch (error) { // console.error("Geocoding error:", error.message); // return [null, null]; // } // } // async function reverseGeocode(lat, lon) { // if (lat == null || lon == null) { // console.log("No coordinates provided for reverse geocoding"); // return "Unknown Address"; // } // if (!GOOGLE_MAPS_API_KEY) { // console.error("GOOGLE_MAPS_API_KEY is not set"); // return "Unknown Address"; // } // console.log(`Reverse geocoding coordinates: ${lat}, ${lon}`); // try { // const url = `https://maps.googleapis.com/maps/api/geocode/json?latlng=${lat},${lon}&key=${GOOGLE_MAPS_API_KEY}`; // const res = await fetch(url, { timeout: 20000 }); // if (!res.ok) { // console.error(`Reverse geocoding API error: ${res.status} ${res.statusText}`); // return "Unknown Address"; // } // const json = await res.json(); // if (json.status === "OK" && json.results && json.results[0] && json.results[0].formatted_address) { // const address = json.results[0].formatted_address; // console.log(`Reverse geocoding successful: ${address.substring(0, 50)}...`); // return address; // } else { // console.warn(`Reverse geocoding failed: ${json.status} - ${json.error_message || 'No results'}`); // return "Unknown Address"; // } // } catch (error) { // console.error("Reverse geocoding error:", error.message); // return "Unknown Address"; // } // } // // Geoapify Static Maps API (for static maps with markers and path) // async function staticMapWithPath(captureLat, captureLng, caseLat = null, caseLng = null, zoom = 17, size = "1800x600") { // if (captureLat == null || captureLng == null) { // console.log("No capture coordinates provided for static map"); // return null; // } // if (!GEOAPIFY_API_KEY) { // console.error("GEOAPIFY_API_KEY is not set"); // return null; // } // console.log(`Generating static map for: ${captureLat}, ${captureLng}`); // try { // let parts = [ // `center=lonlat:${captureLng},${captureLat}`, // `zoom=${zoom}`, // `size=${size}`, // `scaleFactor=2`, // `marker=lonlat:${captureLng},${captureLat};color:%23ff0000;size:large` // ]; // if (caseLat != null && caseLng != null) { // parts.push(`marker=lonlat:${caseLng},${caseLat};color:%23007bff;size:large`); // parts.push(`path=color:%23ff0000;weight:4|lonlat:${caseLng},${caseLat}|lonlat:${captureLng},${captureLat}`); // } // const url = `https://maps.geoapify.com/v1/staticmap?${parts.join("&")}&apiKey=${GEOAPIFY_API_KEY}`; // const res = await fetch(url, { timeout: 30000 }); // if (!res.ok) { // console.error(`Static map API error: ${res.status} ${res.statusText}`); // return null; // } // const buf = Buffer.from(await res.arrayBuffer()); // console.log("Static map generated successfully"); // return buf; // } catch (error) { // console.error("Static map generation error:", error.message); // return null; // } // } // function haversineKm(lat1, lon1, lat2, lon2) { // if ([lat1, lon1, lat2, lon2].some(v => v == null)) return null; // const R = 6371.0; // const phi1 = lat1 * Math.PI / 180; // const phi2 = lat2 * Math.PI / 180; // const dphi = (lat2 - lat1) * Math.PI / 180; // const dlambda = (lon2 - lon1) * Math.PI / 180; // const a = Math.sin(dphi / 2) * Math.sin(dphi / 2) + Math.cos(phi1) * Math.cos(phi2) * Math.sin(dlambda / 2) * Math.sin(dlambda / 2); // return 2 * R * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); // } // // date formatting to IST similar to fmt_ist in python // function fmtIST(dtStr) { // if (!dtStr) return "N/A"; // try { // // Accept Z or offset // const m = moment(dtStr); // const ist = m.tz("Asia/Kolkata").format("DD/MM/YYYY hh:mm:ss A") + " IST"; // return ist; // } catch { // return dtStr; // } // } // function exportedOnStr() { // return moment().tz("Asia/Kolkata").format("MMMM DD, YYYY"); // } // // Mapping display names // const DISPLAY_NAME = { // "aadhaar_photo": "Photo of Aadhar", // "customer_photo": "Customer's Photo", // "residence_photo": "Residence Photo", // "business_photo": "Business Address Photo" // }; // function displayType(fileType) { // return DISPLAY_NAME[fileType] || fileType || "Upload"; // } // // Optimized image compression - resize and compress aggressively // async function compressImageBuffer(imageBuffer, quality = IMAGE_COMPRESSION_QUALITY, maxDimension = MAX_IMAGE_DIMENSION) { // try { // // Get image info // const image = sharp(imageBuffer); // const metadata = await image.metadata(); // // Check if image is already small // if (imageBuffer.length < 100 * 1024) { // Less than 100KB // console.log(`Image already small (${Math.round(imageBuffer.length / 1024)}KB), skipping compression`); // return imageBuffer; // } // console.log(`Compressing image: ${Math.round(imageBuffer.length / 1024)}KB, ${metadata.width}x${metadata.height}`); // // Calculate new dimensions if needed // let newWidth = metadata.width; // let newHeight = metadata.height; // if (metadata.width > maxDimension || metadata.height > maxDimension) { // if (metadata.width > metadata.height) { // newWidth = maxDimension; // newHeight = Math.round((metadata.height / metadata.width) * maxDimension); // } else { // newHeight = maxDimension; // newWidth = Math.round((metadata.width / metadata.height) * maxDimension); // } // console.log(`Resizing image from ${metadata.width}x${metadata.height} to ${newWidth}x${newHeight}`); // } // // Use WebP for best compression (much better than JPEG for same quality) // const compressedBuffer = await sharp(imageBuffer) // .resize(newWidth, newHeight, { // fit: 'inside', // withoutEnlargement: true // }) // .webp({ // quality: Math.max(quality, 50), // Minimum 50% quality // effort: 6, // Maximum compression effort (slowest but best) // lossless: false // }) // .toBuffer() // .catch(async (webpError) => { // console.log(`WebP failed, using JPEG: ${webpError.message}`); // // Fallback to JPEG with mozjpeg for better compression // return await sharp(imageBuffer) // .resize(newWidth, newHeight, { // fit: 'inside', // withoutEnlargement: true // }) // .flatten({ background: { r: 255, g: 255, b: 255 } }) // Remove transparency // .jpeg({ // quality: Math.max(quality - 10, 40), // JPEG needs lower quality for similar size // mozjpeg: true, // Better compression // chromaSubsampling: '4:4:4' // Better quality // }) // .toBuffer(); // }); // const originalSize = imageBuffer.length; // const compressedSize = compressedBuffer.length; // const compressionRatio = ((originalSize - compressedSize) / originalSize * 100).toFixed(1); // console.log(`Image compressed: ${Math.round(originalSize / 1024)}KB → ${Math.round(compressedSize / 1024)}KB (${compressionRatio}% reduction)`); // return compressedBuffer; // } catch (error) { // console.error("Image compression error:", error.message); // // Return original buffer if compression fails // return imageBuffer; // } // } // // Optimized thumbnail creation with aggressive compression // async function makeThumb(imgBuf, maxW = 860, maxH = 640) { // try { // // First compress and resize the image aggressively for thumbnails // const compressedImg = await compressImageBuffer(imgBuf, THUMBNAIL_QUALITY, Math.max(maxW, maxH)); // // Create thumbnail with specific dimensions // const img = sharp(compressedImg) // .rotate() // .resize({ // width: maxW, // height: maxH, // fit: "inside", // withoutEnlargement: true, // kernel: sharp.kernel.lanczos3 // Better downsampling // }) // .flatten({ background: { r: 255, g: 255, b: 255 } }); // // Use WebP for thumbnails // let outputBuffer; // try { // outputBuffer = await img // .webp({ // quality: THUMBNAIL_QUALITY, // effort: 6, // lossless: false // }) // .toBuffer(); // } catch { // // Fallback to JPEG // outputBuffer = await img // .jpeg({ // quality: THUMBNAIL_QUALITY - 10, // mozjpeg: true // }) // .toBuffer(); // } // // Add minimal border // const bordered = await sharp(outputBuffer) // .extend({ // top: 1, // bottom: 1, // left: 1, // right: 1, // background: { r: 170, g: 170, b: 170 } // }); // let finalBuffer; // try { // finalBuffer = await bordered // .webp({ // quality: THUMBNAIL_QUALITY, // effort: 6 // }) // .toBuffer(); // } catch { // finalBuffer = await bordered // .jpeg({ // quality: THUMBNAIL_QUALITY - 10, // mozjpeg: true // }) // .toBuffer(); // } // return finalBuffer; // } catch (error) { // console.error("Thumbnail creation error:", error.message); // // Return original if thumbnail creation fails // return await compressImageBuffer(imgBuf, THUMBNAIL_QUALITY); // } // } // // Compress static maps aggressively (maps don't need high quality) // async function compressStaticMap(mapBuffer) { // if (!mapBuffer) return mapBuffer; // try { // const originalSize = mapBuffer.length; // // If already small, return as is // if (originalSize < 50 * 1024) { // return mapBuffer; // } // console.log(`Compressing static map: ${Math.round(originalSize / 1024)}KB`); // // Static maps can be heavily compressed // const compressedBuffer = await sharp(mapBuffer) // .webp({ // quality: 50, // Low quality for maps // effort: 6, // lossless: false // }) // .toBuffer() // .catch(() => { // // Fallback to JPEG // return sharp(mapBuffer) // .jpeg({ // quality: 55, // mozjpeg: true // }) // .toBuffer(); // }); // const compressedSize = compressedBuffer.length; // const compressionRatio = ((originalSize - compressedSize) / originalSize * 100).toFixed(1); // console.log(`Static map compressed: ${Math.round(originalSize / 1024)}KB → ${Math.round(compressedSize / 1024)}KB (${compressionRatio}% reduction)`); // return compressedBuffer; // } catch (error) { // console.error("Static map compression error:", error.message); // return mapBuffer; // } // } // // Simple PDF optimization - we rely on image compression and PDFKit's built-in compression // async function optimizePDF(inputPath, outputPath) { // if (!PDF_COMPRESSION_ENABLED) { // fs.copyFileSync(inputPath, outputPath); // return outputPath; // } // try { // // For Hugging Face, we can't use Ghostscript, so we rely on the compression // // we've already done at the image level and PDFKit's built-in compression // console.log("Using image-level compression (optimized for Hugging Face)"); // // Just copy the file - compression was already done during creation // fs.copyFileSync(inputPath, outputPath); // return outputPath; // } catch (error) { // console.error("PDF optimization failed:", error.message); // // Fallback to original // fs.copyFileSync(inputPath, outputPath); // return outputPath; // } // } // // text wrapping helper that attempts to match Python's draw_wrapped_label_value. // function drawWrappedLabelValue(doc, x, y, label, text, maxWidthCm = 16.0) { // const maxW = maxWidthCm * CM; // const fontSize = 10; // doc.font("Helvetica").fontSize(fontSize); // const labelStr = `${label}: `; // const labelW = doc.widthOfString(labelStr); // const words = (text || "N/A").toString().split(/\s+/); // let first = true; // let line = ""; // let width = 0; // let lines = []; // for (let w of words) { // const w_w = doc.widthOfString(w + " "); // if (first) { // if (labelW + width + w_w < maxW) { // line += w + " "; // width += w_w; // } else { // lines.push(labelStr + line.trim()); // first = false; // line = w + " "; // width = w_w; // } // } else { // if (width + w_w < maxW) { // line += w + " "; // width += w_w; // } else { // const indentText = " ".repeat(Math.max(0, Math.floor(labelW / (fontSize * 0.6)))); // lines.push(indentText + line.trim()); // line = w + " "; // width = w_w; // } // } // } // if (first) { // lines.push(labelStr + (line.trim())); // } else { // if (line) { // const indentText = " ".repeat(Math.max(0, Math.floor(labelW / (fontSize * 0.6)))); // lines.push(indentText + line.trim()); // } // } // for (let ln of lines) { // doc.text(ln, x, y, { lineBreak: false }); // y += 0.45 * CM; // } // return y; // } // // Draw header/footer helpers // function drawHeader(doc, title) { // // header blue rectangle at top: height 2.4 cm // doc.save(); // doc.rect(0, 0, A4_WIDTH, 2.4 * CM).fill("#0F61A8"); // // logo // let titleX = MARGIN; // if (fs.existsSync(LOGO_PATH)) { // try { // const logoBuffer = fs.readFileSync(LOGO_PATH); // doc.image(logoBuffer, MARGIN, 0.2 * CM, { width: 2.4 * CM, height: 2.0 * CM }); // titleX = MARGIN + 2.6 * CM; // } catch (e) { // console.warn(`Could not load logo from ${LOGO_PATH}:`, e.message); // } // } // doc.fillColor("white").font("Helvetica-Bold").fontSize(15).text(title, titleX, 0.9 * CM, { continued: false }); // doc.font("Helvetica").fontSize(10); // const exp = `Exported on ${exportedOnStr()}`; // const w = doc.widthOfString(exp); // doc.fillColor("white").text(exp, A4_WIDTH - MARGIN - w, 1.0 * CM); // doc.fillColor("black"); // doc.restore(); // } // function drawFooter(doc, page) { // doc.font("Helvetica").fontSize(9).text(`Page ${page}`, A4_WIDTH - MARGIN - 50, A4_HEIGHT - 1.0 * CM); // } // // Optimized Build PDF function // async function buildPdf(caseObj, files, outPath) { // return new Promise(async (resolve, reject) => { // try { // console.log(`Building optimized PDF for case ${caseObj.case_id} with ${files.length} files...`); // const doc = new PDFDocument({ // size: "A4", // margin: 0, // compress: true, // Enable PDF compression // pdfVersion: '1.5', // Better compression // info: { // Title: `Report for ${caseObj.case_applicant_name || ''}`, // Author: 'Case Report System', // Subject: 'Case Inspection Report', // Keywords: 'case,report,inspection', // Creator: 'PDFKit with Compression', // Producer: 'Hugging Face Optimized', // CreationDate: new Date() // } // }); // const ws = fs.createWriteStream(outPath); // doc.pipe(ws); // let page = 1; // const ad = ((caseObj.demographic_details || {}).address_details) || {}; // const caseAddr = `${ad.residential_address || ""}, ${ad.city || ""}, ${ad.state || ""} ${ad.pincode || ""}`.trim(); // console.log(`Case address: ${caseAddr}`); // const [caseLat, caseLng] = await forwardGeocode(caseAddr); // if (caseLat && caseLng) { // console.log(`Case coordinates: ${caseLat}, ${caseLng}`); // } // const title = `Name : ${caseObj.case_applicant_name || ""}`; // drawHeader(doc, title); // let y = 3.4 * CM; // // Customer Details section // y = sectionTitle(doc, "Customer Details", MARGIN, y); // labelValue(doc, MARGIN, y, "Customer Name", caseObj.case_applicant_name); y += 0.6 * CM; // labelValue(doc, MARGIN, y, "Customer ID", caseObj.case_id); y += 0.6 * CM; // labelValue(doc, MARGIN, y, "Phone Number", caseObj.case_applicant_contact); y += 0.6 * CM; // const emailId = (caseObj.demographic_details || {}).contact_information && (caseObj.demographic_details.contact_information.email_id); // labelValue(doc, MARGIN, y, "Email", emailId); y += 0.6 * CM; // y = drawWrappedLabelValue(doc, MARGIN, y, "Customer Address", caseAddr, 18.0); y += 0.4 * CM; // // Inspection Summary // y = sectionTitle(doc, "Inspection Summary", MARGIN, y); // doc.font("Helvetica").fontSize(10); // doc.text(`Request Started: ${fmtIST(caseObj.created_at)}`, MARGIN, y); y += 0.5 * CM; // doc.text(`Total Uploads: ${files.length} uploads`, MARGIN, y); y += 0.5 * CM; // doc.text(`Status: ${caseObj.status}`, MARGIN, y); y += 0.5 * CM; // doc.text(`Priority: ${caseObj.priority}`, MARGIN, y); y += 0.7 * CM; // // render static map if caseLat present // if (caseLat) { // console.log("Generating and compressing overview map..."); // const mapBuf = await staticMapWithPath(caseLat, caseLng, caseLat, caseLng, 12, "1200x400"); // Smaller size // if (mapBuf) { // const compressedMapBuf = await compressStaticMap(mapBuf); // const mapH = 8 * CM; // Smaller height // const mapW = A4_WIDTH - 2 * MARGIN; // doc.image(compressedMapBuf, MARGIN, y, { width: mapW, height: mapH }); // y += mapH + 0.8 * CM; // } // } // drawFooter(doc, page); // doc.addPage(); page += 1; // // Page 2: thumbnails (limited to 6 for size) // drawHeader(doc, title); // y = 3.4 * CM; // doc.font("Helvetica-Bold").fontSize(14).text("Image Thumbnails", MARGIN, y); // y += 0.8 * CM; // const x0 = MARGIN; // const y0 = y; // const colW = (A4_WIDTH - 2 * MARGIN) / 3; // const rowH = 5.5 * CM; // Smaller row height // // Limit thumbnails to 6 max for file size // const maxThumbnails = Math.min(6, files.length); // for (let i = 0; i < maxThumbnails; ++i) { // const f = files[i]; // console.log(`Creating optimized thumbnail for upload ${i+1}...`); // const imgBuf = await makeThumb(f._bin); // const col = i % 3; // const row = Math.floor(i / 3); // const xi = x0 + col * colW + 0.2 * CM; // const yi = y0 + row * rowH + 4.0 * CM; // Adjusted position // try { // doc.image(imgBuf, xi, yi, { width: colW - 0.8 * CM, height: 3.5 * CM }); // Smaller thumbnails // } catch (e) { // console.warn(`Could not draw thumbnail ${i+1}:`, e.message); // } // doc.font("Helvetica").fontSize(8).text(`Upload ${i + 1} — ${displayType(f.file_type)}`, xi, yi - 0.3 * CM); // } // drawFooter(doc, page); // doc.addPage(); page += 1; // // Page 3: Demographic, Business, Financial // drawHeader(doc, title); // y = 3.4 * CM; // y = sectionTitle(doc, "Demographic Details", MARGIN, y); // const demo = caseObj.demographic_details || {}; // const ci = demo.contact_information || {}; // const pd = demo.personal_details || {}; // labelValue(doc, MARGIN, y, "Aadhaar Photo Match", demo.aadhaar_photo_match); y += 0.5 * CM; // labelValue(doc, MARGIN, y, "Email ID", ci.email_id); y += 0.5 * CM; // labelValue(doc, MARGIN, y, "Mobile Number", ci.mobile_number); y += 0.5 * CM; // labelValue(doc, MARGIN, y, "Gender", pd.gender); y += 0.5 * CM; // labelValue(doc, MARGIN, y, "Education", pd.education); y += 0.5 * CM; // labelValue(doc, MARGIN, y, "Family Members", pd.number_of_family_members); y += 1.0 * CM; // y = sectionTitle(doc, "Business Details", MARGIN, y); // const bd = caseObj.business_details || {}; // const ei = bd.enterprise_information || {}; // const bld = bd.business_location_details || {}; // const ba = bd.business_address || {}; // labelValue(doc, MARGIN, y, "Enterprise Name", ei.enterprise_name); y += 0.5 * CM; // labelValue(doc, MARGIN, y, "Organization Type", ei.type_of_organization); y += 0.5 * CM; // labelValue(doc, MARGIN, y, "Business Location Type", bld.business_location); y += 0.5 * CM; // labelValue(doc, MARGIN, y, "Address", ba.address); y += 0.5 * CM; // labelValue(doc, MARGIN, y, "City/District/State/Pincode", `${ba.city || ""}, ${ba.district || ""}, ${ba.state || ""} - ${ba.pincode || ""}`); y += 1.0 * CM; // y = sectionTitle(doc, "Financial Details", MARGIN, y); // const fin = caseObj.financial_details || {}; // const bfi = fin.business_financial_information || {}; // const ffi = fin.family_financial_information || {}; // function fmtInr(v) { // try { // if (v == null || v === "" || isNaN(v)) return "N/A"; // const amount = parseInt(v); // return `₹ ${amount.toLocaleString('en-IN')}`; // } catch { // return "N/A"; // } // } // labelValue(doc, MARGIN, y, "Monthly Income", fmtInr(bfi.monthly_income_from_business)); y += 0.5 * CM; // labelValue(doc, MARGIN, y, "Monthly Expense", fmtInr(bfi.monthly_expense_of_business)); y += 0.5 * CM; // labelValue(doc, MARGIN, y, "Family Income", fmtInr(ffi.monthly_family_income)); y += 0.5 * CM; // drawFooter(doc, page); // doc.addPage(); page += 1; // // For each file, a detailed page (with aggressive image compression) // for (let idx = 0; idx < files.length; ++idx) { // const f = files[idx]; // console.log(`Processing detailed page for upload ${idx+1}...`); // drawHeader(doc, title); // y = 3.4 * CM; // doc.font("Helvetica-Bold").fontSize(14).text(`Upload ${idx + 1} — ${displayType(f.file_type)}`, MARGIN, y); // y += 0.8 * CM; // // integrity_dashboard image (skip if not critical) // try { // const integrityPath = "integrity_dashboard.png"; // if (fs.existsSync(integrityPath) && fs.statSync(integrityPath).size < 100 * 1024) { // const imageWidth = 16 * CM; // const imageHeight = 2.5 * CM; // const centerX = (A4_WIDTH - imageWidth) / 2; // doc.image(integrityPath, centerX, y, { width: imageWidth, height: imageHeight }); // y = y + imageHeight + 0.8 * CM; // } else { // y += 0.5 * CM; // } // } catch (e) { // y += 0.5 * CM; // } // const gl = f.geo_location || {}; // const lat = gl.lat; // const lng = gl.lng; // // Main image with aggressive compression // console.log(`Creating compressed main image for upload ${idx+1}...`); // const imgBuf = await compressImageBuffer(f._bin, IMAGE_COMPRESSION_QUALITY - 10, 1000); // try { // doc.image(imgBuf, MARGIN, y, { width: 8.0 * CM, height: 8.0 * CM }); // Smaller size // } catch (e) { // console.warn(`Could not draw main image ${idx+1}:`, e.message); // } // // Static map (only if coordinates exist) // if (lat != null && lng != null) { // console.log(`Generating compressed static map for upload ${idx+1}...`); // const mimgBuf = await staticMapWithPath(lat, lng, caseLat, caseLng, 17, "800x600"); // Smaller map // if (mimgBuf) { // const compressedMapBuf = await compressStaticMap(mimgBuf); // try { // doc.image(compressedMapBuf, 10.5 * CM, y, { width: 7.5 * CM, height: 7.5 * CM }); // Smaller // } catch (e) { // console.warn(`Could not draw static map ${idx+1}:`, e.message); // } // } // } // let yb = y + 9.0 * CM; // Adjusted for smaller images // // Reverse geocode // const rev = await reverseGeocode(lat, lng); // yb = drawWrappedLabelValue(doc, MARGIN, yb, "Location Address", rev, 18.0); // const distKm = (caseLat != null && lat != null) ? haversineKm(caseLat, caseLng, lat, lng) : null; // const distM = distKm ? Math.round(distKm * 1000) : null; // doc.font("Helvetica").fontSize(10); // doc.text(distM ? `Distance from case location: ${distM} meters` : "Distance from case location: N/A", MARGIN, yb); // yb += 0.7 * CM; // const upAt = fmtIST(f.uploaded_at); // doc.text(`Uploaded At: ${upAt}`, MARGIN, yb); yb += 0.8 * CM; // // Condensed metadata section // doc.font("Helvetica-Bold").fontSize(11).text("Metadata", MARGIN, yb); yb += 0.5 * CM; // doc.font("Helvetica").fontSize(9); // doc.text(`Server Time: ${upAt}`, MARGIN, yb); yb += 0.4 * CM; // if (lat != null) { // doc.text(`Location: ${lat.toFixed(4)}, ${lng.toFixed(4)}`, MARGIN, yb); // } // yb += 0.4 * CM; // drawFooter(doc, page); // // Only add new page if not the last file // if (idx < files.length - 1) { // doc.addPage(); // page += 1; // } // } // // Timeline page (optional - can be skipped for very large reports) // if (files.length <= 10) { // Only add timeline for reasonable number of files // console.log("Creating timeline page..."); // doc.addPage(); page += 1; // drawHeader(doc, title); // y = 3.4 * CM; // doc.font("Helvetica-Bold").fontSize(14).text("Inspection Timeline", MARGIN, y); y += 0.8 * CM; // doc.save(); // doc.roundedRect(MARGIN, y, A4_WIDTH - 2 * MARGIN, 1.1 * CM, 0.3 * CM).fillOpacity(1).fill("#E0F7E6"); // doc.fillColor("#1F743F").font("Helvetica-Bold").fontSize(12).text("Ready for Review", MARGIN + 0.6 * CM, y + 0.4 * CM); // doc.fillColor("black"); // doc.restore(); // y += 2.0 * CM; // const events = []; // function evt(text, ts, sub = null) { // if (ts) events.push({ text, ts, sub }); // } // evt("Case Created", caseObj.created_at); // evt("Assigned to Agent", caseObj.assigned_at); // evt("Accepted by Agent", caseObj.accepted_at); // // Limit timeline events // for (let i = 0; i < Math.min(files.length, 5); i++) { // const f = files[i]; // const sub = await reverseGeocode((f.geo_location || {}).lat, (f.geo_location || {}).lng); // evt(`Upload — ${displayType(f.file_type)}`, f.uploaded_at, sub); // } // evt("Case Updated", caseObj.updated_at); // evt("Case Completed", caseObj.completed_at); // function parseTs(s) { // try { // return new Date(s); // } catch { // return new Date(0); // } // } // events.sort((a, b) => parseTs(a.ts) - parseTs(b.ts)); // for (let ev of events) { // doc.font("Helvetica").fontSize(9).text(`• ${fmtIST(ev.ts)}`, MARGIN, y); y += 0.4 * CM; // doc.font("Helvetica-Bold").fontSize(10).text(ev.text, MARGIN + 0.6 * CM, y); y += 0.4 * CM; // if (ev.sub) { // doc.font("Helvetica").fontSize(8).fillColor("gray"); // const subText = ev.sub.length > 50 ? ev.sub.substring(0, 47) + "..." : ev.sub; // doc.text(` ${subText}`, MARGIN + 0.6 * CM, y); // doc.fillColor("black"); // y += 0.4 * CM; // } // y += 0.2 * CM; // if (y > A4_HEIGHT - 3 * CM) { // drawFooter(doc, page); // doc.addPage(); page += 1; // drawHeader(doc, title); // y = 3.4 * CM; // doc.font("Helvetica-Bold").fontSize(14).text("Inspection Timeline (Cont.)", MARGIN, y); y += 1.0 * CM; // } // } // drawFooter(doc, page); // } // doc.end(); // ws.on("finish", () => { // console.log(`Optimized PDF created successfully at ${outPath}`); // resolve(); // }); // ws.on("error", (e) => { // console.error("PDF write stream error:", e); // reject(e); // }); // } catch (e) { // console.error("PDF build error:", e); // reject(e); // } // }); // } // // Helper functions // function sectionTitle(doc, text, x, y) { // doc.font("Helvetica-Bold").fontSize(14).text(text, x, y); // return y + 0.9 * CM; // } // function labelValue(doc, x, y, label, value) { // doc.font("Helvetica-Bold").fontSize(10).text(`${label}:`, x, y); // doc.font("Helvetica").fontSize(10).text((value == null || value === "" || (Array.isArray(value) && value.length === 0)) ? "N/A" : value.toString(), x + 4.8 * CM, y); // } // // FastAPI-equivalent endpoints // app.get("/health", (req, res) => { // console.log("Health check"); // res.json({ // status: "healthy", // env_loaded: !!ADMIN_EMAIL && !!ADMIN_PASSWORD && !!AES_KEY_STR && !!AES_IV_STR, // pdf_compression: { // enabled: PDF_COMPRESSION_ENABLED, // image_quality: IMAGE_COMPRESSION_QUALITY, // thumbnail_quality: THUMBNAIL_QUALITY, // max_image_dimension: MAX_IMAGE_DIMENSION // } // }); // }); // app.post("/generate-report", async (req, res) => { // console.log("Generate report request received"); // try { // const caseId = (req.body || {}).case_id; // if (!caseId) { // console.error("Missing case_id in request"); // return res.status(400).json({ error: "case_id required" }); // } // console.log(`Generating optimized report for case: ${caseId}`); // await adminLogin(); // const caseObj = await getCase(caseId); // const files = await getFiles(caseId); // console.log(`Downloading and compressing ${files.length} files...`); // // Download and immediately compress files // for (let i = 0; i < files.length; i++) { // console.log(`Processing file ${i+1}/${files.length}...`); // const originalBuffer = await downloadFile(files[i].file_id); // // Store compressed version // files[i]._bin = await compressImageBuffer(originalBuffer, IMAGE_COMPRESSION_QUALITY); // } // // Create output directory // const outDir = "/tmp/output"; // if (!fs.existsSync(outDir)) { // console.log(`Creating output directory: ${outDir}`); // fs.mkdirSync(outDir, { recursive: true }); // } // // Create temporary files // const timestamp = Date.now(); // const originalFilename = `Report_${caseId}_${timestamp}.pdf`; // const optimizedFilename = `Report_${caseId}_${timestamp}_optimized.pdf`; // const originalPath = path.join(outDir, originalFilename); // const optimizedPath = path.join(outDir, optimizedFilename); // console.log(`Building optimized PDF at: ${originalPath}`); // await buildPdf(caseObj, files, originalPath); // // Optimize the PDF // console.log(`Optimizing PDF...`); // await optimizePDF(originalPath, optimizedPath); // // Read the optimized PDF // const pdfContent = fs.readFileSync(optimizedPath); // // Get file sizes for logging // const originalStats = fs.existsSync(originalPath) ? fs.statSync(originalPath) : { size: 0 }; // const optimizedStats = fs.statSync(optimizedPath); // const compressionRatio = originalStats.size > 0 ? // ((originalStats.size - optimizedStats.size) / originalStats.size * 100).toFixed(1) : "N/A"; // console.log(`PDF optimization results:`); // console.log(` Final size: ${Math.round(optimizedStats.size / 1024)}KB`); // if (originalStats.size > 0) { // console.log(` Reduction: ${compressionRatio}% (from ${Math.round(originalStats.size / 1024)}KB)`); // } // // Cleanup // [originalPath, optimizedPath].forEach(filePath => { // try { // if (fs.existsSync(filePath)) { // fs.unlinkSync(filePath); // console.log(`Cleaned up: ${path.basename(filePath)}`); // } // } catch (e) { // console.warn(`Could not delete ${filePath}:`, e.message); // } // }); // const outputFilename = `Report_${caseId}.pdf`; // console.log(`Sending optimized PDF: ${outputFilename} (${Math.round(optimizedStats.size / 1024)}KB)`); // res.setHeader("Content-Type", "application/pdf"); // res.setHeader("Content-Disposition", `attachment; filename="${outputFilename}"`); // res.setHeader("X-PDF-Size", `${Math.round(optimizedStats.size / 1024)}KB`); // res.setHeader("X-PDF-Compression", "Hugging Face Optimized"); // res.send(pdfContent); // console.log("Optimized PDF sent successfully"); // } catch (e) { // console.error("Error generating PDF:", e); // res.status(500).json({ // error: `Error generating PDF: ${e.message || String(e)}`, // stack: process.env.NODE_ENV === 'development' ? e.stack : undefined // }); // } // }); // app.get("/", (req, res) => { // console.log("Root endpoint accessed"); // res.json({ // message: "Case Report Generator API (Hugging Face Optimized)", // status: "running", // pdf_compression: { // enabled: PDF_COMPRESSION_ENABLED, // image_quality: IMAGE_COMPRESSION_QUALITY, // thumbnail_quality: THUMBNAIL_QUALITY, // max_image_dimension: MAX_IMAGE_DIMENSION, // note: "Optimized for Hugging Face Spaces - uses aggressive WebP compression" // }, // endpoints: { // health: "GET /health", // generate: "POST /generate-report" // } // }); // }); // const PORT = process.env.PORT || 7860; // app.listen(PORT, "0.0.0.0", () => { // console.log(`Server listening on port ${PORT}`); // console.log(`PDF Compression: ${PDF_COMPRESSION_ENABLED ? 'ENABLED' : 'DISABLED'}`); // console.log(`Image Quality: ${IMAGE_COMPRESSION_QUALITY}%`); // console.log(`Thumbnail Quality: ${THUMBNAIL_QUALITY}%`); // console.log(`Max Image Dimension: ${MAX_IMAGE_DIMENSION}px`); // console.log(`Optimized for Hugging Face Spaces`); // });