diff --git a/generatePDFHandler.js b/generatePDFHandler.js index 2fc6f75..b3003f3 100644 --- a/generatePDFHandler.js +++ b/generatePDFHandler.js @@ -7,10 +7,7 @@ const sleep = (ms) => new Promise(res => setTimeout(res, ms)); async function generatePDFFromJSON(req, res) { try { - let attachment = req.body.attachment ?? req.body[0]; - - // console.log("PDF Request:",req); - // console.log("PDF Request body:",req.body); + let attachment = req.body.attachment ?? req.body[0]; // Validate attachment is present in incoming message if (!attachment) { @@ -23,11 +20,10 @@ async function generatePDFFromJSON(req, res) { const savedJsonString = Buffer.from(attachment, 'base64').toString('utf-8'); let savedJson; - // console.log("Saved JSON String:",savedJsonString); + try { - savedJson = JSON.parse(savedJsonString); - // console.log("Saved Parsed JSON:",savedJson); + savedJson = JSON.parse(savedJsonString); const { valid, errors } = validateJson(savedJson); if (valid) { @@ -61,8 +57,7 @@ async function generatePDFFromJSON(req, res) { // Generate PDF buffer const pdfBuffer = await generatePDF(savedJsonString); //get the pdf from the savedJson - const pdfBase64 = Buffer.from(pdfBuffer).toString('base64'); - // console.log("PDF Base64:",pdfBase64); + const pdfBase64 = Buffer.from(pdfBuffer).toString('base64'); res.status(200).json({ errorCode: 0, @@ -83,10 +78,9 @@ async function generatePDFFromJSON(req, res) { async function generatePDF(savedJson) { const id = await storeData(savedJson); - const endPointForPDF = process.env.GENERATE_KILN_URL + "?jsonId=" + id; - // console.log("PDF Endpoint:",endPointForPDF); - const pdfBufferFromURL = await getPDFFromURL(endPointForPDF); - deleteData(id); + const endPointForPDF = process.env.GENERATE_KILN_URL + "?jsonId=" + id; + const pdfBufferFromURL = await getPDFFromURL(endPointForPDF); + await deleteData(id); return pdfBufferFromURL; } @@ -146,8 +140,7 @@ async function generatePDFFromHTML(req, res) { async function generatePDFFromURL(req, res) { const { path } = req.body; - try { - + try { const pdfBuffer = await getPDFFromURL(path); // Send PDF back to client @@ -171,9 +164,7 @@ async function getPDFFromURL(url) { const CLICK_WINDOW_MS = Number(process.env.PDF_CLICK_WINDOW_MS ?? 3000); const CLICK_RETRY_EVERY_MS = Number(process.env.PDF_CLICK_INTERVAL_MS ?? 200); const POST_CLICK_MS = Number(process.env.PDF_POST_CLICK_MS ?? 2500); - - // console.log("Puppeteer path:", process.env.PUPPETEER_EXECUTABLE_PATH); - // console.log("Puppeteer URL:", url); + const browser = await puppeteer.launch({ executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || "/usr/bin/chromium", args: [ @@ -193,7 +184,7 @@ async function getPDFFromURL(url) { await page.setViewport({ width: 1280, height: 800 }); // Set the HTML content of the page - await page.goto(url, { waitUntil: 'networkidle2', timeout: 150000 }); + await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 150000 }); // Let app hydrate await sleep(HYDRATE_MS); @@ -242,7 +233,83 @@ async function getPDFFromURL(url) { // Give it time to assemble the printable view await sleep(POST_CLICK_MS); + + console.log("Waiting for print button..."); + + let clickedPrint = false; + const PRINT_DEADLINE = Date.now() + 20000; // up to 20 sec + while (!clickedPrint && Date.now() < PRINT_DEADLINE) { + // Try main document + try { + await page.waitForSelector("#print", { visible: true, timeout: 1000 }); + await page.click("#print"); + clickedPrint = true; + console.log("Clicked print button on main page"); + break; + } catch {} + + // Try every frame + for (const frame of page.frames()) { + try { + await frame.waitForSelector("#print", { visible: true, timeout: 1000 }); + await frame.click("#print"); + clickedPrint = true; + console.log("Clicked print button inside iframe"); + break; + } catch {} + } + + await sleep(250); // avoid hot-looping + } + //await sleep(POST_CLICK_MS); + if (clickedPrint) { + + console.log("Waiting for printable layout to finish…"); + + // Wait for data-form-id attribute + try { + await page.waitForFunction( + () => document.documentElement.getAttribute("data-form-id"), + { timeout: 20000 } + ); + console.log("Printable layout ready (data-form-id detected)"); + } catch { + console.log("Timeout waiting for data-form-id"); + } + + // Svelte stabilization + console.log("Waiting for Svelte DOM stability…"); + await page.waitForFunction(() => { + const now = performance.now(); + if (!window.__lastMutationTime) window.__lastMutationTime = now; + if (!window.__mutationObserverInstalled) { + const observer = new MutationObserver(() => { + window.__lastMutationTime = performance.now(); + }); + observer.observe(document.body, { + childList: true, + subtree: true, + attributes: true, + characterData: true + }); + window.__mutationObserverInstalled = true; + } + + return performance.now() - window.__lastMutationTime > 600; + }, { timeout: 20000 }); + console.log("Svelte DOM stabilized"); + } else { + console.log("Could not click on print button"); + } + + await page.evaluate((t) => { + document.title = t; + const titleTag = document.querySelector("title") || + document.head.appendChild(document.createElement("title")); + titleTag.textContent = t; + }, "Untitled"); + // Generate PDF with print CSS applied await page.emulateMediaType('print'); const pdfBuffer = await page.pdf({ @@ -255,6 +322,7 @@ async function getPDFFromURL(url) { await browser.close(); + console.log("Returning PDF"); return pdfBuffer; } catch (error) { @@ -296,4 +364,4 @@ async function loadSavedJson(req, res) { -module.exports = { generatePDFFromHTML, generatePDFFromURL, generatePDFFromJSON, loadSavedJson }; \ No newline at end of file +module.exports = { generatePDFFromHTML, generatePDFFromURL, generatePDFFromJSON, loadSavedJson ,generatePDF }; \ No newline at end of file diff --git a/helm/templates/hpa.yaml b/helm/templates/hpa.yaml index cf69bd9..0c7d7c1 100644 --- a/helm/templates/hpa.yaml +++ b/helm/templates/hpa.yaml @@ -1,3 +1,4 @@ +{{- if contains "prod" .Release.Namespace }} apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: @@ -15,10 +16,11 @@ spec: name: cpu target: type: Utilization - averageUtilization: 75 + averageUtilization: 80 - type: Resource resource: name: memory target: type: Utilization - averageUtilization: 75 + averageUtilization: 80 +{{- end }} \ No newline at end of file diff --git a/helm/values.yaml b/helm/values.yaml index eaab3d8..b26c76b 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -13,8 +13,8 @@ configMapName: commlayer resources: requests: - memory: "128Mi" - cpu: "50m" + memory: "256Mi" + cpu: "20m" limits: memory: "512Mi" cpu: "250m" diff --git a/routes.js b/routes.js index 2f4850c..6fcb57e 100644 --- a/routes.js +++ b/routes.js @@ -3,7 +3,7 @@ const axios = require("axios"); const xmlparser = require("express-xml-bodyparser"); const { keycloakForSiebel, keycloakForFormRepo } = require("./keycloak.js"); const { generateTemplate,generateNewTemplate } = require("./generateHandler"); -const { saveICMdata, loadICMdata, clearICMLockedFlag } = require("./saveICMdataHandler"); +const { saveICMdata, loadICMdata, clearICMLockedFlag, loadICMdataAsPDF } = require("./saveICMdataHandler"); const { getUsername } = require("./usernameHandler.js"); const renderRouter = require("./renderHandler"); const appCfg = require('./appConfig.js'); @@ -205,5 +205,6 @@ router.post("/loadPortalForm", loadPortalIntegratedForm); router.use("/interface", interface); router.post('/submitForPortalAction', submitForPortalAction); router.post('/cancelForPortalAction', cancelForPortalAction); +router.post("/loadICMDataAsPDF", loadICMdataAsPDF); module.exports = router; \ No newline at end of file diff --git a/saveICMdataHandler.js b/saveICMdataHandler.js index df6997a..ebc5cc9 100644 --- a/saveICMdataHandler.js +++ b/saveICMdataHandler.js @@ -10,6 +10,7 @@ const appCfg = require('./appConfig.js'); const { toICMFormat } = require("./dateConverter.js"); const { formExceptions } = require("./dictionary/jsonXmlConversion.js"); const { propertyExists, propertyNotEmpty, keyExists } = require("./dictionary/dictionaryUtils.js"); +const {generatePDF }= require("./generatePDFHandler.js"); const SIEBEL_ICM_API_FORMS_ENDPOINT = process.env.SIEBEL_ICM_API_FORMS_ENDPOINT; @@ -152,8 +153,7 @@ async function saveICMdata(req, res) { saveJson["DocFileExt"] = "json"; saveJson["Doc Attachment Id"] = Buffer.from(savedFormParam).toString('base64');//savedForm is saved as attachment let saveData = JSON.parse(savedFormParam)["data"];// This is the data part of the savedJson - - + /** * Apply Kiln Version * Kiln V1 uses data: { items: []} @@ -799,7 +799,102 @@ function fixJSONValuesForXML (saveData, truncatedKeysSaveData, toWrapIds, dateIt return truncatedKeysSaveData; } +async function loadICMdataAsPDF(req,res) { + + //load the savedJson form ICM. + let params = req.body; + const rawHost = (req.get("X-Original-Server") || req.hostname); + const configOpt = appCfg[rawHost]; + params = { ...params,...configOpt }; + const attachment_id = params["attachmentId"]; + const office_name = params["OfficeName"]; + console.log("attachment_id>>", attachment_id); + if (!attachment_id) { + return res + .status(400) + .send({ error: getErrorMessage("ATTACHMENT_ID_REQUIRED") }); + } + let username = null; + + if (params["token"]) { + username = await getUsername(params["token"], params["employeeEndpoint"]); + } else if (params["username"]) { + const valid = await isUsernameValid(params["username"], params["employeeEndpoint"]); + username = valid ? params["username"] : null; + } + + if (!username || !isNaN(username)) { + return res + .status(401) + .send({ error: getErrorMessage("INVALID_USER") }); + } + + // Acquire token once and reuse for all ICM calls + let authHeaders; + try { + const grant = await keycloakForSiebel.grantManager.obtainFromClientCredentials(); + authHeaders = { + Authorization: `Bearer ${grant.id_token.token}`, + "X-ICM-TrustedUsername": username, + }; + } catch (error) { + console.error("Failed to acquire Keycloak token:", error); + return res.status(500).send({ error: getErrorMessage("GENERIC_ERROR_MSG") }); + } + + let icm_metadata = await getICMAttachmentStatus(attachment_id, username, params, authHeaders); + let icm_status = icm_metadata["Status"]; + if (!icm_status || icm_status == "") { + console.log("Error fetching Form Instance Thin data for ", attachment_id); + return res + .status(400) + .send({ error: getErrorMessage("FORM_STATUS_NOT_FOUND") }); + } + //let url = buildUrlWithParams('SIEBEL_ICM_API_HOST', 'fwd/v1.0/data/DT FormFoundry Upsert/DT Form Instance Orbeon Revise/'+attachment_id+'/',''); + let url = buildUrlWithParams(params["apiHost"], params["saveEndpoint"] + attachment_id + '/', params); + try { + let response; + const query = { + viewMode: "Catalog", + inlineattachment: true + } + if (params.icmWorkspace) { + query.workspace = params.icmWorkspace; + } + response = await axios.get(url, { params: query, headers: authHeaders }); + let return_data = Buffer.from(response.data["Doc Attachment Id"], 'base64').toString('utf-8'); + //validate the returned data to be of the expected format + const valid = isJsonStringValid(return_data); + if (!valid) { + console.log('JSON is not valid '); + return res + .status(400) + .send({ error: getErrorMessage("FORM_NOT_VALID") }); + } + //validate ends here. Once validated continue to modify json based on status and send back. + if (icm_status == "Complete") { + let new_data = JSON.parse(return_data); + new_data.form_definition.readOnly = true; + return_data = JSON.stringify(new_data); + } + + const pdfBuffer = await generatePDF(return_data); //get the pdf from the savedJson + + const pdfBase64 = Buffer.from(pdfBuffer).toString('base64'); + res.status(200).json({ + pdf_base64: pdfBase64 + }); + } + catch (error) { + console.error(`Error loading data from ICM for PDF:`, error); + return res + .status(400) + .send({ error: getErrorMessage("GENERIC_ERROR_MSG") }); + } +} + module.exports.saveICMdata = saveICMdata; module.exports.loadICMdata = loadICMdata; module.exports.clearICMLockedFlag = clearICMLockedFlag; -module.exports.getICMAttachmentStatus = getICMAttachmentStatus; \ No newline at end of file +module.exports.getICMAttachmentStatus = getICMAttachmentStatus; +module.exports.loadICMdataAsPDF = loadICMdataAsPDF; \ No newline at end of file