Major clean up and alpha.1 release.
This commit is contained in:
@@ -3,4 +3,7 @@
|
||||
# Outstanding Todos
|
||||
* Update certificates and signing.
|
||||
* Create S3 upload buckets
|
||||
* Create S3 EMS upload bucket.
|
||||
* Create S3 EMS upload bucket.
|
||||
|
||||
testing api key
|
||||
c0be5714-ff60-420e-a0af-aff440afdb61
|
||||
@@ -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.",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<APIGatewayPr
|
||||
rawJob,
|
||||
} = JSON.parse(event.body || '{}') as ScrubRequest;
|
||||
const esApiKey = event.headers['x-api-key'] || '';
|
||||
//await uploadJobToHasura(rawJob, esApiKey);
|
||||
|
||||
if (!esApiKey) {
|
||||
return {
|
||||
statusCode: 401,
|
||||
body: JSON.stringify({
|
||||
message: 'Access invalid.',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
uploadJobToHasura(rawJob, esApiKey).catch((error) => {
|
||||
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<APIGatewayPr
|
||||
},
|
||||
});
|
||||
|
||||
const resultPDFUrl = result?.data?.report_link;
|
||||
const reportIssueUrl = `https://insurtechtoolkit.com/pcontactUs.aspx?apiKey=${esApiKey}&file=${fileName}.json`;
|
||||
const pdf_url = result?.data?.report_link;
|
||||
const report_issue_url = `https://insurtechtoolkit.com/pcontactUs.aspx?apiKey=${esApiKey}&file=${fileName}.json`;
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: JSON.stringify({
|
||||
resultPDFUrl,
|
||||
reportIssueUrl,
|
||||
pdf_url,
|
||||
report_issue_url,
|
||||
identified_item: result.data?.identified_item,
|
||||
}),
|
||||
};
|
||||
@@ -83,7 +95,7 @@ export const handler = async (event: APIGatewayProxyEvent): Promise<APIGatewayPr
|
||||
const uploadJobToHasura = async (rawJob: RawJobDataObject, esApiKey: string): Promise<void> => {
|
||||
const graphQLClient = new GraphQLClient(HASURA_URL, {
|
||||
headers: {
|
||||
'x-hasura-admin-secret': 'UXWqeUlNMc2dd2SD7DTOKgjEQlVkZkaW',
|
||||
'x-hasura-admin-secret': HASURA_SECRET,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<void> {
|
||||
const parsedFilePath = path.parse(filepath);
|
||||
@@ -136,7 +138,7 @@ async function ImportJob(filepath: string): Promise<void> {
|
||||
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<void> {
|
||||
});
|
||||
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));
|
||||
|
||||
@@ -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<string> {
|
||||
@@ -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.";
|
||||
|
||||
@@ -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<void> => {
|
||||
// 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<void> => {
|
||||
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<void> => {
|
||||
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
|
||||
|
||||
@@ -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<void> => {
|
||||
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) => {
|
||||
|
||||
@@ -93,110 +93,12 @@ const SettingsWatcherPollingSet = async (
|
||||
return { enabled, interval };
|
||||
};
|
||||
|
||||
const SettingsPpcFilePathGet = async (): Promise<string> => {
|
||||
return Store.get("settings.ppcFilePath");
|
||||
};
|
||||
|
||||
const SettingsPpcFilePathSet = async (): Promise<string> => {
|
||||
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<string> => {
|
||||
return Store.get("settings.emsOutFilePath");
|
||||
};
|
||||
|
||||
const SettingEmsOutFilePathSet = async (): Promise<string> => {
|
||||
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<string | null> => {
|
||||
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<string | null> => {
|
||||
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,
|
||||
};
|
||||
|
||||
@@ -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<void> => {
|
||||
Store.set("user", user);
|
||||
log.debug("Received authentication state change from Renderer.", user);
|
||||
await setReleaseChannel();
|
||||
checkForAppUpdatesContinuously();
|
||||
};
|
||||
|
||||
async function setReleaseChannel(): Promise<void> {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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,
|
||||
};
|
||||
@@ -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<void> {
|
||||
const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.imex.esdp.keepalive</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>Shop Partner Keep Alive</string>
|
||||
<string>imexmedia://keep-alive</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>StartInterval</key>
|
||||
<integer>${KEEP_ALIVE_INTERVAL_SECONDS}</integer>
|
||||
</dict>
|
||||
</plist>`;
|
||||
|
||||
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<boolean> {
|
||||
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
|
||||
}
|
||||
@@ -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<void> {
|
||||
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<boolean> {
|
||||
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
|
||||
}
|
||||
@@ -19,6 +19,7 @@ const store = new Store({
|
||||
x: undefined,
|
||||
y: undefined,
|
||||
},
|
||||
channel: "latest",
|
||||
user: null,
|
||||
isTest: false,
|
||||
bodyshop: {},
|
||||
|
||||
@@ -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<void> {
|
||||
);
|
||||
}
|
||||
}
|
||||
async function checkForAppUpdates(): Promise<void> {
|
||||
await setReleaseChannel();
|
||||
async function checkForAppUpdates(channel?: string | null): Promise<void> {
|
||||
if (channel) {
|
||||
autoUpdater.channel = channel;
|
||||
//Persist to store
|
||||
store.set("app.channel", channel);
|
||||
}
|
||||
|
||||
autoUpdater.checkForUpdates();
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<void> => {
|
||||
const pdfWindow = new BrowserWindow({
|
||||
webPreferences: {
|
||||
plugins: true, // Enable PDF viewing
|
||||
},
|
||||
});
|
||||
|
||||
pdfWindow.loadURL(url);
|
||||
pdfWindow.focus();
|
||||
newWindow(url);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -322,7 +322,7 @@ const Home: FC = () => {
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col xs={24} sm={12} lg={8}>
|
||||
<Card
|
||||
bordered={false}
|
||||
variant="borderless"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${token.colorPrimary} 0%, ${token.colorPrimaryHover} 100%)`,
|
||||
}}
|
||||
@@ -335,13 +335,13 @@ const Home: FC = () => {
|
||||
}
|
||||
value={totalJobs}
|
||||
prefix={<FileTextOutlined style={{ color: "#fff" }} />}
|
||||
valueStyle={{ color: "#fff", fontWeight: 600 }}
|
||||
styles={{ content: { color: "#fff", fontWeight: 600 } }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} lg={8}>
|
||||
<Card
|
||||
bordered={false}
|
||||
variant="borderless"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, #52c41a 0%, #73d13d 100%)`,
|
||||
}}
|
||||
@@ -354,13 +354,13 @@ const Home: FC = () => {
|
||||
}
|
||||
value={totalItemsScrubbed}
|
||||
prefix={<CheckCircleOutlined style={{ color: "#fff" }} />}
|
||||
valueStyle={{ color: "#fff", fontWeight: 600 }}
|
||||
styles={{ content: { color: "#fff", fontWeight: 600 } }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} lg={8}>
|
||||
<Card
|
||||
bordered={false}
|
||||
variant="borderless"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, #722ed1 0%, #9254de 100%)`,
|
||||
}}
|
||||
@@ -377,7 +377,7 @@ const Home: FC = () => {
|
||||
: "—"
|
||||
}
|
||||
prefix={<ClockCircleOutlined style={{ color: "#fff" }} />}
|
||||
valueStyle={{ color: "#fff", fontWeight: 600 }}
|
||||
styles={{ content: { color: "#fff", fontWeight: 600 } }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
@@ -385,7 +385,7 @@ const Home: FC = () => {
|
||||
|
||||
{/* Recent Estimates Card */}
|
||||
<Card
|
||||
bordered={false}
|
||||
variant="borderless"
|
||||
title={
|
||||
<Flex align="center" gap="small">
|
||||
<DatabaseOutlined style={{ fontSize: "20px" }} />
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
15
src/util/newWindow.ts
Normal file
15
src/util/newWindow.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { app, BrowserWindow } from "electron";
|
||||
|
||||
async function newWindow(url: string): Promise<void> {
|
||||
const pdfWindow = new BrowserWindow({
|
||||
title: app.name,
|
||||
webPreferences: {
|
||||
plugins: true, // Enable PDF viewing
|
||||
},
|
||||
});
|
||||
|
||||
pdfWindow.loadURL(url);
|
||||
pdfWindow.focus();
|
||||
}
|
||||
|
||||
export default newWindow;
|
||||
Reference in New Issue
Block a user