tango-app-api-trax 3.9.69 → 3.9.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -29,8 +29,7 @@ import { insertSingleProcessData } from '../controllers/trax.controller.js';
|
|
|
29
29
|
import handlebars from './handlebar-helper.js';
|
|
30
30
|
import { buildVisitChecklistTemplateData, getCompiledVisitChecklistTemplate, createImageCache, resolveTemplateUrls } from '../utils/visitChecklistPdf.utils.js';
|
|
31
31
|
import archiver from 'archiver';
|
|
32
|
-
import
|
|
33
|
-
import { getBrowser as getBrowserInstance } from '../utils/browserPool.utils.js';
|
|
32
|
+
import { getBrowser as getBrowserInstance, withPdfSlot } from '../utils/browserPool.utils.js';
|
|
34
33
|
import fs from 'fs';
|
|
35
34
|
import path from 'path';
|
|
36
35
|
import { fileURLToPath as toPath } from 'url';
|
|
@@ -3765,57 +3764,34 @@ export const downloadInsertPdf = async ( req, res ) => {
|
|
|
3765
3764
|
}
|
|
3766
3765
|
const cdnBase = JSON.parse( process.env.CDNURL )?.TraxAnswerCDN || '';
|
|
3767
3766
|
const pdfTemplate = getCompiledVisitChecklistTemplate();
|
|
3768
|
-
const imageCache = createImageCache();
|
|
3769
3767
|
|
|
3770
3768
|
const safeName = ( str ) =>
|
|
3771
3769
|
( str || '' ).toString().replace( /[<>:"/\\|?*]+/g, '_' );
|
|
3772
3770
|
|
|
3773
|
-
//
|
|
3774
|
-
//
|
|
3775
|
-
//
|
|
3776
|
-
|
|
3777
|
-
// {
|
|
3778
|
-
// term: {
|
|
3779
|
-
// _id: req.body.checklistId,
|
|
3780
|
-
// },
|
|
3781
|
-
// },
|
|
3782
|
-
// ],
|
|
3783
|
-
// },
|
|
3784
|
-
// },
|
|
3785
|
-
// };
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
// 1) Launch browser page + fetch OpenSearch data in parallel
|
|
3789
|
-
const [ browser, aiDetails ] = await Promise.all( [
|
|
3790
|
-
getBrowserInstance(),
|
|
3791
|
-
// getOpenSearchData( JSON.parse( process.env.OPENSEARCH ).traxIndex, query ),
|
|
3792
|
-
processedchecklist.findOne( { _id: req.body.checklistId } ),
|
|
3793
|
-
] );
|
|
3794
|
-
console.log( aiDetails );
|
|
3771
|
+
// 1) Fetch the checklist document. The browser is acquired later, at
|
|
3772
|
+
// point-of-use inside the render slot — grabbing it here and holding it
|
|
3773
|
+
// across the slow image work below risks a stale (recycled) browser.
|
|
3774
|
+
const aiDetails = await processedchecklist.findOne( { _id: req.body.checklistId } );
|
|
3795
3775
|
|
|
3796
3776
|
if ( !aiDetails ) {
|
|
3797
3777
|
return res.sendError( 'Checklist not found', 404 );
|
|
3798
3778
|
}
|
|
3799
3779
|
|
|
3800
|
-
const doc =
|
|
3801
|
-
|
|
3802
|
-
console.log( doc );
|
|
3780
|
+
const doc = aiDetails.toObject();
|
|
3803
3781
|
|
|
3804
|
-
// 2) Fetch brandInfo + compliance data in parallel
|
|
3782
|
+
// 2) Fetch brandInfo + compliance data in parallel.
|
|
3805
3783
|
const complianceURL = JSON.parse( process.env.LAMBDAURL ).complianceHistory;
|
|
3806
|
-
const detectionPayload = {
|
|
3807
|
-
'storeId': [ doc.store_id ],
|
|
3808
|
-
'userEmail': doc.userEmail,
|
|
3809
|
-
'sourceChecklist_id': doc.sourceCheckList_id,
|
|
3810
|
-
};
|
|
3811
|
-
|
|
3812
3784
|
const [ brandInfo, complianceData ] = await Promise.all( [
|
|
3813
3785
|
getBrandInfo( doc.client_id ),
|
|
3814
|
-
LamdaServiceCall( complianceURL,
|
|
3786
|
+
LamdaServiceCall( complianceURL, {
|
|
3787
|
+
storeId: [ doc.store_id ],
|
|
3788
|
+
userEmail: doc.userEmail,
|
|
3789
|
+
sourceChecklist_id: doc.sourceCheckList_id,
|
|
3790
|
+
} ),
|
|
3815
3791
|
] );
|
|
3816
3792
|
|
|
3817
3793
|
if ( complianceData?.data?.length ) {
|
|
3818
|
-
doc
|
|
3794
|
+
doc.historyData = complianceData.data;
|
|
3819
3795
|
}
|
|
3820
3796
|
|
|
3821
3797
|
// CDN fix
|
|
@@ -3830,62 +3806,146 @@ export const downloadInsertPdf = async ( req, res ) => {
|
|
|
3830
3806
|
} );
|
|
3831
3807
|
|
|
3832
3808
|
const templateData = buildVisitChecklistTemplateData( doc, brandInfo );
|
|
3809
|
+
// Keep images as CDN URLs (do NOT inline as base64) — Chrome fetches them
|
|
3810
|
+
// at render time. Inlining produced a multi-MB HTML string that crashed
|
|
3811
|
+
// puppeteer's setContent CDP transfer ("Target closed").
|
|
3833
3812
|
const resolvedData = resolveTemplateUrls( templateData, cdnBase );
|
|
3834
|
-
await imageCache.resolveAllImages( resolvedData );
|
|
3835
3813
|
|
|
3836
3814
|
const html = pdfTemplate( resolvedData );
|
|
3837
3815
|
|
|
3838
|
-
// 3)
|
|
3839
|
-
page
|
|
3840
|
-
|
|
3816
|
+
// 3) Render inside a concurrency slot. Acquire a *fresh* browser from the
|
|
3817
|
+
// pool at point-of-use, and retry on a new page if the render frame
|
|
3818
|
+
// detaches (renderer crash / recycled browser) — that surfaces as
|
|
3819
|
+
// "Execution context is not available in detached frame".
|
|
3820
|
+
// Render inside the concurrency slot; the slot returns just the PDF buffer.
|
|
3821
|
+
const finalBuffer = await withPdfSlot( async () => {
|
|
3822
|
+
const MAX_RENDER_ATTEMPTS = 2;
|
|
3823
|
+
let buffer = null;
|
|
3824
|
+
let lastError;
|
|
3841
3825
|
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
timeout: 15000,
|
|
3846
|
-
} );
|
|
3847
|
-
await page.emulateMediaType( 'screen' );
|
|
3848
|
-
} catch ( err ) {
|
|
3849
|
-
logger.error( { functionName: 'downloadInsertPdf', message: 'setContent failed', error: err } );
|
|
3850
|
-
}
|
|
3851
|
-
|
|
3852
|
-
let pdfBuffer;
|
|
3853
|
-
try {
|
|
3854
|
-
pdfBuffer = await page.pdf( {
|
|
3855
|
-
format: 'A4',
|
|
3856
|
-
printBackground: true,
|
|
3857
|
-
preferCSSPageSize: true,
|
|
3858
|
-
displayHeaderFooter: true,
|
|
3859
|
-
headerTemplate: '<span></span>',
|
|
3860
|
-
footerTemplate: [
|
|
3861
|
-
'<div style="width:100%;padding:0 10mm;font-size:10px;',
|
|
3862
|
-
'font-family:Arial,sans-serif;display:flex;',
|
|
3863
|
-
'justify-content:space-between;align-items:center;',
|
|
3864
|
-
'border-top:1px solid #d9d9d9;padding-top:4px;">',
|
|
3865
|
-
'<span style="color:#999;">Page ',
|
|
3866
|
-
'<span class="pageNumber"></span>',
|
|
3867
|
-
' of <span class="totalPages"></span></span>',
|
|
3868
|
-
'<span style="display:flex;align-items:center;gap:6px;">',
|
|
3869
|
-
'<span style="color:#666;font-weight:400;font-size:10px;">',
|
|
3870
|
-
'Generated by</span>',
|
|
3871
|
-
tangoyeLogoSvg,
|
|
3872
|
-
'</span></div>',
|
|
3873
|
-
].join( '' ),
|
|
3874
|
-
} );
|
|
3875
|
-
} catch ( err ) {
|
|
3876
|
-
logger.error( { functionName: 'downloadInsertPdf', message: 'PDF generation failed', error: err } );
|
|
3877
|
-
return res.sendError( 'PDF generation failed', 500 );
|
|
3878
|
-
}
|
|
3826
|
+
for ( let attempt = 1; attempt <= MAX_RENDER_ATTEMPTS; attempt++ ) {
|
|
3827
|
+
const browser = await getBrowserInstance();
|
|
3828
|
+
page = await browser.newPage();
|
|
3879
3829
|
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3830
|
+
try {
|
|
3831
|
+
await page.setViewport( { width: 1280, height: 1800 } );
|
|
3832
|
+
await page.setContent( html, {
|
|
3833
|
+
waitUntil: 'domcontentloaded',
|
|
3834
|
+
timeout: 30000,
|
|
3835
|
+
} );
|
|
3836
|
+
await page.emulateMediaType( 'screen' );
|
|
3837
|
+
|
|
3838
|
+
// Images load from CDN URLs (not inlined). Wait for them to finish,
|
|
3839
|
+
// capped at 30s so a slow/broken image can't hang the render.
|
|
3840
|
+
await Promise.race( [
|
|
3841
|
+
page.evaluate( async () => {
|
|
3842
|
+
const images = Array.from( document.images );
|
|
3843
|
+
await Promise.all(
|
|
3844
|
+
images.map( ( img ) => {
|
|
3845
|
+
if ( img.complete ) return;
|
|
3846
|
+
return new Promise( ( resolve ) => {
|
|
3847
|
+
img.onload = img.onerror = resolve;
|
|
3848
|
+
} );
|
|
3849
|
+
} ),
|
|
3850
|
+
);
|
|
3851
|
+
} ),
|
|
3852
|
+
new Promise( ( resolve ) => setTimeout( resolve, 30000 ) ),
|
|
3853
|
+
] );
|
|
3854
|
+
|
|
3855
|
+
// Downscale images in-page before printing. They're embedded at
|
|
3856
|
+
// source (camera) resolution otherwise, which bloats the PDF and
|
|
3857
|
+
// print memory. Cap the largest side at 1200px — plenty for the
|
|
3858
|
+
// small display sizes. Processed one at a time to bound peak memory.
|
|
3859
|
+
await page.evaluate( async ( maxDim ) => {
|
|
3860
|
+
const imgs = Array.from( document.images );
|
|
3861
|
+
for ( const img of imgs ) {
|
|
3862
|
+
try {
|
|
3863
|
+
const nw = img.naturalWidth;
|
|
3864
|
+
const nh = img.naturalHeight;
|
|
3865
|
+
if ( !nw || !nh ) continue;
|
|
3866
|
+
const scale = maxDim / Math.max( nw, nh );
|
|
3867
|
+
if ( scale >= 1 ) continue; // already small enough
|
|
3868
|
+
const canvas = document.createElement( 'canvas' );
|
|
3869
|
+
canvas.width = Math.round( nw * scale );
|
|
3870
|
+
canvas.height = Math.round( nh * scale );
|
|
3871
|
+
canvas.getContext( '2d' ).drawImage( img, 0, 0, canvas.width, canvas.height );
|
|
3872
|
+
const dataUrl = canvas.toDataURL( 'image/jpeg', 0.75 );
|
|
3873
|
+
canvas.width = 0;
|
|
3874
|
+
canvas.height = 0;
|
|
3875
|
+
img.src = dataUrl;
|
|
3876
|
+
if ( img.decode ) await img.decode().catch( () => {} );
|
|
3877
|
+
} catch ( e ) {
|
|
3878
|
+
// Keep the original image if downscaling fails.
|
|
3879
|
+
}
|
|
3880
|
+
}
|
|
3881
|
+
}, 900 );
|
|
3882
|
+
|
|
3883
|
+
const pdfBuffer = await page.pdf( {
|
|
3884
|
+
format: 'A4',
|
|
3885
|
+
printBackground: true,
|
|
3886
|
+
preferCSSPageSize: true,
|
|
3887
|
+
displayHeaderFooter: true,
|
|
3888
|
+
headerTemplate: '<span></span>',
|
|
3889
|
+
footerTemplate: [
|
|
3890
|
+
'<div style="width:100%;padding:0 10mm;font-size:10px;',
|
|
3891
|
+
'font-family:Arial,sans-serif;display:flex;',
|
|
3892
|
+
'justify-content:space-between;align-items:center;',
|
|
3893
|
+
'border-top:1px solid #d9d9d9;padding-top:4px;">',
|
|
3894
|
+
'<span style="color:#999;">Page ',
|
|
3895
|
+
'<span class="pageNumber"></span>',
|
|
3896
|
+
' of <span class="totalPages"></span></span>',
|
|
3897
|
+
'<span style="display:flex;align-items:center;gap:6px;">',
|
|
3898
|
+
'<span style="color:#666;font-weight:400;font-size:10px;">',
|
|
3899
|
+
'Generated by</span>',
|
|
3900
|
+
tangoyeLogoSvg,
|
|
3901
|
+
'</span></div>',
|
|
3902
|
+
].join( '' ),
|
|
3903
|
+
} );
|
|
3904
|
+
|
|
3905
|
+
if ( !pdfBuffer || pdfBuffer.length === 0 ) {
|
|
3906
|
+
throw new Error( 'PDF generation resulted in empty buffer' );
|
|
3907
|
+
}
|
|
3908
|
+
|
|
3909
|
+
buffer = Buffer.isBuffer( pdfBuffer ) ? pdfBuffer : Buffer.from( pdfBuffer );
|
|
3910
|
+
break;
|
|
3911
|
+
} catch ( err ) {
|
|
3912
|
+
lastError = err;
|
|
3913
|
+
logger.error( {
|
|
3914
|
+
functionName: 'downloadInsertPdf',
|
|
3915
|
+
message: `PDF render attempt ${attempt} failed`,
|
|
3916
|
+
docId: doc?._id,
|
|
3917
|
+
error: err,
|
|
3918
|
+
} );
|
|
3919
|
+
|
|
3920
|
+
const msg = String( err?.message || '' );
|
|
3921
|
+
const retriable =
|
|
3922
|
+
msg.includes( 'detached frame' ) ||
|
|
3923
|
+
msg.includes( 'Execution context' ) ||
|
|
3924
|
+
msg.includes( 'Target closed' ) ||
|
|
3925
|
+
msg.includes( 'Session closed' );
|
|
3926
|
+
|
|
3927
|
+
if ( !retriable || attempt === MAX_RENDER_ATTEMPTS ) break;
|
|
3928
|
+
} finally {
|
|
3929
|
+
// Free the page (and its renderer) as soon as this attempt ends.
|
|
3930
|
+
if ( page ) {
|
|
3931
|
+
await page.close().catch( () => {} );
|
|
3932
|
+
page = null;
|
|
3933
|
+
}
|
|
3934
|
+
}
|
|
3935
|
+
}
|
|
3884
3936
|
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3937
|
+
if ( !buffer ) {
|
|
3938
|
+
logger.error( { functionName: 'downloadInsertPdf', message: 'PDF generation failed after retries', error: lastError } );
|
|
3939
|
+
}
|
|
3940
|
+
return buffer;
|
|
3941
|
+
} );
|
|
3888
3942
|
|
|
3943
|
+
if ( !finalBuffer ) {
|
|
3944
|
+
return res.sendError( 'PDF generation failed', 500 );
|
|
3945
|
+
}
|
|
3946
|
+
|
|
3947
|
+
// Upload the PDF to S3 and return a signed download URL — a tiny JSON
|
|
3948
|
+
// response, so the client never hits the "max response size" limit.
|
|
3889
3949
|
let pdfName;
|
|
3890
3950
|
if ( doc?.store_id ) {
|
|
3891
3951
|
pdfName = `${safeName(
|
|
@@ -3897,13 +3957,17 @@ export const downloadInsertPdf = async ( req, res ) => {
|
|
|
3897
3957
|
)}.pdf`;
|
|
3898
3958
|
}
|
|
3899
3959
|
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
'
|
|
3960
|
+
const bucket = JSON.parse( process.env.BUCKET );
|
|
3961
|
+
const uploaded = await fileUpload( {
|
|
3962
|
+
fileName: pdfName,
|
|
3963
|
+
Key: 'traxDailyReports/',
|
|
3964
|
+
Bucket: bucket.sop,
|
|
3965
|
+
body: finalBuffer,
|
|
3966
|
+
ContentType: 'application/pdf',
|
|
3904
3967
|
} );
|
|
3968
|
+
const downloadUrl = await signedUrl( { file_path: uploaded.Key, Bucket: bucket.sop } );
|
|
3905
3969
|
|
|
3906
|
-
return res.
|
|
3970
|
+
return res.sendSuccess( { url: downloadUrl, key: uploaded.Key, fileName: pdfName } );
|
|
3907
3971
|
} catch ( e ) {
|
|
3908
3972
|
logger.error( { functionName: 'downloadInsertPdf', error: e } );
|
|
3909
3973
|
return res.sendError( e, 500 );
|
|
@@ -4030,13 +4094,11 @@ export const downloadInsertPdfOld = async ( req, res ) => {
|
|
|
4030
4094
|
},
|
|
4031
4095
|
} );
|
|
4032
4096
|
|
|
4033
|
-
const browser = await
|
|
4034
|
-
|
|
4035
|
-
args: [ '--no-sandbox', '--disable-dev-shm-usage' ],
|
|
4036
|
-
} );
|
|
4097
|
+
const browser = await getBrowserInstance();
|
|
4098
|
+
let page = null;
|
|
4037
4099
|
|
|
4038
4100
|
try {
|
|
4039
|
-
|
|
4101
|
+
page = await browser.newPage();
|
|
4040
4102
|
|
|
4041
4103
|
await page.setViewport( {
|
|
4042
4104
|
width: 1280,
|
|
@@ -4272,7 +4334,7 @@ export const downloadInsertPdfOld = async ( req, res ) => {
|
|
|
4272
4334
|
}
|
|
4273
4335
|
}
|
|
4274
4336
|
} finally {
|
|
4275
|
-
await
|
|
4337
|
+
if ( page ) await page.close().catch( () => {} );
|
|
4276
4338
|
}
|
|
4277
4339
|
|
|
4278
4340
|
// console.log( '🎉 All processing completed' );
|
|
@@ -28,7 +28,7 @@ import isSameOrBefore from 'dayjs/plugin/isSameOrBefore.js';
|
|
|
28
28
|
import * as cameraService from '../services/camera.service.js';
|
|
29
29
|
import * as recurringFlagTracker from '../services/recurringFlagTracker.service.js';
|
|
30
30
|
dayjs.extend( isSameOrBefore );
|
|
31
|
-
import
|
|
31
|
+
import { getBrowser, withPdfSlot } from '../utils/browserPool.utils.js';
|
|
32
32
|
import handlebars from './handlebar-helper.js';
|
|
33
33
|
import fs from 'fs';
|
|
34
34
|
import path from 'path';
|
|
@@ -237,12 +237,13 @@ export async function startChecklist( req, res ) {
|
|
|
237
237
|
} );
|
|
238
238
|
let getupdatedchecklist = await processedchecklist.aggregate( findQuery );
|
|
239
239
|
// let bucket = JSON.parse( process.env.BUCKET );
|
|
240
|
+
const cdnurl = JSON.parse( process.env.CDNURL );
|
|
240
241
|
getupdatedchecklist[0].questionAnswers.forEach( ( section ) => {
|
|
241
|
-
section.questions.forEach(
|
|
242
|
+
section.questions.forEach( ( question ) => {
|
|
242
243
|
if ( question.questionReferenceImage && question.questionReferenceImage!='' ) {
|
|
243
244
|
question.questionReferenceImage = `${cdnurl.TraxAnswerCDN}${question.questionReferenceImage}`;
|
|
244
245
|
}
|
|
245
|
-
question.answers.forEach(
|
|
246
|
+
question.answers.forEach( ( answer ) => {
|
|
246
247
|
if ( answer.referenceImage != '' ) {
|
|
247
248
|
answer.referenceImage = `${cdnurl.TraxAnswerCDN}${answer.referenceImage}`;
|
|
248
249
|
}
|
|
@@ -4216,6 +4217,7 @@ export async function questionList( req, res ) {
|
|
|
4216
4217
|
findQuery.push( {
|
|
4217
4218
|
$project: {
|
|
4218
4219
|
checkListName: { $ifNull: [ '$checkListName', '' ] },
|
|
4220
|
+
checkListDescription: { $ifNull: [ '$checkListDescription', '' ] },
|
|
4219
4221
|
scheduleStartTime: { $ifNull: [ '$scheduleStartTime', '' ] },
|
|
4220
4222
|
scheduleStartTime_iso: { $ifNull: [ '$scheduleStartTime_iso', '' ] },
|
|
4221
4223
|
scheduleEndTime: { $ifNull: [ '$scheduleEndTime', '' ] },
|
|
@@ -4599,6 +4601,7 @@ export async function taskQuestionList( req, res ) {
|
|
|
4599
4601
|
findQuery.push( {
|
|
4600
4602
|
$project: {
|
|
4601
4603
|
checkListName: { $ifNull: [ '$checkListName', '' ] },
|
|
4604
|
+
checkListDescription: { $ifNull: [ '$checkListDescription', '' ] },
|
|
4602
4605
|
scheduleStartTime: { $ifNull: [ '$scheduleStartTime', '' ] },
|
|
4603
4606
|
scheduleStartTime_iso: { $ifNull: [ '$scheduleStartTime_iso', '' ] },
|
|
4604
4607
|
scheduleEndTime: { $ifNull: [ '$scheduleEndTime', '' ] },
|
|
@@ -5244,6 +5247,7 @@ export async function questionListV1( req, res ) {
|
|
|
5244
5247
|
findQuery.push( {
|
|
5245
5248
|
$project: {
|
|
5246
5249
|
checkListName: { $ifNull: [ '$checkListName', '' ] },
|
|
5250
|
+
checkListDescription: { $ifNull: [ '$checkListDescription', '' ] },
|
|
5247
5251
|
scheduleStartTime: { $ifNull: [ '$scheduleStartTime', '' ] },
|
|
5248
5252
|
scheduleStartTime_iso: { $ifNull: [ '$scheduleStartTime_iso', '' ] },
|
|
5249
5253
|
scheduleEndTime: { $ifNull: [ '$scheduleEndTime', '' ] },
|
|
@@ -5507,6 +5511,7 @@ export async function chunkUpload( req, res ) {
|
|
|
5507
5511
|
}
|
|
5508
5512
|
|
|
5509
5513
|
export async function downloadChecklist( req, res ) {
|
|
5514
|
+
let page = null;
|
|
5510
5515
|
try {
|
|
5511
5516
|
let requestData = req.body;
|
|
5512
5517
|
|
|
@@ -5591,54 +5596,53 @@ export async function downloadChecklist( req, res ) {
|
|
|
5591
5596
|
const template = handlebars.compile( templateHtml );
|
|
5592
5597
|
const html = template( { data: checklistDetails } );
|
|
5593
5598
|
|
|
5594
|
-
// Generate PDF
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage' ],
|
|
5598
|
-
} );
|
|
5599
|
+
// Generate PDF on a page from the shared browser pool, capped by withPdfSlot
|
|
5600
|
+
return await withPdfSlot( async () => {
|
|
5601
|
+
const browser = await getBrowser();
|
|
5599
5602
|
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
await page.setContent( html, {
|
|
5603
|
-
waitUntil: 'domcontentloaded',
|
|
5604
|
-
} );
|
|
5603
|
+
page = await browser.newPage();
|
|
5605
5604
|
|
|
5606
|
-
|
|
5605
|
+
await page.setContent( html, {
|
|
5606
|
+
waitUntil: 'domcontentloaded',
|
|
5607
|
+
} );
|
|
5607
5608
|
|
|
5608
|
-
|
|
5609
|
-
const images = Array.from( document.images );
|
|
5609
|
+
/* -------- WAIT FOR ALL IMAGES TO LOAD -------- */
|
|
5610
5610
|
|
|
5611
|
-
await
|
|
5612
|
-
|
|
5613
|
-
if ( img.complete ) return;
|
|
5611
|
+
await page.evaluate( async () => {
|
|
5612
|
+
const images = Array.from( document.images );
|
|
5614
5613
|
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
} ),
|
|
5619
|
-
);
|
|
5620
|
-
} );
|
|
5614
|
+
await Promise.all(
|
|
5615
|
+
images.map( ( img ) => {
|
|
5616
|
+
if ( img.complete ) return;
|
|
5621
5617
|
|
|
5622
|
-
|
|
5618
|
+
return new Promise( ( resolve ) => {
|
|
5619
|
+
img.onload = img.onerror = resolve;
|
|
5620
|
+
} );
|
|
5621
|
+
} ),
|
|
5622
|
+
);
|
|
5623
|
+
} );
|
|
5623
5624
|
|
|
5624
|
-
|
|
5625
|
-
format: 'A4',
|
|
5626
|
-
printBackground: true,
|
|
5627
|
-
} );
|
|
5625
|
+
/* ---------------- GENERATE PDF ---------------- */
|
|
5628
5626
|
|
|
5629
|
-
|
|
5627
|
+
const pdfBuffer = await page.pdf( {
|
|
5628
|
+
format: 'A4',
|
|
5629
|
+
printBackground: true,
|
|
5630
|
+
} );
|
|
5630
5631
|
|
|
5631
|
-
|
|
5632
|
+
/* ---------------- RESPONSE ---------------- */
|
|
5632
5633
|
|
|
5633
|
-
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
|
|
5637
|
-
|
|
5634
|
+
res.setHeader( 'Content-Type', 'application/pdf' );
|
|
5635
|
+
res.setHeader(
|
|
5636
|
+
'Content-Disposition',
|
|
5637
|
+
'attachment; filename=checkListDetails.pdf',
|
|
5638
|
+
);
|
|
5638
5639
|
|
|
5639
|
-
|
|
5640
|
+
return res.end( pdfBuffer );
|
|
5641
|
+
} );
|
|
5640
5642
|
} catch ( e ) {
|
|
5641
5643
|
logger.error( { functionName: 'getChecklistQuestionAnswers', error: e } );
|
|
5642
5644
|
return res.sendError( e, 500 );
|
|
5645
|
+
} finally {
|
|
5646
|
+
if ( page ) await page.close().catch( () => {} );
|
|
5643
5647
|
}
|
|
5644
5648
|
}
|
|
@@ -82,11 +82,13 @@
|
|
|
82
82
|
.q-answer-link{font-size:12px;color:#0085D2;text-decoration:underline;word-break:break-all}
|
|
83
83
|
.q-answer-caption{font-size:11px;color:#666;margin-bottom:4px}
|
|
84
84
|
.q-answer-remarks{font-size:11px;color:#666;margin-top:6px;white-space:pre-line}
|
|
85
|
-
/* User verification (
|
|
85
|
+
/* User verification (Acknowledged by) */
|
|
86
86
|
.user-verify{margin-top:28px;padding-top:18px;border-top:1px solid #d9d9d9;break-inside:avoid}
|
|
87
87
|
.user-verify-title{font-size:15px;font-weight:700;color:#1a1a1a;margin-bottom:14px}
|
|
88
88
|
.user-verify-photo{display:block;width:200px;height:240px;object-fit:cover;border-radius:8px;margin-bottom:10px}
|
|
89
89
|
.user-verify-name{font-size:14px;color:#1a1a1a;font-weight:600}
|
|
90
|
+
.user-verify-ack{font-size:14px;color:#1a1a1a;line-height:1.5;max-width:240px;margin-top:10px}
|
|
91
|
+
.user-verify-ack strong{font-weight:700}
|
|
90
92
|
.user-verify-sign-label{font-size:12px;color:#666;margin-top:16px;margin-bottom:6px;font-weight:600}
|
|
91
93
|
.user-verify-signature{display:block;width:220px;height:auto;max-height:110px;object-fit:contain;border:1px solid #eee;border-radius:6px;padding:6px;background:#fff}
|
|
92
94
|
/* Footer */
|
|
@@ -299,12 +301,12 @@
|
|
|
299
301
|
{{/each}}
|
|
300
302
|
{{#if showUserVerification}}
|
|
301
303
|
<div class="user-verify">
|
|
302
|
-
<div class="user-verify-title">
|
|
304
|
+
<div class="user-verify-title">Acknowledged by</div>
|
|
303
305
|
{{#if userImage}}
|
|
304
|
-
<img class="user-verify-photo" src="{{userImage}}" alt="
|
|
306
|
+
<img class="user-verify-photo" src="{{userImage}}" alt="Acknowledged by photo" />
|
|
305
307
|
{{/if}}
|
|
306
308
|
{{#if submittedBy}}
|
|
307
|
-
<div class="user-verify-
|
|
309
|
+
<div class="user-verify-ack">I, <strong>{{submittedBy}}</strong> acknowledge that the checklist responses are accurate and correct.</div>
|
|
308
310
|
{{/if}}
|
|
309
311
|
</div>
|
|
310
312
|
{{/if}}
|
|
@@ -70,3 +70,43 @@ export async function getBrowser() {
|
|
|
70
70
|
return launchPromise;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
|
|
76
|
+
* Concurrency limiter for PDF generation.
|
|
77
|
+
|
|
78
|
+
* Caps how many PDF jobs (pages/renderers) run at once so a burst of requests
|
|
79
|
+
|
|
80
|
+
* can't spawn unbounded Chrome work and exhaust memory. Configurable via
|
|
81
|
+
|
|
82
|
+
* MAX_CONCURRENT_PDF (default 3). Run each PDF job inside withPdfSlot(); the
|
|
83
|
+
|
|
84
|
+
* slot is always released, even if the task throws.
|
|
85
|
+
|
|
86
|
+
*/
|
|
87
|
+
|
|
88
|
+
const MAX_CONCURRENT_PDF = Number( process.env.MAX_CONCURRENT_PDF ) || 3;
|
|
89
|
+
|
|
90
|
+
let activePdf = 0;
|
|
91
|
+
|
|
92
|
+
const pdfWaiters = [];
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
export async function withPdfSlot( task ) {
|
|
96
|
+
if ( activePdf >= MAX_CONCURRENT_PDF ) {
|
|
97
|
+
await new Promise( ( resolve ) => pdfWaiters.push( resolve ) );
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
activePdf++;
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
return await task();
|
|
104
|
+
} finally {
|
|
105
|
+
activePdf--;
|
|
106
|
+
|
|
107
|
+
const next = pdfWaiters.shift();
|
|
108
|
+
|
|
109
|
+
if ( next ) next();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
@@ -4,7 +4,7 @@ import path from 'path';
|
|
|
4
4
|
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
|
|
7
|
-
import
|
|
7
|
+
import { getBrowser, withPdfSlot } from './browserPool.utils.js';
|
|
8
8
|
|
|
9
9
|
import handlebars from '../controllers/handlebar-helper.js';
|
|
10
10
|
|
|
@@ -1285,14 +1285,19 @@ export function createImageCache() {
|
|
|
1285
1285
|
}
|
|
1286
1286
|
|
|
1287
1287
|
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
const templateHtml = fs.readFileSync( templatePath, 'utf8' );
|
|
1288
|
+
// Compiled once and reused for every request. Reading the .hbs from disk and
|
|
1289
|
+
// recompiling it on each PDF request was pure per-request overhead.
|
|
1290
|
+
// NOTE: editing visit-checklist.hbs now requires a server restart to take effect.
|
|
1291
|
+
let compiledVisitChecklistTemplate = null;
|
|
1293
1292
|
|
|
1293
|
+
export function getCompiledVisitChecklistTemplate() {
|
|
1294
|
+
if ( !compiledVisitChecklistTemplate ) {
|
|
1295
|
+
const templatePath = path.join( __dirname, '../hbs/visit-checklist.hbs' );
|
|
1296
|
+
const templateHtml = fs.readFileSync( templatePath, 'utf8' );
|
|
1297
|
+
compiledVisitChecklistTemplate = handlebars.compile( templateHtml );
|
|
1298
|
+
}
|
|
1294
1299
|
|
|
1295
|
-
return
|
|
1300
|
+
return compiledVisitChecklistTemplate;
|
|
1296
1301
|
}
|
|
1297
1302
|
|
|
1298
1303
|
|
|
@@ -1529,53 +1534,51 @@ export async function generateVisitChecklistPDF( templateData, baseUrl = 'https:
|
|
|
1529
1534
|
const html = template( resolvedData );
|
|
1530
1535
|
|
|
1531
1536
|
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1537
|
+
// Render on a page from the shared browser pool, capped by withPdfSlot
|
|
1538
|
+
return withPdfSlot( async () => {
|
|
1539
|
+
const browser = await getBrowser();
|
|
1535
1540
|
|
|
1536
|
-
|
|
1541
|
+
let page = null;
|
|
1537
1542
|
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
try {
|
|
1542
|
-
const page = await browser.newPage();
|
|
1543
|
+
try {
|
|
1544
|
+
page = await browser.newPage();
|
|
1543
1545
|
|
|
1544
|
-
|
|
1546
|
+
await page.setContent( html, { waitUntil: 'domcontentloaded' } );
|
|
1545
1547
|
|
|
1546
1548
|
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
+
await page.evaluate( async () => {
|
|
1550
|
+
const images = Array.from( document.images );
|
|
1549
1551
|
|
|
1550
|
-
|
|
1552
|
+
await Promise.all(
|
|
1551
1553
|
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
+
images.map( ( img ) => {
|
|
1555
|
+
if ( img.complete ) return;
|
|
1554
1556
|
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1557
|
+
return new Promise( ( resolve ) => {
|
|
1558
|
+
img.onload = img.onerror = resolve;
|
|
1559
|
+
} );
|
|
1560
|
+
} ),
|
|
1559
1561
|
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
+
);
|
|
1563
|
+
} );
|
|
1562
1564
|
|
|
1563
1565
|
|
|
1564
|
-
|
|
1566
|
+
const pdfBuffer = await page.pdf( {
|
|
1565
1567
|
|
|
1566
|
-
|
|
1568
|
+
format: 'A4',
|
|
1567
1569
|
|
|
1568
|
-
|
|
1570
|
+
printBackground: true,
|
|
1569
1571
|
|
|
1570
|
-
|
|
1572
|
+
margin: { top: '10mm', right: '10mm', bottom: '10mm', left: '10mm' },
|
|
1571
1573
|
|
|
1572
|
-
|
|
1574
|
+
} );
|
|
1573
1575
|
|
|
1574
1576
|
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1577
|
+
return pdfBuffer;
|
|
1578
|
+
} finally {
|
|
1579
|
+
if ( page ) await page.close().catch( () => {} );
|
|
1580
|
+
}
|
|
1581
|
+
} );
|
|
1579
1582
|
}
|
|
1580
1583
|
|
|
1581
1584
|
|