tango-app-api-trax 3.9.70 → 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tango-app-api-trax",
3
- "version": "3.9.70",
3
+ "version": "3.9.71",
4
4
  "description": "Trax",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -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 puppeteer from 'puppeteer';
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
- // const query = {
3774
- // query: {
3775
- // bool: {
3776
- // must: [
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 = { ...aiDetails?.toObject() };
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, detectionPayload ),
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['historyData'] = complianceData.data;
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) Create page and render images are already base64 so use domcontentloaded
3839
- page = await browser.newPage();
3840
- await page.setViewport( { width: 1280, height: 1800 } );
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
- try {
3843
- await page.setContent( html, {
3844
- waitUntil: 'domcontentloaded',
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
- if ( !pdfBuffer || pdfBuffer.length === 0 ) {
3881
- logger.error( { functionName: 'downloadInsertPdf', message: 'Empty PDF', docId: doc._id } );
3882
- return res.sendError( 'PDF generation resulted in empty buffer', 500 );
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
- const finalBuffer = Buffer.isBuffer( pdfBuffer ) ?
3886
- pdfBuffer :
3887
- Buffer.from( pdfBuffer );
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
- res.set( {
3901
- 'Content-Type': 'application/pdf',
3902
- 'Content-Disposition': `attachment; filename="${pdfName}"`,
3903
- 'Content-Length': finalBuffer.length,
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.send( finalBuffer );
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 puppeteer.launch( {
4034
- headless: 'new',
4035
- args: [ '--no-sandbox', '--disable-dev-shm-usage' ],
4036
- } );
4097
+ const browser = await getBrowserInstance();
4098
+ let page = null;
4037
4099
 
4038
4100
  try {
4039
- const page = await browser.newPage();
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 browser.close();
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 puppeteer from 'puppeteer';
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( async ( question ) => {
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( async ( answer ) => {
246
+ question.answers.forEach( ( answer ) => {
246
247
  if ( answer.referenceImage != '' ) {
247
248
  answer.referenceImage = `${cdnurl.TraxAnswerCDN}${answer.referenceImage}`;
248
249
  }
@@ -5510,6 +5511,7 @@ export async function chunkUpload( req, res ) {
5510
5511
  }
5511
5512
 
5512
5513
  export async function downloadChecklist( req, res ) {
5514
+ let page = null;
5513
5515
  try {
5514
5516
  let requestData = req.body;
5515
5517
 
@@ -5594,54 +5596,53 @@ export async function downloadChecklist( req, res ) {
5594
5596
  const template = handlebars.compile( templateHtml );
5595
5597
  const html = template( { data: checklistDetails } );
5596
5598
 
5597
- // Generate PDF using puppeteer instead of deprecated html-pdf
5598
- const browser = await puppeteer.launch( {
5599
- headless: 'new',
5600
- args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage' ],
5601
- } );
5599
+ // Generate PDF on a page from the shared browser pool, capped by withPdfSlot
5600
+ return await withPdfSlot( async () => {
5601
+ const browser = await getBrowser();
5602
5602
 
5603
- const page = await browser.newPage();
5604
-
5605
- await page.setContent( html, {
5606
- waitUntil: 'domcontentloaded',
5607
- } );
5603
+ page = await browser.newPage();
5608
5604
 
5609
- /* -------- WAIT FOR ALL IMAGES TO LOAD -------- */
5605
+ await page.setContent( html, {
5606
+ waitUntil: 'domcontentloaded',
5607
+ } );
5610
5608
 
5611
- await page.evaluate( async () => {
5612
- const images = Array.from( document.images );
5609
+ /* -------- WAIT FOR ALL IMAGES TO LOAD -------- */
5613
5610
 
5614
- await Promise.all(
5615
- images.map( ( img ) => {
5616
- if ( img.complete ) return;
5611
+ await page.evaluate( async () => {
5612
+ const images = Array.from( document.images );
5617
5613
 
5618
- return new Promise( ( resolve ) => {
5619
- img.onload = img.onerror = resolve;
5620
- } );
5621
- } ),
5622
- );
5623
- } );
5614
+ await Promise.all(
5615
+ images.map( ( img ) => {
5616
+ if ( img.complete ) return;
5624
5617
 
5625
- /* ---------------- GENERATE PDF ---------------- */
5618
+ return new Promise( ( resolve ) => {
5619
+ img.onload = img.onerror = resolve;
5620
+ } );
5621
+ } ),
5622
+ );
5623
+ } );
5626
5624
 
5627
- const pdfBuffer = await page.pdf( {
5628
- format: 'A4',
5629
- printBackground: true,
5630
- } );
5625
+ /* ---------------- GENERATE PDF ---------------- */
5631
5626
 
5632
- await browser.close();
5627
+ const pdfBuffer = await page.pdf( {
5628
+ format: 'A4',
5629
+ printBackground: true,
5630
+ } );
5633
5631
 
5634
- /* ---------------- RESPONSE ---------------- */
5632
+ /* ---------------- RESPONSE ---------------- */
5635
5633
 
5636
- res.setHeader( 'Content-Type', 'application/pdf' );
5637
- res.setHeader(
5638
- 'Content-Disposition',
5639
- 'attachment; filename=checkListDetails.pdf',
5640
- );
5634
+ res.setHeader( 'Content-Type', 'application/pdf' );
5635
+ res.setHeader(
5636
+ 'Content-Disposition',
5637
+ 'attachment; filename=checkListDetails.pdf',
5638
+ );
5641
5639
 
5642
- return res.end( pdfBuffer );
5640
+ return res.end( pdfBuffer );
5641
+ } );
5643
5642
  } catch ( e ) {
5644
5643
  logger.error( { functionName: 'getChecklistQuestionAnswers', error: e } );
5645
5644
  return res.sendError( e, 500 );
5645
+ } finally {
5646
+ if ( page ) await page.close().catch( () => {} );
5646
5647
  }
5647
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 (Submitted By) */
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">Submitted By</div>
304
+ <div class="user-verify-title">Acknowledged by</div>
303
305
  {{#if userImage}}
304
- <img class="user-verify-photo" src="{{userImage}}" alt="Submitted by photo" />
306
+ <img class="user-verify-photo" src="{{userImage}}" alt="Acknowledged by photo" />
305
307
  {{/if}}
306
308
  {{#if submittedBy}}
307
- <div class="user-verify-name">{{userSignature}}</div>
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 puppeteer from 'puppeteer';
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
- export function getCompiledVisitChecklistTemplate() {
1289
- const templatePath = path.join( __dirname, '../hbs/visit-checklist.hbs' );
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 handlebars.compile( templateHtml );
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
- const browser = await puppeteer.launch( {
1533
-
1534
- headless: 'new',
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
- args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage' ],
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
- await page.setContent( html, { waitUntil: 'domcontentloaded' } );
1546
+ await page.setContent( html, { waitUntil: 'domcontentloaded' } );
1545
1547
 
1546
1548
 
1547
- await page.evaluate( async () => {
1548
- const images = Array.from( document.images );
1549
+ await page.evaluate( async () => {
1550
+ const images = Array.from( document.images );
1549
1551
 
1550
- await Promise.all(
1552
+ await Promise.all(
1551
1553
 
1552
- images.map( ( img ) => {
1553
- if ( img.complete ) return;
1554
+ images.map( ( img ) => {
1555
+ if ( img.complete ) return;
1554
1556
 
1555
- return new Promise( ( resolve ) => {
1556
- img.onload = img.onerror = resolve;
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
- const pdfBuffer = await page.pdf( {
1566
+ const pdfBuffer = await page.pdf( {
1565
1567
 
1566
- format: 'A4',
1568
+ format: 'A4',
1567
1569
 
1568
- printBackground: true,
1570
+ printBackground: true,
1569
1571
 
1570
- margin: { top: '10mm', right: '10mm', bottom: '10mm', left: '10mm' },
1572
+ margin: { top: '10mm', right: '10mm', bottom: '10mm', left: '10mm' },
1571
1573
 
1572
- } );
1574
+ } );
1573
1575
 
1574
1576
 
1575
- return pdfBuffer;
1576
- } finally {
1577
- await browser.close();
1578
- }
1577
+ return pdfBuffer;
1578
+ } finally {
1579
+ if ( page ) await page.close().catch( () => {} );
1580
+ }
1581
+ } );
1579
1582
  }
1580
1583
 
1581
1584