From c5b19d8f222522ce9da8d0dcd92d813409a5368e Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Fri, 27 Sep 2024 19:20:19 -0400 Subject: [PATCH 01/37] hotfix/IO-2969-Fonts-For-Production - Register fonts Signed-off-by: Dave Richer --- .ebextensions/00-install-fonts.config | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .ebextensions/00-install-fonts.config diff --git a/.ebextensions/00-install-fonts.config b/.ebextensions/00-install-fonts.config new file mode 100644 index 000000000..cc8415d4d --- /dev/null +++ b/.ebextensions/00-install-fonts.config @@ -0,0 +1,15 @@ +packages: + dnf: + fontconfig: [] + freetype: [] + google-noto-sans-fonts: [] # Google Noto fonts for wide language support + dejavu-sans-fonts: [] # Common sans-serif fonts compatible with Chart.js + liberation-fonts: [] # Another sans-serif fallback font collection + +container_commands: + 01_install_montserrat: + command: | + cd /tmp + curl -O https://fonts.google.com/download?family=Montserrat + unzip Montserrat.zip -d /usr/share/fonts/montserrat + fc-cache -fv # Rebuild the font cache to include Montserrat From 5b00ded5f6520bb3ac95f3772de034d63442042a Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Fri, 27 Sep 2024 19:29:26 -0400 Subject: [PATCH 02/37] hotfix/IO-2969-Fonts-For-Production - Register fonts Signed-off-by: Dave Richer --- .ebextensions/00-install-fonts.config | 15 --------------- .platform/hooks/postdeploy/00-install-fonts.sh | 13 +++++++++++++ 2 files changed, 13 insertions(+), 15 deletions(-) delete mode 100644 .ebextensions/00-install-fonts.config create mode 100644 .platform/hooks/postdeploy/00-install-fonts.sh diff --git a/.ebextensions/00-install-fonts.config b/.ebextensions/00-install-fonts.config deleted file mode 100644 index cc8415d4d..000000000 --- a/.ebextensions/00-install-fonts.config +++ /dev/null @@ -1,15 +0,0 @@ -packages: - dnf: - fontconfig: [] - freetype: [] - google-noto-sans-fonts: [] # Google Noto fonts for wide language support - dejavu-sans-fonts: [] # Common sans-serif fonts compatible with Chart.js - liberation-fonts: [] # Another sans-serif fallback font collection - -container_commands: - 01_install_montserrat: - command: | - cd /tmp - curl -O https://fonts.google.com/download?family=Montserrat - unzip Montserrat.zip -d /usr/share/fonts/montserrat - fc-cache -fv # Rebuild the font cache to include Montserrat diff --git a/.platform/hooks/postdeploy/00-install-fonts.sh b/.platform/hooks/postdeploy/00-install-fonts.sh new file mode 100644 index 000000000..f9b766ef5 --- /dev/null +++ b/.platform/hooks/postdeploy/00-install-fonts.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +# Install fonts using dnf +dnf install -y fontconfig freetype google-noto-sans-fonts dejavu-sans-fonts liberation-fonts + +# Download and install Montserrat font +cd /tmp +curl -O https://fonts.google.com/download?family=Montserrat +unzip Montserrat.zip -d /usr/share/fonts/montserrat +fc-cache -fv # Rebuild font cache to include Montserrat + +# Ensure permissions are correct +chmod -R 755 /usr/share/fonts/montserrat From db5dcc271d675431b31e4cd05a2d86c02f50c0bb Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Fri, 1 Nov 2024 20:35:44 -0700 Subject: [PATCH 03/37] IO-30015 add new indexes to production. --- .../1730516697278_idx_timetickets_date/down.sql | 3 +++ .../migrations/1730516697278_idx_timetickets_date/up.sql | 1 + .../migrations/1730517308367_run_sql_migration/down.sql | 9 +++++++++ hasura/migrations/1730517308367_run_sql_migration/up.sql | 7 +++++++ .../migrations/1730518121867_run_sql_migration/down.sql | 3 +++ hasura/migrations/1730518121867_run_sql_migration/up.sql | 1 + 6 files changed, 24 insertions(+) create mode 100644 hasura/migrations/1730516697278_idx_timetickets_date/down.sql create mode 100644 hasura/migrations/1730516697278_idx_timetickets_date/up.sql create mode 100644 hasura/migrations/1730517308367_run_sql_migration/down.sql create mode 100644 hasura/migrations/1730517308367_run_sql_migration/up.sql create mode 100644 hasura/migrations/1730518121867_run_sql_migration/down.sql create mode 100644 hasura/migrations/1730518121867_run_sql_migration/up.sql diff --git a/hasura/migrations/1730516697278_idx_timetickets_date/down.sql b/hasura/migrations/1730516697278_idx_timetickets_date/down.sql new file mode 100644 index 000000000..f309d3260 --- /dev/null +++ b/hasura/migrations/1730516697278_idx_timetickets_date/down.sql @@ -0,0 +1,3 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- CREATE INDEX idx_timetickets_date ON timetickets (date ); diff --git a/hasura/migrations/1730516697278_idx_timetickets_date/up.sql b/hasura/migrations/1730516697278_idx_timetickets_date/up.sql new file mode 100644 index 000000000..604b60dc1 --- /dev/null +++ b/hasura/migrations/1730516697278_idx_timetickets_date/up.sql @@ -0,0 +1 @@ +CREATE INDEX idx_timetickets_date ON timetickets (date ); diff --git a/hasura/migrations/1730517308367_run_sql_migration/down.sql b/hasura/migrations/1730517308367_run_sql_migration/down.sql new file mode 100644 index 000000000..8df0a9af1 --- /dev/null +++ b/hasura/migrations/1730517308367_run_sql_migration/down.sql @@ -0,0 +1,9 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- CREATE INDEX idx_jobs_ownr_fn ON jobs USING gin (ownr_fn gin_trgm_ops); +-- CREATE INDEX idx_jobs_ownr_ln ON jobs USING gin (ownr_ln gin_trgm_ops); +-- CREATE INDEX idx_jobs_ownr_co_nm ON jobs USING gin (ownr_co_nm gin_trgm_ops); +-- CREATE INDEX idx_jobs_clm_no ON jobs USING gin (clm_no gin_trgm_ops); +-- CREATE INDEX idx_jobs_v_make_desc ON jobs USING gin (v_make_desc gin_trgm_ops); +-- CREATE INDEX idx_jobs_v_model_desc ON jobs USING gin (v_model_desc gin_trgm_ops); +-- CREATE INDEX idx_jobs_plate_no ON jobs USING gin (plate_no gin_trgm_ops); diff --git a/hasura/migrations/1730517308367_run_sql_migration/up.sql b/hasura/migrations/1730517308367_run_sql_migration/up.sql new file mode 100644 index 000000000..9513eb7fb --- /dev/null +++ b/hasura/migrations/1730517308367_run_sql_migration/up.sql @@ -0,0 +1,7 @@ +CREATE INDEX idx_jobs_ownr_fn ON jobs USING gin (ownr_fn gin_trgm_ops); +CREATE INDEX idx_jobs_ownr_ln ON jobs USING gin (ownr_ln gin_trgm_ops); +CREATE INDEX idx_jobs_ownr_co_nm ON jobs USING gin (ownr_co_nm gin_trgm_ops); +CREATE INDEX idx_jobs_clm_no ON jobs USING gin (clm_no gin_trgm_ops); +CREATE INDEX idx_jobs_v_make_desc ON jobs USING gin (v_make_desc gin_trgm_ops); +CREATE INDEX idx_jobs_v_model_desc ON jobs USING gin (v_model_desc gin_trgm_ops); +CREATE INDEX idx_jobs_plate_no ON jobs USING gin (plate_no gin_trgm_ops); diff --git a/hasura/migrations/1730518121867_run_sql_migration/down.sql b/hasura/migrations/1730518121867_run_sql_migration/down.sql new file mode 100644 index 000000000..fa6b92022 --- /dev/null +++ b/hasura/migrations/1730518121867_run_sql_migration/down.sql @@ -0,0 +1,3 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- CREATE INDEX idx_exportlog_createdat_desc ON exportlog (created_at desc); diff --git a/hasura/migrations/1730518121867_run_sql_migration/up.sql b/hasura/migrations/1730518121867_run_sql_migration/up.sql new file mode 100644 index 000000000..2450e76a7 --- /dev/null +++ b/hasura/migrations/1730518121867_run_sql_migration/up.sql @@ -0,0 +1 @@ +CREATE INDEX idx_exportlog_createdat_desc ON exportlog (created_at desc); From 56472d24d9627b52efa6429c430f23c9b0a87ec9 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Fri, 1 Nov 2024 21:33:03 -0700 Subject: [PATCH 04/37] IO-3015 add additional indexs. --- hasura/migrations/1730521661838_run_sql_migration/down.sql | 4 ++++ hasura/migrations/1730521661838_run_sql_migration/up.sql | 2 ++ 2 files changed, 6 insertions(+) create mode 100644 hasura/migrations/1730521661838_run_sql_migration/down.sql create mode 100644 hasura/migrations/1730521661838_run_sql_migration/up.sql diff --git a/hasura/migrations/1730521661838_run_sql_migration/down.sql b/hasura/migrations/1730521661838_run_sql_migration/down.sql new file mode 100644 index 000000000..87ca7ff08 --- /dev/null +++ b/hasura/migrations/1730521661838_run_sql_migration/down.sql @@ -0,0 +1,4 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- CREATE index idx_messages_unread_agg ON messages (read, isoutbound) +-- WHERE read = false AND isoutbound = false; diff --git a/hasura/migrations/1730521661838_run_sql_migration/up.sql b/hasura/migrations/1730521661838_run_sql_migration/up.sql new file mode 100644 index 000000000..e3728cb7f --- /dev/null +++ b/hasura/migrations/1730521661838_run_sql_migration/up.sql @@ -0,0 +1,2 @@ +CREATE index idx_messages_unread_agg ON messages (read, isoutbound) +WHERE read = false AND isoutbound = false; From 542997b1a7febd5bcbf429c4163e8cbd475ba817 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Tue, 5 Nov 2024 11:02:18 -0800 Subject: [PATCH 05/37] IO-2921 Adjustment for SFTP Private Key Signed-off-by: Allan Carr --- server/data/chatter.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/server/data/chatter.js b/server/data/chatter.js index e610f9791..5c9ce9ddc 100644 --- a/server/data/chatter.js +++ b/server/data/chatter.js @@ -135,17 +135,17 @@ exports.default = async (req, res) => { logger.log("chatter-sftp-error", "ERROR", "api", null, { ...error }); } finally { sftp.end(); + sendServerEmail({ + subject: `Chatter Report ${moment().format("MM-DD-YY")}`, + text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))} + Uploaded: ${JSON.stringify( + allcsvsToUpload.map((x) => ({ filename: x.filename, count: x.count })), + null, + 2 + )}` + }); + res.sendStatus(200); } - sendServerEmail({ - subject: `Chatter Report ${moment().format("MM-DD-YY")}`, - text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))} - Uploaded: ${JSON.stringify( - allcsvsToUpload.map((x) => ({ filename: x.filename, count: x.count })), - null, - 2 - )}` - }); - res.sendStatus(200); } catch (error) { res.status(200).json(error); } @@ -160,8 +160,8 @@ async function getPrivateKey() { try { const { SecretString, SecretBinary } = await client.send(command); if (SecretString || SecretBinary) logger.log("chatter-retrieved-private-key", "DEBUG", "api", null, null); - const chatterPrivateKey = SecretString ? JSON.parse(SecretString) : JSON.parse(Buffer.from(SecretBinary, "base64").toString("ascii")); - return chatterPrivateKey.private_key; + const chatterPrivateKey = SecretString ? SecretString : Buffer.from(SecretBinary, "base64").toString("ascii"); + return chatterPrivateKey; } catch (error) { logger.log("chatter-get-private-key", "ERROR", "api", null, error); throw err; From 338906e28896124a6909c6f4ebed69d5661641be Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Tue, 5 Nov 2024 11:53:42 -0800 Subject: [PATCH 06/37] IO-3018 QBO Standardize name Trim StandardizeName Signed-off-by: Allan Carr --- server/accounting/qbo/qbo-payables.js | 2 +- server/accounting/qbo/qbo.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/accounting/qbo/qbo-payables.js b/server/accounting/qbo/qbo-payables.js index 196520de0..04b0c7e08 100644 --- a/server/accounting/qbo/qbo-payables.js +++ b/server/accounting/qbo/qbo-payables.js @@ -167,7 +167,7 @@ async function QueryVendorRecord(oauthClient, qbo_realmId, req, bill) { async function InsertVendorRecord(oauthClient, qbo_realmId, req, bill) { const Vendor = { - DisplayName: bill.vendor.name + DisplayName: StandardizeName(bill.vendor.name) }; try { const result = await oauthClient.makeApiCall({ diff --git a/server/accounting/qbo/qbo.js b/server/accounting/qbo/qbo.js index 74cc3c742..5c7e9d874 100644 --- a/server/accounting/qbo/qbo.js +++ b/server/accounting/qbo/qbo.js @@ -10,7 +10,7 @@ function urlBuilder(realmId, object, query = null) { } function StandardizeName(str) { - return str.replace(new RegExp(/'/g), "\\'"); + return str.replace(new RegExp(/'/g), "\\'").trim(); } exports.urlBuilder = urlBuilder; From 8ec5831ec56035404ae4ea1fefff479874fdcd9c Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Wed, 6 Nov 2024 15:42:14 -0800 Subject: [PATCH 07/37] IO-2921 Re-factor as batch and get docker compose dev working for sftp testing Signed-off-by: Allan Carr --- docker-compose.yml | 21 ++++ server/data/chatter.js | 228 +++++++++++++++++++++++------------------ 2 files changed, 151 insertions(+), 98 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 503d874b3..50dc56932 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -167,6 +167,27 @@ services: # volumes: # - redis-insight-data:/db +# ##Optional Container for SFTP/SSH Server for testing +# ssh-sftp-server: +# image: atmoz/sftp:alpine # Using an image with SFTP support +# container_name: ssh-sftp-server +# hostname: ssh-sftp-server +# networks: +# - redis-cluster-net +# ports: +# - "2222:22" # Expose port 22 for SSH/SFTP (mapped to 2222 on the host) +# volumes: +# - ./certs/id_rsa.pub:/home/user/.ssh/keys/id_rsa.pub:ro # Mount the SSH public key +# - ./upload:/home/user/upload # Mount a local directory for SFTP uploads +# environment: +# - SFTP_USERS=user:password:1001:100:upload +# command: > +# /bin/sh -c " +# echo 'Match User user' >> /etc/ssh/sshd_config && +# sed -i -e 's#ForceCommand internal-sftp#ForceCommand internal-sftp -d /upload#' /etc/ssh/sshd_config && +# /usr/sbin/sshd -D +# " + networks: redis-cluster-net: driver: bridge diff --git a/server/data/chatter.js b/server/data/chatter.js index 5c9ce9ddc..b879bff59 100644 --- a/server/data/chatter.js +++ b/server/data/chatter.js @@ -22,135 +22,128 @@ const ftpSetup = { serverHostKey: ["ssh-rsa", "ssh-dss", "rsa-sha2-256", "rsa-sha2-512", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384"] } }; + +const allcsvsToUpload = []; +const allErrors = []; + exports.default = async (req, res) => { // Only process if in production environment. if (process.env.NODE_ENV !== "production") { res.sendStatus(403); return; } - + // Only process if the appropriate token is provided. if (req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN) { res.sendStatus(401); return; } - //Query for the List of Bodyshop Clients. - logger.log("chatter-start", "DEBUG", "api", null, null); - const { bodyshops } = await client.request(queries.GET_CHATTER_SHOPS); - const specificShopIds = req.body.bodyshopIds; // ['uuid] - const { start, end, skipUpload } = req.body; //YYYY-MM-DD - - const allcsvsToUpload = []; - const allErrors = []; try { - for (const bodyshop of specificShopIds ? bodyshops.filter((b) => specificShopIds.includes(b.id)) : bodyshops) { - logger.log("chatter-start-shop-extract", "DEBUG", "api", bodyshop.id, { - shopname: bodyshop.shopname - }); - try { - const { jobs, bodyshops_by_pk } = await client.request(queries.CHATTER_QUERY, { - bodyshopid: bodyshop.id, - start: start ? moment(start).startOf("day") : moment().subtract(1, "days").startOf("day"), - ...(end && { end: moment(end).endOf("day") }) - }); + //Query for the List of Bodyshop Clients. + logger.log("chatter-start", "DEBUG", "api", null, null); + const { bodyshops } = await client.request(queries.GET_CHATTER_SHOPS); + const specificShopIds = req.body.bodyshopIds; // ['uuid]; - const chatterObject = jobs.map((j) => { - return { - poc_trigger_code: bodyshops_by_pk.chatterid, - firstname: j.ownr_co_nm ? null : j.ownr_fn, - lastname: j.ownr_co_nm ? j.ownr_co_nm : j.ownr_ln, - transaction_id: j.ro_number, - email: j.ownr_ea, - phone_number: j.ownr_ph1 - }; - }); + const { start, end, skipUpload } = req.body; //YYYY-MM-DD - const ret = converter.json2csv(chatterObject, { emptyFieldValue: "" }); + const batchSize = 10; - allcsvsToUpload.push({ - count: chatterObject.length, - csv: ret, - filename: `${bodyshop.shopname}_solicitation_${moment().format("YYYYMMDD")}.csv` - }); + const shopsToProcess = + specificShopIds?.length > 0 ? bodyshops.filter((shop) => specificShopIds.includes(shop.id)) : bodyshops; + logger.log("chatter-shopsToProcess-generated", "DEBUG", "api", null, null); - logger.log("chatter-end-shop-extract", "DEBUG", "api", bodyshop.id, { - shopname: bodyshop.shopname - }); - } catch (error) { - //Error at the shop level. - logger.log("chatter-error-shop", "ERROR", "api", bodyshop.id, { - ...error - }); - - allErrors.push({ - bodyshopid: bodyshop.id, - imexshopid: bodyshop.imexshopid, - shopname: bodyshop.shopname, - fatal: true, - errors: [error.toString()] - }); - } finally { - allErrors.push({ - bodyshopid: bodyshop.id, - imexshopid: bodyshop.imexshopid, - shopname: bodyshop.shopname - }); - } - } - - if (skipUpload) { - for (const csvObj of allcsvsToUpload) { - fs.writeFile(`./logs/${csvObj.filename}`, csvObj.csv); - } - - sendServerEmail({ - subject: `Chatter Report ${moment().format("MM-DD-YY")}`, - text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))} - Uploaded: ${JSON.stringify( - allcsvsToUpload.map((x) => ({ filename: x.filename, count: x.count })), - null, - 2 - )} - ` - }); - res.json(allcsvsToUpload); + if (shopsToProcess.length === 0) { + logger.log("chatter-shopsToProcess-empty", "DEBUG", "api", null, null); + res.sendStatus(200); return; } - const sftp = new Client(); - sftp.on("error", (errors) => logger.log("chatter-sftp-error", "ERROR", "api", null, { ...errors })); - try { - //Get the private key from AWS Secrets Manager. - ftpSetup.privateKey = await getPrivateKey(); + for (let i = 0; i < shopsToProcess.length; i += batchSize) { + const batch = shopsToProcess.slice(i, i + batchSize); + await processBatch(batch, start, end); - //Connect to the FTP and upload all. - await sftp.connect(ftpSetup); - - for (const csvObj of allcsvsToUpload) { - logger.log("chatter-sftp-upload", "DEBUG", "api", null, { filename: csvObj.filename }); - - const uploadResult = await sftp.put(Buffer.from(csvObj.xml), `/${csvObj.filename}`); - logger.log("chatter-sftp-upload-result", "DEBUG", "api", null, { uploadResult }); + if (skipUpload) { + for (const csvObj of allcsvsToUpload) { + fs.writeFile(`./logs/${csvObj.filename}`, csvObj.csv); + } + } else { + await uploadViaSFTP(allcsvsToUpload); } - } catch (error) { - logger.log("chatter-sftp-error", "ERROR", "api", null, { ...error }); - } finally { - sftp.end(); + sendServerEmail({ subject: `Chatter Report ${moment().format("MM-DD-YY")}`, text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))} Uploaded: ${JSON.stringify( - allcsvsToUpload.map((x) => ({ filename: x.filename, count: x.count })), + allcsvsToUpload.map((x) => ({ filename: x.filename, count: x.count, result: x.result })), null, 2 )}` }); + + logger.log("chatter-end", "DEBUG", "api", null, null); res.sendStatus(200); } } catch (error) { - res.status(200).json(error); + logger.log("chatter-shopsToProcess-error", "ERROR", "api", null, { error: error.message, stack: error.stack }); + res.status(500).json({ error: error.message, stack: error.stack }); } }; +async function processBatch(batch, start, end) { + for (const bodyshop of batch) { + try { + logger.log("chatter-start-shop-extract", "DEBUG", "api", bodyshop.id, { + shopname: bodyshop.shopname + }); + + const { jobs, bodyshops_by_pk } = await client.request(queries.CHATTER_QUERY, { + bodyshopid: bodyshop.id, + start: start ? moment(start).startOf("day") : moment().subtract(1, "days").startOf("day"), + ...(end && { end: moment(end).endOf("day") }) + }); + + const chatterObject = jobs.map((j) => { + return { + poc_trigger_code: bodyshops_by_pk.chatterid, + firstname: j.ownr_co_nm ? null : j.ownr_fn, + lastname: j.ownr_co_nm ? j.ownr_co_nm : j.ownr_ln, + transaction_id: j.ro_number, + email: j.ownr_ea, + phone_number: j.ownr_ph1 + }; + }); + + const ret = converter.json2csv(chatterObject, { emptyFieldValue: "" }); + + allcsvsToUpload.push({ + count: chatterObject.length, + csv: ret, + filename: `${bodyshop.shopname}_solicitation_${moment().format("YYYYMMDD")}.csv` + }); + + logger.log("chatter-end-shop-extract", "DEBUG", "api", bodyshop.id, { + shopname: bodyshop.shopname + }); + } catch (error) { + //Error at the shop level. + logger.log("chatter-error-shop", "ERROR", "api", bodyshop.id, { error: error.message, stack: error.stack }); + + allErrors.push({ + bodyshopid: bodyshop.id, + imexshopid: bodyshop.imexshopid, + shopname: bodyshop.shopname, + fatal: true, + errors: [error.toString()] + }); + } finally { + allErrors.push({ + bodyshopid: bodyshop.id, + imexshopid: bodyshop.imexshopid, + shopname: bodyshop.shopname + }); + } + } +} + async function getPrivateKey() { // Connect to AWS Secrets Manager const client = new SecretsManagerClient({ region: "ca-central-1" }); @@ -160,10 +153,49 @@ async function getPrivateKey() { try { const { SecretString, SecretBinary } = await client.send(command); if (SecretString || SecretBinary) logger.log("chatter-retrieved-private-key", "DEBUG", "api", null, null); - const chatterPrivateKey = SecretString ? SecretString : Buffer.from(SecretBinary, "base64").toString("ascii"); + const chatterPrivateKey = SecretString + ? SecretString + : Buffer.from(SecretBinary, "base64").toString("ascii"); return chatterPrivateKey; } catch (error) { - logger.log("chatter-get-private-key", "ERROR", "api", null, error); - throw err; + logger.log("chatter-get-private-key", "ERROR", "api", null, { error: error.message, stack: error.stack }); + throw error; + } +} + +async function uploadViaSFTP(allcsvsToUpload) { + const sftp = new Client(); + sftp.on("error", (errors) => + logger.log("chatter-sftp-connection-error", "ERROR", "api", null, { error: errors.message, stack: errors.stack }) + ); + try { + //Get the private key from AWS Secrets Manager. + const privateKey = await getPrivateKey(); + + //Connect to the FTP and upload all. + await sftp.connect({ ...ftpSetup, privateKey }); + + for (const csvObj of allcsvsToUpload) { + try { + logger.log("chatter-sftp-upload", "DEBUG", "api", null, { filename: csvObj.filename }); + csvObj.result = await sftp.put(Buffer.from(csvObj.csv), `${csvObj.filename}`); + logger.log("chatter-sftp-upload-result", "DEBUG", "api", null, { + filename: csvObj.filename, + result: csvObj.result + }); + } catch (error) { + logger.log("chatter-sftp-upload-error", "ERROR", "api", null, { + filename: csvObj.filename, + error: error.message, + stack: error.stack + }); + throw error; + } + } + } catch (error) { + logger.log("chatter-sftp-error", "ERROR", "api", null, { error: error.message, stack: error.stack }); + throw error; + } finally { + sftp.end(); } } From 552163d7b98a70bbfa38282b85a018fd8d79a67d Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Wed, 6 Nov 2024 15:53:51 -0800 Subject: [PATCH 08/37] IO-2921 Upload directory Signed-off-by: Allan Carr --- upload/.gitignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 upload/.gitignore diff --git a/upload/.gitignore b/upload/.gitignore new file mode 100644 index 000000000..d6b7ef32c --- /dev/null +++ b/upload/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore From c2b4b66ed12ca089ac18a04568f610a17481187b Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 7 Nov 2024 12:40:13 -0800 Subject: [PATCH 09/37] hotfix/IO-2969-Fonts-For-Production Signed-off-by: Dave Richer --- .../hooks/postdeploy/00-install-fonts.sh | 13 ---------- .platform/hooks/predeploy/00-install-fonts.sh | 24 +++++++++++++++++++ 2 files changed, 24 insertions(+), 13 deletions(-) delete mode 100644 .platform/hooks/postdeploy/00-install-fonts.sh create mode 100644 .platform/hooks/predeploy/00-install-fonts.sh diff --git a/.platform/hooks/postdeploy/00-install-fonts.sh b/.platform/hooks/postdeploy/00-install-fonts.sh deleted file mode 100644 index f9b766ef5..000000000 --- a/.platform/hooks/postdeploy/00-install-fonts.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -# Install fonts using dnf -dnf install -y fontconfig freetype google-noto-sans-fonts dejavu-sans-fonts liberation-fonts - -# Download and install Montserrat font -cd /tmp -curl -O https://fonts.google.com/download?family=Montserrat -unzip Montserrat.zip -d /usr/share/fonts/montserrat -fc-cache -fv # Rebuild font cache to include Montserrat - -# Ensure permissions are correct -chmod -R 755 /usr/share/fonts/montserrat diff --git a/.platform/hooks/predeploy/00-install-fonts.sh b/.platform/hooks/predeploy/00-install-fonts.sh new file mode 100644 index 000000000..af9d08b0b --- /dev/null +++ b/.platform/hooks/predeploy/00-install-fonts.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# Install required packages +dnf install -y fontconfig freetype + +# Move to the /tmp directory for temporary download and extraction +cd /tmp + +# Download the Montserrat font zip file +wget https://images.imex.online/fonts/montserrat.zip -O montserrat.zip + +# Unzip the downloaded font file +unzip montserrat.zip -d montserrat + +# Move the font files to the system fonts directory +mv montserrat/*.ttf /usr/share/fonts + +# Rebuild the font cache +fc-cache -fv + +# Clean up +rm -rf /tmp/montserrat /tmp/montserrat.zip + +echo "Montserrat fonts installed and cached successfully." From 624f8e77cba33c7a1c69fe5d5b6babe57ce820d8 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Thu, 7 Nov 2024 13:56:53 -0800 Subject: [PATCH 10/37] IO-3017 Lifecycle Average Time Signed-off-by: Allan Carr --- .../job-lifecycle-dashboard.component.jsx | 15 ++++++++++----- client/src/translations/en_us/common.json | 2 ++ client/src/translations/es/common.json | 2 ++ client/src/translations/fr/common.json | 2 ++ server/job/job-lifecycle.js | 10 ++++++++-- 5 files changed, 24 insertions(+), 7 deletions(-) diff --git a/client/src/components/dashboard-components/job-lifecycle/job-lifecycle-dashboard.component.jsx b/client/src/components/dashboard-components/job-lifecycle/job-lifecycle-dashboard.component.jsx index 38d939c89..5f52d62c6 100644 --- a/client/src/components/dashboard-components/job-lifecycle/job-lifecycle-dashboard.component.jsx +++ b/client/src/components/dashboard-components/job-lifecycle/job-lifecycle-dashboard.component.jsx @@ -1,10 +1,10 @@ import { Card, Table, Tag } from "antd"; -import LoadingSkeleton from "../../loading-skeleton/loading-skeleton.component"; -import { useTranslation } from "react-i18next"; -import React, { useEffect, useState } from "react"; -import dayjs from "../../../utils/day"; -import DashboardRefreshRequired from "../refresh-required.component"; import axios from "axios"; +import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import dayjs from "../../../utils/day"; +import LoadingSkeleton from "../../loading-skeleton/loading-skeleton.component"; +import DashboardRefreshRequired from "../refresh-required.component"; const fortyFiveDaysAgo = () => dayjs().subtract(45, "day").toLocaleString(); @@ -46,6 +46,11 @@ export default function JobLifecycleDashboardComponent({ data, bodyshop, ...card dataIndex: "humanReadable", key: "humanReadable" }, + { + title: t("job_lifecycle.columns.average_human_readable"), + dataIndex: "averageHumanReadable", + key: "averageHumanReadable" + }, { title: t("job_lifecycle.columns.status_count"), key: "statusCount", diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 38f13e032..d91c369fd 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -1338,6 +1338,8 @@ }, "job_lifecycle": { "columns": { + "average_human_readable": "Average Human Readable", + "average_value": "Average Value", "duration": "Duration", "end": "End", "human_readable": "Human Readable", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index ed7760e9d..7658ca3ce 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -1338,6 +1338,8 @@ }, "job_lifecycle": { "columns": { + "average_human_readable": "", + "average_value": "", "duration": "", "end": "", "human_readable": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 4a64bcc55..a109a127d 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -1338,6 +1338,8 @@ }, "job_lifecycle": { "columns": { + "average_human_readable": "", + "average_value": "", "duration": "", "end": "", "human_readable": "", diff --git a/server/job/job-lifecycle.js b/server/job/job-lifecycle.js index 744639bcf..feb093d6e 100644 --- a/server/job/job-lifecycle.js +++ b/server/job/job-lifecycle.js @@ -81,13 +81,17 @@ const jobLifecycle = async (req, res) => { const percentage = (value / finalTotal) * 100; const color = getLifecycleStatusColor(status); const roundedPercentage = `${Math.round(percentage)}%`; + const averageValue = value / jobIDs.length; + const averageHumanReadable = durationToHumanReadable(moment.duration(averageValue)); finalSummations.push({ status, value, humanReadable, percentage, color, - roundedPercentage + roundedPercentage, + averageValue, + averageHumanReadable }); }); @@ -100,7 +104,9 @@ const jobLifecycle = async (req, res) => { totalStatuses: finalSummations.length, total: finalTotal, statusCounts: finalStatusCounts, - humanReadable: durationToHumanReadable(moment.duration(finalTotal)) + humanReadable: durationToHumanReadable(moment.duration(finalTotal)), + averageValue: finalTotal / jobIDs.length, + averageHumanReadable: durationToHumanReadable(moment.duration(finalTotal / jobIDs.length)) } }); }; From a14b2340b0647e1bdf7d3802dc8adffd21d7ab85 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 8 Nov 2024 09:24:58 -0800 Subject: [PATCH 11/37] IO-3017 Lifecycle NaN prevention Signed-off-by: Allan Carr --- server/job/job-lifecycle.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/server/job/job-lifecycle.js b/server/job/job-lifecycle.js index feb093d6e..ddd58a834 100644 --- a/server/job/job-lifecycle.js +++ b/server/job/job-lifecycle.js @@ -78,10 +78,10 @@ const jobLifecycle = async (req, res) => { Object.keys(flatGroupedAllDurations).forEach((status) => { const value = flatGroupedAllDurations[status].reduce((acc, curr) => acc + curr.value, 0); const humanReadable = durationToHumanReadable(moment.duration(value)); - const percentage = (value / finalTotal) * 100; + const percentage = finalTotal > 0 ? (value / finalTotal) * 100 : 0; const color = getLifecycleStatusColor(status); const roundedPercentage = `${Math.round(percentage)}%`; - const averageValue = value / jobIDs.length; + const averageValue = _size(jobIDs) > 0 ? value / jobIDs.length : 0; const averageHumanReadable = durationToHumanReadable(moment.duration(averageValue)); finalSummations.push({ status, @@ -105,8 +105,11 @@ const jobLifecycle = async (req, res) => { total: finalTotal, statusCounts: finalStatusCounts, humanReadable: durationToHumanReadable(moment.duration(finalTotal)), - averageValue: finalTotal / jobIDs.length, - averageHumanReadable: durationToHumanReadable(moment.duration(finalTotal / jobIDs.length)) + averageValue: _size(jobIDs) > 0 ? finalTotal / jobIDs.length : 0, + averageHumanReadable: + _size(jobIDs) > 0 + ? durationToHumanReadable(moment.duration(finalTotal / jobIDs.length)) + : durationToHumanReadable(moment.duration(0)) } }); }; From 3b19432974974c4dd103f5eb88473eb816ea63c1 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Fri, 8 Nov 2024 09:32:58 -0800 Subject: [PATCH 12/37] feature/IO-3017-Lifecycle-Average-Time - Small fixes Signed-off-by: Dave Richer --- server/job/job-lifecycle.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/job/job-lifecycle.js b/server/job/job-lifecycle.js index ddd58a834..7076069f6 100644 --- a/server/job/job-lifecycle.js +++ b/server/job/job-lifecycle.js @@ -81,7 +81,7 @@ const jobLifecycle = async (req, res) => { const percentage = finalTotal > 0 ? (value / finalTotal) * 100 : 0; const color = getLifecycleStatusColor(status); const roundedPercentage = `${Math.round(percentage)}%`; - const averageValue = _size(jobIDs) > 0 ? value / jobIDs.length : 0; + const averageValue = _.size(jobIDs) > 0 ? value / jobIDs.length : 0; const averageHumanReadable = durationToHumanReadable(moment.duration(averageValue)); finalSummations.push({ status, @@ -105,9 +105,9 @@ const jobLifecycle = async (req, res) => { total: finalTotal, statusCounts: finalStatusCounts, humanReadable: durationToHumanReadable(moment.duration(finalTotal)), - averageValue: _size(jobIDs) > 0 ? finalTotal / jobIDs.length : 0, + averageValue: _.size(jobIDs) > 0 ? finalTotal / jobIDs.length : 0, averageHumanReadable: - _size(jobIDs) > 0 + _.size(jobIDs) > 0 ? durationToHumanReadable(moment.duration(finalTotal / jobIDs.length)) : durationToHumanReadable(moment.duration(0)) } From 8eee371a90f00585a256b6e3dd358d1a42798bd4 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 8 Nov 2024 19:30:33 -0800 Subject: [PATCH 13/37] IO-3025 Autohouse Datapump Refactor Signed-off-by: Allan Carr --- server/data/autohouse.js | 273 ++++++++++++++++++++------------------- 1 file changed, 140 insertions(+), 133 deletions(-) diff --git a/server/data/autohouse.js b/server/data/autohouse.js index 9b6494e67..6f0fbf098 100644 --- a/server/data/autohouse.js +++ b/server/data/autohouse.js @@ -13,6 +13,7 @@ let Client = require("ssh2-sftp-client"); const client = require("../graphql-client/graphql-client").client; const { sendServerEmail } = require("../email/sendemail"); + const AHDineroFormat = "0.00"; const AhDateFormat = "MMDDYYYY"; @@ -26,170 +27,176 @@ const ftpSetup = { password: process.env.AUTOHOUSE_PASSWORD, debug: (message, ...data) => logger.log(message, "DEBUG", "api", null, data), algorithms: { - serverHostKey: ["ssh-rsa", "ssh-dss"] + serverHostKey: ["ssh-rsa", "ssh-dss", "rsa-sha2-256", "rsa-sha2-512", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384"] } }; +const allxmlsToUpload = []; +const allErrors = []; + exports.default = async (req, res) => { // Only process if in production environment. if (process.env.NODE_ENV !== "production") { res.sendStatus(403); return; } - - //Query for the List of Bodyshop Clients. - logger.log("autohouse-start", "DEBUG", "api", null, null); - const { bodyshops } = await client.request(queries.GET_AUTOHOUSE_SHOPS); - - const specificShopIds = req.body.bodyshopIds; // ['uuid] - const { start, end, skipUpload } = req.body; //YYYY-MM-DD + // Only process if the appropriate token is provided. if (req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN) { res.sendStatus(401); return; } - const allxmlsToUpload = []; - const allErrors = []; try { - for (const bodyshop of specificShopIds ? bodyshops.filter((b) => specificShopIds.includes(b.id)) : bodyshops) { - logger.log("autohouse-start-shop-extract", "DEBUG", "api", bodyshop.id, { - shopname: bodyshop.shopname - }); - const erroredJobs = []; - try { - const { jobs, bodyshops_by_pk } = await client.request(queries.AUTOHOUSE_QUERY, { - bodyshopid: bodyshop.id, - start: start ? moment(start).startOf("day") : moment().subtract(5, "days").startOf("day"), - ...(end && { end: moment(end).endOf("day") }) - }); + //Query for the List of Bodyshop Clients. + logger.log("autohouse-start", "DEBUG", "api", null, null); + const { bodyshops } = await client.request(queries.GET_AUTOHOUSE_SHOPS); + const specificShopIds = req.body.bodyshopIds; // ['uuid]; - const autoHouseObject = { - AutoHouseExport: { - RepairOrder: jobs.map((j) => - CreateRepairOrderTag({ ...j, bodyshop: bodyshops_by_pk }, function ({ job, error }) { - erroredJobs.push({ job: job, error: error.toString() }); - }) - ) - } - }; + const { start, end, skipUpload } = req.body; //YYYY-MM-DD - if (erroredJobs.length > 0) { - logger.log("autohouse-failed-jobs", "ERROR", "api", bodyshop.id, { - count: erroredJobs.length, - jobs: JSON.stringify(erroredJobs.map((j) => j.job.ro_number)) - }); - } + const batchSize = 10; - var ret = builder - .create( - { - // version: "1.0", - // encoding: "UTF-8", - //keepNullNodes: true, - }, - autoHouseObject - ) - .end({ allowEmptyTags: true }); + const shopsToProcess = + specificShopIds?.length > 0 ? bodyshops.filter((shop) => specificShopIds.includes(shop.id)) : bodyshops; + logger.log("autohouse-shopsToProcess-generated", "DEBUG", "api", null, null); - allxmlsToUpload.push({ - count: autoHouseObject.AutoHouseExport.RepairOrder.length, - xml: ret, - filename: `IM_${bodyshop.autohouseid}_${moment().format("DDMMYYYY_HHMMss")}.xml` - }); - - logger.log("autohouse-end-shop-extract", "DEBUG", "api", bodyshop.id, { - shopname: bodyshop.shopname - }); - } catch (error) { - //Error at the shop level. - logger.log("autohouse-error-shop", "ERROR", "api", bodyshop.id, { - ...error - }); - - allErrors.push({ - bodyshopid: bodyshop.id, - imexshopid: bodyshop.imexshopid, - autuhouseid: bodyshop.autuhouseid, - fatal: true, - errors: [error.toString()] - }); - } finally { - allErrors.push({ - bodyshopid: bodyshop.id, - imexshopid: bodyshop.imexshopid, - autohouseid: bodyshop.autohouseid, - errors: erroredJobs.map((ej) => ({ - ro_number: ej.job?.ro_number, - jobid: ej.job?.id, - error: ej.error - })) - }); - } - } - - if (skipUpload) { - for (const xmlObj of allxmlsToUpload) { - fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml); - } - - res.json(allxmlsToUpload); - sendServerEmail({ - subject: `Autohouse Report ${moment().format("MM-DD-YY")}`, - text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))} - Uploaded: ${JSON.stringify( - allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count })), - null, - 2 - )} - ` - }); + if (shopsToProcess.length === 0) { + logger.log("autohouse-shopsToProcess-empty", "DEBUG", "api", null, null); + res.sendStatus(200); return; } - let sftp = new Client(); - sftp.on("error", (errors) => - logger.log("autohouse-sftp-error", "ERROR", "api", null, { - ...errors - }) - ); + for (let i = 0; i < shopsToProcess.length; i += batchSize) { + const batch = shopsToProcess.slice(i, i + batchSize); + await processBatch(batch, start, end); + + if (skipUpload) { + for (const xmlObj of allxmlsToUpload) { + fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml); + } + } else { + await uploadViaSFTP(allxmlsToUpload); + } + + sendServerEmail({ + subject: `Autohouse Report ${moment().format("MM-DD-YY")}`, + text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))} + Uploaded: ${JSON.stringify( + allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count, result: x.result })), + null, + 2 + )}` + }); + + logger.log("autohouse-end", "DEBUG", "api", null, null); + res.sendStatus(200); + } + } catch (error) { + logger.log("autohouse-shopsToProcess-error", "ERROR", "api", null, { error: error.message, stack: error.stack }); + res.status(500).json({ error: error.message, stack: error.stack }); + } +}; + +async function processBatch(batch, start, end) { + for (const bodyshop of batch) { + const erroredJobs = []; try { - //Connect to the FTP and upload all. + logger.log("autohouse-start-shop-extract", "DEBUG", "api", bodyshop.id, { + shopname: bodyshop.shopname + }); - await sftp.connect(ftpSetup); + const { jobs, bodyshops_by_pk } = await client.request(queries.AUTOHOUSE_QUERY, { + bodyshopid: bodyshop.id, + start: start ? moment(start).startOf("day") : moment().subtract(5, "days").startOf("day"), + ...(end && { end: moment(end).endOf("day") }) + }); - for (const xmlObj of allxmlsToUpload) { - logger.log("autohouse-sftp-upload", "DEBUG", "api", null, { - filename: xmlObj.filename - }); + const autoHouseObject = { + AutoHouseExport: { + RepairOrder: jobs.map((j) => + CreateRepairOrderTag({ ...j, bodyshop: bodyshops_by_pk }, function ({ job, error }) { + erroredJobs.push({ job: job, error: error.toString() }); + }) + ) + } + }; - const uploadResult = await sftp.put(Buffer.from(xmlObj.xml), `/${xmlObj.filename}`); - logger.log("autohouse-sftp-upload-result", "DEBUG", "api", null, { - uploadResult + if (erroredJobs.length > 0) { + logger.log("autohouse-failed-jobs", "ERROR", "api", bodyshop.id, { + count: erroredJobs.length, + jobs: JSON.stringify(erroredJobs.map((j) => j.job.ro_number)) }); } - //***TODO Change filing naming when creating the cron job. IM_ShopInternalName_DDMMYYYY_HHMMSS.xml + const ret = builder.create({}, autoHouseObject).end({ allowEmptyTags: true }); + + allxmlsToUpload.push({ + count: autoHouseObject.AutoHouseExport.RepairOrder.length, + xml: ret, + filename: `IM_${bodyshop.autohouseid}_${moment().format("DDMMYYYY_HHMMss")}.xml` + }); + + logger.log("autohouse-end-shop-extract", "DEBUG", "api", bodyshop.id, { + shopname: bodyshop.shopname + }); } catch (error) { - logger.log("autohouse-sftp-error", "ERROR", "api", null, { - ...error + //Error at the shop level. + logger.log("autohouse-error-shop", "ERROR", "api", bodyshop.id, { error: error.message, stack: error.stack }); + + allErrors.push({ + bodyshopid: bodyshop.id, + imexshopid: bodyshop.imexshopid, + autuhouseid: bodyshop.autuhouseid, + fatal: true, + errors: [error.toString()] }); } finally { - sftp.end(); + allErrors.push({ + bodyshopid: bodyshop.id, + imexshopid: bodyshop.imexshopid, + autuhouseid: bodyshop.autuhouseid, + errors: erroredJobs.map((ej) => ({ + ro_number: ej.job?.ro_number, + jobid: ej.job?.id, + error: ej.error + })) + }); } - sendServerEmail({ - subject: `Autohouse Report ${moment().format("MM-DD-YY")}`, - text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))} - Uploaded: ${JSON.stringify( - allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count })), - null, - 2 - )} - ` - }); - res.sendStatus(200); - } catch (error) { - res.status(200).json(error); } -}; +} + +async function uploadViaSFTP(allxmlsToUpload) { + const sftp = new Client(); + sftp.on("error", (errors) => + logger.log("autohouse-sftp-connection-error", "ERROR", "api", null, { error: errors.message, stack: errors.stack }) + ); + try { + //Connect to the FTP and upload all. + await sftp.connect(ftpSetup); + + for (const xmlObj of allxmlsToUpload) { + try { + logger.log("autohouse-sftp-upload", "DEBUG", "api", null, { filename: xmlObj.filename }); + xmlObj.result = await sftp.put(Buffer.from(xmlObj.xml), `${xmlObj.filename}`); + logger.log("autohouse-sftp-upload-result", "DEBUG", "api", null, { + filename: xmlObj.filename, + result: xmlObj.result + }); + } catch (error) { + logger.log("autohouse-sftp-upload-error", "ERROR", "api", null, { + filename: xmlObj.filename, + error: error.message, + stack: error.stack + }); + throw error; + } + } + } catch (error) { + logger.log("autohouse-sftp-error", "ERROR", "api", null, { error: error.message, stack: error.stack }); + throw error; + } finally { + sftp.end(); + } +} const CreateRepairOrderTag = (job, errorCallback) => { //Level 2 @@ -287,8 +294,8 @@ const CreateRepairOrderTag = (job, errorCallback) => { InsuranceCo: job.ins_co_nm || "", CompanyName: job.ins_co_nm || "", Address: job.ins_addr1 || "", - City: job.ins_addr1 || "", - State: job.ins_city || "", + City: job.ins_city || "", + State: job.ins_st || "", Zip: job.ins_zip || "", Phone: job.ins_ph1 || "", Fax: job.ins_fax || "", From 5fbfb992c7895d45724dabc7f956f295048dea63 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Fri, 8 Nov 2024 21:18:58 -0800 Subject: [PATCH 14/37] release/2024-11-08 - Small fix to font script Signed-off-by: Dave Richer --- .platform/hooks/predeploy/00-install-fonts.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.platform/hooks/predeploy/00-install-fonts.sh b/.platform/hooks/predeploy/00-install-fonts.sh index af9d08b0b..6f740d0c7 100644 --- a/.platform/hooks/predeploy/00-install-fonts.sh +++ b/.platform/hooks/predeploy/00-install-fonts.sh @@ -13,7 +13,7 @@ wget https://images.imex.online/fonts/montserrat.zip -O montserrat.zip unzip montserrat.zip -d montserrat # Move the font files to the system fonts directory -mv montserrat/*.ttf /usr/share/fonts +mv montserrat/montserrat/*.ttf /usr/share/fonts # Rebuild the font cache fc-cache -fv From 78771ae750807ee09f65d8b49d54b5af6d05290c Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 8 Nov 2024 22:28:02 -0800 Subject: [PATCH 15/37] IO-3025 Shift Email send to outside of batch Signed-off-by: Allan Carr --- server/data/autohouse.js | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/server/data/autohouse.js b/server/data/autohouse.js index 6f0fbf098..506ba53d3 100644 --- a/server/data/autohouse.js +++ b/server/data/autohouse.js @@ -76,20 +76,19 @@ exports.default = async (req, res) => { } else { await uploadViaSFTP(allxmlsToUpload); } - - sendServerEmail({ - subject: `Autohouse Report ${moment().format("MM-DD-YY")}`, - text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))} - Uploaded: ${JSON.stringify( - allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count, result: x.result })), - null, - 2 - )}` - }); - - logger.log("autohouse-end", "DEBUG", "api", null, null); - res.sendStatus(200); } + sendServerEmail({ + subject: `Autohouse Report ${moment().format("MM-DD-YY")}`, + text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))} + Uploaded: ${JSON.stringify( + allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count, result: x.result })), + null, + 2 + )}` + }); + + logger.log("autohouse-end", "DEBUG", "api", null, null); + res.sendStatus(200); } catch (error) { logger.log("autohouse-shopsToProcess-error", "ERROR", "api", null, { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message, stack: error.stack }); From f0717b8b3617aea221f1dfef513bab1f1ed64be8 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 8 Nov 2024 22:30:22 -0800 Subject: [PATCH 16/37] IO-2921 Shift Email outside of Batch Signed-off-by: Allan Carr --- server/data/chatter.js | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/server/data/chatter.js b/server/data/chatter.js index b879bff59..056e5ba22 100644 --- a/server/data/chatter.js +++ b/server/data/chatter.js @@ -68,20 +68,19 @@ exports.default = async (req, res) => { } else { await uploadViaSFTP(allcsvsToUpload); } - - sendServerEmail({ - subject: `Chatter Report ${moment().format("MM-DD-YY")}`, - text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))} - Uploaded: ${JSON.stringify( - allcsvsToUpload.map((x) => ({ filename: x.filename, count: x.count, result: x.result })), - null, - 2 - )}` - }); - - logger.log("chatter-end", "DEBUG", "api", null, null); - res.sendStatus(200); } + sendServerEmail({ + subject: `Chatter Report ${moment().format("MM-DD-YY")}`, + text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))} + Uploaded: ${JSON.stringify( + allcsvsToUpload.map((x) => ({ filename: x.filename, count: x.count, result: x.result })), + null, + 2 + )}` + }); + + logger.log("chatter-end", "DEBUG", "api", null, null); + res.sendStatus(200); } catch (error) { logger.log("chatter-shopsToProcess-error", "ERROR", "api", null, { error: error.message, stack: error.stack }); res.status(500).json({ error: error.message, stack: error.stack }); From 655aeb86fcba57d07ad0af5ff73d25859b57e482 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 8 Nov 2024 23:43:18 -0800 Subject: [PATCH 17/37] IO-2921 Adjust for Promise and change processing Signed-off-by: Allan Carr --- server/data/chatter.js | 46 +++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/server/data/chatter.js b/server/data/chatter.js index 056e5ba22..ea8851e94 100644 --- a/server/data/chatter.js +++ b/server/data/chatter.js @@ -37,12 +37,14 @@ exports.default = async (req, res) => { res.sendStatus(401); return; } - try { - //Query for the List of Bodyshop Clients. - logger.log("chatter-start", "DEBUG", "api", null, null); - const { bodyshops } = await client.request(queries.GET_CHATTER_SHOPS); - const specificShopIds = req.body.bodyshopIds; // ['uuid]; + // Send immediate response and continue processing. + res.status(200).send(); + + try { + logger.log("chatter-start", "DEBUG", "api", null, null); + const { bodyshops } = await client.request(queries.GET_CHATTER_SHOPS); //Query for the List of Bodyshop Clients. + const specificShopIds = req.body.bodyshopIds; // ['uuid]; const { start, end, skipUpload } = req.body; //YYYY-MM-DD const batchSize = 10; @@ -53,26 +55,28 @@ exports.default = async (req, res) => { if (shopsToProcess.length === 0) { logger.log("chatter-shopsToProcess-empty", "DEBUG", "api", null, null); - res.sendStatus(200); return; } + const batchPromises = []; for (let i = 0; i < shopsToProcess.length; i += batchSize) { const batch = shopsToProcess.slice(i, i + batchSize); - await processBatch(batch, start, end); - - if (skipUpload) { - for (const csvObj of allcsvsToUpload) { - fs.writeFile(`./logs/${csvObj.filename}`, csvObj.csv); + const batchPromise = (async () => { + await processBatch(batch, start, end); + if (skipUpload) { + for (const csvObj of allcsvsToUpload) { + await fs.promises.writeFile(`./logs/${csvObj.filename}`, csvObj.csv); + } + } else { + await uploadViaSFTP(allcsvsToUpload); } - } else { - await uploadViaSFTP(allcsvsToUpload); - } + })(); + batchPromises.push(batchPromise); } - sendServerEmail({ + await Promise.all(batchPromises); + await sendServerEmail({ subject: `Chatter Report ${moment().format("MM-DD-YY")}`, - text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))} - Uploaded: ${JSON.stringify( + text: `Errors:\n${JSON.stringify(allErrors, null, 2)}\n\nUploaded:\n${JSON.stringify( allcsvsToUpload.map((x) => ({ filename: x.filename, count: x.count, result: x.result })), null, 2 @@ -80,10 +84,8 @@ exports.default = async (req, res) => { }); logger.log("chatter-end", "DEBUG", "api", null, null); - res.sendStatus(200); } catch (error) { - logger.log("chatter-shopsToProcess-error", "ERROR", "api", null, { error: error.message, stack: error.stack }); - res.status(500).json({ error: error.message, stack: error.stack }); + logger.log("chatter-error", "ERROR", "api", null, { error: error.message, stack: error.stack }); } }; @@ -152,9 +154,7 @@ async function getPrivateKey() { try { const { SecretString, SecretBinary } = await client.send(command); if (SecretString || SecretBinary) logger.log("chatter-retrieved-private-key", "DEBUG", "api", null, null); - const chatterPrivateKey = SecretString - ? SecretString - : Buffer.from(SecretBinary, "base64").toString("ascii"); + const chatterPrivateKey = SecretString ? SecretString : Buffer.from(SecretBinary, "base64").toString("ascii"); return chatterPrivateKey; } catch (error) { logger.log("chatter-get-private-key", "ERROR", "api", null, { error: error.message, stack: error.stack }); From 5cbf00b0c87bbd622ab77a5099e5aaed17dcfab6 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Sat, 9 Nov 2024 00:32:51 -0800 Subject: [PATCH 18/37] IO-3025 Adjust for promise and change processing Signed-off-by: Allan Carr --- server/data/autohouse.js | 42 +++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/server/data/autohouse.js b/server/data/autohouse.js index 506ba53d3..9ea3a415a 100644 --- a/server/data/autohouse.js +++ b/server/data/autohouse.js @@ -45,12 +45,14 @@ exports.default = async (req, res) => { res.sendStatus(401); return; } - try { - //Query for the List of Bodyshop Clients. - logger.log("autohouse-start", "DEBUG", "api", null, null); - const { bodyshops } = await client.request(queries.GET_AUTOHOUSE_SHOPS); - const specificShopIds = req.body.bodyshopIds; // ['uuid]; + // Send immediate response and continue processing. + res.status(200).send(); + + try { + logger.log("autohouse-start", "DEBUG", "api", null, null); + const { bodyshops } = await client.request(queries.GET_AUTOHOUSE_SHOPS); //Query for the List of Bodyshop Clients. + const specificShopIds = req.body.bodyshopIds; // ['uuid]; const { start, end, skipUpload } = req.body; //YYYY-MM-DD const batchSize = 10; @@ -61,26 +63,28 @@ exports.default = async (req, res) => { if (shopsToProcess.length === 0) { logger.log("autohouse-shopsToProcess-empty", "DEBUG", "api", null, null); - res.sendStatus(200); return; } - + const batchPromises = []; for (let i = 0; i < shopsToProcess.length; i += batchSize) { const batch = shopsToProcess.slice(i, i + batchSize); - await processBatch(batch, start, end); + const batchPromise = (async () => { + await processBatch(batch, start, end); - if (skipUpload) { - for (const xmlObj of allxmlsToUpload) { - fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml); + if (skipUpload) { + for (const xmlObj of allxmlsToUpload) { + fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml); + } + } else { + await uploadViaSFTP(allxmlsToUpload); } - } else { - await uploadViaSFTP(allxmlsToUpload); - } + })(); + batchPromises.push(batchPromise); } - sendServerEmail({ + await Promise.all(batchPromises); + await sendServerEmail({ subject: `Autohouse Report ${moment().format("MM-DD-YY")}`, - text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))} - Uploaded: ${JSON.stringify( + text: `Errors:\n${JSON.stringify(allErrors, null, 2)}\n\nUploaded:\n${JSON.stringify( allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count, result: x.result })), null, 2 @@ -88,10 +92,8 @@ exports.default = async (req, res) => { }); logger.log("autohouse-end", "DEBUG", "api", null, null); - res.sendStatus(200); } catch (error) { - logger.log("autohouse-shopsToProcess-error", "ERROR", "api", null, { error: error.message, stack: error.stack }); - res.status(500).json({ error: error.message, stack: error.stack }); + logger.log("autohouse-error", "ERROR", "api", null, { error: error.message, stack: error.stack }); } }; From 8d4195b596abce52470fb3c8c83c4d75888bf980 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Sat, 9 Nov 2024 00:35:18 -0800 Subject: [PATCH 19/37] IO-2921 Adjust SFTP setup Signed-off-by: Allan Carr --- docker-compose.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 50dc56932..581d74062 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -180,9 +180,10 @@ services: # - ./certs/id_rsa.pub:/home/user/.ssh/keys/id_rsa.pub:ro # Mount the SSH public key # - ./upload:/home/user/upload # Mount a local directory for SFTP uploads # environment: -# - SFTP_USERS=user:password:1001:100:upload +# - SFTP_USERS=user:password:1000::upload # command: > # /bin/sh -c " +# chmod -R 007 /home/user/upload && # echo 'Match User user' >> /etc/ssh/sshd_config && # sed -i -e 's#ForceCommand internal-sftp#ForceCommand internal-sftp -d /upload#' /etc/ssh/sshd_config && # /usr/sbin/sshd -D From 1440a6022847a80877a5f84859e54664aebf4a23 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Tue, 12 Nov 2024 12:31:46 -0800 Subject: [PATCH 20/37] feature/IO-3026-Enhanced-Notifications - Initial commit Signed-off-by: Dave Richer --- client/src/contexts/SocketIO/useSocket.js | 21 ++- .../pages/manage/manage.page.component.jsx | 120 +++++++++++++++++- .../redux/application/application.actions.js | 11 ++ .../redux/application/application.reducer.js | 23 +++- .../application/application.selectors.js | 1 + .../redux/application/application.types.js | 4 +- 6 files changed, 167 insertions(+), 13 deletions(-) diff --git a/client/src/contexts/SocketIO/useSocket.js b/client/src/contexts/SocketIO/useSocket.js index 11577c906..1c5a058fc 100644 --- a/client/src/contexts/SocketIO/useSocket.js +++ b/client/src/contexts/SocketIO/useSocket.js @@ -1,8 +1,9 @@ -import { useEffect, useState, useRef } from "react"; +import { useEffect, useRef, useState } from "react"; import SocketIO from "socket.io-client"; import { auth } from "../../firebase/firebase.utils"; import { store } from "../../redux/store"; -import { setWssStatus } from "../../redux/application/application.actions"; +import { addAlerts, setWssStatus } from "../../redux/application/application.actions"; + const useSocket = (bodyshop) => { const socketRef = useRef(null); const [clientId, setClientId] = useState(null); @@ -31,6 +32,14 @@ const useSocket = (bodyshop) => { socketRef.current = socketInstance; const handleBodyshopMessage = (message) => { + if (!message || !message?.type) return; + + switch (message.type) { + case "alert-update": + store.dispatch(addAlerts(message.payload)); + break; + } + if (!import.meta.env.DEV) return; console.log(`Received message for bodyshop ${bodyshop.id}:`, message); }; @@ -39,22 +48,22 @@ const useSocket = (bodyshop) => { console.log("Socket connected:", socketInstance.id); socketInstance.emit("join-bodyshop-room", bodyshop.id); setClientId(socketInstance.id); - store.dispatch(setWssStatus("connected")) + store.dispatch(setWssStatus("connected")); }; const handleReconnect = (attempt) => { console.log(`Socket reconnected after ${attempt} attempts`); - store.dispatch(setWssStatus("connected")) + store.dispatch(setWssStatus("connected")); }; const handleConnectionError = (err) => { console.error("Socket connection error:", err); - store.dispatch(setWssStatus("error")) + store.dispatch(setWssStatus("error")); }; const handleDisconnect = () => { console.log("Socket disconnected"); - store.dispatch(setWssStatus("disconnected")) + store.dispatch(setWssStatus("disconnected")); }; socketInstance.on("connect", handleConnect); diff --git a/client/src/pages/manage/manage.page.component.jsx b/client/src/pages/manage/manage.page.component.jsx index 46d66016f..a6ef08753 100644 --- a/client/src/pages/manage/manage.page.component.jsx +++ b/client/src/pages/manage/manage.page.component.jsx @@ -1,4 +1,4 @@ -import { FloatButton, Layout, Spin } from "antd"; +import { FloatButton, Layout, notification, Spin } from "antd"; // import preval from "preval.macro"; import React, { lazy, Suspense, useContext, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -21,11 +21,12 @@ import ShopSubStatusComponent from "../../components/shop-sub-status/shop-sub-st import { requestForToken } from "../../firebase/firebase.utils"; import SocketContext from "../../contexts/SocketIO/socketContext.jsx"; import { selectBodyshop, selectInstanceConflict } from "../../redux/user/user.selectors"; - import UpdateAlert from "../../components/update-alert/update-alert.component"; import InstanceRenderManager from "../../utils/instanceRenderMgr.js"; import "./manage.page.styles.scss"; import WssStatusDisplayComponent from "../../components/wss-status-display/wss-status-display.component.jsx"; +import { selectAlerts } from "../../redux/application/application.selectors.js"; +import { addAlerts } from "../../redux/application/application.actions.js"; const JobsPage = lazy(() => import("../jobs/jobs.page")); @@ -104,16 +105,125 @@ const { Content, Footer } = Layout; const mapStateToProps = createStructuredSelector({ conflict: selectInstanceConflict, - bodyshop: selectBodyshop + bodyshop: selectBodyshop, + alerts: selectAlerts }); -const mapDispatchToProps = (dispatch) => ({}); +// images.imex.online/alerts/alerts.json +const ALERT_FILE_URL = "http://localhost:5000/alerts.json"; -export function Manage({ conflict, bodyshop }) { +const mapDispatchToProps = (dispatch) => ({ + setAlerts: (alerts) => dispatch(addAlerts(alerts)) +}); + +export function Manage({ conflict, bodyshop, alerts, setAlerts }) { const { t } = useTranslation(); const [chatVisible] = useState(false); const { socket, clientId } = useContext(SocketContext); + // State to track displayed alerts + const [displayedAlertIds, setDisplayedAlertIds] = useState([]); + + // Fetch displayed alerts from localStorage on mount + useEffect(() => { + const displayedAlerts = JSON.parse(localStorage.getItem("displayedAlerts") || "[]"); + setDisplayedAlertIds(displayedAlerts); + }, []); + + // Fetch alerts from the JSON file and dispatch to Redux store + useEffect(() => { + const fetchAlerts = async () => { + try { + const response = await fetch(ALERT_FILE_URL); + const fetchedAlerts = await response.json(); + setAlerts(fetchedAlerts); + } catch (error) { + console.error("Error fetching alerts:", error); + } + }; + + fetchAlerts(); + }, []); + + useEffect(() => { + console.log("Alerts in Manage component:", alerts); + if (alerts && Object.keys(alerts).length > 0) { + // Convert the alerts object into an array + const alertArray = Object.values(alerts); + + // Filter out alerts that have already been displayed + const newAlerts = alertArray.filter((alert) => !displayedAlertIds.includes(alert.id)); + + console.log("New alerts to display:", newAlerts); + + newAlerts.forEach((alert) => { + // Display the notification + notification.open({ + key: "notification-alerts-" + alert.id, + message: alert.message, + description: alert.description, + type: alert.type || "info", + duration: 0, + placement: "bottomRight", + closable: true + }); + + // Update displayed alerts state and localStorage + setDisplayedAlertIds((prevIds) => { + const updatedIds = [...prevIds, alert.id]; + localStorage.setItem("displayedAlerts", JSON.stringify(updatedIds)); + return updatedIds; + }); + }); + } + }, [alerts]); + + // useEffect(() => { + // const fetchAlerts = async () => { + // try { + // const response = await fetch(ALERT_FILE_URL); + // + // // Check if the response is OK (status in the range 200-299) + // if (!response.ok) { + // console.error(`Network response was not ok: ${response.status} ${response.statusText}`); + // return; // Exit the function early since we can't proceed + // } + // + // const alerts = await response.json(); + // + // // Check if alerts is an array + // if (!Array.isArray(alerts)) { + // console.error("Alerts data is not an array"); + // return; + // } + // + // const displayedAlerts = JSON.parse(localStorage.getItem("displayedAlerts") || "[]"); + // const alertsNotDisplayed = alerts.filter((alert) => !displayedAlerts.includes(alert.id)); + // + // // Display notifications for alerts not yet displayed + // alertsNotDisplayed.forEach((alert) => { + // // Update localStorage immediately to prevent duplicate notifications + // displayedAlerts.push(alert.id); + // localStorage.setItem("displayedAlerts", JSON.stringify(displayedAlerts)); + // + // notification.open({ + // key: "notification-alerts-" + alert.id, + // message: alert.message, + // description: alert.description, + // type: alert.type || "info", + // duration: 0, + // placement: "bottomRight", + // closable: true + // }); + // }); + // } catch (error) { + // console.error("Error fetching alerts:", error); + // } + // }; + // + // fetchAlerts(); + // }, []); + useEffect(() => { const widgetId = InstanceRenderManager({ imex: "IABVNO4scRKY11XBQkNr", diff --git a/client/src/redux/application/application.actions.js b/client/src/redux/application/application.actions.js index c8246022b..04f880b9a 100644 --- a/client/src/redux/application/application.actions.js +++ b/client/src/redux/application/application.actions.js @@ -67,6 +67,17 @@ export const setUpdateAvailable = (isUpdateAvailable) => ({ type: ApplicationActionTypes.SET_UPDATE_AVAILABLE, payload: isUpdateAvailable }); + +export const setAlerts = (alerts) => ({ + type: ApplicationActionTypes.SET_ALERTS, + payload: alerts +}); + +export const addAlerts = (alerts) => ({ + type: ApplicationActionTypes.ADD_ALERTS, + payload: alerts +}); + export const setWssStatus = (status) => ({ type: ApplicationActionTypes.SET_WSS_STATUS, payload: status diff --git a/client/src/redux/application/application.reducer.js b/client/src/redux/application/application.reducer.js index 21878e52a..56327a0c0 100644 --- a/client/src/redux/application/application.reducer.js +++ b/client/src/redux/application/application.reducer.js @@ -15,7 +15,8 @@ const INITIAL_STATE = { error: null }, jobReadOnly: false, - partnerVersion: null + partnerVersion: null, + alerts: {} }; const applicationReducer = (state = INITIAL_STATE, action) => { @@ -91,6 +92,26 @@ const applicationReducer = (state = INITIAL_STATE, action) => { case ApplicationActionTypes.SET_WSS_STATUS: { return { ...state, wssStatus: action.payload }; } + case ApplicationActionTypes.SET_ALERTS: { + const alertsMap = {}; + action.payload.forEach((alert) => { + alertsMap[alert.id] = alert; + }); + return { + ...state, + alerts: alertsMap + }; + } + case ApplicationActionTypes.ADD_ALERTS: { + const newAlertsMap = { ...state.alerts }; + action.payload.forEach((alert) => { + newAlertsMap[alert.id] = alert; + }); + return { + ...state, + alerts: newAlertsMap + }; + } default: return state; } diff --git a/client/src/redux/application/application.selectors.js b/client/src/redux/application/application.selectors.js index a4f434cfe..6b0d1c2c4 100644 --- a/client/src/redux/application/application.selectors.js +++ b/client/src/redux/application/application.selectors.js @@ -23,3 +23,4 @@ export const selectOnline = createSelector([selectApplication], (application) => export const selectProblemJobs = createSelector([selectApplication], (application) => application.problemJobs); export const selectUpdateAvailable = createSelector([selectApplication], (application) => application.updateAvailable); export const selectWssStatus = createSelector([selectApplication], (application) => application.wssStatus); +export const selectAlerts = createSelector([selectApplication], (application) => application.alerts); diff --git a/client/src/redux/application/application.types.js b/client/src/redux/application/application.types.js index 1672cda0b..3665d6193 100644 --- a/client/src/redux/application/application.types.js +++ b/client/src/redux/application/application.types.js @@ -13,6 +13,8 @@ const ApplicationActionTypes = { INSERT_AUDIT_TRAIL: "INSERT_AUDIT_TRAIL", SET_PROBLEM_JOBS: "SET_PROBLEM_JOBS", SET_UPDATE_AVAILABLE: "SET_UPDATE_AVAILABLE", - SET_WSS_STATUS: "SET_WSS_STATUS" + SET_WSS_STATUS: "SET_WSS_STATUS", + SET_ALERTS: "SET_ALERTS", + ADD_ALERTS: "ADD_ALERTS" }; export default ApplicationActionTypes; From 6f454dd4cb7597f2ced8a69d930f69e3f28e233a Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Tue, 12 Nov 2024 14:20:49 -0800 Subject: [PATCH 21/37] feature/IO-3026-Enhanced-Notifications - final revisions Signed-off-by: Dave Richer --- .../pages/manage/manage.page.component.jsx | 31 ++++---- .../redux/application/application.actions.js | 5 -- .../redux/application/application.reducer.js | 14 +--- .../redux/application/application.types.js | 1 - server/alerts/alertcheck.js | 76 +++++++++++++++++++ server/routes/miscellaneousRoutes.js | 4 + 6 files changed, 99 insertions(+), 32 deletions(-) create mode 100644 server/alerts/alertcheck.js diff --git a/client/src/pages/manage/manage.page.component.jsx b/client/src/pages/manage/manage.page.component.jsx index a6ef08753..065fe7b46 100644 --- a/client/src/pages/manage/manage.page.component.jsx +++ b/client/src/pages/manage/manage.page.component.jsx @@ -109,8 +109,10 @@ const mapStateToProps = createStructuredSelector({ alerts: selectAlerts }); -// images.imex.online/alerts/alerts.json -const ALERT_FILE_URL = "http://localhost:5000/alerts.json"; +const ALERT_FILE_URL = InstanceRenderManager({ + imex: "https://images.imex.online/alerts/alerts-imex.json", + rome: "https://images.imex.online/alerts/alerts-rome.json" +}); const mapDispatchToProps = (dispatch) => ({ setAlerts: (alerts) => dispatch(addAlerts(alerts)) @@ -145,17 +147,15 @@ export function Manage({ conflict, bodyshop, alerts, setAlerts }) { fetchAlerts(); }, []); + // Use useEffect to watch for new alerts useEffect(() => { - console.log("Alerts in Manage component:", alerts); if (alerts && Object.keys(alerts).length > 0) { // Convert the alerts object into an array const alertArray = Object.values(alerts); - // Filter out alerts that have already been displayed + // Filter out alerts that have already been dismissed const newAlerts = alertArray.filter((alert) => !displayedAlertIds.includes(alert.id)); - console.log("New alerts to display:", newAlerts); - newAlerts.forEach((alert) => { // Display the notification notification.open({ @@ -165,18 +165,19 @@ export function Manage({ conflict, bodyshop, alerts, setAlerts }) { type: alert.type || "info", duration: 0, placement: "bottomRight", - closable: true - }); - - // Update displayed alerts state and localStorage - setDisplayedAlertIds((prevIds) => { - const updatedIds = [...prevIds, alert.id]; - localStorage.setItem("displayedAlerts", JSON.stringify(updatedIds)); - return updatedIds; + closable: true, + onClose: () => { + // When the notification is closed, update displayed alerts state and localStorage + setDisplayedAlertIds((prevIds) => { + const updatedIds = [...prevIds, alert.id]; + localStorage.setItem("displayedAlerts", JSON.stringify(updatedIds)); + return updatedIds; + }); + } }); }); } - }, [alerts]); + }, [alerts, displayedAlertIds]); // useEffect(() => { // const fetchAlerts = async () => { diff --git a/client/src/redux/application/application.actions.js b/client/src/redux/application/application.actions.js index 04f880b9a..4d362c6d8 100644 --- a/client/src/redux/application/application.actions.js +++ b/client/src/redux/application/application.actions.js @@ -68,11 +68,6 @@ export const setUpdateAvailable = (isUpdateAvailable) => ({ payload: isUpdateAvailable }); -export const setAlerts = (alerts) => ({ - type: ApplicationActionTypes.SET_ALERTS, - payload: alerts -}); - export const addAlerts = (alerts) => ({ type: ApplicationActionTypes.ADD_ALERTS, payload: alerts diff --git a/client/src/redux/application/application.reducer.js b/client/src/redux/application/application.reducer.js index 56327a0c0..6d5421e27 100644 --- a/client/src/redux/application/application.reducer.js +++ b/client/src/redux/application/application.reducer.js @@ -92,19 +92,11 @@ const applicationReducer = (state = INITIAL_STATE, action) => { case ApplicationActionTypes.SET_WSS_STATUS: { return { ...state, wssStatus: action.payload }; } - case ApplicationActionTypes.SET_ALERTS: { - const alertsMap = {}; - action.payload.forEach((alert) => { - alertsMap[alert.id] = alert; - }); - return { - ...state, - alerts: alertsMap - }; - } + case ApplicationActionTypes.ADD_ALERTS: { const newAlertsMap = { ...state.alerts }; - action.payload.forEach((alert) => { + + action.payload.alerts.forEach((alert) => { newAlertsMap[alert.id] = alert; }); return { diff --git a/client/src/redux/application/application.types.js b/client/src/redux/application/application.types.js index 3665d6193..26c1b4c7d 100644 --- a/client/src/redux/application/application.types.js +++ b/client/src/redux/application/application.types.js @@ -14,7 +14,6 @@ const ApplicationActionTypes = { SET_PROBLEM_JOBS: "SET_PROBLEM_JOBS", SET_UPDATE_AVAILABLE: "SET_UPDATE_AVAILABLE", SET_WSS_STATUS: "SET_WSS_STATUS", - SET_ALERTS: "SET_ALERTS", ADD_ALERTS: "ADD_ALERTS" }; export default ApplicationActionTypes; diff --git a/server/alerts/alertcheck.js b/server/alerts/alertcheck.js new file mode 100644 index 000000000..e29ff1e6a --- /dev/null +++ b/server/alerts/alertcheck.js @@ -0,0 +1,76 @@ +const axios = require("axios"); +const _ = require("lodash"); +const { default: InstanceMgr } = require("../utils/instanceMgr"); // For deep object comparison + +// Constants +const ALERTS_REDIS_KEY = "alerts_data"; // The key under which we'll store alerts in Redis +const GLOBAL_SOCKET_ID = "global"; // Use 'global' as a socketId to store global data + +const ALERT_FILE_URL = InstanceMgr({ + imex: "https://images.imex.online/alerts/alerts-imex.json", + rome: "https://images.imex.online/alerts/alerts-rome.json" +}); + +const alertCheck = async (req, res) => { + // Access Redis helper functions + const { ioRedis, logger } = req; + const { getSessionData, setSessionData } = req.sessionUtils; + + try { + // Get the JSON Alert file from the server + const response = await axios.get(ALERT_FILE_URL); + const currentAlerts = response.data; + // Retrieve stored alerts from Redis using a global socketId + const storedAlerts = await getSessionData(GLOBAL_SOCKET_ID, ALERTS_REDIS_KEY); + if (!storedAlerts) { + // Alerts not in Redis, store them + await setSessionData(GLOBAL_SOCKET_ID, ALERTS_REDIS_KEY, currentAlerts); + logger.logger.debug("Alerts added to Redis for the first time."); + + // Emit to clients + if (ioRedis) { + ioRedis.emit("bodyshop-message", { + type: "alert-update", + payload: currentAlerts + }); + logger.logger.debug("Alerts emitted to clients for the first time."); + } else { + logger.log("Socket.IO instance not found. (1)", "error"); + } + + return res.status(200).send("Alerts added to Redis and emitted to clients."); + } else { + // Alerts are in Redis, compare them + if (!_.isEqual(currentAlerts, storedAlerts)) { + // Alerts are different, update Redis and emit to clients + await setSessionData(GLOBAL_SOCKET_ID, ALERTS_REDIS_KEY, currentAlerts); + logger.logger.debug("Alerts updated in Redis."); + + // Emit the new alerts to all connected clients + if (ioRedis) { + ioRedis.emit("bodyshop-message", { + type: "alert-update", + payload: currentAlerts + }); + logger.logger.debug("Alerts emitted to clients after update."); + } else { + logger.log("Socket.IO instance not found. (2)", "error"); + } + + return res.status(200).send("Alerts updated in Redis and emitted to clients."); + } else { + return res.status(200).send("No changes in alerts."); + } + } + } catch (error) { + logger.log("Error in alertCheck:", "error", null, null, { + error: { + message: error.message, + stack: error.stack + } + }); + return res.status(500).send("Internal server error."); + } +}; + +module.exports = { alertCheck }; diff --git a/server/routes/miscellaneousRoutes.js b/server/routes/miscellaneousRoutes.js index fded5f44b..d772ffe61 100644 --- a/server/routes/miscellaneousRoutes.js +++ b/server/routes/miscellaneousRoutes.js @@ -12,6 +12,7 @@ const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebas const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware"); const { taskAssignedEmail, tasksRemindEmail } = require("../email/tasksEmails"); const { canvastest } = require("../render/canvas-handler"); +const { alertCheck } = require("../alerts/alertcheck"); //Test route to ensure Express is responding. router.get("/test", async function (req, res) { @@ -53,4 +54,7 @@ router.post("/taskHandler", validateFirebaseIdTokenMiddleware, taskHandler.taskH // Canvas Test router.post("/canvastest", validateFirebaseIdTokenMiddleware, canvastest); +// Alert Check +router.post("/alertcheck", eventAuthorizationMiddleware, alertCheck); + module.exports = router; From 6703bc025d524a81982163b57c19d3e951befa1f Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Tue, 12 Nov 2024 14:51:50 -0800 Subject: [PATCH 22/37] feature/IO-3026-Enhanced-Notifications - final revisions Signed-off-by: Dave Richer --- .../pages/manage/manage.page.component.jsx | 46 ------------------- 1 file changed, 46 deletions(-) diff --git a/client/src/pages/manage/manage.page.component.jsx b/client/src/pages/manage/manage.page.component.jsx index 065fe7b46..6dfd8af6e 100644 --- a/client/src/pages/manage/manage.page.component.jsx +++ b/client/src/pages/manage/manage.page.component.jsx @@ -179,52 +179,6 @@ export function Manage({ conflict, bodyshop, alerts, setAlerts }) { } }, [alerts, displayedAlertIds]); - // useEffect(() => { - // const fetchAlerts = async () => { - // try { - // const response = await fetch(ALERT_FILE_URL); - // - // // Check if the response is OK (status in the range 200-299) - // if (!response.ok) { - // console.error(`Network response was not ok: ${response.status} ${response.statusText}`); - // return; // Exit the function early since we can't proceed - // } - // - // const alerts = await response.json(); - // - // // Check if alerts is an array - // if (!Array.isArray(alerts)) { - // console.error("Alerts data is not an array"); - // return; - // } - // - // const displayedAlerts = JSON.parse(localStorage.getItem("displayedAlerts") || "[]"); - // const alertsNotDisplayed = alerts.filter((alert) => !displayedAlerts.includes(alert.id)); - // - // // Display notifications for alerts not yet displayed - // alertsNotDisplayed.forEach((alert) => { - // // Update localStorage immediately to prevent duplicate notifications - // displayedAlerts.push(alert.id); - // localStorage.setItem("displayedAlerts", JSON.stringify(displayedAlerts)); - // - // notification.open({ - // key: "notification-alerts-" + alert.id, - // message: alert.message, - // description: alert.description, - // type: alert.type || "info", - // duration: 0, - // placement: "bottomRight", - // closable: true - // }); - // }); - // } catch (error) { - // console.error("Error fetching alerts:", error); - // } - // }; - // - // fetchAlerts(); - // }, []); - useEffect(() => { const widgetId = InstanceRenderManager({ imex: "IABVNO4scRKY11XBQkNr", From 5b267f03b9f36bd7594349b66cc46babbc361f70 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Tue, 12 Nov 2024 20:21:43 -0800 Subject: [PATCH 23/37] Add additional GIN indexes for db. --- .../1731471670370_run_sql_migration/down.sql | 10 ++++++++++ .../migrations/1731471670370_run_sql_migration/up.sql | 8 ++++++++ 2 files changed, 18 insertions(+) create mode 100644 hasura/migrations/1731471670370_run_sql_migration/down.sql create mode 100644 hasura/migrations/1731471670370_run_sql_migration/up.sql diff --git a/hasura/migrations/1731471670370_run_sql_migration/down.sql b/hasura/migrations/1731471670370_run_sql_migration/down.sql new file mode 100644 index 000000000..923cf39ca --- /dev/null +++ b/hasura/migrations/1731471670370_run_sql_migration/down.sql @@ -0,0 +1,10 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- CREATE INDEX jobs_search_gin_ro_number ON jobs USING GIN ((ro_number) gin_trgm_ops); +-- CREATE INDEX jobs_search_gin_ownrfn ON jobs USING GIN ((ownr_fn) gin_trgm_ops); +-- CREATE INDEX jobs_search_gin_clm_no ON jobs USING GIN ((clm_no) gin_trgm_ops); +-- CREATE INDEX jobs_search_gin_plate_no ON jobs USING GIN ((plate_no) gin_trgm_ops); +-- CREATE INDEX jobs_search_gin_v_make_desc ON jobs USING GIN (( v_make_desc) gin_trgm_ops); +-- CREATE INDEX jobs_search_gin_v_model_desc ON jobs USING GIN (( v_model_desc) gin_trgm_ops); +-- CREATE INDEX jobs_search_gin_ownr_ln ON jobs USING GIN (( ownr_ln) gin_trgm_ops); +-- CREATE INDEX jobs_search_gin_ownr_co_nm ON jobs USING GIN (( ownr_co_nm) gin_trgm_ops); diff --git a/hasura/migrations/1731471670370_run_sql_migration/up.sql b/hasura/migrations/1731471670370_run_sql_migration/up.sql new file mode 100644 index 000000000..4b5ab065f --- /dev/null +++ b/hasura/migrations/1731471670370_run_sql_migration/up.sql @@ -0,0 +1,8 @@ +CREATE INDEX jobs_search_gin_ro_number ON jobs USING GIN ((ro_number) gin_trgm_ops); + CREATE INDEX jobs_search_gin_ownrfn ON jobs USING GIN ((ownr_fn) gin_trgm_ops); + CREATE INDEX jobs_search_gin_clm_no ON jobs USING GIN ((clm_no) gin_trgm_ops); + CREATE INDEX jobs_search_gin_plate_no ON jobs USING GIN ((plate_no) gin_trgm_ops); + CREATE INDEX jobs_search_gin_v_make_desc ON jobs USING GIN (( v_make_desc) gin_trgm_ops); + CREATE INDEX jobs_search_gin_v_model_desc ON jobs USING GIN (( v_model_desc) gin_trgm_ops); + CREATE INDEX jobs_search_gin_ownr_ln ON jobs USING GIN (( ownr_ln) gin_trgm_ops); + CREATE INDEX jobs_search_gin_ownr_co_nm ON jobs USING GIN (( ownr_co_nm) gin_trgm_ops); From bddeae945c8931ab345f0c21d90bfa0bb45d6935 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Wed, 13 Nov 2024 10:06:01 -0800 Subject: [PATCH 24/37] IO-3028 Word Wrap Line Description Signed-off-by: Allan Carr --- .../src/components/job-detail-lines/job-lines.component.jsx | 3 +-- .../job-lines-upsert-modal.component.jsx | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/client/src/components/job-detail-lines/job-lines.component.jsx b/client/src/components/job-detail-lines/job-lines.component.jsx index 984455466..070798452 100644 --- a/client/src/components/job-detail-lines/job-lines.component.jsx +++ b/client/src/components/job-detail-lines/job-lines.component.jsx @@ -118,8 +118,7 @@ export function JobLinesComponent({ ...(record.critical ? { boxShadow: " -.5em 0 0 #FFC107" } : {}) } }), - sortOrder: state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order, - ellipsis: true + sortOrder: state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order }, { title: t("joblines.fields.oem_partno"), diff --git a/client/src/components/job-lines-upsert-modal/job-lines-upsert-modal.component.jsx b/client/src/components/job-lines-upsert-modal/job-lines-upsert-modal.component.jsx index e07c54606..668452b9a 100644 --- a/client/src/components/job-lines-upsert-modal/job-lines-upsert-modal.component.jsx +++ b/client/src/components/job-lines-upsert-modal/job-lines-upsert-modal.component.jsx @@ -1,10 +1,10 @@ +import { useSplitTreatments } from "@splitsoftware/splitio-react"; import { Form, Input, InputNumber, Modal, Select, Switch } from "antd"; import React, { useEffect } from "react"; import { useTranslation } from "react-i18next"; import InputCurrency from "../form-items-formatted/currency-form-item.component"; -import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import JoblinesPreset from "../job-lines-preset-button/job-lines-preset-button.component"; -import { useSplitTreatments } from "@splitsoftware/splitio-react"; +import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; @@ -61,7 +61,7 @@ export function JobLinesUpsertModalComponent({ bodyshop, open, jobLine, handleCa ]} name="line_desc" > - + From 2b3e64d607989b53e82677a9ccc21449edea79f6 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Wed, 13 Nov 2024 10:49:47 -0800 Subject: [PATCH 25/37] IO-3028 Extend to Notes Signed-off-by: Allan Carr --- .../job-line-note-popup/job-line-note-popup.component.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/components/job-line-note-popup/job-line-note-popup.component.jsx b/client/src/components/job-line-note-popup/job-line-note-popup.component.jsx index 11d943354..2adb3bf72 100644 --- a/client/src/components/job-line-note-popup/job-line-note-popup.component.jsx +++ b/client/src/components/job-line-note-popup/job-line-note-popup.component.jsx @@ -45,7 +45,8 @@ export default function JobLineNotePopup({ jobline, disabled }) { if (editing) return (
- : null} value={note} From f3d8aa3438ca4dfed110edbbd2375be59677fadb Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Wed, 13 Nov 2024 12:59:56 -0800 Subject: [PATCH 26/37] feature/IO-3029-Enhanced-Logging-File-Based: Add File based S3 Logging. Signed-off-by: Dave Richer --- .gitattributes | 1 + docker-compose.yml | 5 +- package-lock.json | 1286 +++++++++++++++++++++++--- package.json | 1 + server/routes/miscellaneousRoutes.js | 26 +- server/utils/logger.js | 67 +- server/utils/s3.js | 112 +++ 7 files changed, 1339 insertions(+), 159 deletions(-) create mode 100644 .gitattributes create mode 100644 server/utils/s3.js diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..fcadb2cf9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text eol=lf diff --git a/docker-compose.yml b/docker-compose.yml index 581d74062..5c9cdc14b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -74,7 +74,7 @@ services: volumes: - /var/run/docker.sock:/var/run/docker.sock environment: - - SERVICES=ses,secretsmanager,cloudwatch,logs + - SERVICES=s3,ses,secretsmanager,cloudwatch,logs - DEBUG=0 - AWS_ACCESS_KEY_ID=test - AWS_SECRET_ACCESS_KEY=test @@ -115,7 +115,8 @@ services: aws --endpoint-url=http://localstack:4566 ses verify-domain-identity --domain imex.online --region ca-central-1 aws --endpoint-url=http://localstack:4566 ses verify-email-identity --email-address noreply@imex.online --region ca-central-1 aws --endpoint-url=http://localstack:4566 secretsmanager create-secret --name CHATTER_PRIVATE_KEY --secret-string file:///tmp/certs/id_rsa - aws --endpoint-url=http://localstack:4566 logs create-log-group --log-group-name development --region ca-central-1 + aws --endpoint-url=http://localstack:4566 logs create-log-group --log-group-name development --region ca-central-1 + aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-large-log --create-bucket-configuration LocationConstraint=ca-central-1 " # Node App: The Main IMEX API node-app: diff --git a/package-lock.json b/package-lock.json index 2f0edfad5..d95191db5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@aws-sdk/client-cloudwatch-logs": "^3.679.0", "@aws-sdk/client-elasticache": "^3.675.0", + "@aws-sdk/client-s3": "^3.689.0", "@aws-sdk/client-secrets-manager": "^3.675.0", "@aws-sdk/client-ses": "^3.675.0", "@aws-sdk/credential-provider-node": "^3.675.0", @@ -86,6 +87,69 @@ "node": ">=16.0.0" } }, + "node_modules/@aws-crypto/crc32c": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", + "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", + "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@aws-crypto/sha256-browser": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", @@ -802,6 +866,566 @@ "node": ">=16.0.0" } }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.689.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.689.0.tgz", + "integrity": "sha512-qYD1GJEPeLM6H3x8BuAAMXZltvVce5vGiwtZc9uMkBBo3HyFnmPitIPTPfaD1q8LOn/7KFdkY4MJ4e8D3YpV9g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha1-browser": "5.2.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.687.0", + "@aws-sdk/client-sts": "3.687.0", + "@aws-sdk/core": "3.686.0", + "@aws-sdk/credential-provider-node": "3.687.0", + "@aws-sdk/middleware-bucket-endpoint": "3.686.0", + "@aws-sdk/middleware-expect-continue": "3.686.0", + "@aws-sdk/middleware-flexible-checksums": "3.689.0", + "@aws-sdk/middleware-host-header": "3.686.0", + "@aws-sdk/middleware-location-constraint": "3.686.0", + "@aws-sdk/middleware-logger": "3.686.0", + "@aws-sdk/middleware-recursion-detection": "3.686.0", + "@aws-sdk/middleware-sdk-s3": "3.687.0", + "@aws-sdk/middleware-ssec": "3.686.0", + "@aws-sdk/middleware-user-agent": "3.687.0", + "@aws-sdk/region-config-resolver": "3.686.0", + "@aws-sdk/signature-v4-multi-region": "3.687.0", + "@aws-sdk/types": "3.686.0", + "@aws-sdk/util-endpoints": "3.686.0", + "@aws-sdk/util-user-agent-browser": "3.686.0", + "@aws-sdk/util-user-agent-node": "3.687.0", + "@aws-sdk/xml-builder": "3.686.0", + "@smithy/config-resolver": "^3.0.10", + "@smithy/core": "^2.5.1", + "@smithy/eventstream-serde-browser": "^3.0.11", + "@smithy/eventstream-serde-config-resolver": "^3.0.8", + "@smithy/eventstream-serde-node": "^3.0.10", + "@smithy/fetch-http-handler": "^4.0.0", + "@smithy/hash-blob-browser": "^3.1.7", + "@smithy/hash-node": "^3.0.8", + "@smithy/hash-stream-node": "^3.1.7", + "@smithy/invalid-dependency": "^3.0.8", + "@smithy/md5-js": "^3.0.8", + "@smithy/middleware-content-length": "^3.0.10", + "@smithy/middleware-endpoint": "^3.2.1", + "@smithy/middleware-retry": "^3.0.25", + "@smithy/middleware-serde": "^3.0.8", + "@smithy/middleware-stack": "^3.0.8", + "@smithy/node-config-provider": "^3.1.9", + "@smithy/node-http-handler": "^3.2.5", + "@smithy/protocol-http": "^4.1.5", + "@smithy/smithy-client": "^3.4.2", + "@smithy/types": "^3.6.0", + "@smithy/url-parser": "^3.0.8", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.25", + "@smithy/util-defaults-mode-node": "^3.0.25", + "@smithy/util-endpoints": "^2.1.4", + "@smithy/util-middleware": "^3.0.8", + "@smithy/util-retry": "^3.0.8", + "@smithy/util-stream": "^3.2.1", + "@smithy/util-utf8": "^3.0.0", + "@smithy/util-waiter": "^3.1.7", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/client-sso": { + "version": "3.687.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.687.0.tgz", + "integrity": "sha512-dfj0y9fQyX4kFill/ZG0BqBTLQILKlL7+O5M4F9xlsh2WNuV2St6WtcOg14Y1j5UODPJiJs//pO+mD1lihT5Kw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.686.0", + "@aws-sdk/middleware-host-header": "3.686.0", + "@aws-sdk/middleware-logger": "3.686.0", + "@aws-sdk/middleware-recursion-detection": "3.686.0", + "@aws-sdk/middleware-user-agent": "3.687.0", + "@aws-sdk/region-config-resolver": "3.686.0", + "@aws-sdk/types": "3.686.0", + "@aws-sdk/util-endpoints": "3.686.0", + "@aws-sdk/util-user-agent-browser": "3.686.0", + "@aws-sdk/util-user-agent-node": "3.687.0", + "@smithy/config-resolver": "^3.0.10", + "@smithy/core": "^2.5.1", + "@smithy/fetch-http-handler": "^4.0.0", + "@smithy/hash-node": "^3.0.8", + "@smithy/invalid-dependency": "^3.0.8", + "@smithy/middleware-content-length": "^3.0.10", + "@smithy/middleware-endpoint": "^3.2.1", + "@smithy/middleware-retry": "^3.0.25", + "@smithy/middleware-serde": "^3.0.8", + "@smithy/middleware-stack": "^3.0.8", + "@smithy/node-config-provider": "^3.1.9", + "@smithy/node-http-handler": "^3.2.5", + "@smithy/protocol-http": "^4.1.5", + "@smithy/smithy-client": "^3.4.2", + "@smithy/types": "^3.6.0", + "@smithy/url-parser": "^3.0.8", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.25", + "@smithy/util-defaults-mode-node": "^3.0.25", + "@smithy/util-endpoints": "^2.1.4", + "@smithy/util-middleware": "^3.0.8", + "@smithy/util-retry": "^3.0.8", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.687.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.687.0.tgz", + "integrity": "sha512-Rdd8kLeTeh+L5ZuG4WQnWgYgdv7NorytKdZsGjiag1D8Wv3PcJvPqqWdgnI0Og717BSXVoaTYaN34FyqFYSx6Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.686.0", + "@aws-sdk/credential-provider-node": "3.687.0", + "@aws-sdk/middleware-host-header": "3.686.0", + "@aws-sdk/middleware-logger": "3.686.0", + "@aws-sdk/middleware-recursion-detection": "3.686.0", + "@aws-sdk/middleware-user-agent": "3.687.0", + "@aws-sdk/region-config-resolver": "3.686.0", + "@aws-sdk/types": "3.686.0", + "@aws-sdk/util-endpoints": "3.686.0", + "@aws-sdk/util-user-agent-browser": "3.686.0", + "@aws-sdk/util-user-agent-node": "3.687.0", + "@smithy/config-resolver": "^3.0.10", + "@smithy/core": "^2.5.1", + "@smithy/fetch-http-handler": "^4.0.0", + "@smithy/hash-node": "^3.0.8", + "@smithy/invalid-dependency": "^3.0.8", + "@smithy/middleware-content-length": "^3.0.10", + "@smithy/middleware-endpoint": "^3.2.1", + "@smithy/middleware-retry": "^3.0.25", + "@smithy/middleware-serde": "^3.0.8", + "@smithy/middleware-stack": "^3.0.8", + "@smithy/node-config-provider": "^3.1.9", + "@smithy/node-http-handler": "^3.2.5", + "@smithy/protocol-http": "^4.1.5", + "@smithy/smithy-client": "^3.4.2", + "@smithy/types": "^3.6.0", + "@smithy/url-parser": "^3.0.8", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.25", + "@smithy/util-defaults-mode-node": "^3.0.25", + "@smithy/util-endpoints": "^2.1.4", + "@smithy/util-middleware": "^3.0.8", + "@smithy/util-retry": "^3.0.8", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.687.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/client-sts": { + "version": "3.687.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.687.0.tgz", + "integrity": "sha512-SQjDH8O4XCTtouuCVYggB0cCCrIaTzUZIkgJUpOsIEJBLlTbNOb/BZqUShAQw2o9vxr2rCeOGjAQOYPysW/Pmg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.687.0", + "@aws-sdk/core": "3.686.0", + "@aws-sdk/credential-provider-node": "3.687.0", + "@aws-sdk/middleware-host-header": "3.686.0", + "@aws-sdk/middleware-logger": "3.686.0", + "@aws-sdk/middleware-recursion-detection": "3.686.0", + "@aws-sdk/middleware-user-agent": "3.687.0", + "@aws-sdk/region-config-resolver": "3.686.0", + "@aws-sdk/types": "3.686.0", + "@aws-sdk/util-endpoints": "3.686.0", + "@aws-sdk/util-user-agent-browser": "3.686.0", + "@aws-sdk/util-user-agent-node": "3.687.0", + "@smithy/config-resolver": "^3.0.10", + "@smithy/core": "^2.5.1", + "@smithy/fetch-http-handler": "^4.0.0", + "@smithy/hash-node": "^3.0.8", + "@smithy/invalid-dependency": "^3.0.8", + "@smithy/middleware-content-length": "^3.0.10", + "@smithy/middleware-endpoint": "^3.2.1", + "@smithy/middleware-retry": "^3.0.25", + "@smithy/middleware-serde": "^3.0.8", + "@smithy/middleware-stack": "^3.0.8", + "@smithy/node-config-provider": "^3.1.9", + "@smithy/node-http-handler": "^3.2.5", + "@smithy/protocol-http": "^4.1.5", + "@smithy/smithy-client": "^3.4.2", + "@smithy/types": "^3.6.0", + "@smithy/url-parser": "^3.0.8", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.25", + "@smithy/util-defaults-mode-node": "^3.0.25", + "@smithy/util-endpoints": "^2.1.4", + "@smithy/util-middleware": "^3.0.8", + "@smithy/util-retry": "^3.0.8", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/core": { + "version": "3.686.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.686.0.tgz", + "integrity": "sha512-Xt3DV4DnAT3v2WURwzTxWQK34Ew+iiLzoUoguvLaZrVMFOqMMrwVjP+sizqIaHp1j7rGmFcN5I8saXnsDLuQLA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.686.0", + "@smithy/core": "^2.5.1", + "@smithy/node-config-provider": "^3.1.9", + "@smithy/property-provider": "^3.1.7", + "@smithy/protocol-http": "^4.1.5", + "@smithy/signature-v4": "^4.2.0", + "@smithy/smithy-client": "^3.4.2", + "@smithy/types": "^3.6.0", + "@smithy/util-middleware": "^3.0.8", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.686.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.686.0.tgz", + "integrity": "sha512-osD7lPO8OREkgxPiTWmA1i6XEmOth1uW9HWWj/+A2YGCj1G/t2sHu931w4Qj9NWHYZtbTTXQYVRg+TErALV7nQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.686.0", + "@aws-sdk/types": "3.686.0", + "@smithy/property-provider": "^3.1.7", + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.686.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.686.0.tgz", + "integrity": "sha512-xyGAD/f3vR/wssUiZrNFWQWXZvI4zRm2wpHhoHA1cC2fbRMNFYtFn365yw6dU7l00ZLcdFB1H119AYIUZS7xbw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.686.0", + "@aws-sdk/types": "3.686.0", + "@smithy/fetch-http-handler": "^4.0.0", + "@smithy/node-http-handler": "^3.2.5", + "@smithy/property-provider": "^3.1.7", + "@smithy/protocol-http": "^4.1.5", + "@smithy/smithy-client": "^3.4.2", + "@smithy/types": "^3.6.0", + "@smithy/util-stream": "^3.2.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.687.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.687.0.tgz", + "integrity": "sha512-6d5ZJeZch+ZosJccksN0PuXv7OSnYEmanGCnbhUqmUSz9uaVX6knZZfHCZJRgNcfSqg9QC0zsFA/51W5HCUqSQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.686.0", + "@aws-sdk/credential-provider-env": "3.686.0", + "@aws-sdk/credential-provider-http": "3.686.0", + "@aws-sdk/credential-provider-process": "3.686.0", + "@aws-sdk/credential-provider-sso": "3.687.0", + "@aws-sdk/credential-provider-web-identity": "3.686.0", + "@aws-sdk/types": "3.686.0", + "@smithy/credential-provider-imds": "^3.2.4", + "@smithy/property-provider": "^3.1.7", + "@smithy/shared-ini-file-loader": "^3.1.8", + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.687.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.687.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.687.0.tgz", + "integrity": "sha512-Pqld8Nx11NYaBUrVk3bYiGGpLCxkz8iTONlpQWoVWFhSOzlO7zloNOaYbD2XgFjjqhjlKzE91drs/f41uGeCTA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.686.0", + "@aws-sdk/credential-provider-http": "3.686.0", + "@aws-sdk/credential-provider-ini": "3.687.0", + "@aws-sdk/credential-provider-process": "3.686.0", + "@aws-sdk/credential-provider-sso": "3.687.0", + "@aws-sdk/credential-provider-web-identity": "3.686.0", + "@aws-sdk/types": "3.686.0", + "@smithy/credential-provider-imds": "^3.2.4", + "@smithy/property-provider": "^3.1.7", + "@smithy/shared-ini-file-loader": "^3.1.8", + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.686.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.686.0.tgz", + "integrity": "sha512-sXqaAgyzMOc+dm4CnzAR5Q6S9OWVHyZjLfW6IQkmGjqeQXmZl24c4E82+w64C+CTkJrFLzH1VNOYp1Hy5gE6Qw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.686.0", + "@aws-sdk/types": "3.686.0", + "@smithy/property-provider": "^3.1.7", + "@smithy/shared-ini-file-loader": "^3.1.8", + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.687.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.687.0.tgz", + "integrity": "sha512-N1YCoE7DovIRF2ReyRrA4PZzF0WNi4ObPwdQQkVxhvSm7PwjbWxrfq7rpYB+6YB1Uq3QPzgVwUFONE36rdpxUQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.687.0", + "@aws-sdk/core": "3.686.0", + "@aws-sdk/token-providers": "3.686.0", + "@aws-sdk/types": "3.686.0", + "@smithy/property-provider": "^3.1.7", + "@smithy/shared-ini-file-loader": "^3.1.8", + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.686.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.686.0.tgz", + "integrity": "sha512-40UqCpPxyHCXDP7CGd9JIOZDgDZf+u1OyLaGBpjQJlz1HYuEsIWnnbTe29Yg3Ah/Zc3g4NBWcUdlGVotlnpnDg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.686.0", + "@aws-sdk/types": "3.686.0", + "@smithy/property-provider": "^3.1.7", + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.686.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.686.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.686.0.tgz", + "integrity": "sha512-+Yc6rO02z+yhFbHmRZGvEw1vmzf/ifS9a4aBjJGeVVU+ZxaUvnk+IUZWrj4YQopUQ+bSujmMUzJLXSkbDq7yuw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.686.0", + "@smithy/protocol-http": "^4.1.5", + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-logger": { + "version": "3.686.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.686.0.tgz", + "integrity": "sha512-cX43ODfA2+SPdX7VRxu6gXk4t4bdVJ9pkktbfnkE5t27OlwNfvSGGhnHrQL8xTOFeyQ+3T+oowf26gf1OI+vIg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.686.0", + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.686.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.686.0.tgz", + "integrity": "sha512-jF9hQ162xLgp9zZ/3w5RUNhmwVnXDBlABEUX8jCgzaFpaa742qR/KKtjjZQ6jMbQnP+8fOCSXFAVNMU+s6v81w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.686.0", + "@smithy/protocol-http": "^4.1.5", + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.687.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.687.0.tgz", + "integrity": "sha512-nUgsKiEinyA50CaDXojAkOasAU3Apdg7Qox6IjNUC4ZjgOu7QWsCDB5N28AYMUt06cNYeYQdfMX1aEzG85a1Mg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.686.0", + "@aws-sdk/types": "3.686.0", + "@aws-sdk/util-endpoints": "3.686.0", + "@smithy/core": "^2.5.1", + "@smithy/protocol-http": "^4.1.5", + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.686.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.686.0.tgz", + "integrity": "sha512-6zXD3bSD8tcsMAVVwO1gO7rI1uy2fCD3czgawuPGPopeLiPpo6/3FoUWCQzk2nvEhj7p9Z4BbjwZGSlRkVrXTw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.686.0", + "@smithy/node-config-provider": "^3.1.9", + "@smithy/types": "^3.6.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/token-providers": { + "version": "3.686.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.686.0.tgz", + "integrity": "sha512-9oL4kTCSePFmyKPskibeiOXV6qavPZ63/kXM9Wh9V6dTSvBtLeNnMxqGvENGKJcTdIgtoqyqA6ET9u0PJ5IRIg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.686.0", + "@smithy/property-provider": "^3.1.7", + "@smithy/shared-ini-file-loader": "^3.1.8", + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sso-oidc": "^3.686.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/types": { + "version": "3.686.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.686.0.tgz", + "integrity": "sha512-xFnrb3wxOoJcW2Xrh63ZgFo5buIu9DF7bOHnwoUxHdNpUXicUh0AHw85TjXxyxIAd0d1psY/DU7QHoNI3OswgQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-endpoints": { + "version": "3.686.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.686.0.tgz", + "integrity": "sha512-7msZE2oYl+6QYeeRBjlDgxQUhq/XRky3cXE0FqLFs2muLS7XSuQEXkpOXB3R782ygAP6JX0kmBxPTLurRTikZg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.686.0", + "@smithy/types": "^3.6.0", + "@smithy/util-endpoints": "^2.1.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.686.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.686.0.tgz", + "integrity": "sha512-YiQXeGYZegF1b7B2GOR61orhgv79qmI0z7+Agm3NXLO6hGfVV3kFUJbXnjtH1BgWo5hbZYW7HQ2omGb3dnb6Lg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.686.0", + "@smithy/types": "^3.6.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.687.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.687.0.tgz", + "integrity": "sha512-idkP6ojSTZ4ek1pJ8wIN7r9U3KR5dn0IkJn3KQBXQ58LWjkRqLtft2vxzdsktWwhPKjjmIKl1S0kbvqLawf8XQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.687.0", + "@aws-sdk/types": "3.686.0", + "@smithy/node-config-provider": "^3.1.9", + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@smithy/fetch-http-handler": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-4.0.0.tgz", + "integrity": "sha512-MLb1f5tbBO2X6K4lMEKJvxeLooyg7guq48C2zKr4qM7F2Gpkz4dc+hdSgu77pCJ76jVqFBjZczHYAs6dp15N+g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^4.1.5", + "@smithy/querystring-builder": "^3.0.8", + "@smithy/types": "^3.6.0", + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" + } + }, "node_modules/@aws-sdk/client-secrets-manager": { "version": "3.675.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.675.0.tgz", @@ -1237,6 +1861,124 @@ "@aws-sdk/client-sts": "^3.667.0" } }, + "node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.686.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.686.0.tgz", + "integrity": "sha512-6qCoWI73/HDzQE745MHQUYz46cAQxHCgy1You8MZQX9vHAQwqBnkcsb2hGp7S6fnQY5bNsiZkMWVQ/LVd2MNjg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.686.0", + "@aws-sdk/util-arn-parser": "3.679.0", + "@smithy/node-config-provider": "^3.1.9", + "@smithy/protocol-http": "^4.1.5", + "@smithy/types": "^3.6.0", + "@smithy/util-config-provider": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-bucket-endpoint/node_modules/@aws-sdk/types": { + "version": "3.686.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.686.0.tgz", + "integrity": "sha512-xFnrb3wxOoJcW2Xrh63ZgFo5buIu9DF7bOHnwoUxHdNpUXicUh0AHw85TjXxyxIAd0d1psY/DU7QHoNI3OswgQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-expect-continue": { + "version": "3.686.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.686.0.tgz", + "integrity": "sha512-5yYqIbyhLhH29vn4sHiTj7sU6GttvLMk3XwCmBXjo2k2j3zHqFUwh9RyFGF9VY6Z392Drf/E/cl+qOGypwULpg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.686.0", + "@smithy/protocol-http": "^4.1.5", + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-expect-continue/node_modules/@aws-sdk/types": { + "version": "3.686.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.686.0.tgz", + "integrity": "sha512-xFnrb3wxOoJcW2Xrh63ZgFo5buIu9DF7bOHnwoUxHdNpUXicUh0AHw85TjXxyxIAd0d1psY/DU7QHoNI3OswgQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums": { + "version": "3.689.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.689.0.tgz", + "integrity": "sha512-6VxMOf3mgmAgg6SMagwKj5pAe+putcx2F2odOAWviLcobFpdM/xK9vNry7p6kY+RDNmSlBvcji9wnU59fjV74Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@aws-crypto/crc32c": "5.2.0", + "@aws-crypto/util": "5.2.0", + "@aws-sdk/core": "3.686.0", + "@aws-sdk/types": "3.686.0", + "@smithy/is-array-buffer": "^3.0.0", + "@smithy/node-config-provider": "^3.1.9", + "@smithy/protocol-http": "^4.1.5", + "@smithy/types": "^3.6.0", + "@smithy/util-middleware": "^3.0.8", + "@smithy/util-stream": "^3.2.1", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums/node_modules/@aws-sdk/core": { + "version": "3.686.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.686.0.tgz", + "integrity": "sha512-Xt3DV4DnAT3v2WURwzTxWQK34Ew+iiLzoUoguvLaZrVMFOqMMrwVjP+sizqIaHp1j7rGmFcN5I8saXnsDLuQLA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.686.0", + "@smithy/core": "^2.5.1", + "@smithy/node-config-provider": "^3.1.9", + "@smithy/property-provider": "^3.1.7", + "@smithy/protocol-http": "^4.1.5", + "@smithy/signature-v4": "^4.2.0", + "@smithy/smithy-client": "^3.4.2", + "@smithy/types": "^3.6.0", + "@smithy/util-middleware": "^3.0.8", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums/node_modules/@aws-sdk/types": { + "version": "3.686.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.686.0.tgz", + "integrity": "sha512-xFnrb3wxOoJcW2Xrh63ZgFo5buIu9DF7bOHnwoUxHdNpUXicUh0AHw85TjXxyxIAd0d1psY/DU7QHoNI3OswgQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@aws-sdk/middleware-host-header": { "version": "3.667.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.667.0.tgz", @@ -1252,6 +1994,33 @@ "node": ">=16.0.0" } }, + "node_modules/@aws-sdk/middleware-location-constraint": { + "version": "3.686.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.686.0.tgz", + "integrity": "sha512-pCLeZzt5zUGY3NbW4J/5x3kaHyJEji4yqtoQcUlJmkoEInhSxJ0OE8sTxAfyL3nIOF4yr6L2xdaLCqYgQT8Aog==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.686.0", + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-location-constraint/node_modules/@aws-sdk/types": { + "version": "3.686.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.686.0.tgz", + "integrity": "sha512-xFnrb3wxOoJcW2Xrh63ZgFo5buIu9DF7bOHnwoUxHdNpUXicUh0AHw85TjXxyxIAd0d1psY/DU7QHoNI3OswgQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@aws-sdk/middleware-logger": { "version": "3.667.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.667.0.tgz", @@ -1281,6 +2050,93 @@ "node": ">=16.0.0" } }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.687.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.687.0.tgz", + "integrity": "sha512-YGHYqiyRiNNucmvLrfx3QxIkjSDWR/+cc72bn0lPvqFUQBRHZgmYQLxVYrVZSmRzzkH2FQ1HsZcXhOafLbq4vQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.686.0", + "@aws-sdk/types": "3.686.0", + "@aws-sdk/util-arn-parser": "3.679.0", + "@smithy/core": "^2.5.1", + "@smithy/node-config-provider": "^3.1.9", + "@smithy/protocol-http": "^4.1.5", + "@smithy/signature-v4": "^4.2.0", + "@smithy/smithy-client": "^3.4.2", + "@smithy/types": "^3.6.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.8", + "@smithy/util-stream": "^3.2.1", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3/node_modules/@aws-sdk/core": { + "version": "3.686.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.686.0.tgz", + "integrity": "sha512-Xt3DV4DnAT3v2WURwzTxWQK34Ew+iiLzoUoguvLaZrVMFOqMMrwVjP+sizqIaHp1j7rGmFcN5I8saXnsDLuQLA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.686.0", + "@smithy/core": "^2.5.1", + "@smithy/node-config-provider": "^3.1.9", + "@smithy/property-provider": "^3.1.7", + "@smithy/protocol-http": "^4.1.5", + "@smithy/signature-v4": "^4.2.0", + "@smithy/smithy-client": "^3.4.2", + "@smithy/types": "^3.6.0", + "@smithy/util-middleware": "^3.0.8", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3/node_modules/@aws-sdk/types": { + "version": "3.686.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.686.0.tgz", + "integrity": "sha512-xFnrb3wxOoJcW2Xrh63ZgFo5buIu9DF7bOHnwoUxHdNpUXicUh0AHw85TjXxyxIAd0d1psY/DU7QHoNI3OswgQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec": { + "version": "3.686.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.686.0.tgz", + "integrity": "sha512-zJXml/CpVHFUdlGQqja87vNQ3rPB5SlDbfdwxlj1KBbjnRRwpBtxxmOlWRShg8lnVV6aIMGv95QmpIFy4ayqnQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.686.0", + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec/node_modules/@aws-sdk/types": { + "version": "3.686.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.686.0.tgz", + "integrity": "sha512-xFnrb3wxOoJcW2Xrh63ZgFo5buIu9DF7bOHnwoUxHdNpUXicUh0AHw85TjXxyxIAd0d1psY/DU7QHoNI3OswgQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@aws-sdk/middleware-user-agent": { "version": "3.669.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.669.0.tgz", @@ -1316,6 +2172,36 @@ "node": ">=16.0.0" } }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.687.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.687.0.tgz", + "integrity": "sha512-vdOQHCRHJPX9mT8BM6xOseazHD6NodvHl9cyF5UjNtLn+gERRJEItIA9hf0hlt62odGD8Fqp+rFRuqdmbNkcNw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "3.687.0", + "@aws-sdk/types": "3.686.0", + "@smithy/protocol-http": "^4.1.5", + "@smithy/signature-v4": "^4.2.0", + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region/node_modules/@aws-sdk/types": { + "version": "3.686.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.686.0.tgz", + "integrity": "sha512-xFnrb3wxOoJcW2Xrh63ZgFo5buIu9DF7bOHnwoUxHdNpUXicUh0AHw85TjXxyxIAd0d1psY/DU7QHoNI3OswgQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@aws-sdk/token-providers": { "version": "3.667.0", "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.667.0.tgz", @@ -1348,6 +2234,18 @@ "node": ">=16.0.0" } }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.679.0.tgz", + "integrity": "sha512-CwzEbU8R8rq9bqUFryO50RFBlkfufV9UfMArHPWlo+lmsC+NlSluHQALoj6Jkq3zf5ppn1CN0c1DDLrEqdQUXg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@aws-sdk/util-endpoints": { "version": "3.667.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.667.0.tgz", @@ -1410,6 +2308,19 @@ } } }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.686.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.686.0.tgz", + "integrity": "sha512-k0z5b5dkYSuOHY0AOZ4iyjcGBeVL9lWsQNF4+c+1oK3OW4fRWl/bNa1soMRMpangsHPzgyn/QkzuDbl7qR4qrw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.23.5", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", @@ -2460,28 +3371,47 @@ } }, "node_modules/@smithy/abort-controller": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-3.1.5.tgz", - "integrity": "sha512-DhNPnqTqPoG8aZ5dWkFOgsuY+i0GQ3CI6hMmvCoduNsnU9gUZWZBwGfDQsTTB7NvFPkom1df7jMIJWU90kuXXg==", + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-3.1.6.tgz", + "integrity": "sha512-0XuhuHQlEqbNQZp7QxxrFTdVWdwxch4vjxYgfInF91hZFkPxf9QDrdQka0KfxFMPqLNzSw0b95uGTrLliQUavQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.5.0", + "@smithy/types": "^3.6.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@smithy/config-resolver": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-3.0.9.tgz", - "integrity": "sha512-5d9oBf40qC7n2xUoHmntKLdqsyTMMo/r49+eqSIjJ73eDfEtljAxEhzIQ3bkgXJtR3xiv7YzMT/3FF3ORkjWdg==", + "node_modules/@smithy/chunked-blob-reader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-4.0.0.tgz", + "integrity": "sha512-jSqRnZvkT4egkq/7b6/QRCNXmmYVcHwnJldqJ3IhVpQE2atObVJ137xmGeuGFhjFUr8gCEVAOKwSY79OvpbDaQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^3.1.8", - "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@smithy/chunked-blob-reader-native": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-3.0.1.tgz", + "integrity": "sha512-VEYtPvh5rs/xlyqpm5NRnfYLZn+q0SRPELbvBV+C/G7IQ+ouTuo+NKKa3ShG5OaFR8NYVMXls9hPYLTvIKKDrQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-3.0.10.tgz", + "integrity": "sha512-Uh0Sz9gdUuz538nvkPiyv1DZRX9+D15EKDtnQP5rYVAzM/dnYk3P8cg73jcxyOitPgT3mE3OVj7ky7sibzHWkw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^3.1.9", + "@smithy/types": "^3.6.0", "@smithy/util-config-provider": "^3.0.0", - "@smithy/util-middleware": "^3.0.7", + "@smithy/util-middleware": "^3.0.8", "tslib": "^2.6.2" }, "engines": { @@ -2489,19 +3419,17 @@ } }, "node_modules/@smithy/core": { - "version": "2.4.8", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-2.4.8.tgz", - "integrity": "sha512-x4qWk7p/a4dcf7Vxb2MODIf4OIcqNbK182WxRvZ/3oKPrf/6Fdic5sSElhO1UtXpWKBazWfqg0ZEK9xN1DsuHA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-2.5.1.tgz", + "integrity": "sha512-DujtuDA7BGEKExJ05W5OdxCoyekcKT3Rhg1ZGeiUWaz2BJIWXjZmsG/DIP4W48GHno7AQwRsaCb8NcBgH3QZpg==", "license": "Apache-2.0", "dependencies": { - "@smithy/middleware-endpoint": "^3.1.4", - "@smithy/middleware-retry": "^3.0.23", - "@smithy/middleware-serde": "^3.0.7", - "@smithy/protocol-http": "^4.1.4", - "@smithy/smithy-client": "^3.4.0", - "@smithy/types": "^3.5.0", + "@smithy/middleware-serde": "^3.0.8", + "@smithy/protocol-http": "^4.1.5", + "@smithy/types": "^3.6.0", "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-middleware": "^3.0.7", + "@smithy/util-middleware": "^3.0.8", + "@smithy/util-stream": "^3.2.1", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, @@ -2510,15 +3438,15 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-3.2.4.tgz", - "integrity": "sha512-S9bb0EIokfYEuar4kEbLta+ivlKCWOCFsLZuilkNy9i0uEUEHSi47IFLPaxqqCl+0ftKmcOTHayY5nQhAuq7+w==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-3.2.5.tgz", + "integrity": "sha512-4FTQGAsuwqTzVMmiRVTn0RR9GrbRfkP0wfu/tXWVHd2LgNpTY0uglQpIScXK4NaEyXbB3JmZt8gfVqO50lP8wg==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^3.1.8", - "@smithy/property-provider": "^3.1.7", - "@smithy/types": "^3.5.0", - "@smithy/url-parser": "^3.0.7", + "@smithy/node-config-provider": "^3.1.9", + "@smithy/property-provider": "^3.1.8", + "@smithy/types": "^3.6.0", + "@smithy/url-parser": "^3.0.8", "tslib": "^2.6.2" }, "engines": { @@ -2605,13 +3533,25 @@ "tslib": "^2.6.2" } }, - "node_modules/@smithy/hash-node": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-3.0.7.tgz", - "integrity": "sha512-SAGHN+QkrwcHFjfWzs/czX94ZEjPJ0CrWJS3M43WswDXVEuP4AVy9gJ3+AF6JQHZD13bojmuf/Ap/ItDeZ+Qfw==", + "node_modules/@smithy/hash-blob-browser": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-3.1.7.tgz", + "integrity": "sha512-4yNlxVNJifPM5ThaA5HKnHkn7JhctFUHvcaz6YXxHlYOSIrzI6VKQPTN8Gs1iN5nqq9iFcwIR9THqchUCouIfg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.5.0", + "@smithy/chunked-blob-reader": "^4.0.0", + "@smithy/chunked-blob-reader-native": "^3.0.1", + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@smithy/hash-node": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-3.0.8.tgz", + "integrity": "sha512-tlNQYbfpWXHimHqrvgo14DrMAgUBua/cNoz9fMYcDmYej7MAmUcjav/QKQbFc3NrcPxeJ7QClER4tWZmfwoPng==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.6.0", "@smithy/util-buffer-from": "^3.0.0", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" @@ -2620,13 +3560,27 @@ "node": ">=16.0.0" } }, - "node_modules/@smithy/invalid-dependency": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-3.0.7.tgz", - "integrity": "sha512-Bq00GsAhHeYSuZX8Kpu4sbI9agH2BNYnqUmmbTGWOhki9NVsWn2jFr896vvoTMH8KAjNX/ErC/8t5QHuEXG+IA==", + "node_modules/@smithy/hash-stream-node": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-3.1.7.tgz", + "integrity": "sha512-xMAsvJ3hLG63lsBVi1Hl6BBSfhd8/Qnp8fC06kjOpJvyyCEXdwHITa5Kvdsk6gaAXLhbZMhQMIGvgUbfnJDP6Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.5.0", + "@smithy/types": "^3.6.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-3.0.8.tgz", + "integrity": "sha512-7Qynk6NWtTQhnGTTZwks++nJhQ1O54Mzi7fz4PqZOiYXb4Z1Flpb2yRvdALoggTS8xjtohWUM+RygOtB30YL3Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.6.0", "tslib": "^2.6.2" } }, @@ -2641,14 +3595,25 @@ "node": ">=16.0.0" } }, - "node_modules/@smithy/middleware-content-length": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-3.0.9.tgz", - "integrity": "sha512-t97PidoGElF9hTtLCrof32wfWMqC5g2SEJNxaVH3NjlatuNGsdxXRYO/t+RPnxA15RpYiS0f+zG7FuE2DeGgjA==", + "node_modules/@smithy/md5-js": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-3.0.8.tgz", + "integrity": "sha512-LwApfTK0OJ/tCyNUXqnWCKoE2b4rDSr4BJlDAVCkiWYeHESr+y+d5zlAanuLW6fnitVJRD/7d9/kN/ZM9Su4mA==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^4.1.4", - "@smithy/types": "^3.5.0", + "@smithy/types": "^3.6.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-3.0.10.tgz", + "integrity": "sha512-T4dIdCs1d/+/qMpwhJ1DzOhxCZjZHbHazEPJWdB4GDi2HjIZllVzeBEcdJUN0fomV8DURsgOyrbEUzg3vzTaOg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^4.1.5", + "@smithy/types": "^3.6.0", "tslib": "^2.6.2" }, "engines": { @@ -2656,17 +3621,18 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-3.1.4.tgz", - "integrity": "sha512-/ChcVHekAyzUbyPRI8CzPPLj6y8QRAfJngWcLMgsWxKVzw/RzBV69mSOzJYDD3pRwushA1+5tHtPF8fjmzBnrQ==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-3.2.1.tgz", + "integrity": "sha512-wWO3xYmFm6WRW8VsEJ5oU6h7aosFXfszlz3Dj176pTij6o21oZnzkCLzShfmRaaCHDkBXWBdO0c4sQAvLFP6zA==", "license": "Apache-2.0", "dependencies": { - "@smithy/middleware-serde": "^3.0.7", - "@smithy/node-config-provider": "^3.1.8", - "@smithy/shared-ini-file-loader": "^3.1.8", - "@smithy/types": "^3.5.0", - "@smithy/url-parser": "^3.0.7", - "@smithy/util-middleware": "^3.0.7", + "@smithy/core": "^2.5.1", + "@smithy/middleware-serde": "^3.0.8", + "@smithy/node-config-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.9", + "@smithy/types": "^3.6.0", + "@smithy/url-parser": "^3.0.8", + "@smithy/util-middleware": "^3.0.8", "tslib": "^2.6.2" }, "engines": { @@ -2674,18 +3640,18 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "3.0.23", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-3.0.23.tgz", - "integrity": "sha512-x9PbGXxkcXIpm6L26qRSCC+eaYcHwybRmqU8LO/WM2RRlW0g8lz6FIiKbKgGvHuoK3dLZRiQVSQJveiCzwnA5A==", + "version": "3.0.25", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-3.0.25.tgz", + "integrity": "sha512-m1F70cPaMBML4HiTgCw5I+jFNtjgz5z5UdGnUbG37vw6kh4UvizFYjqJGHvicfgKMkDL6mXwyPp5mhZg02g5sg==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^3.1.8", - "@smithy/protocol-http": "^4.1.4", - "@smithy/service-error-classification": "^3.0.7", - "@smithy/smithy-client": "^3.4.0", - "@smithy/types": "^3.5.0", - "@smithy/util-middleware": "^3.0.7", - "@smithy/util-retry": "^3.0.7", + "@smithy/node-config-provider": "^3.1.9", + "@smithy/protocol-http": "^4.1.5", + "@smithy/service-error-classification": "^3.0.8", + "@smithy/smithy-client": "^3.4.2", + "@smithy/types": "^3.6.0", + "@smithy/util-middleware": "^3.0.8", + "@smithy/util-retry": "^3.0.8", "tslib": "^2.6.2", "uuid": "^9.0.1" }, @@ -2707,12 +3673,12 @@ } }, "node_modules/@smithy/middleware-serde": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-3.0.7.tgz", - "integrity": "sha512-VytaagsQqtH2OugzVTq4qvjkLNbWehHfGcGr0JLJmlDRrNCeZoWkWsSOw1nhS/4hyUUWF/TLGGml4X/OnEep5g==", + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-3.0.8.tgz", + "integrity": "sha512-Xg2jK9Wc/1g/MBMP/EUn2DLspN8LNt+GMe7cgF+Ty3vl+Zvu+VeZU5nmhveU+H8pxyTsjrAkci8NqY6OuvZnjA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.5.0", + "@smithy/types": "^3.6.0", "tslib": "^2.6.2" }, "engines": { @@ -2720,12 +3686,12 @@ } }, "node_modules/@smithy/middleware-stack": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-3.0.7.tgz", - "integrity": "sha512-EyTbMCdqS1DoeQsO4gI7z2Gzq1MoRFAeS8GkFYIwbedB7Lp5zlLHJdg+56tllIIG5Hnf9ZWX48YKSHlsKvugGA==", + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-3.0.8.tgz", + "integrity": "sha512-d7ZuwvYgp1+3682Nx0MD3D/HtkmZd49N3JUndYWQXfRZrYEnCWYc8BHcNmVsPAp9gKvlurdg/mubE6b/rPS9MA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.5.0", + "@smithy/types": "^3.6.0", "tslib": "^2.6.2" }, "engines": { @@ -2733,14 +3699,14 @@ } }, "node_modules/@smithy/node-config-provider": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-3.1.8.tgz", - "integrity": "sha512-E0rU0DglpeJn5ge64mk8wTGEXcQwmpUTY5Zr7IzTpDLmHKiIamINERNZYrPQjg58Ck236sEKSwRSHA4CwshU6Q==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-3.1.9.tgz", + "integrity": "sha512-qRHoah49QJ71eemjuS/WhUXB+mpNtwHRWQr77J/m40ewBVVwvo52kYAmb7iuaECgGTTcYxHS4Wmewfwy++ueew==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^3.1.7", - "@smithy/shared-ini-file-loader": "^3.1.8", - "@smithy/types": "^3.5.0", + "@smithy/property-provider": "^3.1.8", + "@smithy/shared-ini-file-loader": "^3.1.9", + "@smithy/types": "^3.6.0", "tslib": "^2.6.2" }, "engines": { @@ -2748,15 +3714,15 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-3.2.4.tgz", - "integrity": "sha512-49reY3+JgLMFNm7uTAKBWiKCA6XSvkNp9FqhVmusm2jpVnHORYFeFZ704LShtqWfjZW/nhX+7Iexyb6zQfXYIQ==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-3.2.5.tgz", + "integrity": "sha512-PkOwPNeKdvX/jCpn0A8n9/TyoxjGZB8WVoJmm9YzsnAgggTj4CrjpRHlTQw7dlLZ320n1mY1y+nTRUDViKi/3w==", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^3.1.5", - "@smithy/protocol-http": "^4.1.4", - "@smithy/querystring-builder": "^3.0.7", - "@smithy/types": "^3.5.0", + "@smithy/abort-controller": "^3.1.6", + "@smithy/protocol-http": "^4.1.5", + "@smithy/querystring-builder": "^3.0.8", + "@smithy/types": "^3.6.0", "tslib": "^2.6.2" }, "engines": { @@ -2764,12 +3730,12 @@ } }, "node_modules/@smithy/property-provider": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-3.1.7.tgz", - "integrity": "sha512-QfzLi1GPMisY7bAM5hOUqBdGYnY5S2JAlr201pghksrQv139f8iiiMalXtjczIP5f6owxFn3MINLNUNvUkgtPw==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-3.1.8.tgz", + "integrity": "sha512-ukNUyo6rHmusG64lmkjFeXemwYuKge1BJ8CtpVKmrxQxc6rhUX0vebcptFA9MmrGsnLhwnnqeH83VTU9hwOpjA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.5.0", + "@smithy/types": "^3.6.0", "tslib": "^2.6.2" }, "engines": { @@ -2790,12 +3756,12 @@ } }, "node_modules/@smithy/querystring-builder": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-3.0.7.tgz", - "integrity": "sha512-65RXGZZ20rzqqxTsChdqSpbhA6tdt5IFNgG6o7e1lnPVLCe6TNWQq4rTl4N87hTDD8mV4IxJJnvyE7brbnRkQw==", + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-3.0.8.tgz", + "integrity": "sha512-btYxGVqFUARbUrN6VhL9c3dnSviIwBYD9Rz1jHuN1hgh28Fpv2xjU1HeCeDJX68xctz7r4l1PBnFhGg1WBBPuA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.5.0", + "@smithy/types": "^3.6.0", "@smithy/util-uri-escape": "^3.0.0", "tslib": "^2.6.2" }, @@ -2804,12 +3770,12 @@ } }, "node_modules/@smithy/querystring-parser": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-3.0.7.tgz", - "integrity": "sha512-Fouw4KJVWqqUVIu1gZW8BH2HakwLz6dvdrAhXeXfeymOBrZw+hcqaWs+cS1AZPVp4nlbeIujYrKA921ZW2WMPA==", + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-3.0.8.tgz", + "integrity": "sha512-BtEk3FG7Ks64GAbt+JnKqwuobJNX8VmFLBsKIwWr1D60T426fGrV2L3YS5siOcUhhp6/Y6yhBw1PSPxA5p7qGg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.5.0", + "@smithy/types": "^3.6.0", "tslib": "^2.6.2" }, "engines": { @@ -2817,24 +3783,24 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-3.0.7.tgz", - "integrity": "sha512-91PRkTfiBf9hxkIchhRKJfl1rsplRDyBnmyFca3y0Z3x/q0JJN480S83LBd8R6sBCkm2bBbqw2FHp0Mbh+ecSA==", + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-3.0.8.tgz", + "integrity": "sha512-uEC/kCCFto83bz5ZzapcrgGqHOh/0r69sZ2ZuHlgoD5kYgXJEThCoTuw/y1Ub3cE7aaKdznb+jD9xRPIfIwD7g==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.5.0" + "@smithy/types": "^3.6.0" }, "engines": { "node": ">=16.0.0" } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.8.tgz", - "integrity": "sha512-0NHdQiSkeGl0ICQKcJQ2lCOKH23Nb0EaAa7RDRId6ZqwXkw4LJyIyZ0t3iusD4bnKYDPLGy2/5e2rfUhrt0Acw==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.9.tgz", + "integrity": "sha512-/+OsJRNtoRbtsX0UpSgWVxFZLsJHo/4sTr+kBg/J78sr7iC+tHeOvOJrS5hCpVQ6sWBbhWLp1UNiuMyZhE6pmA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.5.0", + "@smithy/types": "^3.6.0", "tslib": "^2.6.2" }, "engines": { @@ -2861,16 +3827,17 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-3.4.0.tgz", - "integrity": "sha512-nOfJ1nVQsxiP6srKt43r2My0Gp5PLWCW2ASqUioxIiGmu6d32v4Nekidiv5qOmmtzIrmaD+ADX5SKHUuhReeBQ==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-3.4.2.tgz", + "integrity": "sha512-dxw1BDxJiY9/zI3cBqfVrInij6ShjpV4fmGHesGZZUiP9OSE/EVfdwdRz0PgvkEvrZHpsj2htRaHJfftE8giBA==", "license": "Apache-2.0", "dependencies": { - "@smithy/middleware-endpoint": "^3.1.4", - "@smithy/middleware-stack": "^3.0.7", - "@smithy/protocol-http": "^4.1.4", - "@smithy/types": "^3.5.0", - "@smithy/util-stream": "^3.1.9", + "@smithy/core": "^2.5.1", + "@smithy/middleware-endpoint": "^3.2.1", + "@smithy/middleware-stack": "^3.0.8", + "@smithy/protocol-http": "^4.1.5", + "@smithy/types": "^3.6.0", + "@smithy/util-stream": "^3.2.1", "tslib": "^2.6.2" }, "engines": { @@ -2890,13 +3857,13 @@ } }, "node_modules/@smithy/url-parser": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-3.0.7.tgz", - "integrity": "sha512-70UbSSR8J97c1rHZOWhl+VKiZDqHWxs/iW8ZHrHp5fCCPLSBE7GcUlUvKSle3Ca+J9LLbYCj/A79BxztBvAfpA==", + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-3.0.8.tgz", + "integrity": "sha512-4FdOhwpTW7jtSFWm7SpfLGKIBC9ZaTKG5nBF0wK24aoQKQyDIKUw3+KFWCQ9maMzrgTJIuOvOnsV2lLGW5XjTg==", "license": "Apache-2.0", "dependencies": { - "@smithy/querystring-parser": "^3.0.7", - "@smithy/types": "^3.5.0", + "@smithy/querystring-parser": "^3.0.8", + "@smithy/types": "^3.6.0", "tslib": "^2.6.2" } }, @@ -2957,14 +3924,14 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "3.0.23", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-3.0.23.tgz", - "integrity": "sha512-Y07qslyRtXDP/C5aWKqxTPBl4YxplEELG3xRrz2dnAQ6Lq/FgNrcKWmV561nNaZmFH+EzeGOX3ZRMbU8p1T6Nw==", + "version": "3.0.25", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-3.0.25.tgz", + "integrity": "sha512-fRw7zymjIDt6XxIsLwfJfYUfbGoO9CmCJk6rjJ/X5cd20+d2Is7xjU5Kt/AiDt6hX8DAf5dztmfP5O82gR9emA==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^3.1.7", - "@smithy/smithy-client": "^3.4.0", - "@smithy/types": "^3.5.0", + "@smithy/property-provider": "^3.1.8", + "@smithy/smithy-client": "^3.4.2", + "@smithy/types": "^3.6.0", "bowser": "^2.11.0", "tslib": "^2.6.2" }, @@ -2973,17 +3940,17 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "3.0.23", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-3.0.23.tgz", - "integrity": "sha512-9Y4WH7f0vnDGuHUa4lGX9e2p+sMwODibsceSV6rfkZOvMC+BY3StB2LdO1NHafpsyHJLpwAgChxQ38tFyd6vkg==", + "version": "3.0.25", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-3.0.25.tgz", + "integrity": "sha512-H3BSZdBDiVZGzt8TG51Pd2FvFO0PAx/A0mJ0EH8a13KJ6iUCdYnw/Dk/MdC1kTd0eUuUGisDFaxXVXo4HHFL1g==", "license": "Apache-2.0", "dependencies": { - "@smithy/config-resolver": "^3.0.9", - "@smithy/credential-provider-imds": "^3.2.4", - "@smithy/node-config-provider": "^3.1.8", - "@smithy/property-provider": "^3.1.7", - "@smithy/smithy-client": "^3.4.0", - "@smithy/types": "^3.5.0", + "@smithy/config-resolver": "^3.0.10", + "@smithy/credential-provider-imds": "^3.2.5", + "@smithy/node-config-provider": "^3.1.9", + "@smithy/property-provider": "^3.1.8", + "@smithy/smithy-client": "^3.4.2", + "@smithy/types": "^3.6.0", "tslib": "^2.6.2" }, "engines": { @@ -2991,13 +3958,13 @@ } }, "node_modules/@smithy/util-endpoints": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-2.1.3.tgz", - "integrity": "sha512-34eACeKov6jZdHqS5hxBMJ4KyWKztTMulhuQ2UdOoP6vVxMLrOKUqIXAwJe/wiWMhXhydLW664B02CNpQBQ4Aw==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-2.1.4.tgz", + "integrity": "sha512-kPt8j4emm7rdMWQyL0F89o92q10gvCUa6sBkBtDJ7nV2+P7wpXczzOfoDJ49CKXe5CCqb8dc1W+ZdLlrKzSAnQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^3.1.8", - "@smithy/types": "^3.5.0", + "@smithy/node-config-provider": "^3.1.9", + "@smithy/types": "^3.6.0", "tslib": "^2.6.2" }, "engines": { @@ -3030,13 +3997,13 @@ } }, "node_modules/@smithy/util-retry": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-3.0.7.tgz", - "integrity": "sha512-nh1ZO1vTeo2YX1plFPSe/OXaHkLAHza5jpokNiiKX2M5YpNUv6RxGJZhpfmiR4jSvVHCjIDmILjrxKmP+/Ghug==", + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-3.0.8.tgz", + "integrity": "sha512-TCEhLnY581YJ+g1x0hapPz13JFqzmh/pMWL2KEFASC51qCfw3+Y47MrTmea4bUE5vsdxQ4F6/KFbUeSz22Q1ow==", "license": "Apache-2.0", "dependencies": { - "@smithy/service-error-classification": "^3.0.7", - "@smithy/types": "^3.5.0", + "@smithy/service-error-classification": "^3.0.8", + "@smithy/types": "^3.6.0", "tslib": "^2.6.2" }, "engines": { @@ -3044,14 +4011,14 @@ } }, "node_modules/@smithy/util-stream": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-3.1.9.tgz", - "integrity": "sha512-7YAR0Ub3MwTMjDfjnup4qa6W8gygZMxikBhFMPESi6ASsl/rZJhwLpF/0k9TuezScCojsM0FryGdz4LZtjKPPQ==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-3.2.1.tgz", + "integrity": "sha512-R3ufuzJRxSJbE58K9AEnL/uSZyVdHzud9wLS8tIbXclxKzoe09CRohj2xV8wpx5tj7ZbiJaKYcutMm1eYgz/0A==", "license": "Apache-2.0", "dependencies": { - "@smithy/fetch-http-handler": "^3.2.9", - "@smithy/node-http-handler": "^3.2.4", - "@smithy/types": "^3.5.0", + "@smithy/fetch-http-handler": "^4.0.0", + "@smithy/node-http-handler": "^3.2.5", + "@smithy/types": "^3.6.0", "@smithy/util-base64": "^3.0.0", "@smithy/util-buffer-from": "^3.0.0", "@smithy/util-hex-encoding": "^3.0.0", @@ -3062,6 +4029,19 @@ "node": ">=16.0.0" } }, + "node_modules/@smithy/util-stream/node_modules/@smithy/fetch-http-handler": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-4.0.0.tgz", + "integrity": "sha512-MLb1f5tbBO2X6K4lMEKJvxeLooyg7guq48C2zKr4qM7F2Gpkz4dc+hdSgu77pCJ76jVqFBjZczHYAs6dp15N+g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^4.1.5", + "@smithy/querystring-builder": "^3.0.8", + "@smithy/types": "^3.6.0", + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" + } + }, "node_modules/@smithy/util-uri-escape": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-3.0.0.tgz", @@ -3087,13 +4067,13 @@ } }, "node_modules/@smithy/util-waiter": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-3.1.6.tgz", - "integrity": "sha512-xs/KAwWOeCklq8aMlnpk25LgxEYHKOEodfjfKclDMLcBJEVEKzDLxZxBQyztcuPJ7F54213NJS8PxoiHNMdItQ==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-3.1.7.tgz", + "integrity": "sha512-d5yGlQtmN/z5eoTtIYgkvOw27US2Ous4VycnXatyoImIF9tzlcpnKqQ/V7qhvJmb2p6xZne1NopCLakdTnkBBQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^3.1.5", - "@smithy/types": "^3.5.0", + "@smithy/abort-controller": "^3.1.6", + "@smithy/types": "^3.6.0", "tslib": "^2.6.2" }, "engines": { diff --git a/package.json b/package.json index 2a3c24ded..f51af45b5 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "dependencies": { "@aws-sdk/client-cloudwatch-logs": "^3.679.0", "@aws-sdk/client-elasticache": "^3.675.0", + "@aws-sdk/client-s3": "^3.689.0", "@aws-sdk/client-secrets-manager": "^3.675.0", "@aws-sdk/client-ses": "^3.675.0", "@aws-sdk/credential-provider-node": "^3.675.0", diff --git a/server/routes/miscellaneousRoutes.js b/server/routes/miscellaneousRoutes.js index d772ffe61..27ef15f15 100644 --- a/server/routes/miscellaneousRoutes.js +++ b/server/routes/miscellaneousRoutes.js @@ -15,7 +15,7 @@ const { canvastest } = require("../render/canvas-handler"); const { alertCheck } = require("../alerts/alertcheck"); //Test route to ensure Express is responding. -router.get("/test", async function (req, res) { +router.get("/test", eventAuthorizationMiddleware, async function (req, res) { const commit = require("child_process").execSync("git rev-parse --short HEAD"); // console.log(app.get('trust proxy')); // console.log("remoteAddress", req.socket.remoteAddress); @@ -32,6 +32,30 @@ router.get("/test", async function (req, res) { res.status(200).send(`OK - ${commit}`); }); +router.get("/test-logs", eventAuthorizationMiddleware, (req, res) => { + const { logger } = req; + // // Test 1: Log with a message that exceeds the size limit, triggering an upload to S3. + const largeMessage = "A".repeat(256 * 1024 + 1); // Message larger than the log size limit + logger.log(largeMessage, "error", "user123", null, { detail: "large log entry" }); + + // Test 2: Log with a message that is within the size limit, should log directly using winston. + const smallMessage = "A small log message"; + logger.log(smallMessage, "info", "user123", null, { detail: "small log entry" }); + + // Test 3: Log with the `upload` flag set to `true`, forcing the log to be uploaded to S3. + logger.log( + "This log will be uploaded to S3 regardless of size", + "warning", + "user123", + null, + { detail: "upload log" }, + true + ); + + // Test 4: Log with a message that doesn't exceed the size limit and doesn't require an upload. + logger.log("Normal log entry", "debug", "user123", { id: 4 }, { detail: "normal log entry" }); +}); + // Search router.post("/search", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, os.search); router.post("/opensearch", eventAuthorizationMiddleware, os.handler); diff --git a/server/utils/logger.js b/server/utils/logger.js index bfb0c0cda..620ea98a8 100644 --- a/server/utils/logger.js +++ b/server/utils/logger.js @@ -9,6 +9,8 @@ const winston = require("winston"); const WinstonCloudWatch = require("winston-cloudwatch"); const { isString, isEmpty } = require("lodash"); const { networkInterfaces, hostname } = require("node:os"); +const { uploadFileToS3 } = require("./s3"); +const { v4 } = require("uuid"); const LOG_LEVELS = { error: { level: 0, name: "error" }, @@ -20,6 +22,24 @@ const LOG_LEVELS = { silly: { level: 6, name: "silly" } }; +const LOG_LENGTH_LIMIT = 256 * 1024; // 256KB + +const S3_BUCKET_NAME = InstanceManager({ + imex: "imex-large-log", + rome: "rome-large-log" +}); + +const estimateLogSize = (logEntry) => { + let estimatedSize = 0; + for (const key in logEntry) { + if (logEntry.hasOwnProperty(key)) { + const value = logEntry[key]; + estimatedSize += key.length + (typeof value === "string" ? value.length : JSON.stringify(value).length); + } + } + return estimatedSize; +}; + const normalizeLevel = (level) => (level ? level.toLowerCase() : LOG_LEVELS.debug.name); const createLogger = () => { @@ -124,15 +144,56 @@ const createLogger = () => { ); } - const log = (message, type, user, record, meta) => { - winstonLogger.log({ + const log = (message, type, user, record, meta, upload) => { + const logEntry = { level: normalizeLevel(type), message, user, record, hostname: internalHostname, meta - }); + }; + + const uploadLogToS3 = (logEntry, message, type, user) => { + const uniqueId = v4(); + const dateTimeString = new Date().toISOString().replace(/:/g, "-"); + const logStreamName = `${dateTimeString}-${internalHostname}-${uniqueId}`; + const logString = JSON.stringify(logEntry); + + uploadFileToS3({ bucketName: S3_BUCKET_NAME, key: logStreamName, content: logString }) + .then(() => { + log("A log file has been uploaded to S3", "info", "S3", null, { + logStreamName, + message: message?.slice(0, 200), + type, + user + }); + }) + .catch((err) => { + log("Error in S3 Upload", "error", "S3", null, { + logStreamName, + message: message?.slice(0, 100), + type, + user, + errorMessage: err?.message?.slice(0, 100) + }); + }); + }; + + const checkAndUploadLog = () => { + const logString = JSON.stringify(logEntry); + const logSize = Buffer.byteLength(logString, "utf8"); + + if (logSize > LOG_LENGTH_LIMIT * 0.9 || logSize > LOG_LENGTH_LIMIT) { + uploadLogToS3(logEntry, message, type, user); + return true; + } + return false; + }; + + if (upload || checkAndUploadLog()) return; + + winstonLogger.log(logEntry); }; return { diff --git a/server/utils/s3.js b/server/utils/s3.js new file mode 100644 index 000000000..132fcc193 --- /dev/null +++ b/server/utils/s3.js @@ -0,0 +1,112 @@ +const { + S3Client, + PutObjectCommand, + GetObjectCommand, + ListObjectsV2Command, + DeleteObjectCommand, + CopyObjectCommand +} = require("@aws-sdk/client-s3"); +const { defaultProvider } = require("@aws-sdk/credential-provider-node"); +const { default: InstanceManager } = require("./instanceMgr"); +const { isString, isEmpty } = require("lodash"); + +const createS3Client = () => { + const S3Options = { + region: InstanceManager({ + imex: "ca-central-1", + rome: "us-east-2" + }), + credentials: defaultProvider() + }; + + const isLocal = isString(process.env?.LOCALSTACK_HOSTNAME) && !isEmpty(process.env?.LOCALSTACK_HOSTNAME); + + if (isLocal) { + S3Options.endpoint = `http://${process.env.LOCALSTACK_HOSTNAME}:4566`; + S3Options.forcePathStyle = true; // Needed for LocalStack to avoid bucket name as hostname + } + + const s3Client = new S3Client(S3Options); + + /** + * Uploads a file to the specified S3 bucket and key. + */ + const uploadFileToS3 = async ({ bucketName, key, content, contentType }) => { + const params = { + Bucket: bucketName, + Key: key, + Body: content, + ContentType: contentType ?? "application/json" + }; + const command = new PutObjectCommand(params); + return await s3Client.send(command); + }; + + /** + * Downloads a file from the specified S3 bucket and key. + */ + const downloadFileFromS3 = async ({ bucketName, key }) => { + const params = { Bucket: bucketName, Key: key }; + const command = new GetObjectCommand(params); + const data = await s3Client.send(command); + return data.Body; + }; + + /** + * Lists objects in the specified S3 bucket. + */ + const listFilesInS3Bucket = async (bucketName, prefix = "") => { + const params = { Bucket: bucketName, Prefix: prefix }; + const command = new ListObjectsV2Command(params); + const data = await s3Client.send(command); + return data.Contents || []; + }; + + /** + * Deletes a file from the specified S3 bucket and key. + */ + const deleteFileFromS3 = async ({ bucketName, key }) => { + const params = { Bucket: bucketName, Key: key }; + const command = new DeleteObjectCommand(params); + return await s3Client.send(command); + }; + + /** + * Copies a file within S3 from a source bucket/key to a destination bucket/key. + */ + const copyFileInS3 = async ({ sourceBucket, sourceKey, destinationBucket, destinationKey }) => { + const params = { + CopySource: `/${sourceBucket}/${sourceKey}`, + Bucket: destinationBucket, + Key: destinationKey + }; + const command = new CopyObjectCommand(params); + return await s3Client.send(command); + }; + + /** + * Checks if a file exists in the specified S3 bucket and key. + */ + const fileExistsInS3 = async ({ bucketName, key }) => { + try { + await downloadFileFromS3({ bucketName, key }); + return true; + } catch (error) { + if (error.name === "NoSuchKey" || error.name === "NotFound") { + return false; + } + throw error; + } + }; + + return { + uploadFileToS3, + downloadFileFromS3, + listFilesInS3Bucket, + deleteFileFromS3, + copyFileInS3, + fileExistsInS3 + }; +}; + +module.exports = createS3Client(); From cba2da8da73ee22c53f67bb617540ed0bb031e3d Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Wed, 13 Nov 2024 13:09:37 -0800 Subject: [PATCH 27/37] feature/IO-3029-Enhanced-Logging-File-Based: Add fix bugs Signed-off-by: Dave Richer --- server/routes/miscellaneousRoutes.js | 2 ++ server/utils/logger.js | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/server/routes/miscellaneousRoutes.js b/server/routes/miscellaneousRoutes.js index 27ef15f15..ff643a4b0 100644 --- a/server/routes/miscellaneousRoutes.js +++ b/server/routes/miscellaneousRoutes.js @@ -54,6 +54,8 @@ router.get("/test-logs", eventAuthorizationMiddleware, (req, res) => { // Test 4: Log with a message that doesn't exceed the size limit and doesn't require an upload. logger.log("Normal log entry", "debug", "user123", { id: 4 }, { detail: "normal log entry" }); + + return res.status(500).send("Logs tested."); }); // Search diff --git a/server/utils/logger.js b/server/utils/logger.js index 620ea98a8..d9e2da253 100644 --- a/server/utils/logger.js +++ b/server/utils/logger.js @@ -191,7 +191,13 @@ const createLogger = () => { return false; }; - if (upload || checkAndUploadLog()) return; + // Upload log immediately if upload is true, otherwise check the log size. + if (upload) { + uploadLogToS3(logEntry, message, type, user); + return; + } + + if (checkAndUploadLog()) return; winstonLogger.log(logEntry); }; From 763384f05fedb0fb1f2e38dd5409f88778767943 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Wed, 13 Nov 2024 20:14:17 -0800 Subject: [PATCH 28/37] feature/IO-3029-Enhanced-Logging-File-Based: Update Stream Key name Signed-off-by: Dave Richer --- server/utils/logger.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/utils/logger.js b/server/utils/logger.js index d9e2da253..c2b2a2079 100644 --- a/server/utils/logger.js +++ b/server/utils/logger.js @@ -157,7 +157,8 @@ const createLogger = () => { const uploadLogToS3 = (logEntry, message, type, user) => { const uniqueId = v4(); const dateTimeString = new Date().toISOString().replace(/:/g, "-"); - const logStreamName = `${dateTimeString}-${internalHostname}-${uniqueId}`; + const envName = process.env?.NODE_ENV ? process.env.NODE_ENV : ""; + const logStreamName = `[${envName}]-[${internalHostname}]-[${dateTimeString}]-[${uniqueId}].json`; const logString = JSON.stringify(logEntry); uploadFileToS3({ bucketName: S3_BUCKET_NAME, key: logStreamName, content: logString }) From 2439755f9e98365cf4d85cc08f426910e353f650 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 14 Nov 2024 08:34:21 -0800 Subject: [PATCH 29/37] feature/IO-3029-Enhanced-Logging-File-Based: Final Enhancements Signed-off-by: Dave Richer --- server.js | 7 ++----- server/email/mailer.js | 9 ++------- server/intellipay/intellipay.js | 6 ++---- server/utils/instanceMgr.js | 6 ++++++ server/utils/logger.js | 29 +++++++++++++++++++---------- server/utils/s3.js | 7 ++----- 6 files changed, 33 insertions(+), 31 deletions(-) diff --git a/server.js b/server.js index a92ab7241..f98ec67da 100644 --- a/server.js +++ b/server.js @@ -21,7 +21,7 @@ const { applyRedisHelpers } = require("./server/utils/redisHelpers"); const { applyIOHelpers } = require("./server/utils/ioHelpers"); const { redisSocketEvents } = require("./server/web-sockets/redisSocketEvents"); const { ElastiCacheClient, DescribeCacheClustersCommand } = require("@aws-sdk/client-elasticache"); -const { default: InstanceManager } = require("./server/utils/instanceMgr"); +const { InstanceRegion } = require("./server/utils/instanceMgr"); const CLUSTER_RETRY_BASE_DELAY = 100; const CLUSTER_RETRY_MAX_DELAY = 5000; @@ -114,10 +114,7 @@ const applyRoutes = ({ app }) => { */ const getRedisNodesFromAWS = async () => { const client = new ElastiCacheClient({ - region: InstanceManager({ - imex: "ca-central-1", - rome: "us-east-2" - }) + region: InstanceRegion() }); const params = { diff --git a/server/email/mailer.js b/server/email/mailer.js index 654f56cb3..6134053b6 100644 --- a/server/email/mailer.js +++ b/server/email/mailer.js @@ -1,6 +1,6 @@ const { isString, isEmpty } = require("lodash"); const { defaultProvider } = require("@aws-sdk/credential-provider-node"); -const { default: InstanceManager } = require("../utils/instanceMgr"); +const { InstanceRegion } = require("../utils/instanceMgr"); const aws = require("@aws-sdk/client-ses"); const nodemailer = require("nodemailer"); const logger = require("../utils/logger"); @@ -10,12 +10,7 @@ const isLocal = isString(process.env?.LOCALSTACK_HOSTNAME) && !isEmpty(process.e const sesConfig = { apiVersion: "latest", credentials: defaultProvider(), - region: isLocal - ? "ca-central-1" - : InstanceManager({ - imex: "ca-central-1", - rome: "us-east-2" - }) + region: InstanceRegion() }; if (isLocal) { diff --git a/server/intellipay/intellipay.js b/server/intellipay/intellipay.js index c231f8261..080deec90 100644 --- a/server/intellipay/intellipay.js +++ b/server/intellipay/intellipay.js @@ -17,12 +17,10 @@ require("dotenv").config({ const domain = process.env.NODE_ENV ? "secure" : "test"; const { SecretsManagerClient, GetSecretValueCommand } = require("@aws-sdk/client-secrets-manager"); +const { InstanceRegion } = require("../utils/instanceMgr"); const client = new SecretsManagerClient({ - region: InstanceManager({ - imex: "ca-central-1", - rome: "us-east-2" - }) + region: InstanceRegion() }); const gqlClient = require("../graphql-client/graphql-client").client; diff --git a/server/utils/instanceMgr.js b/server/utils/instanceMgr.js index e83e32661..6a01e6904 100644 --- a/server/utils/instanceMgr.js +++ b/server/utils/instanceMgr.js @@ -44,4 +44,10 @@ function InstanceManager({ args, instance, debug, executeFunction, rome, promana return propToReturn === undefined ? null : propToReturn; } +exports.InstanceRegion = () => + InstanceManager({ + imex: "ca-central-1", + rome: "us-east-2" + }); + exports.default = InstanceManager; diff --git a/server/utils/logger.js b/server/utils/logger.js index c2b2a2079..fc8a84c29 100644 --- a/server/utils/logger.js +++ b/server/utils/logger.js @@ -11,6 +11,7 @@ const { isString, isEmpty } = require("lodash"); const { networkInterfaces, hostname } = require("node:os"); const { uploadFileToS3 } = require("./s3"); const { v4 } = require("uuid"); +const { InstanceRegion } = require("./instanceMgr"); const LOG_LEVELS = { error: { level: 0, name: "error" }, @@ -29,12 +30,18 @@ const S3_BUCKET_NAME = InstanceManager({ rome: "rome-large-log" }); +const region = InstanceRegion(); + const estimateLogSize = (logEntry) => { let estimatedSize = 0; for (const key in logEntry) { if (logEntry.hasOwnProperty(key)) { const value = logEntry[key]; - estimatedSize += key.length + (typeof value === "string" ? value.length : JSON.stringify(value).length); + if (value === undefined || value === null) { + estimatedSize += key.length; // Only count the key length if value is undefined or null + } else { + estimatedSize += key.length + (typeof value === "string" ? value.length : JSON.stringify(value).length); + } } } return estimatedSize; @@ -50,10 +57,7 @@ const createLogger = () => { const winstonCloudwatchTransportDefaults = { logGroupName: logGroupName, awsOptions: { - region: InstanceManager({ - imex: "ca-central-1", - rome: "us-east-2" - }) + region }, jsonMessage: true }; @@ -154,17 +158,23 @@ const createLogger = () => { meta }; + //https://${S3_BUCKET_NAME}.s3.${region}.amazonaws.com/%5Btest%5D-%5Bip-172-31-42-218%5D-%5B2024-11-14T04-15-52.708Z%5D-%5B7a03efb9-9547-4f6b-acd4-c63a2d58a0c8%5D.json + const uploadLogToS3 = (logEntry, message, type, user) => { const uniqueId = v4(); const dateTimeString = new Date().toISOString().replace(/:/g, "-"); const envName = process.env?.NODE_ENV ? process.env.NODE_ENV : ""; - const logStreamName = `[${envName}]-[${internalHostname}]-[${dateTimeString}]-[${uniqueId}].json`; + const logStreamName = `${envName}-${internalHostname}-${dateTimeString}-${uniqueId}.json`; const logString = JSON.stringify(logEntry); + const webPath = isLocal + ? `https://${S3_BUCKET_NAME}.s3.localhost.localstack.cloud:4566/${logStreamName}` + : `https://${S3_BUCKET_NAME}.s3.${region}.amazonaws.com/${logStreamName}`; uploadFileToS3({ bucketName: S3_BUCKET_NAME, key: logStreamName, content: logString }) .then(() => { log("A log file has been uploaded to S3", "info", "S3", null, { logStreamName, + webPath, message: message?.slice(0, 200), type, user @@ -173,6 +183,7 @@ const createLogger = () => { .catch((err) => { log("Error in S3 Upload", "error", "S3", null, { logStreamName, + webPath, message: message?.slice(0, 100), type, user, @@ -182,16 +193,14 @@ const createLogger = () => { }; const checkAndUploadLog = () => { - const logString = JSON.stringify(logEntry); - const logSize = Buffer.byteLength(logString, "utf8"); + const estimatedSize = estimateLogSize(logEntry); - if (logSize > LOG_LENGTH_LIMIT * 0.9 || logSize > LOG_LENGTH_LIMIT) { + if (estimatedSize > LOG_LENGTH_LIMIT * 0.9 || estimatedSize > LOG_LENGTH_LIMIT) { uploadLogToS3(logEntry, message, type, user); return true; } return false; }; - // Upload log immediately if upload is true, otherwise check the log size. if (upload) { uploadLogToS3(logEntry, message, type, user); diff --git a/server/utils/s3.js b/server/utils/s3.js index 132fcc193..f87fc0fd2 100644 --- a/server/utils/s3.js +++ b/server/utils/s3.js @@ -7,15 +7,12 @@ const { CopyObjectCommand } = require("@aws-sdk/client-s3"); const { defaultProvider } = require("@aws-sdk/credential-provider-node"); -const { default: InstanceManager } = require("./instanceMgr"); +const { InstanceRegion } = require("./instanceMgr"); const { isString, isEmpty } = require("lodash"); const createS3Client = () => { const S3Options = { - region: InstanceManager({ - imex: "ca-central-1", - rome: "us-east-2" - }), + region: InstanceRegion(), credentials: defaultProvider() }; From ff7dd7d3ea157f645613f14485cd7a024d41c40b Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Thu, 14 Nov 2024 10:37:08 -0800 Subject: [PATCH 30/37] IO-3030 QBO Payment Private Note Signed-off-by: Allan Carr --- server/accounting/qbo/qbo-payments.js | 1 + 1 file changed, 1 insertion(+) diff --git a/server/accounting/qbo/qbo-payments.js b/server/accounting/qbo/qbo-payments.js index 6426bfa4f..99b4447cd 100644 --- a/server/accounting/qbo/qbo-payments.js +++ b/server/accounting/qbo/qbo-payments.js @@ -219,6 +219,7 @@ async function InsertPayment(oauthClient, qbo_realmId, req, payment, parentRef, PaymentMethodRef: { value: paymentMethods[payment.type] }, + PrivateNote: payment.memo.length > 4000 ? payment.memo.substring(0, 4000).trim() : payment.memo.trim(), PaymentRefNum: payment.transactionid, ...(invoices && invoices.length === 1 && invoices[0] ? { From 89fabf85e116aef45274a6fc125ae6cc94d2b80a Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 14 Nov 2024 10:55:14 -0800 Subject: [PATCH 31/37] feature/IO-3029-Enhanced-Logging-File-Based: Adjust XML and JSON log to always upload Signed-off-by: Dave Richer --- server/utils/logger.js | 2 -- server/web-sockets/web-socket.js | 18 +++++++++++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/server/utils/logger.js b/server/utils/logger.js index fc8a84c29..b6f9e865c 100644 --- a/server/utils/logger.js +++ b/server/utils/logger.js @@ -158,8 +158,6 @@ const createLogger = () => { meta }; - //https://${S3_BUCKET_NAME}.s3.${region}.amazonaws.com/%5Btest%5D-%5Bip-172-31-42-218%5D-%5B2024-11-14T04-15-52.708Z%5D-%5B7a03efb9-9547-4f6b-acd4-c63a2d58a0c8%5D.json - const uploadLogToS3 = (logEntry, message, type, user) => { const uniqueId = v4(); const dateTimeString = new Date().toISOString().replace(/:/g, "-"); diff --git a/server/web-sockets/web-socket.js b/server/web-sockets/web-socket.js index c5e5012c8..1e468c387 100644 --- a/server/web-sockets/web-socket.js +++ b/server/web-sockets/web-socket.js @@ -155,10 +155,17 @@ function createJsonEvent(socket, level, message, json) { message }); } - logger.log("ws-log-event-json", level, socket.user.email, socket.recordid, { - wsmessage: message, - json - }); + logger.log( + "ws-log-event-json", + level, + socket.user.email, + socket.recordid, + { + wsmessage: message, + json + }, + true + ); if (socket.logEvents && isArray(socket.logEvents)) { socket.logEvents.push({ @@ -189,7 +196,8 @@ function createXmlEvent(socket, xml, message, isError = false) { { wsmessage: message, xml - } + }, + true ); if (socket.logEvents && isArray(socket.logEvents)) { From bf5138016797086b51d6342027a6a37f1dc77382 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Thu, 14 Nov 2024 11:19:09 -0800 Subject: [PATCH 32/37] IO-3031 View Day when Scheduling Signed-off-by: Allan Carr --- .vscode/settings.json | 13 ++++ .../form-date-time-picker.component.jsx | 74 ++++++++++++++----- .../schedule-job-modal.component.jsx | 6 +- client/src/translations/en_us/common.json | 1 + client/src/translations/es/common.json | 1 + client/src/translations/fr/common.json | 1 + 6 files changed, 73 insertions(+), 23 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index dd6a1330b..626931a53 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,5 +8,18 @@ "pattern": "**/IMEX.xml", "systemId": "logs/IMEX.xsd" } + ], + "cSpell.words": [ + "antd", + "appointmentconfirmation", + "appt", + "bodyshop", + "IMEX", + "labhrs", + "larhrs", + "ownr", + "promanager", + "smartscheduling", + "touchtime" ] } diff --git a/client/src/components/form-date-time-picker/form-date-time-picker.component.jsx b/client/src/components/form-date-time-picker/form-date-time-picker.component.jsx index c9bbc0dd2..3f7948d52 100644 --- a/client/src/components/form-date-time-picker/form-date-time-picker.component.jsx +++ b/client/src/components/form-date-time-picker/form-date-time-picker.component.jsx @@ -1,4 +1,4 @@ -import { DatePicker } from "antd"; +import { DatePicker, Space, TimePicker } from "antd"; import PropTypes from "prop-types"; import React, { useCallback, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -20,6 +20,7 @@ const DateTimePicker = ({ onlyFuture, onlyToday, isDateOnly = false, + isSeparatedTime: isSeparatedTime = false, bodyshop, ...restProps }) => { @@ -87,24 +88,56 @@ const DateTimePicker = ({ return (
- + {isSeparatedTime && ( + + + {value && ( + { + handleChange(value); + onBlur(); + }} + placeholder={t("general.labels.time")} + {...restProps} + /> + )} + + )} + {!isSeparatedTime && ( + + )}
); }; @@ -116,7 +149,8 @@ DateTimePicker.propTypes = { id: PropTypes.string, onlyFuture: PropTypes.bool, onlyToday: PropTypes.bool, - isDateOnly: PropTypes.bool + isDateOnly: PropTypes.bool, + isSeparatedTime: PropTypes.bool }; export default connect(mapStateToProps, null)(DateTimePicker); diff --git a/client/src/components/schedule-job-modal/schedule-job-modal.component.jsx b/client/src/components/schedule-job-modal/schedule-job-modal.component.jsx index ea942e5d2..abc01afbd 100644 --- a/client/src/components/schedule-job-modal/schedule-job-modal.component.jsx +++ b/client/src/components/schedule-job-modal/schedule-job-modal.component.jsx @@ -1,6 +1,5 @@ import { Button, Col, Form, Input, Row, Select, Space, Switch, Typography } from "antd"; import axios from "axios"; -import dayjs from "../../utils/day"; import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; @@ -8,13 +7,14 @@ import { createStructuredSelector } from "reselect"; import { calculateScheduleLoad } from "../../redux/application/application.actions"; import { selectBodyshop } from "../../redux/user/user.selectors"; import { DateFormatter } from "../../utils/DateFormatter"; +import dayjs from "../../utils/day"; +import InstanceRenderManager from "../../utils/instanceRenderMgr"; import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component"; import EmailInput from "../form-items-formatted/email-form-item.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import ScheduleDayViewContainer from "../schedule-day-view/schedule-day-view.container"; import ScheduleExistingAppointmentsList from "../schedule-existing-appointments-list/schedule-existing-appointments-list.component"; import "./schedule-job-modal.scss"; -import InstanceRenderManager from "../../utils/instanceRenderMgr"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop @@ -84,7 +84,7 @@ export function ScheduleJobModalComponent({ } ]} > - + Date: Thu, 14 Nov 2024 11:52:47 -0800 Subject: [PATCH 33/37] IO-3031 Adjust prop Signed-off-by: Allan Carr --- .../form-date-time-picker/form-date-time-picker.component.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/form-date-time-picker/form-date-time-picker.component.jsx b/client/src/components/form-date-time-picker/form-date-time-picker.component.jsx index 3f7948d52..649e02f9a 100644 --- a/client/src/components/form-date-time-picker/form-date-time-picker.component.jsx +++ b/client/src/components/form-date-time-picker/form-date-time-picker.component.jsx @@ -20,7 +20,7 @@ const DateTimePicker = ({ onlyFuture, onlyToday, isDateOnly = false, - isSeparatedTime: isSeparatedTime = false, + isSeparatedTime = false, bodyshop, ...restProps }) => { From e9e1e820a786747ceb90bfac4ec8cac80f21c983 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 14 Nov 2024 11:57:40 -0800 Subject: [PATCH 34/37] release/2024-11-15 - Expose S3 client through createS3Client Signed-off-by: Dave Richer --- server/utils/s3.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/utils/s3.js b/server/utils/s3.js index f87fc0fd2..8b9251e03 100644 --- a/server/utils/s3.js +++ b/server/utils/s3.js @@ -95,14 +95,14 @@ const createS3Client = () => { throw error; } }; - return { uploadFileToS3, downloadFileFromS3, listFilesInS3Bucket, deleteFileFromS3, copyFileInS3, - fileExistsInS3 + fileExistsInS3, + ...s3Client }; }; From ce9a77efcfc53273a9817bca5bf6ae9beff68ba2 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Thu, 14 Nov 2024 16:15:17 -0800 Subject: [PATCH 35/37] IO-3027 Datapumps Refactor Signed-off-by: Allan Carr --- .vscode/settings.json | 30 ++++ server/data/autohouse.js | 15 +- server/data/chatter.js | 6 +- server/data/claimscorp.js | 294 +++++++++++++++++++------------------ server/data/kaizen.js | 297 +++++++++++++++++++------------------- 5 files changed, 344 insertions(+), 298 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index dd6a1330b..74b58f715 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,5 +8,35 @@ "pattern": "**/IMEX.xml", "systemId": "logs/IMEX.xsd" } + ], + "cSpell.words": [ + "antd", + "appointmentconfirmation", + "appt", + "autohouse", + "autohouseid", + "billlines", + "bodyshop", + "bodyshopid", + "bodyshops", + "CIECA", + "claimscorp", + "claimscorpid", + "Dinero", + "driveable", + "IMEX", + "imexshopid", + "jobid", + "joblines", + "Kaizen", + "labhrs", + "larhrs", + "mixdata", + "ownr", + "promanager", + "shopname", + "smartscheduling", + "timetickets", + "touchtime" ] } diff --git a/server/data/autohouse.js b/server/data/autohouse.js index 9ea3a415a..161fd3b99 100644 --- a/server/data/autohouse.js +++ b/server/data/autohouse.js @@ -47,7 +47,11 @@ exports.default = async (req, res) => { } // Send immediate response and continue processing. - res.status(200).send(); + res.status(202).json({ + success: true, + message: "Processing request ...", + timestamp: new Date().toISOString() + }); try { logger.log("autohouse-start", "DEBUG", "api", null, null); @@ -146,7 +150,7 @@ async function processBatch(batch, start, end) { allErrors.push({ bodyshopid: bodyshop.id, imexshopid: bodyshop.imexshopid, - autuhouseid: bodyshop.autuhouseid, + autohouseid: bodyshop.autohouseid, fatal: true, errors: [error.toString()] }); @@ -154,7 +158,7 @@ async function processBatch(batch, start, end) { allErrors.push({ bodyshopid: bodyshop.id, imexshopid: bodyshop.imexshopid, - autuhouseid: bodyshop.autuhouseid, + autohouseid: bodyshop.autohouseid, errors: erroredJobs.map((ej) => ({ ro_number: ej.job?.ro_number, jobid: ej.job?.id, @@ -609,10 +613,7 @@ const CreateRepairOrderTag = (job, errorCallback) => { }; return ret; } catch (error) { - logger.log("autohouse-job-calculate-error", "ERROR", "api", null, { - error - }); - + logger.log("autohouse-job-calculate-error", "ERROR", "api", null, { error: error.message, stack: error.stack }); errorCallback({ jobid: job.id, ro_number: job.ro_number, error }); } }; diff --git a/server/data/chatter.js b/server/data/chatter.js index ea8851e94..ea09d3862 100644 --- a/server/data/chatter.js +++ b/server/data/chatter.js @@ -39,7 +39,11 @@ exports.default = async (req, res) => { } // Send immediate response and continue processing. - res.status(200).send(); + res.status(202).json({ + success: true, + message: "Processing request ...", + timestamp: new Date().toISOString() + }); try { logger.log("chatter-start", "DEBUG", "api", null, null); diff --git a/server/data/claimscorp.js b/server/data/claimscorp.js index 6ebc63f81..4304b75b5 100644 --- a/server/data/claimscorp.js +++ b/server/data/claimscorp.js @@ -26,174 +26,185 @@ const ftpSetup = { password: process.env.CLAIMSCORP_PASSWORD, debug: (message, ...data) => logger.log(message, "DEBUG", "api", null, data), algorithms: { - serverHostKey: ["ssh-rsa", "ssh-dss"] + serverHostKey: ["ssh-rsa", "ssh-dss", "rsa-sha2-256", "rsa-sha2-512", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384"] } }; +const allxmlsToUpload = []; +const allErrors = []; + exports.default = async (req, res) => { // Only process if in production environment. if (process.env.NODE_ENV !== "production") { res.sendStatus(403); return; } - - //Query for the List of Bodyshop Clients. - logger.log("claimscorp-start", "DEBUG", "api", null, null); - const { bodyshops } = await client.request(queries.GET_CLAIMSCORP_SHOPS); - - const specificShopIds = req.body.bodyshopIds; // ['uuid] - const { start, end, skipUpload } = req.body; //YYYY-MM-DD + // Only process if the appropriate token is provided. if (req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN) { res.sendStatus(401); return; } - const allxmlsToUpload = []; - const allErrors = []; + + // Send immediate response and continue processing. + res.status(202).json({ + success: true, + message: "Processing request ...", + timestamp: new Date().toISOString() + }); + try { - for (const bodyshop of specificShopIds ? bodyshops.filter((b) => specificShopIds.includes(b.id)) : bodyshops) { + logger.log("claimscorp-start", "DEBUG", "api", null, null); + const { bodyshops } = await client.request(queries.GET_CLAIMSCORP_SHOPS); //Query for the List of Bodyshop Clients. + const specificShopIds = req.body.bodyshopIds; // ['uuid]; + const { start, end, skipUpload } = req.body; //YYYY-MM-DD + + const batchSize = 10; + + const shopsToProcess = + specificShopIds?.length > 0 ? bodyshops.filter((shop) => specificShopIds.includes(shop.id)) : bodyshops; + logger.log("claimscorp-shopsToProcess-generated", "DEBUG", "api", null, null); + + if (shopsToProcess.length === 0) { + logger.log("claimscorp-shopsToProcess-empty", "DEBUG", "api", null, null); + return; + } + const batchPromises = []; + for (let i = 0; i < shopsToProcess.length; i += batchSize) { + const batch = shopsToProcess.slice(i, i + batchSize); + const batchPromise = (async () => { + await processBatch(batch, start, end); + + if (skipUpload) { + for (const xmlObj of allxmlsToUpload) { + fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml); + } + } else { + await uploadViaSFTP(allxmlsToUpload); + } + })(); + batchPromises.push(batchPromise); + } + await Promise.all(batchPromises); + await sendServerEmail({ + subject: `ClaimsCorp Report ${moment().format("MM-DD-YY")}`, + text: `Errors:\n${JSON.stringify(allErrors, null, 2)}\n\nUploaded:\n${JSON.stringify( + allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count, result: x.result })), + null, + 2 + )}` + }); + + logger.log("claimscorp-end", "DEBUG", "api", null, null); + } catch (error) { + logger.log("claimscorp-error", "ERROR", "api", null, { error: error.message, stack: error.stack }); + } +}; + +async function processBatch(batch, start, end) { + for (const bodyshop of batch) { + const erroredJobs = []; + try { logger.log("claimscorp-start-shop-extract", "DEBUG", "api", bodyshop.id, { shopname: bodyshop.shopname }); - const erroredJobs = []; - try { - const { jobs, bodyshops_by_pk } = await client.request(queries.CLAIMSCORP_QUERY, { - bodyshopid: bodyshop.id, - start: start ? moment(start).startOf("day") : moment().subtract(5, "days").startOf("day"), - ...(end && { end: moment(end).endOf("day") }) - }); - const claimsCorpObject = { - DataFeed: { - ShopInfo: { - ShopID: bodyshops_by_pk.claimscorpid, - ShopName: bodyshops_by_pk.shopname, - RO: jobs.map((j) => - CreateRepairOrderTag({ ...j, bodyshop: bodyshops_by_pk }, function ({ job, error }) { - erroredJobs.push({ job: job, error: error.toString() }); - }) - ) - } - } - }; - - if (erroredJobs.length > 0) { - logger.log("claimscorp-failed-jobs", "ERROR", "api", bodyshop.id, { - count: erroredJobs.length, - jobs: JSON.stringify(erroredJobs.map((j) => j.job.ro_number)) - }); - } - - var ret = builder - .create( - { - // version: "1.0", - // encoding: "UTF-8", - //keepNullNodes: true, - }, - claimsCorpObject - ) - .end({ allowEmptyTags: true }); - - allxmlsToUpload.push({ - count: claimsCorpObject.DataFeed.ShopInfo.RO.length, - xml: ret, - filename: `${bodyshop.claimscorpid}-${moment().format("YYYYMMDDTHHMMss")}.xml` - }); - - logger.log("claimscorp-end-shop-extract", "DEBUG", "api", bodyshop.id, { - shopname: bodyshop.shopname - }); - } catch (error) { - //Error at the shop level. - logger.log("claimscorp-error-shop", "ERROR", "api", bodyshop.id, { - ...error - }); - - allErrors.push({ - bodyshopid: bodyshop.id, - imexshopid: bodyshop.imexshopid, - claimscorpid: bodyshop.claimscorpid, - fatal: true, - errors: [error.toString()] - }); - } finally { - allErrors.push({ - bodyshopid: bodyshop.id, - imexshopid: bodyshop.imexshopid, - claimscorpid: bodyshop.claimscorpid, - errors: erroredJobs.map((ej) => ({ - ro_number: ej.job?.ro_number, - jobid: ej.job?.id, - error: ej.error - })) - }); - } - } - - if (skipUpload) { - for (const xmlObj of allxmlsToUpload) { - fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml); - } - - res.json(allxmlsToUpload); - sendServerEmail({ - subject: `ClaimsCorp Report ${moment().format("MM-DD-YY")}`, - text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))} - Uploaded: ${JSON.stringify( - allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count })), - null, - 2 - )} - ` + const { jobs, bodyshops_by_pk } = await client.request(queries.CLAIMSCORP_QUERY, { + bodyshopid: bodyshop.id, + start: start ? moment(start).startOf("day") : moment().subtract(5, "days").startOf("day"), + ...(end && { end: moment(end).endOf("day") }) }); - return; - } - let sftp = new Client(); - sftp.on("error", (errors) => - logger.log("claimscorp-sftp-error", "ERROR", "api", null, { - ...errors - }) - ); - try { - //Connect to the FTP and upload all. + const claimsCorpObject = { + DataFeed: { + ShopInfo: { + ShopID: bodyshops_by_pk.claimscorpid, + ShopName: bodyshops_by_pk.shopname, + RO: jobs.map((j) => + CreateRepairOrderTag({ ...j, bodyshop: bodyshops_by_pk }, function ({ job, error }) { + erroredJobs.push({ job: job, error: error.toString() }); + }) + ) + } + } + }; - await sftp.connect(ftpSetup); - - for (const xmlObj of allxmlsToUpload) { - logger.log("claimscorp-sftp-upload", "DEBUG", "api", null, { - filename: xmlObj.filename - }); - - const uploadResult = await sftp.put(Buffer.from(xmlObj.xml), `/${xmlObj.filename}`); - logger.log("claimscorp-sftp-upload-result", "DEBUG", "api", null, { - uploadResult + if (erroredJobs.length > 0) { + logger.log("claimscorp-failed-jobs", "ERROR", "api", bodyshop.id, { + count: erroredJobs.length, + jobs: JSON.stringify(erroredJobs.map((j) => j.job.ro_number)) }); } - //***TODO Change filing naming when creating the cron job. IM_ShopInternalName_DDMMYYYY_HHMMSS.xml + const ret = builder.create({}, claimsCorpObject).end({ allowEmptyTags: true }); + + allxmlsToUpload.push({ + count: claimsCorpObject.DataFeed.ShopInfo.RO.length, + xml: ret, + filename: `${bodyshop.claimscorpid}-${moment().format("YYYYMMDDTHHMMss")}.xml` + }); + + logger.log("claimscorp-end-shop-extract", "DEBUG", "api", bodyshop.id, { + shopname: bodyshop.shopname + }); } catch (error) { - logger.log("claimscorp-sftp-error", "ERROR", "api", null, { - ...error + //Error at the shop level. + logger.log("claimscorp-error-shop", "ERROR", "api", bodyshop.id, { error: error.message, stack: error.stack }); + + allErrors.push({ + bodyshopid: bodyshop.id, + imexshopid: bodyshop.imexshopid, + claimscorpid: bodyshop.claimscorpid, + fatal: true, + errors: [error.toString()] }); } finally { - sftp.end(); + allErrors.push({ + bodyshopid: bodyshop.id, + imexshopid: bodyshop.imexshopid, + claimscorpid: bodyshop.claimscorpid, + errors: erroredJobs.map((ej) => ({ + ro_number: ej.job?.ro_number, + jobid: ej.job?.id, + error: ej.error + })) + }); } - sendServerEmail({ - subject: `ClaimsCorp Report ${moment().format("MM-DD-YY")}`, - text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))} - Uploaded: ${JSON.stringify( - allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count })), - null, - 2 - )} - ` - }); - res.sendStatus(200); - } catch (error) { - res.status(200).json(error); } -}; +} + +async function uploadViaSFTP(allxmlsToUpload) { + const sftp = new Client(); + sftp.on("error", (errors) => + logger.log("claimscorp-sftp-connection-error", "ERROR", "api", null, { error: errors.message, stack: errors.stack }) + ); + try { + //Connect to the FTP and upload all. + await sftp.connect(ftpSetup); + + for (const xmlObj of allxmlsToUpload) { + try { + logger.log("claimscorp-sftp-upload", "DEBUG", "api", null, { filename: xmlObj.filename }); + xmlObj.result = await sftp.put(Buffer.from(xmlObj.xml), `${xmlObj.filename}`); + logger.log("claimscorp-sftp-upload-result", "DEBUG", "api", null, { + filename: xmlObj.filename, + result: xmlObj.result + }); + } catch (error) { + logger.log("claimscorp-sftp-upload-error", "ERROR", "api", null, { + filename: xmlObj.filename, + error: error.message, + stack: error.stack + }); + throw error; + } + } + } catch (error) { + logger.log("claimscorp-sftp-error", "ERROR", "api", null, { error: error.message, stack: error.stack }); + throw error; + } finally { + sftp.end(); + } +} const CreateRepairOrderTag = (job, errorCallback) => { //Level 2 @@ -445,10 +456,7 @@ const CreateRepairOrderTag = (job, errorCallback) => { }; return ret; } catch (error) { - logger.log("claimscorp-job-calculate-error", "ERROR", "api", null, { - error - }); - + logger.log("claimscorp-job-calculate-error", "ERROR", "api", null, { error: error.message, stack: error.stack }); errorCallback({ jobid: job.id, ro_number: job.ro_number, error }); } }; diff --git a/server/data/kaizen.js b/server/data/kaizen.js index c9794acff..f48bb637d 100644 --- a/server/data/kaizen.js +++ b/server/data/kaizen.js @@ -16,8 +16,7 @@ const { sendServerEmail } = require("../email/sendemail"); const DineroFormat = "0,0.00"; const DateFormat = "MM/DD/YYYY"; -const repairOpCodes = ["OP4", "OP9", "OP10"]; -const replaceOpCodes = ["OP2", "OP5", "OP11", "OP12"]; +const kaizenShopsIDs = ["SUMMIT", "STRATHMORE", "SUNRIDGE", "SHAW"]; const ftpSetup = { host: process.env.KAIZEN_HOST, @@ -30,173 +29,180 @@ const ftpSetup = { } }; +const allxmlsToUpload = []; +const allErrors = []; + exports.default = async (req, res) => { // Only process if in production environment. if (process.env.NODE_ENV !== "production") { res.sendStatus(403); return; } - - //Query for the List of Bodyshop Clients. - logger.log("kaizen-start", "DEBUG", "api", null, null); - const kaizenShopsIDs = ["SUMMIT", "STRATHMORE", "SUNRIDGE", "SHAW"]; - - const { bodyshops } = await client.request(queries.GET_KAIZEN_SHOPS, { - imexshopid: kaizenShopsIDs - }); - - const specificShopIds = req.body.bodyshopIds; // ['uuid] - const { start, end, skipUpload } = req.body; //YYYY-MM-DD + // Only process if the appropriate token is provided. if (req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN) { res.sendStatus(401); return; } - const allxmlsToUpload = []; - const allErrors = []; + + // Send immediate response and continue processing. + res.status(202).json({ + success: true, + message: "Processing request ...", + timestamp: new Date().toISOString() + }); + try { - for (const bodyshop of specificShopIds ? bodyshops.filter((b) => specificShopIds.includes(b.id)) : bodyshops) { + logger.log("kaizen-start", "DEBUG", "api", null, null); + const { bodyshops } = await client.request(queries.GET_KAIZEN_SHOPS, { imexshopid: kaizenShopsIDs }); //Query for the List of Bodyshop Clients. + const specificShopIds = req.body.bodyshopIds; // ['uuid]; + const { start, end, skipUpload } = req.body; //YYYY-MM-DD + + const batchSize = 10; + + const shopsToProcess = + specificShopIds?.length > 0 ? bodyshops.filter((shop) => specificShopIds.includes(shop.id)) : bodyshops; + logger.log("kaizen-shopsToProcess-generated", "DEBUG", "api", null, null); + + if (shopsToProcess.length === 0) { + logger.log("kaizen-shopsToProcess-empty", "DEBUG", "api", null, null); + return; + } + const batchPromises = []; + for (let i = 0; i < shopsToProcess.length; i += batchSize) { + const batch = shopsToProcess.slice(i, i + batchSize); + const batchPromise = (async () => { + await processBatch(batch, start, end); + + if (skipUpload) { + for (const xmlObj of allxmlsToUpload) { + fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml); + } + } else { + await uploadViaSFTP(allxmlsToUpload); + } + })(); + batchPromises.push(batchPromise); + } + await Promise.all(batchPromises); + await sendServerEmail({ + subject: `Kaizen Report ${moment().format("MM-DD-YY")}`, + text: `Errors:\n${JSON.stringify(allErrors, null, 2)}\n\nUploaded:\n${JSON.stringify( + allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count, result: x.result })), + null, + 2 + )}` + }); + + logger.log("kaizen-end", "DEBUG", "api", null, null); + } catch (error) { + logger.log("kaizen-error", "ERROR", "api", null, { error: error.message, stack: error.stack }); + } +}; + +async function processBatch(batch, start, end) { + for (const bodyshop of batch) { + const erroredJobs = []; + try { logger.log("kaizen-start-shop-extract", "DEBUG", "api", bodyshop.id, { shopname: bodyshop.shopname }); - const erroredJobs = []; - try { - const { jobs, bodyshops_by_pk } = await client.request(queries.KAIZEN_QUERY, { - bodyshopid: bodyshop.id, - start: start ? moment(start).startOf("day") : moment().subtract(5, "days").startOf("day"), - ...(end && { end: moment(end).endOf("day") }) - }); - const kaizenObject = { - DataFeed: { - ShopInfo: { - ShopName: bodyshops_by_pk.shopname, - Jobs: jobs.map((j) => - CreateRepairOrderTag({ ...j, bodyshop: bodyshops_by_pk }, function ({ job, error }) { - erroredJobs.push({ job: job, error: error.toString() }); - }) - ) - } - } - }; - - if (erroredJobs.length > 0) { - logger.log("kaizen-failed-jobs", "ERROR", "api", bodyshop.id, { - count: erroredJobs.length, - jobs: JSON.stringify(erroredJobs.map((j) => j.job.ro_number)) - }); - } - - var ret = builder - .create( - { - // version: "1.0", - // encoding: "UTF-8", - //keepNullNodes: true, - }, - kaizenObject - ) - .end({ allowEmptyTags: true }); - - allxmlsToUpload.push({ - count: kaizenObject.DataFeed.ShopInfo.Jobs.length, - xml: ret, - filename: `${bodyshop.shopname}-${moment().format("YYYYMMDDTHHMMss")}.xml` - }); - - logger.log("kaizen-end-shop-extract", "DEBUG", "api", bodyshop.id, { - shopname: bodyshop.shopname - }); - } catch (error) { - //Error at the shop level. - logger.log("kaizen-error-shop", "ERROR", "api", bodyshop.id, { - ...error - }); - - allErrors.push({ - bodyshopid: bodyshop.id, - imexshopid: bodyshop.imexshopid, - shopname: bodyshop.shopname, - fatal: true, - errors: [error.toString()] - }); - } finally { - allErrors.push({ - bodyshopid: bodyshop.id, - imexshopid: bodyshop.imexshopid, - shopname: bodyshop.shopname, - errors: erroredJobs.map((ej) => ({ - ro_number: ej.job?.ro_number, - jobid: ej.job?.id, - error: ej.error - })) - }); - } - } - - if (skipUpload) { - for (const xmlObj of allxmlsToUpload) { - fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml); - } - - res.json(allxmlsToUpload); - sendServerEmail({ - subject: `Kaizen Report ${moment().format("MM-DD-YY")}`, - text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))} - Uploaded: ${JSON.stringify( - allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count })), - null, - 2 - )} - ` + const { jobs, bodyshops_by_pk } = await client.request(queries.KAIZEN_QUERY, { + bodyshopid: bodyshop.id, + start: start ? moment(start).startOf("day") : moment().subtract(5, "days").startOf("day"), + ...(end && { end: moment(end).endOf("day") }) }); - return; - } - let sftp = new Client(); - sftp.on("error", (errors) => - logger.log("kaizen-sftp-error", "ERROR", "api", null, { - ...errors - }) - ); - try { - //Connect to the FTP and upload all. + const kaizenObject = { + DataFeed: { + ShopInfo: { + ShopName: bodyshops_by_pk.shopname, + Jobs: jobs.map((j) => + CreateRepairOrderTag({ ...j, bodyshop: bodyshops_by_pk }, function ({ job, error }) { + erroredJobs.push({ job: job, error: error.toString() }); + }) + ) + } + } + }; - await sftp.connect(ftpSetup); - - for (const xmlObj of allxmlsToUpload) { - logger.log("kaizen-sftp-upload", "DEBUG", "api", null, { - filename: xmlObj.filename - }); - - const uploadResult = await sftp.put(Buffer.from(xmlObj.xml), `/${xmlObj.filename}`); - logger.log("kaizen-sftp-upload-result", "DEBUG", "api", null, { - uploadResult + if (erroredJobs.length > 0) { + logger.log("kaizen-failed-jobs", "ERROR", "api", bodyshop.id, { + count: erroredJobs.length, + jobs: JSON.stringify(erroredJobs.map((j) => j.job.ro_number)) }); } - //***TODO Change filing naming when creating the cron job. IM_ShopInternalName_DDMMYYYY_HHMMSS.xml + const ret = builder.create({}, kaizenObject).end({ allowEmptyTags: true }); + + allxmlsToUpload.push({ + count: kaizenObject.DataFeed.ShopInfo.Jobs.length, + xml: ret, + filename: `${bodyshop.shopname}-${moment().format("YYYYMMDDTHHMMss")}.xml` + }); + + logger.log("kaizen-end-shop-extract", "DEBUG", "api", bodyshop.id, { + shopname: bodyshop.shopname + }); } catch (error) { - logger.log("kaizen-sftp-error", "ERROR", "api", null, { - ...error + //Error at the shop level. + logger.log("kaizen-error-shop", "ERROR", "api", bodyshop.id, { error: error.message, stack: error.stack }); + + allErrors.push({ + bodyshopid: bodyshop.id, + imexshopid: bodyshop.imexshopid, + shopname: bodyshop.shopname, + fatal: true, + errors: [error.toString()] }); } finally { - sftp.end(); + allErrors.push({ + bodyshopid: bodyshop.id, + imexshopid: bodyshop.imexshopid, + shopname: bodyshop.shopname, + errors: erroredJobs.map((ej) => ({ + ro_number: ej.job?.ro_number, + jobid: ej.job?.id, + error: ej.error + })) + }); } - sendServerEmail({ - subject: `Kaizen Report ${moment().format("MM-DD-YY")}`, - text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))} - Uploaded: ${JSON.stringify( - allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count })), - null, - 2 - )} - ` - }); - res.sendStatus(200); - } catch (error) { - res.status(200).json(error); } -}; +} + +async function uploadViaSFTP(allxmlsToUpload) { + const sftp = new Client(); + sftp.on("error", (errors) => + logger.log("kaizen-sftp-connection-error", "ERROR", "api", null, { error: errors.message, stack: errors.stack }) + ); + try { + //Connect to the FTP and upload all. + await sftp.connect(ftpSetup); + + for (const xmlObj of allxmlsToUpload) { + try { + logger.log("kaizen-sftp-upload", "DEBUG", "api", null, { filename: xmlObj.filename }); + xmlObj.result = await sftp.put(Buffer.from(xmlObj.xml), `${xmlObj.filename}`); + logger.log("kaizen-sftp-upload-result", "DEBUG", "api", null, { + filename: xmlObj.filename, + result: xmlObj.result + }); + } catch (error) { + logger.log("kaizen-sftp-upload-error", "ERROR", "api", null, { + filename: xmlObj.filename, + error: error.message, + stack: error.stack + }); + throw error; + } + } + } catch (error) { + logger.log("kaizen-sftp-error", "ERROR", "api", null, { error: error.message, stack: error.stack }); + throw error; + } finally { + sftp.end(); + } +} const CreateRepairOrderTag = (job, errorCallback) => { //Level 2 @@ -420,10 +426,7 @@ const CreateRepairOrderTag = (job, errorCallback) => { }; return ret; } catch (error) { - logger.log("kaizen-job-calculate-error", "ERROR", "api", null, { - error - }); - + logger.log("kaizen-job-calculate-error", "ERROR", "api", null, { error: error.message, stack: error.stack }); errorCallback({ jobid: job.id, ro_number: job.ro_number, error }); } }; From 95aa0e45a6c3f39643f84ab723c3fe7b1b65b806 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Thu, 14 Nov 2024 16:47:35 -0800 Subject: [PATCH 36/37] IO-3033 Total Loss Indicator Signed-off-by: Allan Carr --- .../jobs-detail-general/jobs-detail-general.component.jsx | 5 ++++- client/src/graphql/jobs.queries.js | 1 + client/src/translations/en_us/common.json | 1 + client/src/translations/es/common.json | 1 + client/src/translations/fr/common.json | 1 + 5 files changed, 8 insertions(+), 1 deletion(-) diff --git a/client/src/components/jobs-detail-general/jobs-detail-general.component.jsx b/client/src/components/jobs-detail-general/jobs-detail-general.component.jsx index 1fee1a6b5..f36269c83 100644 --- a/client/src/components/jobs-detail-general/jobs-detail-general.component.jsx +++ b/client/src/components/jobs-detail-general/jobs-detail-general.component.jsx @@ -5,6 +5,7 @@ import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { selectJobReadOnly } from "../../redux/application/application.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors"; +import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx"; import CurrencyInput from "../form-items-formatted/currency-form-item.component"; import FormItemEmail from "../form-items-formatted/email-form-item.component"; import FormItemPhone, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component"; @@ -12,7 +13,6 @@ import Car from "../job-damage-visual/job-damage-visual.component"; import JobsDetailChangeEstimator from "../jobs-detail-change-estimator/jobs-detail-change-estimator.component"; import JobsDetailChangeFileHandler from "../jobs-detail-change-filehandler/jobs-detail-change-filehandler.component"; import FormRow from "../layout-form-row/layout-form-row.component"; -import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx"; const mapStateToProps = createStructuredSelector({ jobRO: selectJobReadOnly, @@ -185,6 +185,9 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) { + + + diff --git a/client/src/graphql/jobs.queries.js b/client/src/graphql/jobs.queries.js index a71d08d10..51b44a1b2 100644 --- a/client/src/graphql/jobs.queries.js +++ b/client/src/graphql/jobs.queries.js @@ -692,6 +692,7 @@ export const GET_JOB_BY_PK = gql` tax_str_rt tax_sub_rt tax_tow_rt + tlos_ind towin towing_payable unit_number diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index d91c369fd..c656d5822 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -1873,6 +1873,7 @@ "tax_str_rt": "Storage Tax Rate", "tax_sub_rt": "Sublet Tax Rate", "tax_tow_rt": "Towing Tax Rate", + "tlos_ind": "Total Loss Indicator", "towin": "Tow In", "towing_payable": "Towing Payable", "unitnumber": "Unit #", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 7658ca3ce..7839fc04f 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -1873,6 +1873,7 @@ "tax_str_rt": "", "tax_sub_rt": "", "tax_tow_rt": "", + "tlos_ind": "", "towin": "", "towing_payable": "Remolque a pagar", "unitnumber": "Unidad #", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index a109a127d..bff86bca4 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -1873,6 +1873,7 @@ "tax_str_rt": "", "tax_sub_rt": "", "tax_tow_rt": "", + "tlos_ind": "", "towin": "", "towing_payable": "Remorquage à payer", "unitnumber": "Unité #", From 78678dd3dcf2c7802aa73e5825610ac26554091c Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 15 Nov 2024 10:04:03 -0800 Subject: [PATCH 37/37] IO-3027 Datapumps Refactor Signed-off-by: Allan Carr --- server/data/autohouse.js | 3 +-- server/data/chatter.js | 3 +-- server/data/claimscorp.js | 3 +-- server/data/kaizen.js | 3 +-- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/server/data/autohouse.js b/server/data/autohouse.js index 161fd3b99..7d8447f68 100644 --- a/server/data/autohouse.js +++ b/server/data/autohouse.js @@ -180,9 +180,8 @@ async function uploadViaSFTP(allxmlsToUpload) { for (const xmlObj of allxmlsToUpload) { try { - logger.log("autohouse-sftp-upload", "DEBUG", "api", null, { filename: xmlObj.filename }); xmlObj.result = await sftp.put(Buffer.from(xmlObj.xml), `${xmlObj.filename}`); - logger.log("autohouse-sftp-upload-result", "DEBUG", "api", null, { + logger.log("autohouse-sftp-upload", "DEBUG", "api", null, { filename: xmlObj.filename, result: xmlObj.result }); diff --git a/server/data/chatter.js b/server/data/chatter.js index ea09d3862..8890fafe9 100644 --- a/server/data/chatter.js +++ b/server/data/chatter.js @@ -180,9 +180,8 @@ async function uploadViaSFTP(allcsvsToUpload) { for (const csvObj of allcsvsToUpload) { try { - logger.log("chatter-sftp-upload", "DEBUG", "api", null, { filename: csvObj.filename }); csvObj.result = await sftp.put(Buffer.from(csvObj.csv), `${csvObj.filename}`); - logger.log("chatter-sftp-upload-result", "DEBUG", "api", null, { + logger.log("chatter-sftp-upload", "DEBUG", "api", null, { filename: csvObj.filename, result: csvObj.result }); diff --git a/server/data/claimscorp.js b/server/data/claimscorp.js index 4304b75b5..cafa03df1 100644 --- a/server/data/claimscorp.js +++ b/server/data/claimscorp.js @@ -183,9 +183,8 @@ async function uploadViaSFTP(allxmlsToUpload) { for (const xmlObj of allxmlsToUpload) { try { - logger.log("claimscorp-sftp-upload", "DEBUG", "api", null, { filename: xmlObj.filename }); xmlObj.result = await sftp.put(Buffer.from(xmlObj.xml), `${xmlObj.filename}`); - logger.log("claimscorp-sftp-upload-result", "DEBUG", "api", null, { + logger.log("claimscorp-sftp-upload", "DEBUG", "api", null, { filename: xmlObj.filename, result: xmlObj.result }); diff --git a/server/data/kaizen.js b/server/data/kaizen.js index f48bb637d..b79fe4745 100644 --- a/server/data/kaizen.js +++ b/server/data/kaizen.js @@ -181,9 +181,8 @@ async function uploadViaSFTP(allxmlsToUpload) { for (const xmlObj of allxmlsToUpload) { try { - logger.log("kaizen-sftp-upload", "DEBUG", "api", null, { filename: xmlObj.filename }); xmlObj.result = await sftp.put(Buffer.from(xmlObj.xml), `${xmlObj.filename}`); - logger.log("kaizen-sftp-upload-result", "DEBUG", "api", null, { + logger.log("kaizen-sftp-upload", "DEBUG", "api", null, { filename: xmlObj.filename, result: xmlObj.result });