From 1c44e92fb028d51cbf7e94b3d40a819d7a640ec1 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Thu, 26 Feb 2026 12:31:24 -0800 Subject: [PATCH] Major clean up and alpha.1 release. --- README.md | 5 +- package.json | 3 +- serverless/serverless.yml | 8 + serverless/src/handlers/scrub.ts | 28 ++- src/main/db/scrub-history-db.ts | 25 ++- src/main/decoder/decoder.ts | 39 +++- .../estimate-scrubber/estimate-scrubber.ts | 68 ++++--- src/main/index.ts | 183 ++++++++---------- src/main/ipc/ipcMainConfig.ts | 52 +---- src/main/ipc/ipcMainHandler.settings.ts | 102 +--------- src/main/ipc/ipcMainHandler.user.ts | 102 ---------- src/main/setup-keep-alive-agent.ts | 77 -------- src/main/setup-keep-alive-task.ts | 52 ----- src/main/store/store.ts | 1 + src/main/util/checkForAppUpdates.ts | 11 +- src/main/watcher/watcher.ts | 25 +++ src/preload/index.ts | 12 +- src/renderer/src/components/Home/Home.tsx | 14 +- src/renderer/src/util/ipcRendererHandler.ts | 9 + src/util/newWindow.ts | 15 ++ 20 files changed, 285 insertions(+), 546 deletions(-) delete mode 100644 src/main/ipc/ipcMainHandler.user.ts delete mode 100644 src/main/setup-keep-alive-agent.ts delete mode 100644 src/main/setup-keep-alive-task.ts create mode 100644 src/util/newWindow.ts diff --git a/README.md b/README.md index c144c72..ca8602f 100644 --- a/README.md +++ b/README.md @@ -3,4 +3,7 @@ # Outstanding Todos * Update certificates and signing. * Create S3 upload buckets -* Create S3 EMS upload bucket. \ No newline at end of file +* Create S3 EMS upload bucket. + +testing api key +c0be5714-ff60-420e-a0af-aff440afdb61 \ No newline at end of file diff --git a/package.json b/package.json index d80815a..aca68e8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "esdp", - "version": "0.0.1", + "productName": "EMS Uploader", + "version": "0.0.1-alpha.1", "description": "EMS Uploader", "main": "./out/main/index.js", "author": "ImEX Systems Inc.", diff --git a/serverless/serverless.yml b/serverless/serverless.yml index 6bbf5af..11e20cb 100644 --- a/serverless/serverless.yml +++ b/serverless/serverless.yml @@ -12,6 +12,7 @@ stages: domain: es.imex.online es_user: Imex2 es_password: Patrick + hasura_url: https://es.db.imex.online/v1/graphql beta: # Enables observability in the prod stage observability: false @@ -22,6 +23,7 @@ stages: domain: beta.es.imex.online es_user: Imex2 es_password: Patrick + hasura_url: https://es.db.imex.online/v1/graphql alpha: # Enables observability in the prod stage observability: false @@ -31,6 +33,7 @@ stages: domain: alpha.es.imex.online es_user: Imex2 es_password: Patrick + hasura_url: https://es.db.imex.online/v1/graphql dev: # Enables observability in the prod stage observability: false @@ -40,6 +43,7 @@ stages: domain: dev.es.imex.online es_user: Imex2 es_password: Patrick + hasura_url: https://es.db.imex.online/v1/graphql # params: # dev: @@ -82,6 +86,10 @@ functions: ES_ENDPOINT: ${param:es_endpoint} ES_USER: ${param:es_user} ES_PASSWORD: ${param:es_password} + HASURA_URL: ${param:hasura_url} + # Resolve at deploy-time from AWS Secrets Manager (value is not stored in this file). + # This secret is expected to be JSON; we pull the `admin_secret` field. + HASURA_SECRET: '{{resolve:secretsmanager:arn:aws:secretsmanager:ca-central-1:714144183158:secret:esdp-hasura-credentials-s81i1u-BDFgPi:SecretString:admin_secret::}}' events: - httpApi: path: /scrub diff --git a/serverless/src/handlers/scrub.ts b/serverless/src/handlers/scrub.ts index 58d6eb6..e8f639d 100644 --- a/serverless/src/handlers/scrub.ts +++ b/serverless/src/handlers/scrub.ts @@ -10,8 +10,8 @@ import { UUID } from 'node:crypto'; const ES_USER = process.env.ES_USER || ''; const ES_PASSWORD = process.env.ES_PASSWORD || ''; const ES_ENDPOINT = process.env.ES_ENDPOINT || ''; -const HASURA_URL = process.env.HASURA_URL || ''; - +const HASURA_URL = process.env.HASURA_URL || 'https://db.es.imex.online/v1/graphql'; +const HASURA_SECRET = process.env.HASURA_SECRET || ''; interface ScrubRequest { esApiKey: string; rawJob: RawJobDataObject; @@ -29,7 +29,19 @@ export const handler = async (event: APIGatewayProxyEvent): Promise { + console.error('Failed to upload job to Hasura:', error); + }); // Transform the raw job object to ES format const estimate: ESJobObject = await transformJobForEstimateScrubber(rawJob); @@ -56,14 +68,14 @@ export const handler = async (event: APIGatewayProxyEvent): Promise => { const graphQLClient = new GraphQLClient(HASURA_URL, { headers: { - 'x-hasura-admin-secret': 'UXWqeUlNMc2dd2SD7DTOKgjEQlVkZkaW', + 'x-hasura-admin-secret': HASURA_SECRET, }, }); diff --git a/src/main/db/scrub-history-db.ts b/src/main/db/scrub-history-db.ts index 324e94f..2b174a6 100644 --- a/src/main/db/scrub-history-db.ts +++ b/src/main/db/scrub-history-db.ts @@ -12,6 +12,7 @@ export type ScrubHistoryJobRow = { vehicle: string; claim_number: string; pdf_url: string | null; + report_issue_url?: string | null; }; export type ScrubHistoryScrubResultRow = { @@ -99,7 +100,8 @@ function getDb(): Database.Database { ownr_name TEXT NOT NULL, vehicle TEXT NOT NULL, claim_number TEXT NOT NULL, - pdf_url TEXT + pdf_url TEXT, + report_issue_url TEXT ); CREATE TABLE IF NOT EXISTS scrub_results ( @@ -141,6 +143,7 @@ function buildJobRow( job: RawJobDataObject, createdAt: number, pdfUrl: string | null, + reportIssueUrl: string | null = null, ): ScrubHistoryJobRow { const ownrName = `${job.ownr_fn ?? ""} ${job.ownr_ln ?? ""}`.trim(); const vehicle = @@ -153,17 +156,24 @@ function buildJobRow( vehicle: vehicle || "Unknown", claim_number: claimNumber || "Unknown", pdf_url: pdfUrl, + report_issue_url: reportIssueUrl, }; } export function insertScrubRun(params: { job: RawJobDataObject; identifiedItems: unknown; - pdfUrl: string | null; + pdf_url: string | null; + report_issue_url?: string | null; }): { jobId: string; createdAt: number; insertedResultsCount: number } { const database = getDb(); const createdAt = Date.now(); - const jobRow = buildJobRow(params.job, createdAt, params.pdfUrl); + const jobRow = buildJobRow( + params.job, + createdAt, + params.pdf_url, + params.report_issue_url, + ); const items: IdentifiedItem[] = Array.isArray(params.identifiedItems) ? (params.identifiedItems as IdentifiedItem[]) @@ -174,7 +184,7 @@ export function insertScrubRun(params: { const insertTx = database.transaction(() => { database .prepare( - "INSERT INTO jobs (id, created_at, ownr_name, vehicle, claim_number, pdf_url) VALUES (?, ?, ?, ?, ?, ?)", + "INSERT INTO jobs (id, created_at, ownr_name, vehicle, claim_number, pdf_url, report_issue_url) VALUES (?, ?, ?, ?, ?, ?, ?)", ) .run( jobRow.id, @@ -183,6 +193,7 @@ export function insertScrubRun(params: { jobRow.vehicle, jobRow.claim_number, jobRow.pdf_url, + jobRow.report_issue_url, ); const stmt = database.prepare( @@ -212,7 +223,7 @@ export function getScrubHistory(): ScrubHistoryItem[] { const jobs = database .prepare( - "SELECT id, created_at, ownr_name, vehicle, claim_number, pdf_url FROM jobs ORDER BY created_at DESC", + "SELECT id, created_at, ownr_name, vehicle, claim_number, pdf_url, report_issue_url FROM jobs ORDER BY created_at DESC", ) .all() as ScrubHistoryJobRow[]; @@ -246,6 +257,7 @@ export function getScrubHistory(): ScrubHistoryItem[] { vehicle: j.vehicle, claimNumber: j.claim_number, pdfUrl: j.pdf_url ?? null, + reportIssueUrl: j.report_issue_url ?? null, results: resultsByJobId.get(j.id) ?? [], })); } @@ -301,7 +313,7 @@ export function getScrubHistoryPage(params: { const jobs = database .prepare( - "SELECT id, created_at, ownr_name, vehicle, claim_number, pdf_url FROM jobs ORDER BY created_at DESC LIMIT ? OFFSET ?", + "SELECT id, created_at, ownr_name, vehicle, claim_number, pdf_url, report_issue_url FROM jobs ORDER BY created_at DESC LIMIT ? OFFSET ?", ) .all(pageSize, offset) as ScrubHistoryJobRow[]; @@ -348,6 +360,7 @@ export function getScrubHistoryPage(params: { vehicle: j.vehicle, claimNumber: j.claim_number, pdfUrl: j.pdf_url ?? null, + reportIssueUrl: j.report_issue_url ?? null, results: resultsByJobId.get(j.id) ?? [], })), totalJobs, diff --git a/src/main/decoder/decoder.ts b/src/main/decoder/decoder.ts index 4ce07a7..6658da8 100644 --- a/src/main/decoder/decoder.ts +++ b/src/main/decoder/decoder.ts @@ -36,6 +36,8 @@ import { DecodedTtl } from "./decode-ttl.interface"; import DecodeVeh from "./decode-veh"; import { DecodedVeh } from "./decode-veh.interface"; import UploadEmsToS3 from "./emsbackup"; +import getMainWindow from "../../util/getMainWindow"; +import newWindow from "../../util/newWindow"; async function ImportJob(filepath: string): Promise { const parsedFilePath = path.parse(filepath); @@ -136,7 +138,7 @@ async function ImportJob(filepath: string): Promise { console.log("Available Job record to upload;", newAvailableJob); //Scrub the estimate - const scrubResults = await ScrubEstimate({ job: jobObject }); + const scrubPdfURL = await ScrubEstimate({ job: jobObject }); setAppProgressbar(0.95); const esApiKey = store.get("settings.esApiKey") as string; @@ -149,17 +151,42 @@ async function ImportJob(filepath: string): Promise { }); setAppProgressbar(-1); + const items = ["One", "Two", "Three"]; + const n = new Notification({ + title: "Choose an Action!", + actions: [ + { type: "button", text: "Action 1" }, + { type: "button", text: "Action 2" }, + { type: "selection", text: "Apply", items }, + ], + }); + 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" }], + actions: [ + { text: "View in App", type: "button" as const }, + ...(scrubPdfURL ? [{ text: "View PDF", type: "button" as const }] : []), + ], }); - uploadNotification.on("click", () => { - if (scrubResults?.data?.resultPDFUrl) { - shell.openExternal(scrubResults.data.resultPDFUrl); + + uploadNotification.on("action", (e) => { + // e.actionIndex + if (e.actionIndex === 0) { + const mainWindow = getMainWindow(); + mainWindow?.show(); + } else if (e.actionIndex === 1) { + if (scrubPdfURL) { + newWindow(scrubPdfURL); + } } }); + // uploadNotification.on("click", () => { + // if (scrubPdfURL) { + // shell.openExternal(scrubPdfURL); + // } + // }); + uploadNotification.show(); } catch (error) { log.error("Error encountered while decoding job. ", errorTypeCheck(error)); diff --git a/src/main/estimate-scrubber/estimate-scrubber.ts b/src/main/estimate-scrubber/estimate-scrubber.ts index fcb0475..a1b574e 100644 --- a/src/main/estimate-scrubber/estimate-scrubber.ts +++ b/src/main/estimate-scrubber/estimate-scrubber.ts @@ -8,6 +8,7 @@ import { RawJobDataObject } from "../decoder/decoder"; import store from "../store/store"; import ipcTypes from "../../util/ipcTypes.json"; import { insertScrubRun } from "../db/scrub-history-db"; +import { Notification } from "electron/main"; // Function to write job object to logs subfolder async function writeJobToLogsFolder(job, fileName): Promise { @@ -47,21 +48,24 @@ async function ScrubEstimate({ // No transformation here - send raw job to Lambda const currentChannel = autoUpdater.channel; let estimateScrubberUrl: string; - switch (currentChannel) { - case null: - case "dev": - estimateScrubberUrl = "https://dev.es.imex.online/scrub"; //dev specific URL. - break; - case "alpha": - estimateScrubberUrl = "https://alpha.es.imex.online/scrub"; //dev specific URL. - break; - case "beta": - estimateScrubberUrl = "https://beta.es.imex.online/scrub"; //Beta specific URL. - break; - case "latest": - default: - estimateScrubberUrl = "https://es.imex.online/scrub"; //Production route. - break; + if (process.env.NODE_ENV !== "production") { + estimateScrubberUrl = "https://dev.es.imex.online/scrub"; //dev specific URL. + } else { + switch (currentChannel) { + case "dev": + estimateScrubberUrl = "https://dev.es.imex.online/scrub"; //dev specific URL. + break; + case "alpha": + estimateScrubberUrl = "https://alpha.es.imex.online/scrub"; //dev specific URL. + break; + case "beta": + estimateScrubberUrl = "https://beta.es.imex.online/scrub"; //Beta specific URL. + break; + case "latest": + default: + estimateScrubberUrl = "https://es.imex.online/scrub"; //Production route. + break; + } } log.log(`Estimate Scrubber URL: [${currentChannel} |`, estimateScrubberUrl); @@ -98,13 +102,15 @@ async function ScrubEstimate({ }, ); - const { resultPDFUrl, identified_item } = result?.data ?? {}; + const { report_issue_url, identified_item, pdf_url } = 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, + pdf_url: typeof pdf_url === "string" ? pdf_url : null, + report_issue_url: + typeof report_issue_url === "string" ? report_issue_url : null, }); const mainWindow = BrowserWindow.getAllWindows()[0]; if (mainWindow && !mainWindow.isDestroyed()) { @@ -119,19 +125,11 @@ async function ScrubEstimate({ // b.webContents.send(ipcTypes.app.toRenderer.scrubResults, { // jobid: job.id, // items: result.data?.identified_item, - // pdfUrl: resultPDFUrl, - // reportIssueUrl, + // pdf_url: resultPDFUrl, + // report_issue_url, // }); - const pdfWindow = new BrowserWindow({ - webPreferences: { - plugins: true, // Enable PDF viewing - }, - }); - - pdfWindow.loadURL(resultPDFUrl); - pdfWindow.focus(); - return resultPDFUrl; + return pdf_url; } catch (error) { const err = error as AxiosError; log.error("Error while scrubbing estimate:", err.message, err.stack); @@ -150,13 +148,23 @@ async function ScrubEstimate({ if (status === 400) { mainWindow?.webContents.send(ipcTypes.toRenderer.scrub.scrubError, { message: - responseMessage || + JSON.parse(responseMessage).message || "Error encountered sending estimate to Estimate Scrubber.", }); } else if (status === 401) { + const notificationError = new Notification({ + title: "Error scrubbing estimate", + body: + JSON.parse(responseMessage).message || + "Authentication with Estimate Scrubber failed.", + }); + + notificationError.show(); + mainWindow?.webContents.send(ipcTypes.toRenderer.scrub.scrubError, { message: - responseMessage || "Authentication with Estimate Scrubber failed.", + JSON.parse(responseMessage).message || + "Authentication with Estimate Scrubber failed.", }); } return "Error: Unable to scrub estimate."; diff --git a/src/main/index.ts b/src/main/index.ts index beee400..1610920 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,4 +1,3 @@ -import "source-map-support/register"; import { is, optimizer, platform } from "@electron-toolkit/utils"; import Sentry from "@sentry/electron/main"; import { @@ -14,6 +13,7 @@ import { import log from "electron-log/main"; import { autoUpdater } from "electron-updater"; import path, { join } from "path"; +import "source-map-support/register"; import imexAppIcon from "../../resources/icon.png?asset"; import { @@ -24,19 +24,11 @@ import ipcTypes from "../util/ipcTypes.json"; import ImportJob from "./decoder/decoder"; import { dumpMemoryStatsToFile } from "../util/memUsage"; -import { - isKeepAliveAgentInstalled, - setupKeepAliveAgent, -} from "./setup-keep-alive-agent"; -import { - isKeepAliveTaskInstalled, - setupKeepAliveTask, -} from "./setup-keep-alive-task"; import store from "./store/store"; import { checkForAppUpdates } from "./util/checkForAppUpdates"; import ensureWindowOnScreen from "./util/ensureWindowOnScreen"; import { getMainWindow } from "./util/toRenderer"; -import { GetAllEnvFiles } from "./watcher/watcher"; +import { GetAllEnvFiles, GetLatestEnvFile } from "./watcher/watcher"; const appIconToUse = imexAppIcon; @@ -93,7 +85,7 @@ function createWindow(): void { icon: appIconToUse, } : {}), - title: "EMS Uploader", + title: app.name, webPreferences: { preload: join(__dirname, "../preload/index.js"), sandbox: false, @@ -109,8 +101,6 @@ function createWindow(): void { { label: app.name, submenu: [ - { role: "about" }, - { type: "separator" }, { role: "services" }, { type: "separator" }, { role: "hide" }, @@ -126,8 +116,6 @@ function createWindow(): void { { label: "File", submenu: [ - // @ts-ignore - ...(!isMac ? [{ role: "about" }] : []), // @ts-ignore isMac ? { role: "close" } : { role: "quit" }, ], @@ -193,36 +181,72 @@ function createWindow(): void { } }, }, + { + label: "Rescrub Last Estimate", + click: (): void => { + const latestFile = GetLatestEnvFile(); + if (latestFile) { + ImportJob(latestFile); + } + }, + }, { label: `Check for Updates (${app.getVersion()})`, click: (): void => { checkForAppUpdates(); }, }, + { label: "Development", id: "development", visible: import.meta.env.DEV, submenu: [ + // { + // label: "Connect to Test", + // checked: store.get("app.isTest") as boolean, + // type: "checkbox", + // id: "toggleTest", + // click: (): void => { + // const currentSetting = store.get("app.isTest") as boolean; + // store.set("app.isTest", !currentSetting); + // log.info("Setting isTest to: ", !currentSetting); + // app.relaunch(); // Relaunch the app + // preQuitMethods(); //Quitting handlers aren't called. Manually execute to clean up the app. + // app.exit(0); // Exit the current instance + // }, + // }, + // { + // label: "Check for updates", + // click: (): void => { + // checkForAppUpdates(); + // }, + // }, { - label: "Connect to Test", - checked: store.get("app.isTest") as boolean, - type: "checkbox", - id: "toggleTest", - click: (): void => { - const currentSetting = store.get("app.isTest") as boolean; - store.set("app.isTest", !currentSetting); - log.info("Setting isTest to: ", !currentSetting); - app.relaunch(); // Relaunch the app - preQuitMethods(); //Quitting handlers aren't called. Manually execute to clean up the app. - app.exit(0); // Exit the current instance - }, - }, - { - label: "Check for updates", - click: (): void => { - checkForAppUpdates(); - }, + label: `Release Path`, + submenu: [ + { + label: "Alpha", + checked: store.get("app.channel") === "alpha", + onClick: (): void => { + checkForAppUpdates("alpha"); + }, + }, + { + label: "Beta", + checked: store.get("app.channel") === "beta", + onClick: (): void => { + checkForAppUpdates("beta"); + }, + }, + { + label: "Latest", + checked: store.get("app.channel") === "latest", + onClick: (): void => { + checkForAppUpdates("latest"); + }, + }, + ], }, { label: "Open Log File", @@ -293,34 +317,35 @@ function createWindow(): void { // ImportJob(`C:\\EMS\\CCC\\9ee762f4.ENV`); // }, // }, + // { + // label: "Install Keep Alive", + // enabled: true, // Default to enabled, update dynamically + // click: async (): Promise => { + // try { + // if (platform.isWindows) { + // log.debug("Creating Windows keep-alive task"); + // await setupKeepAliveTask(); + // log.info("Successfully installed Windows keep-alive task"); + // } else if (platform.isMacOS) { + // log.debug("Creating macOS keep-alive agent"); + // await setupKeepAliveAgent(); + // log.info("Successfully installed macOS keep-alive agent"); + // } + // // Wait to ensure task/agent is registered + // await new Promise((resolve) => setTimeout(resolve, 1500)); + // // Rebuild menu and update enabled state + // await updateKeepAliveMenuItem(); + // } catch (error) { + // log.error( + // `Failed to install keep-alive: ${error instanceof Error ? error.message : String(error)}`, + // ); + // // Optionally notify user (e.g., via dialog or log) + // } + // }, + // }, + { - label: "Install Keep Alive", - enabled: true, // Default to enabled, update dynamically - click: async (): Promise => { - try { - if (platform.isWindows) { - log.debug("Creating Windows keep-alive task"); - await setupKeepAliveTask(); - log.info("Successfully installed Windows keep-alive task"); - } else if (platform.isMacOS) { - log.debug("Creating macOS keep-alive agent"); - await setupKeepAliveAgent(); - log.info("Successfully installed macOS keep-alive agent"); - } - // Wait to ensure task/agent is registered - await new Promise((resolve) => setTimeout(resolve, 1500)); - // Rebuild menu and update enabled state - await updateKeepAliveMenuItem(); - } catch (error) { - log.error( - `Failed to install keep-alive: ${error instanceof Error ? error.message : String(error)}`, - ); - // Optionally notify user (e.g., via dialog or log) - } - }, - }, - { - label: "Add All Estimates in watched directories", + label: "Scrub all estimates in watched directories", click: (): void => { GetAllEnvFiles().forEach((file) => ImportJob(file)); }, @@ -348,45 +373,8 @@ function createWindow(): void { }, ]; - // Dynamically update Install Keep Alive enabled state - const updateKeepAliveMenuItem = async (): Promise => { - try { - const isInstalled = platform.isWindows - ? await isKeepAliveTaskInstalled() - : platform.isMacOS - ? await isKeepAliveAgentInstalled() - : false; - const developmentMenu = template - .find((item) => item.label === "Application") - // @ts-ignore - ?.submenu?.find((item: { id: string }) => item.id === "development") - ?.submenu as Electron.MenuItemConstructorOptions[]; - const keepAliveItem = developmentMenu?.find( - (item) => item.label === "Install Keep Alive", - ); - if (keepAliveItem) { - keepAliveItem.enabled = !isInstalled; // Enable if not installed, disable if installed - const menu = Menu.buildFromTemplate(template); - Menu.setApplicationMenu(menu); - log.debug( - `Updated Install Keep Alive menu item: enabled=${keepAliveItem.enabled}`, - ); - } - } catch (error) { - log.error( - `Error updating Keep Alive menu item: ${error instanceof Error ? error.message : String(error)}`, - ); - } - }; - const menu: Electron.Menu = Menu.buildFromTemplate(template); Menu.setApplicationMenu(menu); - // Update menu item enabled state on app start - updateKeepAliveMenuItem().catch((error) => { - log.error( - `Error updating Keep Alive menu item: ${error instanceof Error ? error.message : String(error)}`, - ); - }); // Register a global shortcut to show the hidden item globalShortcut.register("CommandOrControl+Shift+T", () => { @@ -535,6 +523,7 @@ app.whenReady().then(async () => { //Check for app updates. autoUpdater.logger = log; autoUpdater.allowDowngrade = true; + autoUpdater.channel = store.get("app.channel") as string; // Set the channel from the persisted store value // if (import.meta.env.DEV) { // // Useful for some dev/debugging tasks, but download can // // not be validated because dev app is not signed diff --git a/src/main/ipc/ipcMainConfig.ts b/src/main/ipc/ipcMainConfig.ts index 48eb3fd..9267534 100644 --- a/src/main/ipc/ipcMainConfig.ts +++ b/src/main/ipc/ipcMainConfig.ts @@ -6,6 +6,11 @@ import ipcTypes from "../../util/ipcTypes.json"; import ImportJob from "../decoder/decoder"; import store from "../store/store"; import { StartWatcher, StopWatcher } from "../watcher/watcher"; +import { + ScrubHistoryClearAll, + ScrubHistoryDeleteJob, + ScrubHistoryGetAll, +} from "./ipcMainHandler.scrubHistory"; import { getSetting, setSetting, @@ -19,15 +24,6 @@ import { SettingsWatcherPollingGet, SettingsWatcherPollingSet, } from "./ipcMainHandler.settings"; -import { - ipcMainHandleAuthStateChanged, - ipMainHandleResetPassword, -} from "./ipcMainHandler.user"; -import { - ScrubHistoryClearAll, - ScrubHistoryDeleteJob, - ScrubHistoryGetAll, -} from "./ipcMainHandler.scrubHistory"; // Log all IPC messages and their payloads const logIpcMessages = (): void => { @@ -57,34 +53,11 @@ ipcMain.on(ipcTypes.toMain.test, () => console.log("** Verify that ipcMain is loaded and working."), ); -// Auth handler -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"); - - ipcMain.on(ipcTypes.toMain.debug.decodeEstimate, async (): Promise => { - const relativeEmsFilepath = `_reference/ems/MPI_1/3698420.ENV`; - const rootDir = app.getAppPath(); - const absoluteFilepath = path.join(rootDir, relativeEmsFilepath); - - log.debug("[IPC Debug Function] Decode test Estimate", absoluteFilepath); - await ImportJob(absoluteFilepath); - - const job2 = `/Users/pfic/Downloads/12285264/2285264.ENV`; - const job3 = `/Users/pfic/Downloads/14033376/4033376.ENV`; - await ImportJob(job2); - await ImportJob(job3); - }); -} - // Settings Handlers ipcMain.handle( ipcTypes.toMain.settings.filepaths.get, @@ -109,21 +82,6 @@ ipcMain.handle( SettingsWatcherPollingSet, ); -ipcMain.handle(ipcTypes.toMain.settings.getPpcFilePath, SettingsPpcFilePathGet); -ipcMain.handle(ipcTypes.toMain.settings.setPpcFilePath, SettingsPpcFilePathSet); -ipcMain.handle( - ipcTypes.toMain.settings.getEmsOutFilePath, - SettingEmsOutFilePathGet, -); -ipcMain.handle( - ipcTypes.toMain.settings.setEmsOutFilePath, - SettingEmsOutFilePathSet, -); - -ipcMain.handle(ipcTypes.toMain.user.getActiveShop, () => { - return store.get("app.bodyshop.shopname"); -}); - // Watcher Handlers ipcMain.on(ipcTypes.toMain.watcher.start, () => { StartWatcher().catch((error) => { diff --git a/src/main/ipc/ipcMainHandler.settings.ts b/src/main/ipc/ipcMainHandler.settings.ts index 40a0073..b9921de 100644 --- a/src/main/ipc/ipcMainHandler.settings.ts +++ b/src/main/ipc/ipcMainHandler.settings.ts @@ -93,110 +93,12 @@ const SettingsWatcherPollingSet = async ( return { enabled, interval }; }; -const SettingsPpcFilePathGet = async (): Promise => { - return Store.get("settings.ppcFilePath"); -}; - -const SettingsPpcFilePathSet = async (): Promise => { - const mainWindow = getMainWindow(); - if (!mainWindow) { - log.error("No main window found when trying to open dialog"); - return ""; - } - const result = await dialog.showOpenDialog(mainWindow, { - properties: ["openDirectory"], - }); - - if (!result.canceled) { - Store.set("settings.ppcFilePath", result.filePaths[0]); - } - - return (Store.get("settings.ppcFilePath") as string) || ""; -}; - -const SettingEmsOutFilePathGet = async (): Promise => { - return Store.get("settings.emsOutFilePath"); -}; - -const SettingEmsOutFilePathSet = async (): Promise => { - const mainWindow = getMainWindow(); - if (!mainWindow) { - log.error("No main window found when trying to open dialog"); - return ""; - } - const result = await dialog.showOpenDialog(mainWindow, { - properties: ["openDirectory"], - }); - - if (!result.canceled) { - Store.set("settings.emsOutFilePath", result.filePaths[0]); - } - - return (Store.get("settings.emsOutFilePath") as string) || ""; -}; - -const SettingsPaintScaleInputPathSet = async ( - _event: IpcMainInvokeEvent, -): Promise => { - try { - const mainWindow = getMainWindow(); - if (!mainWindow) { - log.error("No main window found when trying to open dialog"); - return null; - } - const result = await dialog.showOpenDialog(mainWindow, { - properties: ["openDirectory"], - }); - if (result.canceled) { - log.debug("Paint scale input path selection canceled"); - return null; - } - const path = result.filePaths[0]; - log.debug("Selected paint scale input path:", path); - return path; - } catch (error) { - log.error("Error setting paint scale input path:", error); - throw error; - } -}; - -const SettingsPaintScaleOutputPathSet = async ( - _event: IpcMainInvokeEvent, -): Promise => { - try { - const mainWindow = getMainWindow(); - if (!mainWindow) { - log.error("No main window found when trying to open dialog"); - return null; - } - const result = await dialog.showOpenDialog(mainWindow, { - properties: ["openDirectory"], - }); - if (result.canceled) { - log.debug("Paint scale output path selection canceled"); - return null; - } - const path = result.filePaths[0]; - log.debug("Selected paint scale output path:", path); - return path; - } catch (error) { - log.error("Error setting paint scale output path:", error); - throw error; - } -}; - export { - SettingsPpcFilePathGet, - SettingsPpcFilePathSet, + getSetting, + setSetting, SettingsWatchedFilePathsAdd, SettingsWatchedFilePathsGet, SettingsWatchedFilePathsRemove, SettingsWatcherPollingGet, SettingsWatcherPollingSet, - SettingEmsOutFilePathGet, - SettingEmsOutFilePathSet, - SettingsPaintScaleInputPathSet, - SettingsPaintScaleOutputPathSet, - getSetting, - setSetting, }; diff --git a/src/main/ipc/ipcMainHandler.user.ts b/src/main/ipc/ipcMainHandler.user.ts deleted file mode 100644 index d0562c4..0000000 --- a/src/main/ipc/ipcMainHandler.user.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { IpcMainEvent, shell } from "electron"; -import log from "electron-log/main"; -import { autoUpdater } from "electron-updater"; -import { User } from "firebase/auth"; -import errorTypeCheck from "../../util/errorTypeCheck"; -import ipcTypes from "../../util/ipcTypes.json"; -import client from "../graphql/graphql-client"; -import { - ActiveBodyshopQueryResult, - MasterdataQueryResult, - QUERY_ACTIVE_BODYSHOP_TYPED, - QUERY_MASTERDATA_TYPED, -} from "../graphql/queries"; -import { default as Store, default as store } from "../store/store"; -import { checkForAppUpdatesContinuously } from "../util/checkForAppUpdates"; -import { getMainWindow, sendIpcToRenderer } from "../util/toRenderer"; - -const ipcMainHandleAuthStateChanged = async ( - _event: IpcMainEvent, - user: User | null, -): Promise => { - Store.set("user", user); - log.debug("Received authentication state change from Renderer.", user); - await setReleaseChannel(); - checkForAppUpdatesContinuously(); -}; - -async function setReleaseChannel(): Promise { - try { - //Need to query the currently active shop, and store the metadata as well. - //Also need to query the OP Codes for decoding reference. - await handleShopMetaDataFetch(); - //Check for updates - const bodyshop = Store.get("app.bodyshop"); - if (bodyshop?.convenient_company?.toLowerCase() === "alpha") { - autoUpdater.channel = "alpha"; - log.debug("Setting update channel to ALPHA channel."); - } else if (bodyshop?.convenient_company?.toLowerCase() === "beta") { - autoUpdater.channel = "beta"; - log.debug("Setting update channel to BETA channel."); - } else { - log.debug("Setting update channel to LATEST channel."); - } - } catch (error) { - log.error( - "Error while querying active bodyshop or master data", - errorTypeCheck(error), - ); - sendIpcToRenderer( - ipcTypes.toRenderer.general.showErrorMessage, - "Error connecting to ImEX Online servers to get shop data. Please try again.", - ); - } -} - -const handleShopMetaDataFetch = async ( - reloadWindow?: boolean, -): Promise => { - try { - log.debug("Requery shop information & master data."); - const activeBodyshop: ActiveBodyshopQueryResult = await client.request( - QUERY_ACTIVE_BODYSHOP_TYPED, - ); - - Store.set("app.bodyshop", activeBodyshop.bodyshops[0]); - - const OpCodes: MasterdataQueryResult = await client.request( - QUERY_MASTERDATA_TYPED, - { - key: `${activeBodyshop.bodyshops[0].region_config}_ciecaopcodes`, - }, - ); - Store.set( - "app.masterdata.opcodes", - JSON.parse(OpCodes.masterdata[0]?.value), - ); - if (reloadWindow) { - const mainWindow = getMainWindow(); - if (mainWindow) { - mainWindow.reload(); - } - } - } catch (error) { - log.error("Error while fetching shop metadata", errorTypeCheck(error)); - throw error; - } -}; - -const ipMainHandleResetPassword = async (): Promise => { - shell.openExternal( - store.get("app.isTest") - ? `${import.meta.env.VITE_FE_URL_TEST}/resetpassword` - : `${import.meta.env.VITE_FE_URL}/resetpassword`, - ); -}; - -export { - handleShopMetaDataFetch, - ipcMainHandleAuthStateChanged, - ipMainHandleResetPassword, - setReleaseChannel, -}; diff --git a/src/main/setup-keep-alive-agent.ts b/src/main/setup-keep-alive-agent.ts deleted file mode 100644 index ea71cc0..0000000 --- a/src/main/setup-keep-alive-agent.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { promises as fs } from "fs"; -import { join } from "path"; -import { homedir } from "os"; -import { exec } from "child_process"; -import { promisify } from "util"; -import log from "electron-log/main"; - -const execPromise = promisify(exec); - -// Define the interval as a variable (in seconds) -const KEEP_ALIVE_INTERVAL_SECONDS = 15 * 60; // 15 minutes - -export async function setupKeepAliveAgent(): Promise { - const plistContent = ` - - - - Label - com.imex.esdp.keepalive - ProgramArguments - - Shop Partner Keep Alive - imexmedia://keep-alive - - RunAtLoad - - StartInterval - ${KEEP_ALIVE_INTERVAL_SECONDS} - -`; - - const plistPath = join( - homedir(), - "/Library/LaunchAgents/com.imex.esdp.keepalive.plist", - ); - - try { - await fs.writeFile(plistPath, plistContent); - const { stdout, stderr } = await execPromise(`launchctl load ${plistPath}`); - log.info(`Launch agent created and loaded: ${stdout}`); - if (stderr) log.warn(`Launch agent stderr: ${stderr}`); - } catch (error) { - log.error( - `Error setting up launch agent: ${error instanceof Error ? error.message : String(error)}`, - ); - throw error; // Rethrow to allow caller to handle - } -} - -export async function isKeepAliveAgentInstalled(): Promise { - const plistPath = join( - homedir(), - "/Library/LaunchAgents/com.imex.esdp.keepalive.plist", - ); - const maxRetries = 3; - const retryDelay = 500; // 500ms delay between retries - - for (let attempt = 1; attempt <= maxRetries; attempt++) { - try { - await fs.access(plistPath, fs.constants.F_OK); - const { stdout } = await execPromise( - `launchctl list | grep com.imex.esdp.keepalive`, - ); - return !!stdout; // Return true if plist exists and agent is loaded - } catch (error) { - log.debug( - `Launch agent not found (attempt ${attempt}/${maxRetries}): ${error instanceof Error ? error.message : String(error)}`, - ); - if (attempt === maxRetries) { - return false; // Return false after all retries fail - } - // Wait before retrying - await new Promise((resolve) => setTimeout(resolve, retryDelay)); - } - } - return false; // Fallback return -} diff --git a/src/main/setup-keep-alive-task.ts b/src/main/setup-keep-alive-task.ts deleted file mode 100644 index 6e16f74..0000000 --- a/src/main/setup-keep-alive-task.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { exec } from "child_process"; -import { promisify } from "util"; -import log from "electron-log/main"; - -const execPromise = promisify(exec); - -// Define the interval as a variable (in minutes) -const KEEP_ALIVE_INTERVAL_MINUTES = 15; -const taskName = "ShopPartnerKeepAlive"; - -export async function setupKeepAliveTask(): Promise { - const protocolUrl = "imexmedia://keep-alive"; - // Use rundll32.exe to silently open the URL as a protocol - const command = `rundll32.exe url.dll,OpenURL "${protocolUrl}"`; - // Escape quotes for schtasks /tr parameter - const escapedCommand = command.replace(/"/g, '\\"'); - - const schtasksCommand = `schtasks /create /tn "${taskName}" /tr "${escapedCommand}" /sc minute /mo ${KEEP_ALIVE_INTERVAL_MINUTES} /f`; - - try { - const { stdout, stderr } = await execPromise(schtasksCommand); - log.info(`Scheduled task created: ${stdout}`); - if (stderr) log.warn(`Scheduled task stderr: ${stderr}`); - } catch (error) { - log.error( - `Error creating scheduled task: ${error instanceof Error ? error.message : String(error)}`, - ); - throw error; // Rethrow to allow caller to handle - } -} - -export async function isKeepAliveTaskInstalled(): Promise { - const maxRetries = 3; - const retryDelay = 500; // 500ms delay between retries - - for (let attempt = 1; attempt <= maxRetries; attempt++) { - try { - const { stdout } = await execPromise(`schtasks /query /tn "${taskName}"`); - return !!stdout; // Return true if task exists - } catch (error) { - log.debug( - `Scheduled task ${taskName} not found (attempt ${attempt}/${maxRetries}): ${error instanceof Error ? error.message : String(error)}`, - ); - if (attempt === maxRetries) { - return false; // Return false after all retries fail - } - // Wait before retrying - await new Promise((resolve) => setTimeout(resolve, retryDelay)); - } - } - return false; // Fallback return -} diff --git a/src/main/store/store.ts b/src/main/store/store.ts index 8d86c1d..fbfa18d 100644 --- a/src/main/store/store.ts +++ b/src/main/store/store.ts @@ -19,6 +19,7 @@ const store = new Store({ x: undefined, y: undefined, }, + channel: "latest", user: null, isTest: false, bodyshop: {}, diff --git a/src/main/util/checkForAppUpdates.ts b/src/main/util/checkForAppUpdates.ts index ea5e3c4..47e1da7 100644 --- a/src/main/util/checkForAppUpdates.ts +++ b/src/main/util/checkForAppUpdates.ts @@ -1,5 +1,5 @@ import { autoUpdater } from "electron-updater"; -import { setReleaseChannel } from "../ipc/ipcMainHandler.user"; +import store from "../store/store"; let continuousUpdatesTriggered = false; @@ -15,8 +15,13 @@ async function checkForAppUpdatesContinuously(): Promise { ); } } -async function checkForAppUpdates(): Promise { - await setReleaseChannel(); +async function checkForAppUpdates(channel?: string | null): Promise { + if (channel) { + autoUpdater.channel = channel; + //Persist to store + store.set("app.channel", channel); + } + autoUpdater.checkForUpdates(); } diff --git a/src/main/watcher/watcher.ts b/src/main/watcher/watcher.ts index cd3bf11..cad711c 100644 --- a/src/main/watcher/watcher.ts +++ b/src/main/watcher/watcher.ts @@ -159,10 +159,35 @@ function GetAllEnvFiles(): string[] { }); return files; } +function GetLatestEnvFile(): string | null { + const directories = store.get("settings.filepaths") as string[]; + let latestFile: string | null = null; + let latestMTime = 0; + directories.forEach((directory) => { + try { + const envFiles = fs + .readdirSync(directory) + .filter((file: string) => file.toLowerCase().endsWith(".env")); + envFiles.forEach((file) => { + const fullPath = path.join(directory, file); + const stats = fs.statSync(fullPath); + if (stats.mtimeMs > latestMTime) { + latestMTime = stats.mtimeMs; + latestFile = fullPath; + } + }); + } catch (error) { + log.error(`Failed to read directory ${directory}:`, error); + throw error; + } + }); + return latestFile; +} export { addWatcherPath, GetAllEnvFiles, + GetLatestEnvFile, removeWatcherPath, StartWatcher, StopWatcher, diff --git a/src/preload/index.ts b/src/preload/index.ts index b9f3df3..234da7e 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -1,7 +1,8 @@ -import { BrowserWindow, contextBridge, shell } from "electron"; import { electronAPI } from "@electron-toolkit/preload"; +import { contextBridge } from "electron"; import "electron-log/preload"; import store from "../main/store/store"; +import newWindow from "../util/newWindow"; // Custom APIs for renderer interface Api { @@ -12,14 +13,7 @@ interface Api { 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(); + newWindow(url); }, }; diff --git a/src/renderer/src/components/Home/Home.tsx b/src/renderer/src/components/Home/Home.tsx index 6b83d33..b824fa2 100644 --- a/src/renderer/src/components/Home/Home.tsx +++ b/src/renderer/src/components/Home/Home.tsx @@ -322,7 +322,7 @@ const Home: FC = () => { { } value={totalJobs} prefix={} - valueStyle={{ color: "#fff", fontWeight: 600 }} + styles={{ content: { color: "#fff", fontWeight: 600 } }} /> { } value={totalItemsScrubbed} prefix={} - valueStyle={{ color: "#fff", fontWeight: 600 }} + styles={{ content: { color: "#fff", fontWeight: 600 } }} /> { : "—" } prefix={} - valueStyle={{ color: "#fff", fontWeight: 600 }} + styles={{ content: { color: "#fff", fontWeight: 600 } }} /> @@ -385,7 +385,7 @@ const Home: FC = () => { {/* Recent Estimates Card */} diff --git a/src/renderer/src/util/ipcRendererHandler.ts b/src/renderer/src/util/ipcRendererHandler.ts index 8182169..014aa32 100644 --- a/src/renderer/src/util/ipcRendererHandler.ts +++ b/src/renderer/src/util/ipcRendererHandler.ts @@ -95,3 +95,12 @@ ipcRenderer.on( }); }, ); +ipcRenderer.on( + ipcTypes.toRenderer.scrub.scrubError, + (_event: Electron.IpcRendererEvent, { message }) => { + notification.error({ + message: i18n.t("errors.notificationtitle"), + description: message, + }); + }, +); diff --git a/src/util/newWindow.ts b/src/util/newWindow.ts new file mode 100644 index 0000000..da9de74 --- /dev/null +++ b/src/util/newWindow.ts @@ -0,0 +1,15 @@ +import { app, BrowserWindow } from "electron"; + +async function newWindow(url: string): Promise { + const pdfWindow = new BrowserWindow({ + title: app.name, + webPreferences: { + plugins: true, // Enable PDF viewing + }, + }); + + pdfWindow.loadURL(url); + pdfWindow.focus(); +} + +export default newWindow;