Compare commits

...

26 Commits

Author SHA1 Message Date
Dave Richer
bfde72eed8 feature/IO-3052-Skia-Canvas-Handler: Fix missing checks
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-12-05 12:29:11 -08:00
Dave Richer
8fbd08d57f Merge branch 'feature/IO-3052-Skia-Canvas-Handler' of bitbucket.org:snaptsoft/bodyshop into feature/IO-3052-Skia-Canvas-Handler 2024-12-05 12:16:47 -08:00
Dave Richer
20bddb43b6 feature/IO-3052-Skia-Canvas-Handler: Fix missing checks
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-12-05 12:16:32 -08:00
Dave Richer
b38e0f611b Merged release/2024-12-06 into feature/IO-3052-Skia-Canvas-Handler 2024-12-05 20:14:53 +00:00
Dave Richer
c84fbcaba1 feature/IO-3052-Skia-Canvas-Handler: Optimizations
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-12-05 12:14:06 -08:00
Dave Richer
8f752d575a feature/IO-3052-Skia-Canvas-Handler: Optimizations
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-12-05 12:13:49 -08:00
Patrick Fic
2fe9ae513d Merged in feature/IO-3053-datadog (pull request #1999)
IO-3053 Add datadog watcher for Production and Test instances.
2024-12-05 20:10:24 +00:00
Dave Richer
a04dcffc4c feature/IO-3052-Skia-Canvas-Handler: Merge release and fix PR's
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-12-05 12:01:58 -08:00
Dave Richer
50c99f7a1e feature/IO-3052-Skia-Canvas-Handler: Cleanup
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-12-05 11:52:14 -08:00
Dave Richer
86f3179bc0 feature/IO-3052-Skia-Canvas-Handler: Initial commit
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-12-05 11:26:36 -08:00
Dave Richer
6336e7568f feature/IO-3052-Skia-Canvas-Handler: Initial commit
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-12-05 11:26:23 -08:00
Allan Carr
f0f199335c Merged in feature/IO-3050-QBO-BillEmail (pull request #1995)
IO-3050 QBO BillEmail required if NeedToSend

Approved-by: Dave Richer
2024-12-05 17:23:18 +00:00
Allan Carr
9c7c9f4b6d Merged in feature/IO-3042-Jobs-Marked-Total-Loss (pull request #1996)
IO-3042 Jobs Marked as Total Loss

Approved-by: Dave Richer
2024-12-05 17:23:01 +00:00
Allan Carr
9001ceaed8 Merged in feature/IO-3051-canvas-handler-optimization (pull request #1994)
IO-3051 canvas-handler optimization

Approved-by: Dave Richer
2024-12-05 17:22:42 +00:00
Allan Carr
ab82e85c57 Merge branch 'release/2024-12-06' into feature/IO-3042-Jobs-Marked-Total-Loss
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>

# Conflicts:
#	client/src/utils/TemplateConstants.js
2024-12-04 18:33:35 -08:00
Allan Carr
2effe5ef50 IO-3042 Jobs Marked as Total Loss
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-12-04 18:30:22 -08:00
Allan Carr
006a2a5dca IO-3050 QBO BillEmail required if NeedToSend
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-12-04 15:59:05 -08:00
Allan Carr
a885bdec74 IO-3051 canvas-handler optimization
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-12-04 14:22:04 -08:00
Dave Richer
5d7eabbfa9 Merged in feature/IO-3048-Fix-Job-Bug-Messaging (pull request #1992)
feature/IO-3048-Fix-Job-Bug-Messaging - Unread count
2024-12-03 22:01:37 +00:00
Dave Richer
3a6af12446 Merged in feature/IO-3048-Fix-Job-Bug-Messaging (pull request #1990)
feature/IO-3048-Fix-Job-Bug-Messaging - Do not allow more than 1 of the same job to be associated with a conversation
2024-12-03 20:24:09 +00:00
Dave Richer
ca462f51ec Merged in feature/IO-3048-Fix-Job-Bug-Messaging (pull request #1987)
feature/IO-3048-Fix-Job-Bug-Messaging - Do not allow more than 1 of the same job to be associated with a conversation
2024-12-03 18:40:29 +00:00
Dave Richer
8ed81e9aed Merged in feature/IO-3048-Fix-Job-Bug-Messaging (pull request #1984)
feature/IO-3048-Fix-Job-Bug-Messaging - Fix tag weirdness and a vite error

Approved-by: Patrick Fic
2024-12-03 17:55:30 +00:00
Allan Carr
aad22f2e2d Merged in feature/IO-3047-accountingid-on-Owner-Page (pull request #1982)
IO-3047 Accounting ID on Owner Page

Approved-by: Dave Richer
2024-12-02 20:30:50 +00:00
Allan Carr
7a11b18037 Merged in feature/IO-3046-purchase_return_ratio_excel (pull request #1983)
IO-3046 purchase_return_ratio_excel

Approved-by: Dave Richer
2024-12-02 20:30:16 +00:00
Allan Carr
241322fa30 IO-3046 purchase_return_ratio_excel
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-12-02 11:09:30 -08:00
Allan Carr
f0461270de IO-3047 Accounting ID on Owner Page
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-12-02 08:52:18 -08:00
13 changed files with 296 additions and 55 deletions

View File

@@ -7,7 +7,6 @@ RUN dnf install -y git \
&& dnf install -y nodejs \
&& dnf clean all
# Install dependencies required by node-canvas
RUN dnf install -y \
gcc \
@@ -19,9 +18,22 @@ RUN dnf install -y \
libpng-devel \
make \
python3 \
fontconfig \
freetype \
python3-pip \
wget \
unzip \
&& dnf clean all
# Install Montserrat fonts
RUN cd /tmp \
&& wget https://images.imex.online/fonts/montserrat.zip -O montserrat.zip \
&& unzip montserrat.zip -d montserrat \
&& mv montserrat/montserrat/*.ttf /usr/share/fonts \
&& fc-cache -fv \
&& rm -rf /tmp/montserrat /tmp/montserrat.zip \
&& echo "Montserrat fonts installed and cached successfully."
# Set the working directory
WORKDIR /app

View File

@@ -25,6 +25,9 @@ export default function OwnerDetailFormComponent({ form, loading }) {
<Form.Item label={t("owners.fields.ownr_co_nm")} name="ownr_co_nm">
<Input />
</Form.Item>
<Form.Item label={t("owners.fields.accountingid")} name="accountingid">
<Input disabled/>
</Form.Item>
</LayoutFormRow>
<LayoutFormRow header={t("owners.forms.address")}>
<Form.Item label={t("owners.fields.ownr_addr1")} name="ownr_addr1">

View File

@@ -48,6 +48,7 @@ export const QUERY_OWNER_BY_ID = gql`
query QUERY_OWNER_BY_ID($id: uuid!) {
owners_by_pk(id: $id) {
id
accountingid
allow_text_message
ownr_addr1
ownr_addr2

View File

@@ -2394,6 +2394,7 @@
"selectexistingornew": "Select an existing owner record or create a new one. "
},
"fields": {
"accountingid": "Accounting ID",
"address": "Address",
"allow_text_message": "Permission to Text?",
"name": "Name",
@@ -3057,6 +3058,7 @@
"production_not_production_status": "Production not in Production Status",
"production_over_time": "Production Level over Time",
"psr_by_make": "Percent of Sales by Vehicle Make",
"purchase_return_ratio_excel": "Purchase & Return Ratio - Excel",
"purchase_return_ratio_grouped_by_vendor_detail": "Purchase & Return Ratio by Vendor (Detail)",
"purchase_return_ratio_grouped_by_vendor_summary": "Purchase & Return Ratio by Vendor (Summary)",
"purchases_by_cost_center_detail": "Purchases by Cost Center (Detail)",
@@ -3082,6 +3084,7 @@
"timetickets": "Time Tickets",
"timetickets_employee": "Employee Time Tickets",
"timetickets_summary": "Time Tickets Summary",
"total_loss_jobs": "Jobs Marked as Total Loss",
"unclaimed_hrs": "Unflagged Hours",
"void_ros": "Void ROs",
"work_in_progress_committed_labour": "Work in Progress - Committed Labor",

View File

@@ -2394,6 +2394,7 @@
"selectexistingornew": ""
},
"fields": {
"accountingid": "",
"address": "Dirección",
"allow_text_message": "Permiso de texto?",
"name": "Nombre",
@@ -3057,6 +3058,7 @@
"production_not_production_status": "",
"production_over_time": "",
"psr_by_make": "",
"purchase_return_ratio_excel": "",
"purchase_return_ratio_grouped_by_vendor_detail": "",
"purchase_return_ratio_grouped_by_vendor_summary": "",
"purchases_by_cost_center_detail": "",
@@ -3082,6 +3084,7 @@
"timetickets": "",
"timetickets_employee": "",
"timetickets_summary": "",
"total_loss_jobs": "",
"unclaimed_hrs": "",
"void_ros": "",
"work_in_progress_committed_labour": "",

View File

@@ -2394,6 +2394,7 @@
"selectexistingornew": ""
},
"fields": {
"accountingid": "",
"address": "Adresse",
"allow_text_message": "Autorisation de texte?",
"name": "Prénom",
@@ -3057,6 +3058,7 @@
"production_not_production_status": "",
"production_over_time": "",
"psr_by_make": "",
"purchase_return_ratio_excel": "",
"purchase_return_ratio_grouped_by_vendor_detail": "",
"purchase_return_ratio_grouped_by_vendor_summary": "",
"purchases_by_cost_center_detail": "",
@@ -3082,6 +3084,7 @@
"timetickets": "",
"timetickets_employee": "",
"timetickets_summary": "",
"total_loss_jobs": "",
"unclaimed_hrs": "",
"void_ros": "",
"work_in_progress_committed_labour": "",

View File

@@ -2184,6 +2184,30 @@ export const TemplateList = (type, context) => {
},
group: "payroll",
adp_payroll: true
},
purchase_return_ratio_excel: {
title: i18n.t("reportcenter.templates.purchase_return_ratio_excel"),
subject: i18n.t("reportcenter.templates.purchase_return_ratio_excel"),
key: "purchase_return_ratio_excel",
//idtype: "vendor",
reporttype: "excel",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.bills"),
field: i18n.t("bills.fields.date")
},
group: "purchases"
},
total_loss_jobs: {
title: i18n.t("reportcenter.templates.total_loss_jobs"),
subject: i18n.t("reportcenter.templates.total_loss_jobs"),
key: "total_loss_jobs",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.jobs"),
field: i18n.t("jobs.fields.date_open")
},
group: "jobs"
}
}
: {}),

98
package-lock.json generated
View File

@@ -53,6 +53,7 @@
"recursive-diff": "^1.0.9",
"redis": "^4.7.0",
"rimraf": "^6.0.1",
"skia-canvas": "^2.0.0",
"soap": "^1.1.6",
"socket.io": "^4.8.1",
"socket.io-adapter": "^2.5.5",
@@ -3875,6 +3876,15 @@
"node": ">=6"
}
},
"node_modules/cargo-cp-artifact": {
"version": "0.1.9",
"resolved": "https://registry.npmjs.org/cargo-cp-artifact/-/cargo-cp-artifact-0.1.9.tgz",
"integrity": "sha512-6F+UYzTaGB+awsTXg0uSJA1/b/B3DDJzpKVRu0UmyI7DmNeaAl2RFHuTGIN6fEgpadRxoXGb7gbC1xo4C3IdyA==",
"license": "MIT",
"bin": {
"cargo-cp-artifact": "bin/cargo-cp-artifact.js"
}
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -7136,6 +7146,12 @@
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz",
"integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw=="
},
"node_modules/parenthesis": {
"version": "3.1.8",
"resolved": "https://registry.npmjs.org/parenthesis/-/parenthesis-3.1.8.tgz",
"integrity": "sha512-KF/U8tk54BgQewkJPvB4s/US3VQY68BRDpH638+7O/n58TpnwiwnOtGIOsT2/i+M78s61BBpeC83STB88d8sqw==",
"license": "MIT"
},
"node_modules/parse5": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
@@ -7168,6 +7184,12 @@
"node": ">= 0.8"
}
},
"node_modules/path-browserify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
"license": "MIT"
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
@@ -7847,6 +7869,73 @@
"is-arrayish": "^0.3.1"
}
},
"node_modules/skia-canvas": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/skia-canvas/-/skia-canvas-2.0.0.tgz",
"integrity": "sha512-wpYkmr9mCxBme5HAnlm6YOEiuaN9tIm9CL+HN8e5AFD4K2FAJXCcWiWvc9+LM8jUXt+AyYXgiwUTBxdQ6P+PEg==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@mapbox/node-pre-gyp": "^1.0.11",
"cargo-cp-artifact": "^0.1",
"glob": "^11.0.0",
"path-browserify": "^1.0.1",
"simple-get": "^4.0.1",
"string-split-by": "^1.0.0"
}
},
"node_modules/skia-canvas/node_modules/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"license": "MIT",
"dependencies": {
"mimic-response": "^3.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/skia-canvas/node_modules/mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/skia-canvas/node_modules/simple-get": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"decompress-response": "^6.0.0",
"once": "^1.3.1",
"simple-concat": "^1.0.0"
}
},
"node_modules/slick": {
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/slick/-/slick-1.12.2.tgz",
@@ -8239,6 +8328,15 @@
}
]
},
"node_modules/string-split-by": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/string-split-by/-/string-split-by-1.0.0.tgz",
"integrity": "sha512-KaJKY+hfpzNyet/emP81PJA9hTVSfxNLS9SFTWxdCnnW1/zOOwiV248+EfoX7IQFcBaOp4G5YE6xTJMF+pLg6A==",
"license": "MIT",
"dependencies": {
"parenthesis": "^3.1.5"
}
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",

View File

@@ -63,6 +63,7 @@
"recursive-diff": "^1.0.9",
"redis": "^4.7.0",
"rimraf": "^6.0.1",
"skia-canvas": "^2.0.0",
"soap": "^1.1.6",
"socket.io": "^4.8.1",
"socket.io-adapter": "^2.5.5",

View File

@@ -328,6 +328,7 @@ async function InsertOwner(oauthClient, qbo_realmId, req, job, isThreeTier, pare
PostalCode: job.ownr_zip,
CountrySubDivisionCode: job.ownr_st
},
...(job.ownr_ea ? { BillEmail: { Address: job.ownr_ea.trim() } } : {}),
...(isThreeTier
? {
Job: true,
@@ -395,7 +396,7 @@ async function InsertJob(oauthClient, qbo_realmId, req, job, parentTierRef) {
PostalCode: job.ownr_zip,
CountrySubDivisionCode: job.ownr_st
},
...(job.ownr_ea ? { BillEmail: { Address: job.ownr_ea.trim() } } : {}),
Job: true,
ParentRef: {
value: parentTierRef.Id
@@ -556,7 +557,8 @@ async function InsertInvoice(oauthClient, qbo_realmId, req, job, bodyshop, paren
Line3: `${job.ownr_city || ""}, ${job.ownr_st || ""} ${job.ownr_zip || ""}`.trim(),
Line2: job.ownr_addr1 || "",
Line1: `${job.ownr_fn || ""} ${job.ownr_ln || ""} ${job.ownr_co_nm || ""}`
}
},
...(job.ownr_ea ? { BillEmail: { Address: job.ownr_ea.trim() } } : {})
};
logger.log("qbo-receivable-objectlog", "DEBUG", req.user.email, job.id, {
@@ -673,7 +675,8 @@ async function InsertInvoiceMultiPayerInvoice(
Line3: `${job.ownr_city || ""}, ${job.ownr_st || ""} ${job.ownr_zip || ""}`.trim(),
Line2: job.ownr_addr1 || "",
Line1: `${job.ownr_fn || ""} ${job.ownr_ln || ""} ${job.ownr_co_nm || ""}`
}
},
...(job.ownr_ea ? { BillEmail: { Address: job.ownr_ea.trim() } } : {})
};
logger.log("qbo-receivable-objectlog", "DEBUG", req.user.email, job.id, {

View File

@@ -0,0 +1,32 @@
const { isObject } = require("lodash");
const validateCanvasInputMiddleware = (req, res, next) => {
const { values, keys, override, w, h } = req.body;
if (!Array.isArray(values) || !Array.isArray(keys)) {
return res.status(400).send("Invalid input: 'values' and 'keys' must be arrays.");
}
if (values.some((value) => typeof value !== "number")) {
return res.status(400).send("Invalid input: 'values' must be an array of numbers.");
}
if (keys.some((key) => typeof key !== "string")) {
return res.status(400).send("Invalid input: 'keys' must be an array of strings.");
}
if (override && !isObject(override)) {
return res.status(400).send("Override must be an object");
}
if (w && (!Number.isFinite(w) || w <= 0)) {
return res.status(400).send("Width must be a positive number");
}
if (h && (!Number.isFinite(h) || h <= 0)) {
return res.status(400).send("Height must be a positive number");
}
next(); // Proceed to the next middleware or route handler
};
module.exports = validateCanvasInputMiddleware;

View File

@@ -1,43 +1,31 @@
const { createCanvas } = require("canvas");
const { Canvas, FontLibrary } = require("skia-canvas");
const Chart = require("chart.js/auto");
const logger = require("../utils/logger");
const { backgroundColors, borderColors } = require("./canvas-colors");
const { isObject, defaultsDeep, isNumber } = require("lodash");
const { defaultsDeep, isNumber } = require("lodash");
exports.canvastest = function (req, res) {
//console.log("Incoming test request.", req);
res.status(200).send("OK");
};
const CANVAS_QUEUE_LIMIT = 100;
exports.canvas = function (req, res) {
const { w, h, values, keys, override } = req.body;
//console.log("Incoming Canvas Request:", w, h, values, keys, override);
logger.log("inbound-canvas-creation", "debug", "jsr", null, { w, h, values, keys, override });
// Gate required values
if (!values || !keys) {
res.status(400).send("Missing required data");
return;
}
let isProcessing = false;
const requestQueue = [];
// Override must be an object if it exists
if (override && !isObject(override)) {
res.status(400).send("Override must be an object");
return;
}
try {
FontLibrary.use("Montserrat", [
"/usr/share/fonts/Montserrat-Regular.ttf",
"/usr/share/fonts/Montserrat-Bold.ttf",
"/usr/share/fonts/Montserrat-Italic.ttf"
]);
} catch (error) {
console.error(
"Error loading fonts Skia Canvas Fonts, please be sure to install Montserrat font package",
error.message
);
}
// Set the default Width and Height
let [width, height] = [500, 275];
// Allow for custom width and height
if (isNumber(w)) {
width = w;
}
if (isNumber(h)) {
height = h;
}
const configuration = {
// Utility to create a chart configuration
const getChartConfiguration = (keys, values, override) => {
const defaultConfiguration = {
type: "doughnut",
data: {
labels: keys,
@@ -53,6 +41,7 @@ exports.canvas = function (req, res) {
options: {
devicePixelRatio: 4,
responsive: false,
animation: false,
maintainAspectRatio: true,
circumference: 180,
rotation: -90,
@@ -73,21 +62,88 @@ exports.canvas = function (req, res) {
}
};
// If we have a valid override object, merge it with the default configuration object.
// This allows for you to override the default configuration with a custom one.
const defaults = () => {
if (!override || !isObject(override)) {
return configuration;
}
return defaultsDeep(override, configuration);
};
res.status(200).send(
(() => {
const canvas = createCanvas(width, height);
const ctx = canvas.getContext("2d");
new Chart(ctx, defaults());
return canvas.toDataURL();
})()
);
return defaultsDeep(override || {}, defaultConfiguration);
};
const processCanvasRequest = async (req, res, isSkia = false) => {
const { logger } = req;
const { w, h, values, keys, override } = req.body;
logger.log("inbound-canvas-creation", "debug", "jsr", null, { w, h, values, keys, override });
// Default width and height
const width = isNumber(w) && w > 0 ? w : 500;
const height = isNumber(h) && h > 0 ? h : 275;
const configuration = getChartConfiguration(keys, values, override);
// Placeholders to allow fine control over GAC
let canvas = null;
let ctx = null;
let chart = null;
let chartImage = null;
try {
// Create the canvas
canvas = isSkia ? new Canvas(width, height) : createCanvas(width, height);
ctx = canvas.getContext("2d");
// Render the chart
chart = new Chart(ctx, configuration);
// Generate and send the image
chartImage = isSkia ? (await canvas.toBuffer("image/png")).toString("base64") : canvas.toDataURL();
res.status(200).send(isSkia ? `data:image/png;base64,${chartImage}` : chartImage);
} catch (error) {
// Log the error and send the response
logger.log("canvas-error", "error", "jsr", null, { error: error.message });
res.status(500).send("Failed to generate canvas.");
} finally {
// Cleanup resources
if (chart) {
chart.destroy();
}
ctx = null; // Explicitly nullify for garbage collection
canvas = null; // Explicitly nullify for garbage collection
chartImage = null;
}
};
const enqueueRequest = (req, res, isSkia) => {
if (requestQueue.length >= CANVAS_QUEUE_LIMIT) {
res.status(503).send("Server is busy. Please try again later.");
return false;
}
requestQueue.push({ req, res, isSkia });
req.logger.log("inbound-canvas-creation-queue", "debug", "jsr", null, { queue: requestQueue.length });
return true;
};
const processNextInQueue = async () => {
while (requestQueue.length > 0) {
const { req, res, isSkia } = requestQueue.shift();
try {
await processCanvasRequest(req, res, isSkia);
} catch (err) {
console.error("canvas-queue-error", "error", "jsr", null, { error: err.message });
}
}
isProcessing = false;
};
exports.canvastest = function (req, res) {
res.status(200).send("OK");
};
exports.canvas = async (req, res) => {
if (isProcessing || !enqueueRequest(req, res, false)) return;
isProcessing = true;
processNextInQueue().catch((err) => console.error("canvas-processing-error", { error: err.message }));
};
exports.canvasSkia = async (req, res) => {
if (isProcessing || !enqueueRequest(req, res, true)) return;
isProcessing = true;
processNextInQueue().catch((err) => console.error("canvas-processing-error", { error: err.message }));
};

View File

@@ -2,10 +2,12 @@ const express = require("express");
const router = express.Router();
const { inlinecss } = require("../render/inlinecss");
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
const { canvas } = require("../render/canvas-handler");
const { canvas, canvasSkia } = require("../render/canvas-handler");
const validateCanvasInputMiddleware = require("../middleware/validateCanvasInputMiddleware");
// Define the route for inline CSS rendering
router.post("/inlinecss", validateFirebaseIdTokenMiddleware, inlinecss);
router.post("/canvas", validateFirebaseIdTokenMiddleware, canvas);
router.post("/canvas", [validateFirebaseIdTokenMiddleware, validateCanvasInputMiddleware], canvas);
router.post("/canvas-skia", [validateFirebaseIdTokenMiddleware, validateCanvasInputMiddleware], canvasSkia);
module.exports = router;