From 4915e05ac9700108bb49e8b5da6c77e863366b57 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Wed, 25 Feb 2026 10:50:46 -0800 Subject: [PATCH] Local history for scrubbing. --- package-lock.json | 586 ++++++++++-------- package.json | 1 + serverless/src/handlers/scrub.ts | 7 +- src/main/db/scrub-history-db.ts | 386 ++++++++++++ src/main/decoder/decoder.ts | 83 ++- src/main/decoder/emsbackup.ts | 2 +- .../estimate-scrubber/estimate-scrubber.ts | 57 +- src/main/ipc/ipcMainConfig.ts | 10 + src/main/ipc/ipcMainHandler.scrubHistory.ts | 67 ++ src/preload/index.ts | 13 +- src/renderer/src/components/Home/Home.tsx | 536 +++++++++++----- src/renderer/src/main.tsx | 1 + src/util/ipcTypes.json | 8 +- src/util/translations/en-US/renderer.json | 14 + translations.babel | 121 +++- 15 files changed, 1413 insertions(+), 479 deletions(-) create mode 100644 src/main/db/scrub-history-db.ts create mode 100644 src/main/ipc/ipcMainHandler.scrubHistory.ts diff --git a/package-lock.json b/package-lock.json index 7d85c2e..cab5b27 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@sentry/electron": "^7.8.0", "@sentry/vite-plugin": "^5.1.0", "axios": "^1.13.5", + "better-sqlite3": "^12.6.2", "dayjs": "^1.11.19", "electron-log": "^5.4.3", "electron-updater": "^6.8.3", @@ -727,6 +728,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "dev": true, "license": "MIT", "dependencies": { "debug": "^4.1.1", @@ -969,72 +971,6 @@ "node": ">= 10.0.0" } }, - "node_modules/@electron/windows-sign": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", - "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "peer": true, - "dependencies": { - "cross-dirname": "^0.1.0", - "debug": "^4.3.4", - "fs-extra": "^11.1.1", - "minimist": "^1.2.8", - "postject": "^1.0.0-alpha.6" - }, - "bin": { - "electron-windows-sign": "bin/electron-windows-sign.js" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@electron/windows-sign/node_modules/fs-extra": { - "version": "11.3.3", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", - "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@electron/windows-sign/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/windows-sign/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/@emotion/hash": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", @@ -4231,6 +4167,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4244,6 +4181,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4257,6 +4195,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4270,6 +4209,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4283,6 +4223,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4296,6 +4237,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4309,6 +4251,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4322,6 +4265,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4335,6 +4279,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4348,6 +4293,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4361,6 +4307,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4374,6 +4321,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4387,6 +4335,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4400,6 +4349,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4413,6 +4363,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4426,6 +4377,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4439,6 +4391,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4452,6 +4405,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4465,6 +4419,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4478,6 +4433,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4491,6 +4447,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4504,6 +4461,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5019,6 +4977,7 @@ "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -5045,6 +5004,7 @@ "version": "4.0.6", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, "license": "MIT", "dependencies": { "defer-to-connect": "^2.0.0" @@ -5113,6 +5073,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, "license": "MIT", "dependencies": { "@types/http-cache-semantics": "*", @@ -5161,6 +5122,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, "license": "MIT" }, "node_modules/@types/express": { @@ -5202,6 +5164,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "dev": true, "license": "MIT" }, "node_modules/@types/http-errors": { @@ -5222,6 +5185,7 @@ "version": "3.1.4", "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -5343,6 +5307,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -5418,6 +5383,7 @@ "version": "2.10.3", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -6682,7 +6648,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -6711,11 +6676,33 @@ "node": ">=6.0.0" } }, + "node_modules/better-sqlite3": { + "version": "12.6.2", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.6.2.tgz", + "integrity": "sha512-8VYKM3MjCa9WcaSAI3hzwhmyHVlH8tiGFwf0RlTsZPWJ1I5MkzjiudCo4KC4DxOaL/53A5B1sI/IbldNFDbsKA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, "license": "MIT", "dependencies": { "buffer": "^5.5.0", @@ -6770,6 +6757,7 @@ "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, "license": "MIT", "optional": true }, @@ -6819,7 +6807,6 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, "funding": [ { "type": "github", @@ -6844,6 +6831,7 @@ "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, "license": "MIT", "engines": { "node": "*" @@ -7000,6 +6988,7 @@ "version": "5.0.4", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, "license": "MIT", "engines": { "node": ">=10.6.0" @@ -7009,6 +6998,7 @@ "version": "7.0.4", "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, "license": "MIT", "dependencies": { "clone-response": "^1.0.2", @@ -7237,6 +7227,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, "license": "MIT", "dependencies": { "mimic-response": "^1.0.0" @@ -7608,15 +7599,6 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/cross-dirname": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", - "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/cross-env": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", @@ -7794,6 +7776,15 @@ "dev": true, "license": "MIT" }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -7818,6 +7809,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -7827,7 +7819,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", @@ -7845,7 +7837,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "define-data-property": "^1.0.1", @@ -7882,7 +7874,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -7892,6 +7883,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, "license": "MIT", "optional": true }, @@ -8130,6 +8122,7 @@ "version": "40.6.0", "resolved": "https://registry.npmjs.org/electron/-/electron-40.6.0.tgz", "integrity": "sha512-ett8W+yOFGDuM0vhJMamYSkrbV3LoaffzJd9GfjI96zRAxyrNqUSKqBpf/WGbQCweDxX2pkUCUfrv4wwKpsFZA==", + "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -8170,19 +8163,6 @@ "node": ">=14.0.0" } }, - "node_modules/electron-builder-squirrel-windows": { - "version": "26.8.1", - "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.8.1.tgz", - "integrity": "sha512-o288fIdgPLHA76eDrFADHPoo7VyGkDCYbLV1GzndaMSAVBoZrGvM9m2IehdcVMzdAZJ2eV9bgyissQXHv5tGzA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "app-builder-lib": "26.8.1", - "builder-util": "26.8.1", - "electron-winstaller": "5.4.0" - } - }, "node_modules/electron-builder/node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -8411,48 +8391,11 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/electron-winstaller": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", - "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@electron/asar": "^3.2.1", - "debug": "^4.1.1", - "fs-extra": "^7.0.1", - "lodash": "^4.17.21", - "temp": "^0.9.0" - }, - "engines": { - "node": ">=8.0.0" - }, - "optionalDependencies": { - "@electron/windows-sign": "^1.1.2" - } - }, - "node_modules/electron-winstaller/node_modules/fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, "node_modules/electron/node_modules/@types/node": { "version": "24.10.13", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.13.tgz", "integrity": "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==", + "dev": true, "license": "MIT", "dependencies": { "undici-types": "~7.16.0" @@ -8462,6 +8405,7 @@ "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, "license": "MIT" }, "node_modules/emoji-regex": { @@ -8485,6 +8429,7 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -8495,6 +8440,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -8517,6 +8463,7 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -8703,6 +8650,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, "license": "MIT", "optional": true }, @@ -8768,7 +8716,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -9155,6 +9103,15 @@ "node": ">=0.8.x" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/exponential-backoff": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", @@ -9210,6 +9167,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "debug": "^4.1.1", @@ -9306,6 +9264,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, "license": "MIT", "dependencies": { "pend": "~1.2.0" @@ -9324,6 +9283,12 @@ "node": ">=16.0.0" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, "node_modules/filelist": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.5.tgz", @@ -9597,10 +9562,17 @@ "node": ">= 0.8" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", @@ -9635,6 +9607,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -9745,6 +9718,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, "license": "MIT", "dependencies": { "pump": "^3.0.0" @@ -9774,6 +9748,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/glob": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", @@ -9812,6 +9792,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, "license": "BSD-3-Clause", "optional": true, "dependencies": { @@ -9830,6 +9811,7 @@ "version": "7.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, "license": "ISC", "optional": true, "bin": { @@ -9856,7 +9838,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "define-properties": "^1.2.1", @@ -9885,6 +9867,7 @@ "version": "11.8.6", "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, "license": "MIT", "dependencies": { "@sindresorhus/is": "^4.0.0", @@ -9916,6 +9899,7 @@ "version": "16.13.0", "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.13.0.tgz", "integrity": "sha512-uSisMYERbaB9bkA9M4/4dnqyktaEkf1kMHNKq/7DHyxVeWqHQ2mBmVqm5u6/FVHwF3iCNalKcg82Zfl+tffWoA==", + "dev": true, "license": "MIT", "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" @@ -9976,7 +9960,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" @@ -10104,6 +10088,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "dev": true, "license": "BSD-2-Clause" }, "node_modules/http-errors": { @@ -10152,6 +10137,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, "license": "MIT", "dependencies": { "quick-lru": "^5.1.1", @@ -10260,7 +10246,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, "funding": [ { "type": "github", @@ -10336,7 +10321,12 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, "node_modules/internal-slot": { @@ -10924,6 +10914,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, "license": "MIT" }, "node_modules/json-schema-traverse": { @@ -10951,6 +10942,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, "license": "ISC", "optional": true }, @@ -10980,6 +10972,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, "license": "MIT", "optionalDependencies": { "graceful-fs": "^4.1.6" @@ -11005,6 +10998,7 @@ "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, "license": "MIT", "dependencies": { "json-buffer": "3.0.1" @@ -11166,6 +11160,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -11219,6 +11214,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -11323,6 +11319,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -11347,7 +11344,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -11505,19 +11501,11 @@ "node": ">= 18" } }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" }, "node_modules/module-details-from-path": { "version": "1.0.4", @@ -11550,6 +11538,12 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -11754,6 +11748,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -11789,7 +11784,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -11984,6 +11979,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -12138,6 +12134,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, "license": "MIT" }, "node_modules/pg-int8": { @@ -12302,34 +12299,55 @@ "node": ">=0.10.0" } }, - "node_modules/postject": { - "version": "1.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", - "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", - "dev": true, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", "license": "MIT", - "optional": true, - "peer": true, "dependencies": { - "commander": "^9.4.0" + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" }, "bin": { - "postject": "dist/cli.js" + "prebuild-install": "bin.js" }, "engines": { - "node": ">=14.0.0" + "node": ">=10" } }, - "node_modules/postject/node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", - "dev": true, + "node_modules/prebuild-install/node_modules/node-abi": { + "version": "3.87.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", + "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==", "license": "MIT", - "optional": true, - "peer": true, + "dependencies": { + "semver": "^7.3.5" + }, "engines": { - "node": "^12.20.0 || >=14" + "node": ">=10" + } + }, + "node_modules/prebuild-install/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/prelude-ls": { @@ -12530,6 +12548,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -12581,11 +12600,26 @@ "url": "https://opencollective.com/express" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, "node_modules/react": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -12595,7 +12629,7 @@ "version": "19.2.4", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "scheduler": "^0.27.0" @@ -12733,7 +12767,6 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -12932,12 +12965,14 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, "license": "MIT" }, "node_modules/responselike": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, "license": "MIT", "dependencies": { "lowercase-keys": "^2.0.0" @@ -12970,74 +13005,11 @@ "node": ">= 4" } }, - "node_modules/rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/rimraf/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/minimatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", - "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/roarr": { "version": "2.15.4", "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, "license": "BSD-3-Clause", "optional": true, "dependencies": { @@ -13056,6 +13028,7 @@ "version": "4.52.5", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz", "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==", + "dev": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -13110,16 +13083,6 @@ "node": ">= 18" } }, - "node_modules/rxjs": { - "version": "7.8.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", - "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "tslib": "^2.1.0" - } - }, "node_modules/safe-array-concat": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", @@ -13144,7 +13107,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -13200,7 +13162,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/sanitize-filename": { @@ -13223,7 +13185,7 @@ "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/scroll-into-view-if-needed": { @@ -13249,6 +13211,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, "license": "MIT", "optional": true }, @@ -13279,6 +13242,7 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -13295,6 +13259,7 @@ "version": "0.13.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, "license": "(MIT OR CC0-1.0)", "optional": true, "engines": { @@ -13489,6 +13454,51 @@ "dev": true, "license": "ISC" }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -13605,6 +13615,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, "license": "BSD-3-Clause", "optional": true }, @@ -13659,7 +13670,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" @@ -13828,6 +13838,15 @@ "node": ">=8" } }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/stubborn-fs": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-1.2.5.tgz", @@ -13845,6 +13864,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, "license": "Apache-2.0", "dependencies": { "debug": "^4.1.0" @@ -13926,6 +13946,40 @@ "node": ">=18" } }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/tar-fs/node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tar-stream": { "version": "3.1.7", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", @@ -13948,21 +14002,6 @@ "node": ">=18" } }, - "node_modules/temp": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", - "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "mkdirp": "^0.5.1", - "rimraf": "~2.6.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/temp-file": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", @@ -14171,6 +14210,18 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -14399,6 +14450,7 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 4.0.0" @@ -14475,7 +14527,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, "license": "MIT" }, "node_modules/vary": { @@ -15449,6 +15500,7 @@ "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, "license": "MIT", "dependencies": { "buffer-crc32": "~0.2.3", diff --git a/package.json b/package.json index f6ddafa..d80815a 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@sentry/electron": "^7.8.0", "@sentry/vite-plugin": "^5.1.0", "axios": "^1.13.5", + "better-sqlite3": "^12.6.2", "dayjs": "^1.11.19", "electron-log": "^5.4.3", "electron-updater": "^6.8.3", diff --git a/serverless/src/handlers/scrub.ts b/serverless/src/handlers/scrub.ts index 0e0ac9f..58d6eb6 100644 --- a/serverless/src/handlers/scrub.ts +++ b/serverless/src/handlers/scrub.ts @@ -24,8 +24,11 @@ interface ScrubResponse { export const handler = async (event: APIGatewayProxyEvent): Promise => { try { - const { esApiKey, rawJob } = JSON.parse(event.body || '{}') as ScrubRequest; - + const { + //esApiKey, + rawJob, + } = JSON.parse(event.body || '{}') as ScrubRequest; + const esApiKey = event.headers['x-api-key'] || ''; //await uploadJobToHasura(rawJob, esApiKey); // Transform the raw job object to ES format const estimate: ESJobObject = await transformJobForEstimateScrubber(rawJob); diff --git a/src/main/db/scrub-history-db.ts b/src/main/db/scrub-history-db.ts new file mode 100644 index 0000000..324e94f --- /dev/null +++ b/src/main/db/scrub-history-db.ts @@ -0,0 +1,386 @@ +import { app } from "electron"; +import log from "electron-log/main"; +import Database from "better-sqlite3"; +import crypto from "crypto"; +import path from "path"; +import type { RawJobDataObject } from "../decoder/decoder"; + +export type ScrubHistoryJobRow = { + id: string; + created_at: number; + ownr_name: string; + vehicle: string; + claim_number: string; + pdf_url: string | null; +}; + +export type ScrubHistoryScrubResultRow = { + id: number; + created_at: number; + job_id: string; + anchor: string | null; + category: string | null; + subcategory: string | null; + left_text: string | null; + right_text: string | null; + linktext: string | null; +}; + +export type ScrubHistoryItem = { + id: string; + createdAt: number; + ownrName: string; + vehicle: string; + claimNumber: string; + pdfUrl: string | null; + results: Array<{ + createdAt: number; + anchor: string | null; + category: string | null; + subcategory: string | null; + left: string | null; + right: string | null; + linktext: string | null; + }>; +}; + +export type ScrubHistoryPage = { + items: ScrubHistoryItem[]; + totalJobs: number; + page: number; + pageSize: number; + totalResults: number; + lastProcessed: number | null; +}; + +type IdentifiedItem = { + Anchor?: unknown; + Category?: unknown; + SubCategory?: unknown; + L?: unknown; + R?: unknown; + LinkText?: unknown; +}; + +let db: Database.Database | undefined; + +function toNullableString(value: unknown): string | null { + if (value === null || value === undefined) return null; + if (typeof value === "string") return value; + if (typeof value === "number" || typeof value === "boolean") { + return String(value); + } + try { + return JSON.stringify(value); + } catch { + return String(value); + } +} + +function getDbPath(): string { + const userDataDir = app.getPath("userData"); + return path.join(userDataDir, "scrub-history.sqlite3"); +} + +function getDb(): Database.Database { + if (db) return db; + + const dbPath = getDbPath(); + log.info(`[scrub-history-db] opening sqlite db at ${dbPath}`); + db = new Database(dbPath); + + db.pragma("journal_mode = WAL"); + db.pragma("foreign_keys = ON"); + + db.exec(` + CREATE TABLE IF NOT EXISTS jobs ( + id TEXT PRIMARY KEY, + created_at INTEGER NOT NULL, + ownr_name TEXT NOT NULL, + vehicle TEXT NOT NULL, + claim_number TEXT NOT NULL, + pdf_url TEXT + ); + + CREATE TABLE IF NOT EXISTS scrub_results ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_at INTEGER NOT NULL, + job_id TEXT NOT NULL, + anchor TEXT, + category TEXT, + subcategory TEXT, + left_text TEXT, + right_text TEXT, + linktext TEXT, + FOREIGN KEY(job_id) REFERENCES jobs(id) ON DELETE CASCADE + ); + + CREATE INDEX IF NOT EXISTS idx_jobs_created_at ON jobs(created_at DESC); + CREATE INDEX IF NOT EXISTS idx_scrub_results_job_id ON scrub_results(job_id); + CREATE INDEX IF NOT EXISTS idx_scrub_results_created_at ON scrub_results(created_at DESC); + `); + + // Lightweight migrations for existing installs + const ensureJobsColumn = (columnName: string, definition: string): void => { + const cols = db!.prepare("PRAGMA table_info(jobs)").all() as Array<{ + name: string; + }>; + const hasCol = cols.some((c) => c.name === columnName); + if (!hasCol) { + log.info(`[scrub-history-db] migrating jobs: adding ${columnName}`); + db!.exec(`ALTER TABLE jobs ADD COLUMN ${definition}`); + } + }; + + ensureJobsColumn("pdf_url", "pdf_url TEXT"); + + return db; +} + +function buildJobRow( + job: RawJobDataObject, + createdAt: number, + pdfUrl: string | null, +): ScrubHistoryJobRow { + const ownrName = `${job.ownr_fn ?? ""} ${job.ownr_ln ?? ""}`.trim(); + const vehicle = + `${job.v_model_yr ?? ""} ${job.v_make_desc ?? ""} ${job.v_model_desc ?? ""}`.trim(); + const claimNumber = `${job.clm_no ?? ""}`.trim(); + return { + id: crypto.randomUUID(), + created_at: createdAt, + ownr_name: ownrName || "Unknown", + vehicle: vehicle || "Unknown", + claim_number: claimNumber || "Unknown", + pdf_url: pdfUrl, + }; +} + +export function insertScrubRun(params: { + job: RawJobDataObject; + identifiedItems: unknown; + pdfUrl: string | null; +}): { jobId: string; createdAt: number; insertedResultsCount: number } { + const database = getDb(); + const createdAt = Date.now(); + const jobRow = buildJobRow(params.job, createdAt, params.pdfUrl); + + const items: IdentifiedItem[] = Array.isArray(params.identifiedItems) + ? (params.identifiedItems as IdentifiedItem[]) + : params.identifiedItems + ? [params.identifiedItems as IdentifiedItem] + : []; + + const insertTx = database.transaction(() => { + database + .prepare( + "INSERT INTO jobs (id, created_at, ownr_name, vehicle, claim_number, pdf_url) VALUES (?, ?, ?, ?, ?, ?)", + ) + .run( + jobRow.id, + jobRow.created_at, + jobRow.ownr_name, + jobRow.vehicle, + jobRow.claim_number, + jobRow.pdf_url, + ); + + const stmt = database.prepare( + "INSERT INTO scrub_results (created_at, job_id, anchor, category, subcategory, left_text, right_text, linktext) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + ); + + for (const item of items) { + stmt.run( + createdAt, + jobRow.id, + toNullableString(item.Anchor), + toNullableString(item.Category), + toNullableString(item.SubCategory), + toNullableString(item.L), + toNullableString(item.R), + toNullableString(item.LinkText), + ); + } + }); + + insertTx(); + return { jobId: jobRow.id, createdAt, insertedResultsCount: items.length }; +} + +export function getScrubHistory(): ScrubHistoryItem[] { + const database = getDb(); + + const jobs = database + .prepare( + "SELECT id, created_at, ownr_name, vehicle, claim_number, pdf_url FROM jobs ORDER BY created_at DESC", + ) + .all() as ScrubHistoryJobRow[]; + + if (jobs.length === 0) return []; + + const results = database + .prepare( + "SELECT id, created_at, job_id, anchor, category, subcategory, left_text, right_text, linktext FROM scrub_results ORDER BY created_at DESC, id DESC", + ) + .all() as ScrubHistoryScrubResultRow[]; + + const resultsByJobId = new Map(); + for (const r of results) { + const bucket = resultsByJobId.get(r.job_id) ?? []; + bucket.push({ + createdAt: r.created_at, + anchor: r.anchor, + category: r.category, + subcategory: r.subcategory, + left: r.left_text, + right: r.right_text, + linktext: r.linktext, + }); + resultsByJobId.set(r.job_id, bucket); + } + + return jobs.map((j) => ({ + id: j.id, + createdAt: j.created_at, + ownrName: j.ownr_name, + vehicle: j.vehicle, + claimNumber: j.claim_number, + pdfUrl: j.pdf_url ?? null, + results: resultsByJobId.get(j.id) ?? [], + })); +} + +function normalizePageParams(params: { page: number; pageSize: number }): { + page: number; + pageSize: number; + offset: number; +} { + const page = Number.isFinite(params.page) ? Math.floor(params.page) : 1; + const pageSize = Number.isFinite(params.pageSize) + ? Math.floor(params.pageSize) + : 10; + + const safePage = Math.max(1, page); + const safePageSize = Math.min(100, Math.max(1, pageSize)); + const offset = (safePage - 1) * safePageSize; + return { page: safePage, pageSize: safePageSize, offset }; +} + +export function getScrubHistoryPage(params: { + page: number; + pageSize: number; +}): ScrubHistoryPage { + const database = getDb(); + const { page, pageSize, offset } = normalizePageParams(params); + + const totalJobsRow = database + .prepare("SELECT COUNT(1) as c FROM jobs") + .get() as { c: number }; + const totalJobs = totalJobsRow?.c ?? 0; + + const totalResultsRow = database + .prepare("SELECT COUNT(1) as c FROM scrub_results") + .get() as { c: number }; + const totalResults = totalResultsRow?.c ?? 0; + + const lastProcessedRow = database + .prepare("SELECT MAX(created_at) as m FROM jobs") + .get() as { m: number | null }; + const lastProcessed = lastProcessedRow?.m ?? null; + + if (totalJobs === 0) { + return { + items: [], + totalJobs, + page, + pageSize, + totalResults, + lastProcessed, + }; + } + + const jobs = database + .prepare( + "SELECT id, created_at, ownr_name, vehicle, claim_number, pdf_url FROM jobs ORDER BY created_at DESC LIMIT ? OFFSET ?", + ) + .all(pageSize, offset) as ScrubHistoryJobRow[]; + + if (jobs.length === 0) { + return { + items: [], + totalJobs, + page, + pageSize, + totalResults, + lastProcessed, + }; + } + + const jobIds = jobs.map((j) => j.id); + const placeholders = jobIds.map(() => "?").join(","); + + const results = database + .prepare( + `SELECT id, created_at, job_id, anchor, category, subcategory, left_text, right_text, linktext FROM scrub_results WHERE job_id IN (${placeholders}) ORDER BY created_at DESC, id DESC`, + ) + .all(...jobIds) as ScrubHistoryScrubResultRow[]; + + const resultsByJobId = new Map(); + for (const r of results) { + const bucket = resultsByJobId.get(r.job_id) ?? []; + bucket.push({ + createdAt: r.created_at, + anchor: r.anchor, + category: r.category, + subcategory: r.subcategory, + left: r.left_text, + right: r.right_text, + linktext: r.linktext, + }); + resultsByJobId.set(r.job_id, bucket); + } + + return { + items: jobs.map((j) => ({ + id: j.id, + createdAt: j.created_at, + ownrName: j.ownr_name, + vehicle: j.vehicle, + claimNumber: j.claim_number, + pdfUrl: j.pdf_url ?? null, + results: resultsByJobId.get(j.id) ?? [], + })), + totalJobs, + page, + pageSize, + totalResults, + lastProcessed, + }; +} + +export function deleteScrubHistoryJob(jobId: string): { deletedJobs: number } { + const database = getDb(); + const trimmed = jobId.trim(); + if (!trimmed) return { deletedJobs: 0 }; + + const tx = database.transaction(() => { + const res = database.prepare("DELETE FROM jobs WHERE id = ?").run(trimmed); + return res.changes; + }); + + return { deletedJobs: tx() }; +} + +export function clearScrubHistory(): { clearedJobs: number } { + const database = getDb(); + + const tx = database.transaction(() => { + const countBefore = database + .prepare("SELECT COUNT(1) as c FROM jobs") + .get() as { c: number }; + database.prepare("DELETE FROM jobs").run(); + return countBefore?.c ?? 0; + }); + + return { clearedJobs: tx() }; +} diff --git a/src/main/decoder/decoder.ts b/src/main/decoder/decoder.ts index 73b11ab..4ce07a7 100644 --- a/src/main/decoder/decoder.ts +++ b/src/main/decoder/decoder.ts @@ -50,33 +50,33 @@ async function ImportJob(filepath: string): Promise { //The below all end up returning parts of the job object. //Some of them return additional info - e.g. owner or vehicle record data at both the job and corresponding table level. - setAppProgressbar(0.1); - const env: DecodedEnv = await DecodeEnv(extensionlessFilePath); - setAppProgressbar(0.15); - const ad1: DecodedAd1 = await DecodeAD1(extensionlessFilePath); - setAppProgressbar(0.2); - const ad2: DecodedAD2 = await DecodeAD2(extensionlessFilePath); - setAppProgressbar(0.25); - const veh: DecodedVeh = await DecodeVeh(extensionlessFilePath); - setAppProgressbar(0.3); - const lin: DecodedLin = await DecodeLin(extensionlessFilePath); - setAppProgressbar(0.35); - const pfh: DecodedPfh = await DecodePfh(extensionlessFilePath); - setAppProgressbar(0.4); - const pfl: DecodedPfl = await DecodePfl(extensionlessFilePath); - setAppProgressbar(0.45); - const pft: DecodedPft = await DecodePft(extensionlessFilePath); setAppProgressbar(0.5); + const env: DecodedEnv = await DecodeEnv(extensionlessFilePath); + setAppProgressbar(0.1); + const ad1: DecodedAd1 = await DecodeAD1(extensionlessFilePath); + setAppProgressbar(0.15); + const ad2: DecodedAD2 = await DecodeAD2(extensionlessFilePath); + setAppProgressbar(0.2); + const veh: DecodedVeh = await DecodeVeh(extensionlessFilePath); + setAppProgressbar(0.25); + const lin: DecodedLin = await DecodeLin(extensionlessFilePath); + setAppProgressbar(0.3); + const pfh: DecodedPfh = await DecodePfh(extensionlessFilePath); + setAppProgressbar(0.35); + const pfl: DecodedPfl = await DecodePfl(extensionlessFilePath); + setAppProgressbar(0.4); + const pft: DecodedPft = await DecodePft(extensionlessFilePath); + setAppProgressbar(0.45); const pfm: DecodedPfm = await DecodePfm(extensionlessFilePath); - setAppProgressbar(0.55); + setAppProgressbar(0.5); const pfo: DecodedPfo = await DecodePfo(extensionlessFilePath); // TODO: This will be the `cieca_pfo` object - setAppProgressbar(0.6); + setAppProgressbar(0.55); const stl: DecodedStl = await DecodeStl(extensionlessFilePath); // TODO: This will be the `cieca_stl` object - setAppProgressbar(0.65); + setAppProgressbar(0.6); const ttl: DecodedTtl = await DecodeTtl(extensionlessFilePath); - setAppProgressbar(0.7); + setAppProgressbar(0.65); const pfp: DecodedPfp = await DecodePfp(extensionlessFilePath); - setAppProgressbar(0.75); + setAppProgressbar(0.7); const jobObjectUncleaned: RawJobDataObject = { ...env, @@ -97,7 +97,6 @@ async function ImportJob(filepath: string): Promise { // Replace owner information with claimant information if necessary const jobObject = ReplaceOwnerInfoWithClaimant(jobObjectUncleaned); - setAppProgressbar(0.8); if (import.meta.env.DEV) { // Save jobObject to a timestamped JSON file @@ -132,33 +131,14 @@ async function ImportJob(filepath: string): Promise { issupplement: false, jobid: null, }; - setAppProgressbar(0.85); + setAppProgressbar(0.73); console.log("Available Job record to upload;", newAvailableJob); - setAppProgressbar(0.95); - - setAppProgressbar(-1); - const uploadNotification = new Notification({ - title: "Job Imported", - //subtitle: `${newAvailableJob.ownr_name} - ${newAvailableJob.vehicle_info}`, - body: `${newAvailableJob.ownr_name} - ${newAvailableJob.vehicle_info}. Click to view.`, - actions: [{ text: "View Job", type: "button" }], - }); - uploadNotification.on("click", () => { - shell.openExternal( - `${ - store.get("app.isTest") - ? import.meta.env.VITE_FE_URL_TEST - : import.meta.env.VITE_FE_URL - }/manage/available`, - ); - }); - uploadNotification.show(); - //Scrub the estimate const scrubResults = await ScrubEstimate({ job: jobObject }); + setAppProgressbar(0.95); const esApiKey = store.get("settings.esApiKey") as string; UploadEmsToS3({ extensionlessFilePath, @@ -167,11 +147,24 @@ async function ImportJob(filepath: string): Promise { clm_no: jobObject.clm_no ?? "", ownr_ln: jobObject.ownr_ln ?? "", }); - console.log("Got past the job upload."); + setAppProgressbar(-1); + + const uploadNotification = new Notification({ + title: "Job Scrubbed", + //subtitle: `${newAvailableJob.ownr_name} - ${newAvailableJob.vehicle_info}`, + body: `${newAvailableJob.ownr_name} - ${newAvailableJob.vehicle_info}. Click to view.`, + actions: [{ text: "View Job", type: "button" }], + }); + uploadNotification.on("click", () => { + if (scrubResults?.data?.resultPDFUrl) { + shell.openExternal(scrubResults.data.resultPDFUrl); + } + }); + uploadNotification.show(); } catch (error) { log.error("Error encountered while decoding job. ", errorTypeCheck(error)); const uploadNotificationFailure = new Notification({ - title: "Job Upload Failure", + title: "Job Scrub Failure", body: errorTypeCheck(error).message, //TODO: Remove after debug. }); diff --git a/src/main/decoder/emsbackup.ts b/src/main/decoder/emsbackup.ts index 533b34f..3c72c1e 100644 --- a/src/main/decoder/emsbackup.ts +++ b/src/main/decoder/emsbackup.ts @@ -71,7 +71,7 @@ async function UploadEmsToS3({ ownr_ln, }, { - headers: {}, + headers: { "x-api-key": esApiKey }, }, ); diff --git a/src/main/estimate-scrubber/estimate-scrubber.ts b/src/main/estimate-scrubber/estimate-scrubber.ts index a3bc450..fcb0475 100644 --- a/src/main/estimate-scrubber/estimate-scrubber.ts +++ b/src/main/estimate-scrubber/estimate-scrubber.ts @@ -7,6 +7,7 @@ import path from "path"; import { RawJobDataObject } from "../decoder/decoder"; import store from "../store/store"; import ipcTypes from "../../util/ipcTypes.json"; +import { insertScrubRun } from "../db/scrub-history-db"; // Function to write job object to logs subfolder async function writeJobToLogsFolder(job, fileName): Promise { @@ -84,13 +85,34 @@ async function ScrubEstimate({ } // Send raw job to Lambda - transformation happens server-side - const result = await axios.post(estimateScrubberUrl, { - esApiKey, - rawJob: job, // Changed from 'estimate' to 'rawJob' - }); + const result = await axios.post( + estimateScrubberUrl, + { + esApiKey, + rawJob: job, // Changed from 'estimate' to 'rawJob' + }, + { + headers: { + "x-api-key": esApiKey, + }, + }, + ); - const { resultPDFUrl, reportIssueUrl, identified_item } = - result?.data ?? {}; + const { resultPDFUrl, identified_item } = result?.data ?? {}; + + try { + insertScrubRun({ + job, + identifiedItems: identified_item.slice(1, identified_item.length), // Remove first item which is the result metadata + pdfUrl: typeof resultPDFUrl === "string" ? resultPDFUrl : null, + }); + const mainWindow = BrowserWindow.getAllWindows()[0]; + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send(ipcTypes.toRenderer.scrub.historyUpdated); + } + } catch (dbError) { + log.error("Failed to persist scrub history:", dbError); + } // log.log("Estimate Scrubber Result:", result.data, resultPDFUrl); // const b = BrowserWindow.getAllWindows()[0]; @@ -112,20 +134,29 @@ async function ScrubEstimate({ return resultPDFUrl; } catch (error) { const err = error as AxiosError; - log.error("Error while scrubbing estimate:", error, err.stack); + log.error("Error while scrubbing estimate:", err.message, err.stack); log.error("Error Response Data:", err.response?.data); const mainWindow = BrowserWindow.getAllWindows()[0]; - if (error.status === 400) { - mainWindow.webContents.send(ipcTypes.toRenderer.scrub.scrubError, { + const status = err.response?.status; + const responseData = err.response?.data; + const responseMessage = + typeof responseData === "string" + ? responseData + : responseData + ? JSON.stringify(responseData) + : undefined; + + if (status === 400) { + mainWindow?.webContents.send(ipcTypes.toRenderer.scrub.scrubError, { message: - error.response?.data || + responseMessage || "Error encountered sending estimate to Estimate Scrubber.", }); - } else if (error.status === 401) { - mainWindow.webContents.send(ipcTypes.toRenderer.scrub.scrubError, { + } else if (status === 401) { + mainWindow?.webContents.send(ipcTypes.toRenderer.scrub.scrubError, { message: - err.response?.data || "Authentication with Estimate Scrubber failed."ta, + responseMessage || "Authentication with Estimate Scrubber failed.", }); } return "Error: Unable to scrub estimate."; diff --git a/src/main/ipc/ipcMainConfig.ts b/src/main/ipc/ipcMainConfig.ts index cbacd9f..48eb3fd 100644 --- a/src/main/ipc/ipcMainConfig.ts +++ b/src/main/ipc/ipcMainConfig.ts @@ -23,6 +23,11 @@ import { ipcMainHandleAuthStateChanged, ipMainHandleResetPassword, } from "./ipcMainHandler.user"; +import { + ScrubHistoryClearAll, + ScrubHistoryDeleteJob, + ScrubHistoryGetAll, +} from "./ipcMainHandler.scrubHistory"; // Log all IPC messages and their payloads const logIpcMessages = (): void => { @@ -56,6 +61,11 @@ ipcMain.on(ipcTypes.toMain.test, () => ipcMain.on(ipcTypes.toMain.authStateChanged, ipcMainHandleAuthStateChanged); ipcMain.on(ipcTypes.toMain.user.resetPassword, ipMainHandleResetPassword); +// Scrub History Handlers +ipcMain.handle(ipcTypes.toMain.scrubHistory.getAll, ScrubHistoryGetAll); +ipcMain.handle(ipcTypes.toMain.scrubHistory.deleteJob, ScrubHistoryDeleteJob); +ipcMain.handle(ipcTypes.toMain.scrubHistory.clearAll, ScrubHistoryClearAll); + // Add debug handlers if in development if (import.meta.env.DEV) { log.debug("[IPC Debug Functions] Adding Debug Handlers"); diff --git a/src/main/ipc/ipcMainHandler.scrubHistory.ts b/src/main/ipc/ipcMainHandler.scrubHistory.ts new file mode 100644 index 0000000..08ccc1c --- /dev/null +++ b/src/main/ipc/ipcMainHandler.scrubHistory.ts @@ -0,0 +1,67 @@ +import { BrowserWindow } from "electron"; +import log from "electron-log/main"; +import ipcTypes from "../../util/ipcTypes.json"; +import { + clearScrubHistory, + deleteScrubHistoryJob, + getScrubHistory, + getScrubHistoryPage, +} from "../db/scrub-history-db"; + +export async function ScrubHistoryGetAll( + _event: Electron.IpcMainInvokeEvent, + params?: { page?: number; pageSize?: number }, +): Promise< + ReturnType | ReturnType +> { + try { + if ( + params && + (params.page !== undefined || params.pageSize !== undefined) + ) { + return getScrubHistoryPage({ + page: params.page ?? 1, + pageSize: params.pageSize ?? 10, + }); + } + return getScrubHistory(); + } catch (error) { + log.error("[ScrubHistoryGetAll] failed", error); + return []; + } +} + +function notifyHistoryUpdated(): void { + const mainWindow = BrowserWindow.getAllWindows()[0]; + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send(ipcTypes.toRenderer.scrub.historyUpdated); + } +} + +export async function ScrubHistoryDeleteJob( + _event: Electron.IpcMainInvokeEvent, + jobId: string, +): Promise<{ ok: boolean; deletedJobs: number }> { + try { + const { deletedJobs } = deleteScrubHistoryJob(jobId); + if (deletedJobs > 0) notifyHistoryUpdated(); + return { ok: true, deletedJobs }; + } catch (error) { + log.error("[ScrubHistoryDeleteJob] failed", error); + return { ok: false, deletedJobs: 0 }; + } +} + +export async function ScrubHistoryClearAll(): Promise<{ + ok: boolean; + clearedJobs: number; +}> { + try { + const { clearedJobs } = clearScrubHistory(); + if (clearedJobs > 0) notifyHistoryUpdated(); + return { ok: true, clearedJobs }; + } catch (error) { + log.error("[ScrubHistoryClearAll] failed", error); + return { ok: false, clearedJobs: 0 }; + } +} diff --git a/src/preload/index.ts b/src/preload/index.ts index 4485593..b9f3df3 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -1,4 +1,4 @@ -import { contextBridge } from "electron"; +import { BrowserWindow, contextBridge, shell } from "electron"; import { electronAPI } from "@electron-toolkit/preload"; import "electron-log/preload"; import store from "../main/store/store"; @@ -6,10 +6,21 @@ import store from "../main/store/store"; // Custom APIs for renderer interface Api { isTest: () => boolean; + openExternal: (url: string) => Promise; } const api: Api = { isTest: (): boolean => store.get("app.isTest") || false, + openExternal: async (url: string): Promise => { + const pdfWindow = new BrowserWindow({ + webPreferences: { + plugins: true, // Enable PDF viewing + }, + }); + + pdfWindow.loadURL(url); + pdfWindow.focus(); + }, }; // Use `contextBridge` APIs to expose Electron APIs to diff --git a/src/renderer/src/components/Home/Home.tsx b/src/renderer/src/components/Home/Home.tsx index 04373e2..6b83d33 100644 --- a/src/renderer/src/components/Home/Home.tsx +++ b/src/renderer/src/components/Home/Home.tsx @@ -4,6 +4,7 @@ import { Card, Col, Flex, + Popconfirm, Row, Space, Statistic, @@ -11,145 +12,289 @@ import { Tag, Typography, theme, - List, - Avatar, - Divider, } from "antd"; -import { FC } from "react"; +import { FC, useCallback, useEffect, useMemo, useState } from "react"; import { useNavigate } from "react-router"; import { FileTextOutlined, CheckCircleOutlined, ClockCircleOutlined, SettingOutlined, - EyeOutlined, - RightOutlined, + DatabaseOutlined, } from "@ant-design/icons"; +import ipcTypes from "../../../../util/ipcTypes.json"; +import { useTranslation } from "react-i18next"; -const { Title, Text, Paragraph } = Typography; +const { Title, Text } = Typography; -// Placeholder data for recently scrubbed estimates -const placeholderEstimates = [ - { - id: "EST-2026-001", - fileName: "estimate_john_doe.pdf", - scrubbedAt: "2026-01-12T10:30:00", - status: "completed", - itemsProcessed: 24, - }, - { - id: "EST-2026-002", - fileName: "estimate_jane_smith.pdf", - scrubbedAt: "2026-01-12T09:15:00", - status: "completed", - itemsProcessed: 18, - }, - { - id: "EST-2026-003", - fileName: "estimate_acme_corp.pdf", - scrubbedAt: "2026-01-11T16:45:00", - status: "completed", - itemsProcessed: 32, - }, - { - id: "EST-2026-004", - fileName: "estimate_bob_johnson.pdf", - scrubbedAt: "2026-01-11T14:20:00", - status: "completed", - itemsProcessed: 15, - }, - { - id: "EST-2026-005", - fileName: "estimate_tech_solutions.pdf", - scrubbedAt: "2026-01-11T11:00:00", - status: "completed", - itemsProcessed: 27, - }, -]; +const categoryConfig = { + "Administrative Items": { color: "blue", priority: 1, icon: "📎" }, + "Rates Issues": { color: "blue", priority: 2, icon: "💵" }, + "MPI Guidelines Items": { color: "blue", priority: 3, icon: "📋" }, + "Estimator Recommendations": { color: "blue", priority: 4, icon: "✅" }, + "Estimate Parts Found": { color: "blue", priority: 5, icon: "🔧" }, + "All Parts Found": { color: "blue", priority: 6, icon: "🔧" }, +} as const; -const columns = [ - { - title: "Estimate", - dataIndex: "id", - key: "id", - responsive: ["md"] as const, - render: (text: string, record: (typeof placeholderEstimates)[0]) => ( - - {text} - - {record.fileName} - - - ), - }, - { - title: "File", - dataIndex: "fileName", - key: "fileName", - responsive: ["xs"] as const, - render: (text: string, record: (typeof placeholderEstimates)[0]) => ( - - {record.id} - - {text} - - - ), - }, - { - title: "Scrubbed", - dataIndex: "scrubbedAt", - key: "scrubbedAt", - responsive: ["sm"] as const, - render: (text: string) => { - const date = new Date(text); - return ( - - {date.toLocaleDateString()} - - {date.toLocaleTimeString()} - - - ); - }, - }, - { - title: "Items", - dataIndex: "itemsProcessed", - key: "itemsProcessed", - responsive: ["lg"] as const, - render: (value: number) => {value}, - }, - { - title: "Status", - dataIndex: "status", - key: "status", - responsive: ["sm"] as const, - render: (status: string) => ( - } - style={{ margin: 0 }} - > - {status === "completed" ? "Done" : "Processing"} - - ), - }, - { - title: "", - key: "action", - width: 100, - render: () => ( - - ), - }, -]; +type ScrubHistoryItem = { + id: string; + createdAt: number; + ownrName: string; + vehicle: string; + claimNumber: string; + pdfUrl: string | null; + results: Array; +}; + +type ScrubHistoryResultItem = { + createdAt: number; + anchor: string | null; + category: string | null; + subcategory: string | null; + left: string | null; + right: string | null; + linktext: string | null; +}; + +type ScrubHistoryPage = { + items: ScrubHistoryItem[]; + totalJobs: number; + page: number; + pageSize: number; + totalResults: number; + lastProcessed: number | null; +}; + +function isScrubHistoryPage(value: unknown): value is ScrubHistoryPage { + if (!value || typeof value !== "object") return false; + const v = value as Partial; + return Array.isArray(v.items) && typeof v.totalJobs === "number"; +} const Home: FC = () => { + const { t } = useTranslation(); const navigate = useNavigate(); const { token } = theme.useToken(); + const ipcRenderer = window.electron.ipcRenderer; + + const [history, setHistory] = useState([]); + const [loading, setLoading] = useState(true); + const [totalJobs, setTotalJobs] = useState(0); + const [totalResults, setTotalResults] = useState(0); + const [lastProcessed, setLastProcessed] = useState(null); + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + + const refresh = useCallback( + async (page: number, size: number) => { + setLoading(true); + try { + const response = (await ipcRenderer.invoke( + ipcTypes.toMain.scrubHistory.getAll, + { page, pageSize: size }, + )) as unknown; + + if (isScrubHistoryPage(response)) { + setHistory(response.items); + setTotalJobs(response.totalJobs); + setTotalResults(response.totalResults); + setLastProcessed(response.lastProcessed); + + if ( + response.items.length === 0 && + response.totalJobs > 0 && + page > 1 + ) { + setCurrentPage(page - 1); + } + } else if (Array.isArray(response)) { + setHistory(response as ScrubHistoryItem[]); + setTotalJobs((response as ScrubHistoryItem[]).length); + setTotalResults( + (response as ScrubHistoryItem[]).reduce( + (acc, job) => acc + (job.results?.length ?? 0), + 0, + ), + ); + setLastProcessed( + (response as ScrubHistoryItem[])[0]?.createdAt ?? null, + ); + } else { + setHistory([]); + setTotalJobs(0); + setTotalResults(0); + setLastProcessed(null); + } + } finally { + setLoading(false); + } + }, + [ipcRenderer], + ); + + useEffect(() => { + refresh(currentPage, pageSize).catch(() => { + setLoading(false); + }); + + const handler = () => { + refresh(currentPage, pageSize).catch(() => undefined); + }; + + ipcRenderer.on(ipcTypes.toRenderer.scrub.historyUpdated, handler); + return () => { + ipcRenderer.removeListener( + ipcTypes.toRenderer.scrub.historyUpdated, + handler, + ); + }; + }, [ipcRenderer, refresh, currentPage, pageSize]); + + const totalItemsScrubbed = totalResults; + + const deleteJob = useCallback( + async (jobId: string) => { + await ipcRenderer.invoke(ipcTypes.toMain.scrubHistory.deleteJob, jobId); + await refresh(currentPage, pageSize); + }, + [ipcRenderer, refresh, currentPage, pageSize], + ); + + const clearAll = useCallback(async () => { + await ipcRenderer.invoke(ipcTypes.toMain.scrubHistory.clearAll); + setCurrentPage(1); + await refresh(1, pageSize); + }, [ipcRenderer, refresh, pageSize]); + + const jobColumns = useMemo( + () => [ + { + title: "Claim #", + dataIndex: "claimNumber", + key: "claimNumber", + render: (text: string) => {text}, + }, + { + title: "Owner", + dataIndex: "ownrName", + key: "ownrName", + }, + { + title: "Vehicle", + dataIndex: "vehicle", + key: "vehicle", + responsive: ["md" as const], + }, + { + title: "Scrubbed", + dataIndex: "createdAt", + key: "createdAt", + responsive: ["sm" as const], + render: (value: number) => { + const date = new Date(value); + return ( + + {date.toLocaleDateString()} + + {date.toLocaleTimeString()} + + + ); + }, + }, + { + title: "PDF", + key: "pdf", + width: 90, + render: (_: unknown, record: ScrubHistoryItem) => ( + + ), + }, + { + title: "Items", + key: "items", + width: 90, + align: "right" as const, + render: (_: unknown, record: ScrubHistoryItem) => ( + {record.results?.length ?? 0} + ), + }, + { + title: "Status", + key: "status", + width: 120, + render: () => ( + } + style={{ margin: 0 }} + > + Done + + ), + }, + { + title: "", + key: "actions", + width: 110, + render: (_: unknown, record: ScrubHistoryItem) => ( + deleteJob(record.id)} + > + + + ), + }, + ], + [deleteJob], + ); + + const resultColumns = useMemo( + () => [ + { + title: "Subcategory", + dataIndex: "subcategory", + key: "subcategory", + width: "20%", + }, + { title: "Item", dataIndex: "left", key: "left", width: "20%" }, + { title: "Description", dataIndex: "right", key: "right", width: "50%" }, + { + title: "Link", + dataIndex: "linktext", + key: "linktext", + render: (text: string | null, record: ScrubHistoryResultItem) => + record.linktext ? ( + + ) : null, + width: "10%", + }, + ], + [], + ); return (
@@ -161,7 +306,7 @@ const Home: FC = () => { {/* Header */} - Dashboard + {t("dashboard.labels.dashboard")} + + } styles={{ header: { @@ -267,12 +415,104 @@ const Home: FC = () => { {/* Table view for larger screens */}
{ + setCurrentPage(nextPage); + setPageSize(nextSize); + }, + }} scroll={{ x: 800 }} style={{ overflow: "auto" }} + expandable={{ + expandedRowRender: (record: ScrubHistoryItem) => { + const grouped = (record.results ?? []).reduce( + (acc, item) => { + const key = item.category ?? "Uncategorized"; + (acc[key] ??= []).push(item); + return acc; + }, + {} as Record, + ); + + const groups = Object.entries(grouped) + .map(([category, items]) => ({ category, items })) + .sort((a, b) => { + const aCfg = + categoryConfig[ + a.category as keyof typeof categoryConfig + ]; + const bCfg = + categoryConfig[ + b.category as keyof typeof categoryConfig + ]; + + const aPriority = + aCfg?.priority ?? Number.POSITIVE_INFINITY; + const bPriority = + bCfg?.priority ?? Number.POSITIVE_INFINITY; + + if (aPriority !== bPriority) return aPriority - bPriority; + return a.category.localeCompare(b.category); + }); + + return ( + + {groups.map(({ category, items }) => { + const cfg = + categoryConfig[ + category as keyof typeof categoryConfig + ]; + + return ( +
+ + + + {cfg?.icon ? `${cfg.icon} ` : ""} + {category} + + + {items.length} + + + + +
+ `${category}-${row.createdAt}-${idx}` + } + pagination={false} + size="small" + scroll={{ x: 1200 }} + /> + + ); + })} + + ); + }, + rowExpandable: (record: ScrubHistoryItem) => + (record.results?.length ?? 0) > 0, + }} /> diff --git a/src/renderer/src/main.tsx b/src/renderer/src/main.tsx index 6d18478..0ddb2db 100644 --- a/src/renderer/src/main.tsx +++ b/src/renderer/src/main.tsx @@ -10,6 +10,7 @@ declare global { interface Window { api: { isTest: () => boolean; + openExternal: (url: string) => Promise; }; } } diff --git a/src/util/ipcTypes.json b/src/util/ipcTypes.json index 5dc9f4b..993755f 100644 --- a/src/util/ipcTypes.json +++ b/src/util/ipcTypes.json @@ -2,6 +2,11 @@ "toMain": { "test": "toMain_test", "authStateChanged": "toMain_authStateChanged", + "scrubHistory": { + "getAll": "toMain_scrubHistory_getAll", + "deleteJob": "toMain_scrubHistory_deleteJob", + "clearAll": "toMain_scrubHistory_clearAll" + }, "debug": { "decodeEstimate": "toMain_debug_decodeEstimate" }, @@ -56,7 +61,8 @@ "polling": "toRenderer_watcher_polling" }, "scrub": { - "scrubError": "toRenderer_scrubError" + "scrubError": "toRenderer_scrubError", + "historyUpdated": "toRenderer_scrub_historyUpdated" }, "updates": { "checking": "toRenderer_updates_checking", diff --git a/src/util/translations/en-US/renderer.json b/src/util/translations/en-US/renderer.json index 94ef64b..1288474 100644 --- a/src/util/translations/en-US/renderer.json +++ b/src/util/translations/en-US/renderer.json @@ -10,6 +10,18 @@ "resetpassword": "Reset Password" } }, + "dashboard": { + "actions": { + "clear_all": "Clear All" + }, + "labels": { + "dashboard": "Dashboard", + "estimate_scrub_history": "Scrub History", + "last_processed": "Last Processed at", + "scrub_results": "Scrub Results", + "total_estimates": "Total Estimates" + } + }, "errors": { "errorboundary": "Uh oh - we've hit an error.", "notificationtitle": "Error Encountered" @@ -31,7 +43,9 @@ "labels": { "actions": "Actions", "addPaintScalePath": "Add Paint Scale Path", + "config": "Configuration", "emsOutFilePath": "EMS Out File Path (Parts Order, etc.)", + "esApiKey": "Estimate Scrubber API Key", "invalidPath": "Path not set or invalid", "paintScalePath": "Paint Scale Path", "paintScaleSettingsInput": "BSMS To Paint Scale", diff --git a/translations.babel b/translations.babel index fd071eb..309d542 100644 --- a/translations.babel +++ b/translations.babel @@ -1,4 +1,4 @@ - +