Compare commits
41 Commits
feature/IO
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e766337e9c | ||
|
|
07a4427a0b | ||
|
|
680ae4ca08 | ||
|
|
e6e1785413 | ||
|
|
bf1e137c6c | ||
|
|
6674f33be9 | ||
|
|
7742d6f89f | ||
|
|
6d830ae98b | ||
|
|
19bd41375e | ||
|
|
cf0d457d1c | ||
|
|
83ca7a251b | ||
|
|
41caa76b28 | ||
|
|
45bc12a2f5 | ||
|
|
093012c8f7 | ||
|
|
bd9fa67087 | ||
|
|
c5bdb62cb6 | ||
|
|
e9dd8ff760 | ||
|
|
1c839ee3f8 | ||
|
|
9fdd88526c | ||
|
|
d5b40ef6f4 | ||
|
|
59162e3028 | ||
|
|
f41728550c | ||
|
|
3b918d3fcb | ||
|
|
cf18acc661 | ||
|
|
be079a2e48 | ||
|
|
9b90b780ef | ||
|
|
a19026e048 | ||
|
|
47608a8cde | ||
|
|
a9fdf327b6 | ||
|
|
bd63af69e2 | ||
|
|
6fa97a081f | ||
|
|
be0eafec7e | ||
|
|
6e4c8b5558 | ||
|
|
fb106ec87f | ||
|
|
2a5c2b43e1 | ||
|
|
eab52bf8c1 | ||
|
|
cd5ddc4fa1 | ||
|
|
3f2501cd90 | ||
|
|
019e1b161b | ||
|
|
cdad47b82f | ||
|
|
2953aa420e |
@@ -2,8 +2,8 @@ VITE_FIREBASE_CONFIG={"apiKey":"AIzaSyDPLT8GiDHDR1R4nI66Qi0BY1aYviDPioc","authDo
|
|||||||
VITE_GRAPHQL_ENDPOINT=https://db.dev.imex.online/v1/graphql
|
VITE_GRAPHQL_ENDPOINT=https://db.dev.imex.online/v1/graphql
|
||||||
VITE_FIREBASE_CONFIG_TEST={ "apiKey":"AIzaSyBw7_GTy7GtQyfkIRPVrWHEGKfcqeyXw0c", "authDomain":"imex-test.firebaseapp.com", "projectId":"imex-test", "storageBucket":"imex-test.appspot.com", "messagingSenderId":"991923618608", "appId":"1:991923618608:web:633437569cdad78299bef5", "measurementId":"G-TW0XLZEH18"}
|
VITE_FIREBASE_CONFIG_TEST={ "apiKey":"AIzaSyBw7_GTy7GtQyfkIRPVrWHEGKfcqeyXw0c", "authDomain":"imex-test.firebaseapp.com", "projectId":"imex-test", "storageBucket":"imex-test.appspot.com", "messagingSenderId":"991923618608", "appId":"1:991923618608:web:633437569cdad78299bef5", "measurementId":"G-TW0XLZEH18"}
|
||||||
VITE_GRAPHQL_ENDPOINT_TEST=https://db.test.bodyshop.app/v1/graphql
|
VITE_GRAPHQL_ENDPOINT_TEST=https://db.test.bodyshop.app/v1/graphql
|
||||||
VITE_COMPANY=IMEX
|
VITE_COMPANY=ROME
|
||||||
VITE_FE_URL=https://imex.online
|
VITE_FE_URL=https://imex.online
|
||||||
VITE_FE_URL_TEST=https://test.imex.online
|
VITE_FE_URL_TEST=https://test.imex.online
|
||||||
VITE_API_URL="http://localhost:4000"
|
VITE_API_URL="http://localhost:4000"
|
||||||
VITE_API_TEST_URL="http://api.test.imex.online"
|
VITE_API_TEST_URL="https://api.test.imex.online"
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -14,5 +14,6 @@ out
|
|||||||
|
|
||||||
# Build Files
|
# Build Files
|
||||||
macbuild.sh
|
macbuild.sh
|
||||||
|
deploy.ps1
|
||||||
# Sentry Config File
|
# Sentry Config File
|
||||||
.env.sentry-build-plugin
|
.env.sentry-build-plugin
|
||||||
|
|||||||
3
.vscode/launch.json
vendored
3
.vscode/launch.json
vendored
@@ -13,7 +13,8 @@
|
|||||||
"runtimeArgs": ["--sourcemap"],
|
"runtimeArgs": ["--sourcemap"],
|
||||||
"env": {
|
"env": {
|
||||||
"REMOTE_DEBUGGING_PORT": "9222"
|
"REMOTE_DEBUGGING_PORT": "9222"
|
||||||
}
|
},
|
||||||
|
"experimentalNetworking": "off"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Debug Renderer Process",
|
"name": "Debug Renderer Process",
|
||||||
|
|||||||
17
build/com.convenient-brands.bodyshop-desktop.keepalive.plist
Normal file
17
build/com.convenient-brands.bodyshop-desktop.keepalive.plist
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?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.convenientbrands.bodyshop-desktop.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>
|
||||||
53
deploy/set-artifact-name.js
Normal file
53
deploy/set-artifact-name.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Cross-platform script to set artifact naming based on version
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { spawn } = require('child_process');
|
||||||
|
|
||||||
|
// Read the package.json to get the version
|
||||||
|
const packageJsonPath = path.join(__dirname, '..', 'package.json');
|
||||||
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||||
|
const version = packageJson.version;
|
||||||
|
|
||||||
|
console.log(`Current version: ${version}`);
|
||||||
|
|
||||||
|
// Determine the artifact suffix based on the version
|
||||||
|
let artifactSuffix = '';
|
||||||
|
|
||||||
|
if (version.includes('alpha')) {
|
||||||
|
artifactSuffix = `alpha-${version}-`;
|
||||||
|
console.log(`Detected alpha version, setting suffix to: ${artifactSuffix}`);
|
||||||
|
} else if (version.includes('beta')) {
|
||||||
|
artifactSuffix = `beta-${version}-`;
|
||||||
|
console.log(`Detected beta version, setting suffix to: ${artifactSuffix}`);
|
||||||
|
} else {
|
||||||
|
artifactSuffix = '';
|
||||||
|
console.log('Detected release version, no suffix will be added');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the environment variable for the current process
|
||||||
|
process.env.ARTIFACT_SUFFIX = artifactSuffix;
|
||||||
|
|
||||||
|
console.log(`ARTIFACT_SUFFIX set to: '${artifactSuffix}'`);
|
||||||
|
|
||||||
|
// If arguments are passed, execute the remaining command with the environment variable set
|
||||||
|
if (process.argv.length > 2) {
|
||||||
|
const command = process.argv[2];
|
||||||
|
const args = process.argv.slice(3);
|
||||||
|
|
||||||
|
console.log(`Executing: ${command} ${args.join(' ')}`);
|
||||||
|
|
||||||
|
const child = spawn(command, args, {
|
||||||
|
stdio: 'inherit',
|
||||||
|
env: { ...process.env, ARTIFACT_SUFFIX: artifactSuffix },
|
||||||
|
shell: true
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('close', (code) => {
|
||||||
|
process.exit(code);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Just setting the environment variable
|
||||||
|
console.log('Environment variable set. Use this script with additional arguments to run commands with the variable set.');
|
||||||
|
}
|
||||||
38
deploy/set-artifact-name.ps1
Normal file
38
deploy/set-artifact-name.ps1
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# PowerShell script to set artifact naming based on version
|
||||||
|
param(
|
||||||
|
[string]$ConfigType = "imex"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Read the package.json to get the version
|
||||||
|
$packageJsonPath = Join-Path $PSScriptRoot "..\package.json"
|
||||||
|
$packageJson = Get-Content $packageJsonPath | ConvertFrom-Json
|
||||||
|
$version = $packageJson.version
|
||||||
|
|
||||||
|
Write-Host "Current version: $version"
|
||||||
|
|
||||||
|
# Determine the artifact suffix based on the version
|
||||||
|
$artifactSuffix = ""
|
||||||
|
|
||||||
|
if ($version -match "alpha") {
|
||||||
|
$artifactSuffix = "alpha-${version}-"
|
||||||
|
Write-Host "Detected alpha version, setting suffix to: $artifactSuffix"
|
||||||
|
}
|
||||||
|
elseif ($version -match "beta") {
|
||||||
|
$artifactSuffix = "beta-${version}-"
|
||||||
|
Write-Host "Detected beta version, setting suffix to: $artifactSuffix"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$artifactSuffix = ""
|
||||||
|
Write-Host "Detected release version, no suffix will be added"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set the environment variable
|
||||||
|
$env:ARTIFACT_SUFFIX = $artifactSuffix
|
||||||
|
|
||||||
|
# Export for the current session
|
||||||
|
[Environment]::SetEnvironmentVariable("ARTIFACT_SUFFIX", $artifactSuffix, "Process")
|
||||||
|
|
||||||
|
Write-Host "ARTIFACT_SUFFIX set to: '$env:ARTIFACT_SUFFIX'"
|
||||||
|
|
||||||
|
# Return the suffix for use in other scripts
|
||||||
|
return $artifactSuffix
|
||||||
33
deploy/set-artifact-name.sh
Normal file
33
deploy/set-artifact-name.sh
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Bash script to set artifact naming based on version
|
||||||
|
|
||||||
|
# Get the directory where this script is located
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
# Read the package.json to get the version
|
||||||
|
PACKAGE_JSON_PATH="$SCRIPT_DIR/../package.json"
|
||||||
|
VERSION=$(node -p "require('$PACKAGE_JSON_PATH').version")
|
||||||
|
|
||||||
|
echo "Current version: $VERSION"
|
||||||
|
|
||||||
|
# Determine the artifact suffix based on the version
|
||||||
|
ARTIFACT_SUFFIX=""
|
||||||
|
|
||||||
|
if [[ $VERSION == *"alpha"* ]]; then
|
||||||
|
ARTIFACT_SUFFIX="alpha-${VERSION}-"
|
||||||
|
echo "Detected alpha version, setting suffix to: $ARTIFACT_SUFFIX"
|
||||||
|
elif [[ $VERSION == *"beta"* ]]; then
|
||||||
|
ARTIFACT_SUFFIX="beta-${VERSION}-"
|
||||||
|
echo "Detected beta version, setting suffix to: $ARTIFACT_SUFFIX"
|
||||||
|
else
|
||||||
|
ARTIFACT_SUFFIX=""
|
||||||
|
echo "Detected release version, no suffix will be added"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Export the environment variable
|
||||||
|
export ARTIFACT_SUFFIX="$ARTIFACT_SUFFIX"
|
||||||
|
|
||||||
|
echo "ARTIFACT_SUFFIX set to: '$ARTIFACT_SUFFIX'"
|
||||||
|
|
||||||
|
# Also write to a temporary file for sourcing in other scripts
|
||||||
|
echo "export ARTIFACT_SUFFIX='$ARTIFACT_SUFFIX'" > "$SCRIPT_DIR/.artifact-suffix.env"
|
||||||
64
deploy/test-artifact-naming-cross-platform.js
Normal file
64
deploy/test-artifact-naming-cross-platform.js
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Cross-platform test script to demonstrate artifact naming for different versions
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
console.log('=== Artifact Naming Test (Cross-Platform) ===');
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// Get current version
|
||||||
|
const packageJsonPath = path.join(__dirname, '..', 'package.json');
|
||||||
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||||
|
const currentVersion = packageJson.version;
|
||||||
|
|
||||||
|
console.log(`Current version: ${currentVersion}`);
|
||||||
|
|
||||||
|
// Function to get artifact suffix
|
||||||
|
function getArtifactSuffix(version) {
|
||||||
|
if (version.includes('alpha')) {
|
||||||
|
return `alpha-${version}-`;
|
||||||
|
} else if (version.includes('beta')) {
|
||||||
|
return `beta-${version}-`;
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test scenarios
|
||||||
|
const testVersions = [
|
||||||
|
'1.0.5', // Release version
|
||||||
|
'1.0.5-alpha.2', // Alpha version
|
||||||
|
'1.0.5-beta.1', // Beta version
|
||||||
|
'2.0.0-alpha.1', // Another alpha
|
||||||
|
'1.5.0-beta.3' // Another beta
|
||||||
|
];
|
||||||
|
|
||||||
|
console.log('Test scenarios:');
|
||||||
|
console.log('==================');
|
||||||
|
|
||||||
|
testVersions.forEach(version => {
|
||||||
|
const suffix = getArtifactSuffix(version);
|
||||||
|
|
||||||
|
// Different artifact names for different platforms
|
||||||
|
const windowsArtifact = `imex-partner-${suffix}x64.exe`;
|
||||||
|
const macArtifact = `imex-partner-${suffix}x64.dmg`;
|
||||||
|
const linuxArtifact = `imex-partner-${suffix}x64.AppImage`;
|
||||||
|
|
||||||
|
console.log(`Version: ${version}`);
|
||||||
|
console.log(` Suffix: '${suffix}'`);
|
||||||
|
console.log(` Windows: ${windowsArtifact}`);
|
||||||
|
console.log(` Mac: ${macArtifact}`);
|
||||||
|
console.log(` Linux: ${linuxArtifact}`);
|
||||||
|
console.log('');
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Current configuration will produce:');
|
||||||
|
const currentSuffix = getArtifactSuffix(currentVersion);
|
||||||
|
console.log(` Windows: imex-partner-${currentSuffix}x64.exe`);
|
||||||
|
console.log(` Mac: imex-partner-${currentSuffix}x64.dmg`);
|
||||||
|
console.log(` Linux: imex-partner-${currentSuffix}x64.AppImage`);
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
console.log(`Platform detected: ${process.platform}`);
|
||||||
|
console.log(`Architecture: ${process.arch}`);
|
||||||
48
deploy/test-artifact-naming.ps1
Normal file
48
deploy/test-artifact-naming.ps1
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# Test script to demonstrate artifact naming for different versions
|
||||||
|
Write-Host "=== Artifact Naming Test ==="
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# Test current version
|
||||||
|
$packageJsonPath = ".\package.json"
|
||||||
|
$packageJson = Get-Content $packageJsonPath | ConvertFrom-Json
|
||||||
|
$currentVersion = $packageJson.version
|
||||||
|
|
||||||
|
Write-Host "Current version: $currentVersion"
|
||||||
|
|
||||||
|
# Function to get artifact suffix
|
||||||
|
function Get-ArtifactSuffix($version) {
|
||||||
|
if ($version -match "alpha") {
|
||||||
|
return "alpha-${version}-"
|
||||||
|
} elseif ($version -match "beta") {
|
||||||
|
return "beta-${version}-"
|
||||||
|
} else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test scenarios
|
||||||
|
$testVersions = @(
|
||||||
|
"1.0.5", # Release version
|
||||||
|
"1.0.5-alpha.2", # Alpha version
|
||||||
|
"1.0.5-beta.1", # Beta version
|
||||||
|
"2.0.0-alpha.1", # Another alpha
|
||||||
|
"1.5.0-beta.3" # Another beta
|
||||||
|
)
|
||||||
|
|
||||||
|
Write-Host "Test scenarios:"
|
||||||
|
Write-Host "=================="
|
||||||
|
|
||||||
|
foreach ($version in $testVersions) {
|
||||||
|
$suffix = Get-ArtifactSuffix $version
|
||||||
|
$artifactName = "imex-partner-${suffix}x64.exe"
|
||||||
|
|
||||||
|
Write-Host "Version: $version"
|
||||||
|
Write-Host " Suffix: '$suffix'"
|
||||||
|
Write-Host " Result: $artifactName"
|
||||||
|
Write-Host ""
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Current configuration will produce:"
|
||||||
|
$currentSuffix = Get-ArtifactSuffix $currentVersion
|
||||||
|
$currentArtifact = "imex-partner-${currentSuffix}x64.exe"
|
||||||
|
Write-Host " $currentArtifact"
|
||||||
50
deploy/test-artifact-naming.sh
Normal file
50
deploy/test-artifact-naming.sh
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Test script to demonstrate artifact naming for different versions on Mac
|
||||||
|
|
||||||
|
echo "=== Artifact Naming Test (Mac) ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Get current version
|
||||||
|
PACKAGE_JSON_PATH="./package.json"
|
||||||
|
CURRENT_VERSION=$(node -p "require('$PACKAGE_JSON_PATH').version")
|
||||||
|
|
||||||
|
echo "Current version: $CURRENT_VERSION"
|
||||||
|
|
||||||
|
# Function to get artifact suffix
|
||||||
|
get_artifact_suffix() {
|
||||||
|
local version=$1
|
||||||
|
if [[ $version == *"alpha"* ]]; then
|
||||||
|
echo "alpha-${version}-"
|
||||||
|
elif [[ $version == *"beta"* ]]; then
|
||||||
|
echo "beta-${version}-"
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test scenarios
|
||||||
|
TEST_VERSIONS=(
|
||||||
|
"1.0.5" # Release version
|
||||||
|
"1.0.5-alpha.2" # Alpha version
|
||||||
|
"1.0.5-beta.1" # Beta version
|
||||||
|
"2.0.0-alpha.1" # Another alpha
|
||||||
|
"1.5.0-beta.3" # Another beta
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "Test scenarios:"
|
||||||
|
echo "=================="
|
||||||
|
|
||||||
|
for version in "${TEST_VERSIONS[@]}"; do
|
||||||
|
suffix=$(get_artifact_suffix "$version")
|
||||||
|
artifact_name="imex-partner-${suffix}x64.dmg"
|
||||||
|
|
||||||
|
echo "Version: $version"
|
||||||
|
echo " Suffix: '$suffix'"
|
||||||
|
echo " Result: $artifact_name"
|
||||||
|
echo ""
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Current configuration will produce:"
|
||||||
|
current_suffix=$(get_artifact_suffix "$CURRENT_VERSION")
|
||||||
|
current_artifact="imex-partner-${current_suffix}x64.dmg"
|
||||||
|
echo " $current_artifact"
|
||||||
@@ -22,8 +22,9 @@ win:
|
|||||||
endpoint: https://eus.codesigning.azure.net
|
endpoint: https://eus.codesigning.azure.net
|
||||||
certificateProfileName: ImEXRPS
|
certificateProfileName: ImEXRPS
|
||||||
codeSigningAccountName: ImEX
|
codeSigningAccountName: ImEX
|
||||||
|
publisherName: ImEX Systems Inc.
|
||||||
nsis:
|
nsis:
|
||||||
artifactName: ${name}-${version}-setup.${ext}
|
artifactName: imex-partner-${env.ARTIFACT_SUFFIX}${arch}.${ext}
|
||||||
shortcutName: ${productName}
|
shortcutName: ${productName}
|
||||||
uninstallDisplayName: ${productName}
|
uninstallDisplayName: ${productName}
|
||||||
createDesktopShortcut: always
|
createDesktopShortcut: always
|
||||||
@@ -49,17 +50,9 @@ mac:
|
|||||||
arch:
|
arch:
|
||||||
- x64
|
- x64
|
||||||
dmg:
|
dmg:
|
||||||
artifactName: ${name}-${version}-${arch}.${ext}
|
artifactName: imex-partner-${env.ARTIFACT_SUFFIX}${arch}.${ext}
|
||||||
linux:
|
|
||||||
target:
|
|
||||||
- AppImage
|
|
||||||
- snap
|
|
||||||
- deb
|
|
||||||
maintainer: electronjs.org
|
|
||||||
category: Utility
|
|
||||||
desktop: scripts/imex-shop-partner.desktop
|
|
||||||
appImage:
|
appImage:
|
||||||
artifactName: ${name}-${version}.${ext}
|
artifactName: imex-partner-${env.ARTIFACT_SUFFIX}${arch}.${ext}
|
||||||
npmRebuild: false
|
npmRebuild: false
|
||||||
publish:
|
publish:
|
||||||
provider: s3
|
provider: s3
|
||||||
|
|||||||
@@ -17,13 +17,14 @@ asarUnpack:
|
|||||||
- resources/**
|
- resources/**
|
||||||
win:
|
win:
|
||||||
executableName: ShopPartner
|
executableName: ShopPartner
|
||||||
icon: resources/icon.png
|
icon: resources/ro-icon.png
|
||||||
azureSignOptions:
|
azureSignOptions:
|
||||||
endpoint: https://eus.codesigning.azure.net
|
endpoint: https://eus.codesigning.azure.net
|
||||||
certificateProfileName: ImEXRPS
|
certificateProfileName: ImEXRPS
|
||||||
codeSigningAccountName: ImEX
|
codeSigningAccountName: ImEX
|
||||||
|
publisherName: ImEX Systems Inc.
|
||||||
nsis:
|
nsis:
|
||||||
artifactName: ${name}-${version}-setup.${ext}
|
artifactName: rome-partner-${env.ARTIFACT_SUFFIX}${arch}.${ext}
|
||||||
shortcutName: ${productName}
|
shortcutName: ${productName}
|
||||||
uninstallDisplayName: ${productName}
|
uninstallDisplayName: ${productName}
|
||||||
createDesktopShortcut: always
|
createDesktopShortcut: always
|
||||||
@@ -31,6 +32,7 @@ nsis:
|
|||||||
mac:
|
mac:
|
||||||
entitlementsInherit: build/entitlements.mac.plist
|
entitlementsInherit: build/entitlements.mac.plist
|
||||||
category: public.app-category.business
|
category: public.app-category.business
|
||||||
|
icon: resources/ro-icon.png
|
||||||
extendInfo:
|
extendInfo:
|
||||||
- NSCameraUsageDescription: Application requests access to the device's camera.
|
- NSCameraUsageDescription: Application requests access to the device's camera.
|
||||||
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
||||||
@@ -49,17 +51,9 @@ mac:
|
|||||||
arch:
|
arch:
|
||||||
- x64
|
- x64
|
||||||
dmg:
|
dmg:
|
||||||
artifactName: ${name}-${version}-${arch}.${ext}
|
artifactName: rome-partner-${env.ARTIFACT_SUFFIX}${arch}.${ext}
|
||||||
linux:
|
|
||||||
target:
|
|
||||||
- AppImage
|
|
||||||
- snap
|
|
||||||
- deb
|
|
||||||
maintainer: electronjs.org
|
|
||||||
category: Utility
|
|
||||||
desktop: scripts/rome-shop-partner.desktop
|
|
||||||
appImage:
|
appImage:
|
||||||
artifactName: ${name}-${version}.${ext}
|
artifactName: rome-partner-${env.ARTIFACT_SUFFIX}${arch}.${ext}
|
||||||
npmRebuild: false
|
npmRebuild: false
|
||||||
publish:
|
publish:
|
||||||
provider: s3
|
provider: s3
|
||||||
|
|||||||
@@ -6,10 +6,18 @@ import react from "@vitejs/plugin-react";
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
main: {
|
main: {
|
||||||
plugins: [
|
plugins: [
|
||||||
externalizeDepsPlugin(),
|
externalizeDepsPlugin({
|
||||||
|
exclude: ["electron-store"],
|
||||||
|
}),
|
||||||
sentryVitePlugin({
|
sentryVitePlugin({
|
||||||
org: "imex",
|
org: "imex",
|
||||||
project: "imex-partner",
|
project: "imex-partner",
|
||||||
|
sourcemaps: {
|
||||||
|
filesToDeleteAfterUpload: ["**.js.map"],
|
||||||
|
},
|
||||||
|
release: {
|
||||||
|
name: `bodyshop-desktop@${process.env.npm_package_version}`,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
build: {
|
build: {
|
||||||
@@ -32,6 +40,12 @@ export default defineConfig({
|
|||||||
sentryVitePlugin({
|
sentryVitePlugin({
|
||||||
org: "imex",
|
org: "imex",
|
||||||
project: "imex-partner",
|
project: "imex-partner",
|
||||||
|
sourcemaps: {
|
||||||
|
filesToDeleteAfterUpload: ["**.js.map"],
|
||||||
|
},
|
||||||
|
release: {
|
||||||
|
name: `bodyshop-desktop@${process.env.npm_package_version}`,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
build: {
|
build: {
|
||||||
|
|||||||
5254
package-lock.json
generated
5254
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
105
package.json
105
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "bodyshop-desktop",
|
"name": "bodyshop-desktop",
|
||||||
"version": "0.0.1-alpha.8",
|
"version": "1.0.7",
|
||||||
"description": "Shop Management System Partner",
|
"description": "Shop Management System Partner",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "Convenient Brands, LLC",
|
"author": "Convenient Brands, LLC",
|
||||||
@@ -13,78 +13,79 @@
|
|||||||
"typecheck": "npm run typecheck:node && npm run typecheck:web",
|
"typecheck": "npm run typecheck:node && npm run typecheck:web",
|
||||||
"start": "electron-vite preview",
|
"start": "electron-vite preview",
|
||||||
"dev": "electron-vite dev",
|
"dev": "electron-vite dev",
|
||||||
"build:imex": "electron-vite build --mode imex && electron-builder --config electron-builder.imex.yml",
|
"build:imex": "node deploy/set-artifact-name.js electron-vite build --mode imex && node deploy/set-artifact-name.js electron-builder --config electron-builder.imex.yml",
|
||||||
"build:rome": "electron-vite build --mode rome && electron-builder --config electron-builder.rome.yml",
|
"build:rome": "node deploy/set-artifact-name.js electron-vite build --mode rome && node deploy/set-artifact-name.js electron-builder --config electron-builder.rome.yml",
|
||||||
"build:imex:publish": "electron-vite build --mode imex && electron-builder --config electron-builder.imex.yml --publish always",
|
"build:imex:publish": "node deploy/set-artifact-name.js electron-vite build --mode imex && node deploy/set-artifact-name.js electron-builder --config electron-builder.imex.yml --publish always",
|
||||||
"build:rome:publish": "electron-vite build --mode rome && electron-builder --config electron-builder.rome.yml --publish always",
|
"build:rome:publish": "node deploy/set-artifact-name.js electron-vite build --mode rome && node deploy/set-artifact-name.js electron-builder --config electron-builder.rome.yml --publish always",
|
||||||
"build:imex:linux": "electron-vite build --mode imex && electron-builder --config electron-builder.imex.yml --linux",
|
"build:imex:linux": "node deploy/set-artifact-name.js electron-vite build --mode imex && node deploy/set-artifact-name.js electron-builder --config electron-builder.imex.yml --linux",
|
||||||
"build:rome:linux": "electron-vite build --mode rome && electron-builder --config electron-builder.rome.yml --linux",
|
"build:rome:linux": "node deploy/set-artifact-name.js electron-vite build --mode rome && node deploy/set-artifact-name.js electron-builder --config electron-builder.rome.yml --linux",
|
||||||
"postinstall": "electron-builder install-app-deps",
|
"postinstall": "electron-builder install-app-deps",
|
||||||
"build:unpack": "electron-vite build --mode imex && electron-builder --dir",
|
"build:unpack": "node deploy/set-artifact-name.js electron-vite build --mode imex && node deploy/set-artifact-name.js electron-builder --dir",
|
||||||
"build:win": "electron-vite build --mode imex && electron-builder --win",
|
"build:win": "node deploy/set-artifact-name.js electron-vite build --mode imex && node deploy/set-artifact-name.js electron-builder --win",
|
||||||
"build:mac": "electron-vite build --mode imex && electron-builder --mac",
|
"build:mac": "node deploy/set-artifact-name.js electron-vite build --mode imex && node deploy/set-artifact-name.js electron-builder --mac",
|
||||||
"build:linux": "electron-vite build --mode imex && electron-builder --linux"
|
"build:linux": "node deploy/set-artifact-name.js electron-vite build --mode imex && node deploy/set-artifact-name.js electron-builder --linux"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.13.6",
|
"@apollo/client": "^3.13.6",
|
||||||
"@electron-toolkit/preload": "^3.0.1",
|
"@electron-toolkit/preload": "^3.0.2",
|
||||||
"@electron-toolkit/utils": "^4.0.0",
|
"@electron-toolkit/utils": "^4.0.0",
|
||||||
"@sentry/electron": "^6.5.0",
|
"@sentry/electron": "^7.2.0",
|
||||||
"@sentry/vite-plugin": "^3.3.1",
|
"@sentry/vite-plugin": "^4.5.0",
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.12.2",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.18",
|
||||||
"electron-log": "^5.3.3",
|
"electron-log": "^5.4.3",
|
||||||
"electron-store": "^8.2.0",
|
|
||||||
"electron-updater": "^6.6.2",
|
"electron-updater": "^6.6.2",
|
||||||
"winax": "^3.6.2"
|
"winax": "^3.6.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ant-design/v5-patch-for-react-19": "^1.0.3",
|
"@ant-design/v5-patch-for-react-19": "^1.0.3",
|
||||||
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
|
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
|
||||||
"@electron-toolkit/eslint-config-ts": "^3.0.0",
|
"@electron-toolkit/eslint-config-ts": "^3.1.0",
|
||||||
"@electron-toolkit/tsconfig": "^1.0.1",
|
"@electron-toolkit/tsconfig": "^2.0.0",
|
||||||
"@playwright/test": "^1.51.1",
|
"@playwright/test": "^1.56.1",
|
||||||
"@reduxjs/toolkit": "^2.6.1",
|
"@reduxjs/toolkit": "^2.9.1",
|
||||||
"@types/cors": "^2.8.17",
|
"@types/cors": "^2.8.19",
|
||||||
"@types/express": "^5.0.1",
|
"@types/express": "^5.0.3",
|
||||||
"@types/lodash": "^4.17.16",
|
"@types/lodash": "^4.17.20",
|
||||||
"@types/node": "^22.14.0",
|
"@types/node": "^24.9.1",
|
||||||
"@types/node-cron": "^3.0.11",
|
"@types/node-cron": "^3.0.11",
|
||||||
"@types/react": "^19.1.0",
|
"@types/react": "^19.2.2",
|
||||||
"@types/react-dom": "^19.1.2",
|
"@types/react-dom": "^19.2.2",
|
||||||
"@types/xml2js": "^0.4.14",
|
"@types/xml2js": "^0.4.14",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^5.0.4",
|
||||||
"antd": "^5.24.6",
|
"antd": "^5.27.6",
|
||||||
|
"archiver": "^7.0.1",
|
||||||
"chokidar": "^4.0.3",
|
"chokidar": "^4.0.3",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
"cross-env": "^10.1.0",
|
||||||
"dbffile": "^1.12.0",
|
"dbffile": "^1.12.0",
|
||||||
"electron": "^35.1.5",
|
"electron": "^38.3.0",
|
||||||
"electron-builder": "^25.1.8",
|
"electron-builder": "^26.0.12",
|
||||||
"electron-store": "^8.2.0",
|
"electron-store": "^11.0.2",
|
||||||
"electron-vite": "^3.1.0",
|
"electron-vite": "^4.0.1",
|
||||||
"eslint": "^9.24.0",
|
"eslint": "^9.38.0",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^7.0.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.19",
|
"eslint-plugin-react-refresh": "^0.4.24",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"firebase": "^11.6.0",
|
"firebase": "^12.4.0",
|
||||||
"graphql": "^16.10.0",
|
"graphql": "^16.11.0",
|
||||||
"graphql-request": "^7.1.2",
|
"graphql-request": "^7.3.1",
|
||||||
"i18next": "^24.2.3",
|
"i18next": "^25.6.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"playwright": "^1.51.1",
|
"node-cron": "^4.2.1",
|
||||||
"prettier": "^3.5.3",
|
"playwright": "^1.56.1",
|
||||||
"react": "^19.1.0",
|
"prettier": "^3.6.2",
|
||||||
"react-dom": "^19.1.0",
|
"react": "^19.2.0",
|
||||||
"react-error-boundary": "^5.0.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-i18next": "^15.4.1",
|
"react-error-boundary": "^6.0.0",
|
||||||
|
"react-i18next": "^16.1.4",
|
||||||
"react-redux": "^9.2.0",
|
"react-redux": "^9.2.0",
|
||||||
"react-router": "^7.5.0",
|
"react-router": "^7.9.4",
|
||||||
"redux-logger": "^3.0.6",
|
"redux-logger": "^3.0.6",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "6.2.6",
|
"vite": "7.1.11",
|
||||||
"xml2js": "^0.6.2",
|
"xml2js": "^0.6.2",
|
||||||
"xmlbuilder2": "^3.1.1",
|
"xmlbuilder2": "^4.0.0"
|
||||||
"node-cron": "^3.0.3"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
resources/ro-icon.png
Normal file
BIN
resources/ro-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
@@ -182,7 +182,9 @@ const DecodeAD1 = async (
|
|||||||
if (!rawAd1Data.ownr_ph1 || _.isEmpty(rawAd1Data.ownr_ph1)) {
|
if (!rawAd1Data.ownr_ph1 || _.isEmpty(rawAd1Data.ownr_ph1)) {
|
||||||
rawAd1Data.ownr_ph1 = rawAd1Data.ownr_ph2;
|
rawAd1Data.ownr_ph1 = rawAd1Data.ownr_ph2;
|
||||||
}
|
}
|
||||||
|
if (rawAd1Data.clm_no === "") {
|
||||||
|
rawAd1Data.clm_no = undefined;
|
||||||
|
}
|
||||||
let ownerRecord: OwnerRecordInterface;
|
let ownerRecord: OwnerRecordInterface;
|
||||||
//Check if the owner information is there. If not, use the insured information as a fallback.
|
//Check if the owner information is there. If not, use the insured information as a fallback.
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import { DecodedTtl } from "./decode-ttl.interface";
|
|||||||
import DecodeVeh from "./decode-veh";
|
import DecodeVeh from "./decode-veh";
|
||||||
import { DecodedVeh } from "./decode-veh.interface";
|
import { DecodedVeh } from "./decode-veh.interface";
|
||||||
import setAppProgressbar from "../util/setAppProgressBar";
|
import setAppProgressbar from "../util/setAppProgressBar";
|
||||||
|
import UploadEmsToS3 from "./emsbackup";
|
||||||
|
|
||||||
async function ImportJob(filepath: string): Promise<void> {
|
async function ImportJob(filepath: string): Promise<void> {
|
||||||
const parsedFilePath = path.parse(filepath);
|
const parsedFilePath = path.parse(filepath);
|
||||||
@@ -156,6 +157,7 @@ async function ImportJob(filepath: string): Promise<void> {
|
|||||||
console.log("Available Job record to upload;", newAvailableJob);
|
console.log("Available Job record to upload;", newAvailableJob);
|
||||||
|
|
||||||
setAppProgressbar(0.95);
|
setAppProgressbar(0.95);
|
||||||
|
if (jobObject.clm_no) {
|
||||||
const existingJobRecord: QueryJobByClmNoResult = await client.request(
|
const existingJobRecord: QueryJobByClmNoResult = await client.request(
|
||||||
QUERY_JOB_BY_CLM_NO_TYPED,
|
QUERY_JOB_BY_CLM_NO_TYPED,
|
||||||
{ clm_no: jobObject.clm_no },
|
{ clm_no: jobObject.clm_no },
|
||||||
@@ -165,6 +167,7 @@ async function ImportJob(filepath: string): Promise<void> {
|
|||||||
newAvailableJob.issupplement = true;
|
newAvailableJob.issupplement = true;
|
||||||
newAvailableJob.jobid = existingJobRecord.jobs[0].id;
|
newAvailableJob.jobid = existingJobRecord.jobs[0].id;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const insertRecordResult: InsertAvailableJobResult = await client.request(
|
const insertRecordResult: InsertAvailableJobResult = await client.request(
|
||||||
INSERT_AVAILABLE_JOB_TYPED,
|
INSERT_AVAILABLE_JOB_TYPED,
|
||||||
@@ -191,6 +194,14 @@ async function ImportJob(filepath: string): Promise<void> {
|
|||||||
uploadNotification.show();
|
uploadNotification.show();
|
||||||
|
|
||||||
log.debug("Job inserted", insertRecordResult);
|
log.debug("Job inserted", insertRecordResult);
|
||||||
|
|
||||||
|
UploadEmsToS3({
|
||||||
|
extensionlessFilePath,
|
||||||
|
bodyshopid: newAvailableJob.bodyshopid,
|
||||||
|
ciecaid: jobObject.ciecaid ?? "",
|
||||||
|
clm_no: jobObject.clm_no ?? "",
|
||||||
|
ownr_ln: jobObject.ownr_ln ?? "",
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error("Error encountered while decoding job. ", errorTypeCheck(error));
|
log.error("Error encountered while decoding job. ", errorTypeCheck(error));
|
||||||
const uploadNotificationFailure = new Notification({
|
const uploadNotificationFailure = new Notification({
|
||||||
|
|||||||
104
src/main/decoder/emsbackup.ts
Normal file
104
src/main/decoder/emsbackup.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import archiver from "archiver";
|
||||||
|
import errorTypeCheck from "../../util/errorTypeCheck";
|
||||||
|
import { UUID } from "crypto";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import stream from "stream";
|
||||||
|
import { getTokenFromRenderer } from "../graphql/graphql-client";
|
||||||
|
import store from "../store/store";
|
||||||
|
|
||||||
|
async function UploadEmsToS3({
|
||||||
|
extensionlessFilePath,
|
||||||
|
bodyshopid,
|
||||||
|
clm_no,
|
||||||
|
ciecaid,
|
||||||
|
ownr_ln,
|
||||||
|
}: {
|
||||||
|
extensionlessFilePath: string;
|
||||||
|
bodyshopid: UUID;
|
||||||
|
clm_no: string;
|
||||||
|
ciecaid: string;
|
||||||
|
ownr_ln: string;
|
||||||
|
}): Promise<boolean> {
|
||||||
|
// This function is a placeholder for the actual upload logic
|
||||||
|
try {
|
||||||
|
const directory = path.dirname(extensionlessFilePath);
|
||||||
|
const baseFilename = path.basename(extensionlessFilePath);
|
||||||
|
|
||||||
|
// Find all files in the directory that start with the base filename
|
||||||
|
const filesToZip = fs
|
||||||
|
.readdirSync(directory)
|
||||||
|
.filter((file) => file.startsWith(baseFilename))
|
||||||
|
.map((file) => path.join(directory, file));
|
||||||
|
|
||||||
|
if (filesToZip.length === 0) {
|
||||||
|
console.error("No files found to zip.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a zip archive in memory
|
||||||
|
const archive = archiver("zip", { zlib: { level: 9 } });
|
||||||
|
const zipBuffer = await new Promise<Buffer>((resolve, reject) => {
|
||||||
|
const buffers: Buffer[] = [];
|
||||||
|
const writableStream = new stream.Writable({
|
||||||
|
write(chunk, _encoding, callback) {
|
||||||
|
buffers.push(chunk);
|
||||||
|
callback();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
writableStream.on("finish", () => resolve(Buffer.concat(buffers)));
|
||||||
|
writableStream.on("error", reject);
|
||||||
|
|
||||||
|
archive.pipe(writableStream);
|
||||||
|
|
||||||
|
// Append files to the archive
|
||||||
|
filesToZip.forEach((file) => {
|
||||||
|
archive.file(file, { name: path.basename(file) });
|
||||||
|
});
|
||||||
|
|
||||||
|
archive.finalize();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get the presigned URL from the server
|
||||||
|
const presignedUrlResponse = await axios.post(
|
||||||
|
`${
|
||||||
|
store.get("app.isTest")
|
||||||
|
? import.meta.env.VITE_API_TEST_URL
|
||||||
|
: import.meta.env.VITE_API_URL
|
||||||
|
}/emsupload`,
|
||||||
|
{
|
||||||
|
bodyshopid,
|
||||||
|
ciecaid,
|
||||||
|
clm_no,
|
||||||
|
ownr_ln,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${await getTokenFromRenderer()}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const presignedUrl = presignedUrlResponse.data?.presignedUrl;
|
||||||
|
if (!presignedUrl) {
|
||||||
|
console.error("Failed to retrieve presigned URL.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload the zip file to S3 using the presigned URL
|
||||||
|
await axios.put(presignedUrl, zipBuffer, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/zip",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error uploading EMS to S3:", errorTypeCheck(error));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true; // Return true if the upload is successful
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UploadEmsToS3;
|
||||||
@@ -19,6 +19,8 @@ const EmsPartsOrderGenerateLinFile = async (
|
|||||||
DB_REF: partsOrderLine.jobline?.db_ref,
|
DB_REF: partsOrderLine.jobline?.db_ref,
|
||||||
UNQ_SEQ: partsOrderLine.jobline?.unq_seq,
|
UNQ_SEQ: partsOrderLine.jobline?.unq_seq,
|
||||||
WHO_PAYS: partsOrderLine.jobline?.who_pays,
|
WHO_PAYS: partsOrderLine.jobline?.who_pays,
|
||||||
|
PART_DESCJ: partsOrderLine.jobline?.part_descj,
|
||||||
|
|
||||||
LINE_DESC: partsOrderLine.jobline?.line_desc,
|
LINE_DESC: partsOrderLine.jobline?.line_desc,
|
||||||
PART_TYPE:
|
PART_TYPE:
|
||||||
partsOrderLine.priceChange === true
|
partsOrderLine.priceChange === true
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const EmsPartsOrderGenerateTtlFile = async (
|
|||||||
ttlFieldLineDescriptors,
|
ttlFieldLineDescriptors,
|
||||||
);
|
);
|
||||||
|
|
||||||
await dbf.appendRecords(records);
|
await dbf.appendRecords([records]);
|
||||||
console.log(`${records.length} TTL file records added.`);
|
console.log(`${records.length} TTL file records added.`);
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ export interface Jobline {
|
|||||||
bett_type: string | null;
|
bett_type: string | null;
|
||||||
cert_part: boolean;
|
cert_part: boolean;
|
||||||
est_seq: string | null;
|
est_seq: string | null;
|
||||||
|
part_descj: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parts Order Line export interface
|
// Parts Order Line export interface
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { GraphQLClient, RequestMiddleware } from "graphql-request";
|
|||||||
import errorTypeCheck from "../../util/errorTypeCheck.js";
|
import errorTypeCheck from "../../util/errorTypeCheck.js";
|
||||||
import ipcTypes from "../../util/ipcTypes.json";
|
import ipcTypes from "../../util/ipcTypes.json";
|
||||||
import store from "../store/store.js";
|
import store from "../store/store.js";
|
||||||
|
import getMainWindow from "../../util/getMainWindow.js";
|
||||||
|
|
||||||
const requestMiddleware: RequestMiddleware = async (request) => {
|
const requestMiddleware: RequestMiddleware = async (request) => {
|
||||||
const token = await getTokenFromRenderer();
|
const token = await getTokenFromRenderer();
|
||||||
@@ -32,9 +33,9 @@ const client: GraphQLClient = new GraphQLClient(
|
|||||||
export async function getTokenFromRenderer(): Promise<string> {
|
export async function getTokenFromRenderer(): Promise<string> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
try {
|
try {
|
||||||
const mainWindow = BrowserWindow.getAllWindows()[0]; //TODO: Filter to only main window once a proper key has been set.
|
const mainWindow = getMainWindow();
|
||||||
//TODO: Verify that this will work if the app is minimized/closed.
|
//TODO: Verify that this will work if the app is minimized/closed.
|
||||||
mainWindow.webContents.send(ipcTypes.toRenderer.user.getToken);
|
mainWindow?.webContents.send(ipcTypes.toRenderer.user.getToken);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(
|
log.error(
|
||||||
"Unable to send request to renderer process for token",
|
"Unable to send request to renderer process for token",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { UUID } from "crypto";
|
|||||||
import { parse, TypedQueryDocumentNode } from "graphql";
|
import { parse, TypedQueryDocumentNode } from "graphql";
|
||||||
import { gql } from "graphql-request";
|
import { gql } from "graphql-request";
|
||||||
import { AvailableJobSchema } from "../decoder/decoder";
|
import { AvailableJobSchema } from "../decoder/decoder";
|
||||||
|
|
||||||
// Define types for the query result and variables
|
// Define types for the query result and variables
|
||||||
export interface ActiveBodyshopQueryResult {
|
export interface ActiveBodyshopQueryResult {
|
||||||
bodyshops: Array<{
|
bodyshops: Array<{
|
||||||
@@ -11,9 +12,8 @@ export interface ActiveBodyshopQueryResult {
|
|||||||
convenient_company: string;
|
convenient_company: string;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
// No variables needed for this query
|
|
||||||
|
|
||||||
// Transform the string query into a TypedQueryDocumentNode
|
// No variables needed for this query
|
||||||
export const QUERY_ACTIVE_BODYSHOP_TYPED: TypedQueryDocumentNode<
|
export const QUERY_ACTIVE_BODYSHOP_TYPED: TypedQueryDocumentNode<
|
||||||
ActiveBodyshopQueryResult,
|
ActiveBodyshopQueryResult,
|
||||||
Record<never, never>
|
Record<never, never>
|
||||||
@@ -34,9 +34,11 @@ export interface MasterdataQueryResult {
|
|||||||
key: string;
|
key: string;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MasterdataQueryVariables {
|
interface MasterdataQueryVariables {
|
||||||
key: string;
|
key: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const QUERY_MASTERDATA_TYPED: TypedQueryDocumentNode<
|
export const QUERY_MASTERDATA_TYPED: TypedQueryDocumentNode<
|
||||||
MasterdataQueryResult,
|
MasterdataQueryResult,
|
||||||
MasterdataQueryVariables
|
MasterdataQueryVariables
|
||||||
@@ -54,9 +56,11 @@ export interface VehicleQueryResult {
|
|||||||
id: UUID;
|
id: UUID;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface VehicleQueryVariables {
|
interface VehicleQueryVariables {
|
||||||
vin: string;
|
vin: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const QUERY_VEHICLE_BY_VIN_TYPED: TypedQueryDocumentNode<
|
export const QUERY_VEHICLE_BY_VIN_TYPED: TypedQueryDocumentNode<
|
||||||
VehicleQueryResult,
|
VehicleQueryResult,
|
||||||
VehicleQueryVariables
|
VehicleQueryVariables
|
||||||
@@ -73,9 +77,11 @@ export interface QueryJobByClmNoResult {
|
|||||||
id: UUID;
|
id: UUID;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QueryJobByClmNoVariables {
|
export interface QueryJobByClmNoVariables {
|
||||||
clm_no: string;
|
clm_no: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const QUERY_JOB_BY_CLM_NO_TYPED: TypedQueryDocumentNode<
|
export const QUERY_JOB_BY_CLM_NO_TYPED: TypedQueryDocumentNode<
|
||||||
QueryJobByClmNoResult,
|
QueryJobByClmNoResult,
|
||||||
QueryJobByClmNoVariables
|
QueryJobByClmNoVariables
|
||||||
@@ -92,9 +98,11 @@ export interface InsertAvailableJobResult {
|
|||||||
id: UUID;
|
id: UUID;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InsertAvailableJobVariables {
|
export interface InsertAvailableJobVariables {
|
||||||
jobInput: Array<AvailableJobSchema>;
|
jobInput: Array<AvailableJobSchema>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const INSERT_AVAILABLE_JOB_TYPED: TypedQueryDocumentNode<
|
export const INSERT_AVAILABLE_JOB_TYPED: TypedQueryDocumentNode<
|
||||||
InsertAvailableJobResult,
|
InsertAvailableJobResult,
|
||||||
InsertAvailableJobVariables
|
InsertAvailableJobVariables
|
||||||
@@ -125,3 +133,140 @@ export const INSERT_AVAILABLE_JOB_TYPED: TypedQueryDocumentNode<
|
|||||||
InsertAvailableJobResult,
|
InsertAvailableJobResult,
|
||||||
InsertAvailableJobVariables
|
InsertAvailableJobVariables
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
// Add PpgData Query
|
||||||
|
export interface PpgDataQueryResult {
|
||||||
|
bodyshops_by_pk: {
|
||||||
|
id: string;
|
||||||
|
shopname: string;
|
||||||
|
imexshopid: string;
|
||||||
|
} | null;
|
||||||
|
jobs: Array<{
|
||||||
|
id: string;
|
||||||
|
ro_number: string;
|
||||||
|
status: string;
|
||||||
|
ownr_fn: string;
|
||||||
|
ownr_ln: string;
|
||||||
|
ownr_co_nm: string;
|
||||||
|
v_vin: string;
|
||||||
|
v_model_yr: string;
|
||||||
|
v_make_desc: string;
|
||||||
|
v_model_desc: string;
|
||||||
|
v_color: string;
|
||||||
|
plate_no: string;
|
||||||
|
ins_co_nm: string;
|
||||||
|
est_ct_fn: string;
|
||||||
|
est_ct_ln: string;
|
||||||
|
rate_mapa: number;
|
||||||
|
rate_lab: number;
|
||||||
|
job_totals: {
|
||||||
|
rates?: {
|
||||||
|
mapa?: {
|
||||||
|
total?: {
|
||||||
|
amount?: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
totals?: {
|
||||||
|
subtotal?: {
|
||||||
|
amount?: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
vehicle: {
|
||||||
|
v_paint_codes: {
|
||||||
|
paint_cd1?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
labhrs: {
|
||||||
|
aggregate: {
|
||||||
|
sum: {
|
||||||
|
mod_lb_hrs: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
larhrs: {
|
||||||
|
aggregate: {
|
||||||
|
sum: {
|
||||||
|
mod_lb_hrs: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PpgDataQueryVariables {
|
||||||
|
today: string;
|
||||||
|
todayplus5: string;
|
||||||
|
shopid: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PPG_DATA_QUERY_TYPED: TypedQueryDocumentNode<
|
||||||
|
PpgDataQueryResult,
|
||||||
|
PpgDataQueryVariables
|
||||||
|
> = parse(gql`
|
||||||
|
query PpgData(
|
||||||
|
$today: timestamptz!
|
||||||
|
$todayplus5: timestamptz!
|
||||||
|
$shopid: uuid!
|
||||||
|
) {
|
||||||
|
bodyshops_by_pk(id: $shopid) {
|
||||||
|
id
|
||||||
|
shopname
|
||||||
|
imexshopid
|
||||||
|
}
|
||||||
|
jobs(
|
||||||
|
where: {
|
||||||
|
_or: [
|
||||||
|
{
|
||||||
|
_and: [
|
||||||
|
{ scheduled_in: { _lte: $todayplus5 } }
|
||||||
|
{ scheduled_in: { _gte: $today } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
{ inproduction: { _eq: true } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
ro_number
|
||||||
|
status
|
||||||
|
ownr_fn
|
||||||
|
ownr_ln
|
||||||
|
ownr_co_nm
|
||||||
|
v_vin
|
||||||
|
v_model_yr
|
||||||
|
v_make_desc
|
||||||
|
v_model_desc
|
||||||
|
v_color
|
||||||
|
plate_no
|
||||||
|
ins_co_nm
|
||||||
|
est_ct_fn
|
||||||
|
est_ct_ln
|
||||||
|
rate_mapa
|
||||||
|
rate_lab
|
||||||
|
job_totals
|
||||||
|
vehicle {
|
||||||
|
v_paint_codes
|
||||||
|
}
|
||||||
|
labhrs: joblines_aggregate(
|
||||||
|
where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }
|
||||||
|
) {
|
||||||
|
aggregate {
|
||||||
|
sum {
|
||||||
|
mod_lb_hrs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
larhrs: joblines_aggregate(
|
||||||
|
where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }
|
||||||
|
) {
|
||||||
|
aggregate {
|
||||||
|
sum {
|
||||||
|
mod_lb_hrs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`) as TypedQueryDocumentNode<PpgDataQueryResult, PpgDataQueryVariables>;
|
||||||
|
|||||||
@@ -31,6 +31,12 @@ export default class LocalServer {
|
|||||||
"https://localhost:3000",
|
"https://localhost:3000",
|
||||||
"https://test.imex.online",
|
"https://test.imex.online",
|
||||||
"https://imex.online",
|
"https://imex.online",
|
||||||
|
"https://test.romeonline.io",
|
||||||
|
"https://romeonline.io",
|
||||||
|
"https://www.test.imex.online",
|
||||||
|
"https://www.imex.online",
|
||||||
|
"https://www.test.romeonline.io",
|
||||||
|
"https://www.romeonline.io",
|
||||||
];
|
];
|
||||||
|
|
||||||
this.app.use(
|
this.app.use(
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ import {
|
|||||||
import log from "electron-log/main";
|
import log from "electron-log/main";
|
||||||
import { autoUpdater } from "electron-updater";
|
import { autoUpdater } from "electron-updater";
|
||||||
import path, { join } from "path";
|
import path, { join } from "path";
|
||||||
import appIcon from "../../resources/icon.png?asset";
|
import imexAppIcon from "../../resources/icon.png?asset";
|
||||||
|
import romeAppIcon from "../../resources/ro-icon.png?asset";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
default as ErrorTypeCheck,
|
default as ErrorTypeCheck,
|
||||||
default as errorTypeCheck,
|
default as errorTypeCheck,
|
||||||
@@ -33,12 +35,24 @@ import {
|
|||||||
isKeepAliveTaskInstalled,
|
isKeepAliveTaskInstalled,
|
||||||
setupKeepAliveTask,
|
setupKeepAliveTask,
|
||||||
} from "./setup-keep-alive-task";
|
} from "./setup-keep-alive-task";
|
||||||
|
import ensureWindowOnScreen from "./util/ensureWindowOnScreen";
|
||||||
|
import ongoingMemoryDump, { dumpMemoryStatsToFile } from "../util/memUsage";
|
||||||
|
|
||||||
|
const appIconToUse =
|
||||||
|
import.meta.env.VITE_COMPANY === "IMEX" ? imexAppIcon : romeAppIcon;
|
||||||
|
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: "https://ba41d22656999a8c1fd63bcb7df98650@o492140.ingest.us.sentry.io/4509074139447296",
|
dsn: "https://ba41d22656999a8c1fd63bcb7df98650@o492140.ingest.us.sentry.io/4509074139447296",
|
||||||
});
|
});
|
||||||
|
|
||||||
log.initialize();
|
log.initialize();
|
||||||
|
|
||||||
|
// Configure log format to include process ID
|
||||||
|
log.transports.file.format =
|
||||||
|
"[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] [PID:{processId}] {text}";
|
||||||
|
log.transports.console.format =
|
||||||
|
"[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] [PID:{processId}] {text}";
|
||||||
|
log.transports.file.maxSize = 50 * 1024 * 1024; // 50 MB
|
||||||
const isMac: boolean = process.platform === "darwin";
|
const isMac: boolean = process.platform === "darwin";
|
||||||
const protocol: string = "imexmedia";
|
const protocol: string = "imexmedia";
|
||||||
let isAppQuitting = false; //Needed on Mac as an override to allow us to fully quit the app.
|
let isAppQuitting = false; //Needed on Mac as an override to allow us to fully quit the app.
|
||||||
@@ -47,6 +61,14 @@ let isKeepAliveLaunch = false; // Track if launched via keep-alive
|
|||||||
const localServer = new LocalServer();
|
const localServer = new LocalServer();
|
||||||
const gotTheLock = app.requestSingleInstanceLock();
|
const gotTheLock = app.requestSingleInstanceLock();
|
||||||
|
|
||||||
|
if (!gotTheLock) {
|
||||||
|
log.warn(
|
||||||
|
"Another instance is already running and could not obtain mutex lock. Exiting this instance.",
|
||||||
|
);
|
||||||
|
isAppQuitting = true;
|
||||||
|
app.quit(); // Quit the app if another instance is already running
|
||||||
|
}
|
||||||
|
|
||||||
function createWindow(): void {
|
function createWindow(): void {
|
||||||
// Create the browser window.
|
// Create the browser window.
|
||||||
const { width, height, x, y } = store.get("app.windowBounds") as {
|
const { width, height, x, y } = store.get("app.windowBounds") as {
|
||||||
@@ -56,16 +78,23 @@ function createWindow(): void {
|
|||||||
y: number | undefined;
|
y: number | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Validate window position is on screen
|
||||||
|
const { validX, validY } = ensureWindowOnScreen(x, y, width, height);
|
||||||
|
|
||||||
const mainWindow = new BrowserWindow({
|
const mainWindow = new BrowserWindow({
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
x,
|
x: validX,
|
||||||
y,
|
y: validY,
|
||||||
show: false, // Start hidden, show later if not keep-alive
|
show: false, // Start hidden, show later if not keep-alive
|
||||||
minWidth: 600,
|
minWidth: 600,
|
||||||
minHeight: 400,
|
minHeight: 400,
|
||||||
//autoHideMenuBar: true,
|
//autoHideMenuBar: true,
|
||||||
...(process.platform === "linux" ? { icon: appIcon } : {}),
|
...(process.platform === "linux"
|
||||||
|
? {
|
||||||
|
icon: appIconToUse,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
title: "Shop Partner",
|
title: "Shop Partner",
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: join(__dirname, "../preload/index.js"),
|
preload: join(__dirname, "../preload/index.js"),
|
||||||
@@ -239,6 +268,27 @@ function createWindow(): void {
|
|||||||
{
|
{
|
||||||
type: "separator",
|
type: "separator",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Enable Memory Logging",
|
||||||
|
checked: store.get("settings.enableMemDebug") as boolean,
|
||||||
|
type: "checkbox",
|
||||||
|
click: (): void => {
|
||||||
|
const currentSetting = store.get(
|
||||||
|
"settings.enableMemDebug",
|
||||||
|
) as boolean;
|
||||||
|
store.set("settings.enableMemDebug", !currentSetting);
|
||||||
|
log.info("Enable Memory Logging set to", !currentSetting);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Dump Memory Stats Now",
|
||||||
|
click: (): void => {
|
||||||
|
dumpMemoryStatsToFile();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "separator",
|
||||||
|
},
|
||||||
// {
|
// {
|
||||||
// label: "Decode Hardcoded Estimate",
|
// label: "Decode Hardcoded Estimate",
|
||||||
// click: (): void => {
|
// click: (): void => {
|
||||||
@@ -416,9 +466,6 @@ function createWindow(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!gotTheLock) {
|
|
||||||
app.quit(); // Quit the app if another instance is already running
|
|
||||||
}
|
|
||||||
// This method will be called when Electron has finished
|
// This method will be called when Electron has finished
|
||||||
// initialization and is ready to create browser windows.
|
// initialization and is ready to create browser windows.
|
||||||
// Some APIs can only be used after this event occurs.
|
// Some APIs can only be used after this event occurs.
|
||||||
@@ -426,6 +473,8 @@ app.whenReady().then(async () => {
|
|||||||
// Default open or close DevTools by F12 in development
|
// Default open or close DevTools by F12 in development
|
||||||
// and ignore CommandOrControl + R in production.
|
// and ignore CommandOrControl + R in production.
|
||||||
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
|
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
|
||||||
|
log.debug("App is ready, initializing shortcuts and protocol handlers.");
|
||||||
|
|
||||||
if (platform.isWindows) {
|
if (platform.isWindows) {
|
||||||
app.setAppUserModelId("Shop Partner");
|
app.setAppUserModelId("Shop Partner");
|
||||||
}
|
}
|
||||||
@@ -457,38 +506,28 @@ app.whenReady().then(async () => {
|
|||||||
log.warn("Failed to register protocol handler.");
|
log.warn("Failed to register protocol handler.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add this event handler for second instance
|
|
||||||
app.on("second-instance", (_event: Electron.Event, argv: string[]) => {
|
|
||||||
const url = argv.find((arg) => arg.startsWith(`${protocol}://`));
|
|
||||||
if (url) {
|
|
||||||
if (url.startsWith(`${protocol}://keep-alive`)) {
|
|
||||||
log.info("Keep-alive protocol received, app is already running.");
|
|
||||||
// Do nothing if already running
|
|
||||||
return; // Skip openMainWindow to avoid focusing the window
|
|
||||||
} else {
|
|
||||||
openInExplorer(url);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
openMainWindow(); // Focus window if no URL
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//Dynamically load ipcMain handlers once ready.
|
//Dynamically load ipcMain handlers once ready.
|
||||||
try {
|
try {
|
||||||
const module = await import("./ipc/ipcMainConfig");
|
const { initializeCronTasks } = await import("./ipc/ipcMainConfig");
|
||||||
log.debug("Successfully loaded ipcMainConfig");
|
log.debug("Successfully loaded ipcMainConfig");
|
||||||
|
|
||||||
// Initialize cron tasks after loading ipcMainConfig
|
try {
|
||||||
await module.initializeCronTasks();
|
await initializeCronTasks();
|
||||||
log.info("Cron tasks initialized successfully");
|
log.info("Cron tasks initialized successfully");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error("Failed to load ipcMainConfig or initialize cron tasks", {
|
log.warn("Non-fatal: Failed to initialize cron tasks", {
|
||||||
...ErrorTypeCheck(error),
|
...ErrorTypeCheck(error),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log.error("Fatal: Failed to load ipcMainConfig", {
|
||||||
|
...ErrorTypeCheck(error),
|
||||||
|
});
|
||||||
|
throw error; // Adjust based on whether the app should continue
|
||||||
|
}
|
||||||
|
|
||||||
//Create Tray
|
//Create Tray
|
||||||
const trayicon = nativeImage.createFromPath(appIcon);
|
const trayicon = nativeImage.createFromPath(appIconToUse);
|
||||||
const tray = new Tray(trayicon.resize({ width: 16 }));
|
const tray = new Tray(trayicon.resize({ width: 16 }));
|
||||||
const contextMenu = Menu.buildFromTemplate([
|
const contextMenu = Menu.buildFromTemplate([
|
||||||
{
|
{
|
||||||
@@ -513,7 +552,7 @@ app.whenReady().then(async () => {
|
|||||||
|
|
||||||
//Check for app updates.
|
//Check for app updates.
|
||||||
autoUpdater.logger = log;
|
autoUpdater.logger = log;
|
||||||
|
autoUpdater.allowDowngrade = true;
|
||||||
// if (import.meta.env.DEV) {
|
// if (import.meta.env.DEV) {
|
||||||
// // Useful for some dev/debugging tasks, but download can
|
// // Useful for some dev/debugging tasks, but download can
|
||||||
// // not be validated because dev app is not signed
|
// // not be validated because dev app is not signed
|
||||||
@@ -528,19 +567,19 @@ app.whenReady().then(async () => {
|
|||||||
|
|
||||||
autoUpdater.on("checking-for-update", () => {
|
autoUpdater.on("checking-for-update", () => {
|
||||||
log.info("Checking for update...");
|
log.info("Checking for update...");
|
||||||
const mainWindow = BrowserWindow.getAllWindows()[0];
|
const mainWindow = getMainWindow();
|
||||||
mainWindow?.webContents.send(ipcTypes.toRenderer.updates.checking);
|
mainWindow?.webContents.send(ipcTypes.toRenderer.updates.checking);
|
||||||
});
|
});
|
||||||
autoUpdater.on("update-available", (info) => {
|
autoUpdater.on("update-available", (info) => {
|
||||||
log.info("Update available.", info);
|
log.info("Update available.", info);
|
||||||
const mainWindow = BrowserWindow.getAllWindows()[0];
|
const mainWindow = getMainWindow();
|
||||||
mainWindow?.webContents.send(ipcTypes.toRenderer.updates.available, info);
|
mainWindow?.webContents.send(ipcTypes.toRenderer.updates.available, info);
|
||||||
});
|
});
|
||||||
autoUpdater.on("download-progress", (progress) => {
|
autoUpdater.on("download-progress", (progress) => {
|
||||||
log.info(`Download speed: ${progress.bytesPerSecond}`);
|
log.info(`Download speed: ${progress.bytesPerSecond}`);
|
||||||
log.info(`Downloaded ${progress.percent}%`);
|
log.info(`Downloaded ${progress.percent}%`);
|
||||||
log.info(`Total downloaded ${progress.transferred}/${progress.total}`);
|
log.info(`Total downloaded ${progress.transferred}/${progress.total}`);
|
||||||
const mainWindow = BrowserWindow.getAllWindows()[0];
|
const mainWindow = getMainWindow();
|
||||||
mainWindow?.webContents.send(
|
mainWindow?.webContents.send(
|
||||||
ipcTypes.toRenderer.updates.downloading,
|
ipcTypes.toRenderer.updates.downloading,
|
||||||
progress,
|
progress,
|
||||||
@@ -548,7 +587,7 @@ app.whenReady().then(async () => {
|
|||||||
});
|
});
|
||||||
autoUpdater.on("update-downloaded", (info) => {
|
autoUpdater.on("update-downloaded", (info) => {
|
||||||
log.info("Update downloaded", info);
|
log.info("Update downloaded", info);
|
||||||
const mainWindow = BrowserWindow.getAllWindows()[0];
|
const mainWindow = getMainWindow();
|
||||||
mainWindow?.webContents.send(ipcTypes.toRenderer.updates.downloaded, info);
|
mainWindow?.webContents.send(ipcTypes.toRenderer.updates.downloaded, info);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -559,7 +598,8 @@ app.whenReady().then(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//The update itself will run when the bodyshop record is queried to know what release channel to use.
|
//The update itself will run when the bodyshop record is queried to know what release channel to use.
|
||||||
createWindow();
|
openMainWindow();
|
||||||
|
ongoingMemoryDump();
|
||||||
|
|
||||||
app.on("activate", function () {
|
app.on("activate", function () {
|
||||||
openMainWindow();
|
openMainWindow();
|
||||||
@@ -568,20 +608,33 @@ app.whenReady().then(async () => {
|
|||||||
|
|
||||||
app.on("open-url", (event: Electron.Event, url: string) => {
|
app.on("open-url", (event: Electron.Event, url: string) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
//Don't do anything for now. We just want to open the app.
|
|
||||||
if (url.startsWith(`${protocol}://keep-alive`)) {
|
if (url.startsWith(`${protocol}://keep-alive`)) {
|
||||||
log.info("Keep-alive protocol received.");
|
log.info("Keep-alive protocol received.");
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
// Do nothing, whether app is running or not
|
||||||
isKeepAliveLaunch = true;
|
return;
|
||||||
openMainWindow(); // Launch app if not running
|
|
||||||
}
|
|
||||||
// Do nothing if already running
|
|
||||||
return; // Skip openMainWindow to avoid focusing the window
|
|
||||||
} else {
|
} else {
|
||||||
openInExplorer(url);
|
openInExplorer(url);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add this event handler for second instance
|
||||||
|
app.on("second-instance", (_event: Electron.Event, argv: string[]) => {
|
||||||
|
const url = argv.find((arg) => arg.startsWith(`${protocol}://`));
|
||||||
|
if (url) {
|
||||||
|
if (url.startsWith(`${protocol}://keep-alive`)) {
|
||||||
|
log.info(
|
||||||
|
"Keep-alive protocol received, app is already running. Nothing to do.",
|
||||||
|
);
|
||||||
|
// Do nothing if already running
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
log.info("Received Media URL: ", url);
|
||||||
|
openInExplorer(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// No action taken if no URL is provided
|
||||||
|
});
|
||||||
|
|
||||||
// Quit when all windows are closed, except on macOS. There, it's common
|
// Quit when all windows are closed, except on macOS. There, it's common
|
||||||
// for applications and their menu bar to stay active until the user quits
|
// for applications and their menu bar to stay active until the user quits
|
||||||
// explicitly with Cmd + Q.
|
// explicitly with Cmd + Q.
|
||||||
@@ -591,8 +644,7 @@ app.on("window-all-closed", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on("before-quit", (props) => {
|
app.on("before-quit", () => {
|
||||||
console.log(props);
|
|
||||||
preQuitMethods();
|
preQuitMethods();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -622,7 +674,7 @@ function preQuitMethods(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openMainWindow(): void {
|
function openMainWindow(): void {
|
||||||
const mainWindow = BrowserWindow.getAllWindows()[0];
|
const mainWindow = getMainWindow();
|
||||||
if (mainWindow) {
|
if (mainWindow) {
|
||||||
mainWindow.show();
|
mainWindow.show();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
} from "../watcher/watcher";
|
} from "../watcher/watcher";
|
||||||
import { PaintScaleConfig } from "../../util/types/paintScale";
|
import { PaintScaleConfig } from "../../util/types/paintScale";
|
||||||
|
|
||||||
|
|
||||||
// Initialize paint scale input configs in store if not set
|
// Initialize paint scale input configs in store if not set
|
||||||
if (!Store.get("settings.paintScaleInputConfigs")) {
|
if (!Store.get("settings.paintScaleInputConfigs")) {
|
||||||
Store.set("settings.paintScaleInputConfigs", []);
|
Store.set("settings.paintScaleInputConfigs", []);
|
||||||
@@ -131,11 +130,13 @@ const SettingEmsOutFilePathSet = async (): Promise<string> => {
|
|||||||
return (Store.get("settings.emsOutFilePath") as string) || "";
|
return (Store.get("settings.emsOutFilePath") as string) || "";
|
||||||
};
|
};
|
||||||
|
|
||||||
const SettingsPaintScaleInputConfigsGet = async (
|
const SettingsPaintScaleInputConfigsGet = (
|
||||||
_event?: IpcMainInvokeEvent,
|
_event?: IpcMainInvokeEvent,
|
||||||
): Promise<PaintScaleConfig[]> => {
|
): PaintScaleConfig[] => {
|
||||||
try {
|
try {
|
||||||
const configs = Store.get("settings.paintScaleInputConfigs") as PaintScaleConfig[];
|
const configs = Store.get(
|
||||||
|
"settings.paintScaleInputConfigs",
|
||||||
|
) as PaintScaleConfig[];
|
||||||
log.debug("Retrieved paint scale input configs:", configs);
|
log.debug("Retrieved paint scale input configs:", configs);
|
||||||
return configs || [];
|
return configs || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -183,11 +184,13 @@ const SettingsPaintScaleInputPathSet = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const SettingsPaintScaleOutputConfigsGet = async (
|
const SettingsPaintScaleOutputConfigsGet = (
|
||||||
_event?: IpcMainInvokeEvent,
|
_event?: IpcMainInvokeEvent,
|
||||||
): Promise<PaintScaleConfig[]> => {
|
): PaintScaleConfig[] => {
|
||||||
try {
|
try {
|
||||||
const configs = Store.get("settings.paintScaleOutputConfigs") as PaintScaleConfig[];
|
const configs = Store.get(
|
||||||
|
"settings.paintScaleOutputConfigs",
|
||||||
|
) as PaintScaleConfig[];
|
||||||
log.debug("Retrieved paint scale output configs:", configs);
|
log.debug("Retrieved paint scale output configs:", configs);
|
||||||
return configs || [];
|
return configs || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -20,19 +20,26 @@ const ipcMainHandleAuthStateChanged = async (
|
|||||||
user: User | null,
|
user: User | null,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
Store.set("user", user);
|
Store.set("user", user);
|
||||||
|
log.debug("Received authentication state change from Renderer.", user);
|
||||||
|
await setReleaseChannel();
|
||||||
|
checkForAppUpdatesContinuously();
|
||||||
|
};
|
||||||
|
|
||||||
|
async function setReleaseChannel() {
|
||||||
try {
|
try {
|
||||||
//Need to query the currently active shop, and store the metadata as well.
|
//Need to query the currently active shop, and store the metadata as well.
|
||||||
//Also need to query the OP Codes for decoding reference.
|
//Also need to query the OP Codes for decoding reference.
|
||||||
log.debug("Received authentication state change from Renderer.", user);
|
await handleShopMetaDataFetch();
|
||||||
handleShopMetaDataFetch();
|
|
||||||
//Check for updates
|
//Check for updates
|
||||||
const convCo = Store.get("app.bodyshop");
|
const bodyshop = Store.get("app.bodyshop");
|
||||||
if (convCo === "alpha") {
|
if (bodyshop?.convenient_company?.toLowerCase() === "alpha") {
|
||||||
autoUpdater.channel = "alpha";
|
autoUpdater.channel = "alpha";
|
||||||
log.debug("Setting update channel to ALPHA channel.");
|
log.debug("Setting update channel to ALPHA channel.");
|
||||||
} else if (convCo === "beta") {
|
} else if (bodyshop?.convenient_company?.toLowerCase() === "beta") {
|
||||||
autoUpdater.channel = "beta";
|
autoUpdater.channel = "beta";
|
||||||
log.debug("Setting update channel to BETA channel.");
|
log.debug("Setting update channel to BETA channel.");
|
||||||
|
} else {
|
||||||
|
log.debug("Setting update channel to LATEST channel.");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(
|
log.error(
|
||||||
@@ -44,8 +51,7 @@ const ipcMainHandleAuthStateChanged = async (
|
|||||||
"Error connecting to ImEX Online servers to get shop data. Please try again.",
|
"Error connecting to ImEX Online servers to get shop data. Please try again.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
checkForAppUpdatesContinuously();
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const handleShopMetaDataFetch = async (
|
const handleShopMetaDataFetch = async (
|
||||||
reloadWindow?: boolean,
|
reloadWindow?: boolean,
|
||||||
@@ -89,7 +95,8 @@ const ipMainHandleResetPassword = async (): Promise<void> => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
handleShopMetaDataFetch,
|
||||||
ipcMainHandleAuthStateChanged,
|
ipcMainHandleAuthStateChanged,
|
||||||
ipMainHandleResetPassword,
|
ipMainHandleResetPassword,
|
||||||
handleShopMetaDataFetch,
|
setReleaseChannel,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,24 +5,43 @@ import axios from "axios";
|
|||||||
import { create } from "xmlbuilder2";
|
import { create } from "xmlbuilder2";
|
||||||
import { parseStringPromise } from "xml2js";
|
import { parseStringPromise } from "xml2js";
|
||||||
import store from "../../store/store";
|
import store from "../../store/store";
|
||||||
import client from "../../graphql/graphql-client";
|
import client, { getTokenFromRenderer } from "../../graphql/graphql-client";
|
||||||
import { PaintScaleConfig } from "../../../util/types/paintScale";
|
import { PaintScaleConfig } from "../../../util/types/paintScale";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import {
|
||||||
|
PPG_DATA_QUERY_TYPED,
|
||||||
|
PpgDataQueryResult,
|
||||||
|
PpgDataQueryVariables,
|
||||||
|
} from "../../graphql/queries";
|
||||||
|
|
||||||
// PPG Input Handler
|
|
||||||
export async function ppgInputHandler(config: PaintScaleConfig): Promise<void> {
|
export async function ppgInputHandler(config: PaintScaleConfig): Promise<void> {
|
||||||
try {
|
try {
|
||||||
log.info(
|
log.info(
|
||||||
`Polling input directory for PPG config ${config.id}: ${config.path}`,
|
`Polling input directory for PPG config ${config.id}: ${config.path}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
log.debug(
|
||||||
|
`Archive dir: ${path.join(config.path!, "archive")}, Error dir: ${path.join(config.path!, "error")}`,
|
||||||
|
);
|
||||||
|
|
||||||
// Ensure archive and error directories exist
|
// Ensure archive and error directories exist
|
||||||
const archiveDir = path.join(config.path!, "archive");
|
const archiveDir = path.join(config.path!, "archive");
|
||||||
const errorDir = path.join(config.path!, "error");
|
const errorDir = path.join(config.path!, "error");
|
||||||
|
try {
|
||||||
await fs.mkdir(archiveDir, { recursive: true });
|
await fs.mkdir(archiveDir, { recursive: true });
|
||||||
await fs.mkdir(errorDir, { recursive: true });
|
await fs.mkdir(errorDir, { recursive: true });
|
||||||
|
log.debug(
|
||||||
|
`Archive and error directories ensured: ${archiveDir}, ${errorDir}`,
|
||||||
|
);
|
||||||
|
} catch (dirError) {
|
||||||
|
log.error(`Failed to create directories for ${config.path}:`, dirError);
|
||||||
|
throw dirError;
|
||||||
|
}
|
||||||
|
|
||||||
// Check for files
|
// Check for files
|
||||||
const files = await fs.readdir(config.path!);
|
const files = await fs.readdir(config.path!);
|
||||||
|
log.debug(`Found ${files.length} files in ${config.path}:`, files);
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
// Only process XML files
|
// Only process XML files
|
||||||
if (!file.toLowerCase().endsWith(".xml")) {
|
if (!file.toLowerCase().endsWith(".xml")) {
|
||||||
@@ -30,10 +49,15 @@ export async function ppgInputHandler(config: PaintScaleConfig): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const filePath = path.join(config.path!, file);
|
const filePath = path.join(config.path!, file);
|
||||||
|
try {
|
||||||
const stats = await fs.stat(filePath);
|
const stats = await fs.stat(filePath);
|
||||||
if (!stats.isFile()) {
|
if (!stats.isFile()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
} catch (statError) {
|
||||||
|
log.warn(`Failed to stat file ${filePath}:`, statError);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
log.debug(`Processing input file: ${filePath}`);
|
log.debug(`Processing input file: ${filePath}`);
|
||||||
|
|
||||||
@@ -46,40 +70,61 @@ export async function ppgInputHandler(config: PaintScaleConfig): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate XML structure
|
// Validate XML structure
|
||||||
let xmlContent : BlobPart;
|
let xmlContent: BlobPart;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
xmlContent = await fs.readFile(filePath, "utf8");
|
xmlContent = await fs.readFile(filePath, "utf8");
|
||||||
await parseStringPromise(xmlContent);
|
await parseStringPromise(xmlContent);
|
||||||
|
log.debug(`Successfully validated XML for ${filePath}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(`Invalid XML in ${filePath}:`, error);
|
log.error(`Invalid XML in ${filePath}:`, error);
|
||||||
const timestamp = Date.now().toString(); // similar to DateTime.Now.Ticks in C#
|
const timestamp = dayjs().format("YYYYMMDD_HHmmss");
|
||||||
const errorPath = path.join(errorDir, `${timestamp}.xml`);
|
const originalFilename = path.basename(file, path.extname(file));
|
||||||
|
const errorPath = path.join(
|
||||||
|
errorDir,
|
||||||
|
`${originalFilename}-${timestamp}.xml`,
|
||||||
|
);
|
||||||
|
try {
|
||||||
await fs.rename(filePath, errorPath);
|
await fs.rename(filePath, errorPath);
|
||||||
log.debug(`Moved invalid file to error: ${errorPath}`);
|
log.debug(`Moved invalid file to error: ${errorPath}`);
|
||||||
|
} catch (moveError) {
|
||||||
|
log.error(
|
||||||
|
`Failed to move invalid file to error directory ${errorPath}:`,
|
||||||
|
moveError,
|
||||||
|
);
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get authentication token
|
// Get authentication token
|
||||||
const token = (store.get("user") as any)?.stsTokenManager?.accessToken;
|
let token: string | null;
|
||||||
|
try {
|
||||||
|
token = await getTokenFromRenderer();
|
||||||
if (!token) {
|
if (!token) {
|
||||||
log.error(`No authentication token for file: ${filePath}`);
|
log.error(`No authentication token for file: ${filePath}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
log.debug(
|
||||||
|
`Obtained authentication token for ${filePath}: ${token.slice(0, 10)}...`,
|
||||||
|
);
|
||||||
|
} catch (tokenError) {
|
||||||
|
log.error(
|
||||||
|
`Failed to obtain authentication token for ${filePath}:`,
|
||||||
|
tokenError,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Upload file to API
|
// Upload file to API
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", new Blob([xmlContent]), path.basename(filePath));
|
formData.append("file", new Blob([xmlContent]), path.basename(filePath));
|
||||||
formData.append(
|
const shopId = (store.get("app.bodyshop") as any)?.shopname || "";
|
||||||
"shopId",
|
formData.append("shopId", shopId);
|
||||||
(store.get("app.bodyshop") as any)?.shopname || "",
|
log.debug(`Shop ID: ${shopId}`);
|
||||||
);
|
|
||||||
|
|
||||||
const baseURL = store.get("app.isTest")
|
const baseURL = store.get("app.isTest")
|
||||||
? import.meta.env.VITE_API_TEST_URL
|
? import.meta.env.VITE_API_TEST_URL
|
||||||
: import.meta.env.VITE_API_URL;
|
: import.meta.env.VITE_API_URL;
|
||||||
const finalUrl = `${baseURL}/mixdata/upload`;
|
const finalUrl = `${baseURL}/mixdata/upload`;
|
||||||
|
|
||||||
log.debug(`Uploading file to ${finalUrl}`);
|
log.debug(`Uploading file to ${finalUrl}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -88,23 +133,52 @@ export async function ppgInputHandler(config: PaintScaleConfig): Promise<void> {
|
|||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
"Content-Type": "multipart/form-data",
|
"Content-Type": "multipart/form-data",
|
||||||
},
|
},
|
||||||
|
timeout: 10000, // 10-second timeout
|
||||||
|
});
|
||||||
|
|
||||||
|
log.info(`Upload response for ${filePath}:`, {
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
data: response.data,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
log.info(`Successful upload of ${filePath}`);
|
log.info(`Successful upload of ${filePath}`);
|
||||||
// Move file to archive
|
// Move file to archive
|
||||||
const timestamp = Date.now().toString(); // generate new timestamp
|
const timestamp = dayjs().format("YYYYMMDD_HHmmss");
|
||||||
const archivePath = path.join(archiveDir, `${timestamp}.xml`);
|
const originalFilename = path.basename(file, path.extname(file));
|
||||||
|
const archivePath = path.join(
|
||||||
|
archiveDir,
|
||||||
|
`${originalFilename}-${timestamp}.xml`,
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await fs.access(archiveDir, fs.constants.W_OK); // Verify archiveDir is writable
|
||||||
await fs.rename(filePath, archivePath);
|
await fs.rename(filePath, archivePath);
|
||||||
log.debug(`Moved file to archive: ${archivePath}`);
|
log.info(`Moved file to archive: ${archivePath}`);
|
||||||
|
} catch (moveError) {
|
||||||
|
log.error(
|
||||||
|
`Failed to move file to archive directory ${archivePath}:`,
|
||||||
|
moveError,
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
log.error(
|
log.error(
|
||||||
`Failed to upload ${filePath}: ${response.status} ${response.statusText}`,
|
`Failed to upload ${filePath}: ${response.status} ${response.statusText}`,
|
||||||
response.data,
|
{ responseData: response.data },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
log.error(`Error uploading ${filePath}:`, error);
|
log.error(`Error uploading ${filePath}:`, {
|
||||||
|
message: error.message,
|
||||||
|
code: error.code,
|
||||||
|
response: error.response
|
||||||
|
? {
|
||||||
|
status: error.response.status,
|
||||||
|
statusText: error.response.statusText,
|
||||||
|
data: error.response.data,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -121,70 +195,16 @@ export async function ppgOutputHandler(
|
|||||||
|
|
||||||
await fs.mkdir(config.path!, { recursive: true });
|
await fs.mkdir(config.path!, { recursive: true });
|
||||||
|
|
||||||
const query = `
|
const variables: PpgDataQueryVariables = {
|
||||||
query PpgData($today: timestamptz!, $todayplus5: timestamptz!, $shopid: uuid!) {
|
today: dayjs().toISOString(),
|
||||||
bodyshops_by_pk(id:$shopid) {
|
todayplus5: dayjs().add(5, "day").toISOString(),
|
||||||
id
|
|
||||||
shopname
|
|
||||||
imexshopid
|
|
||||||
}
|
|
||||||
jobs(where: {
|
|
||||||
_or: [
|
|
||||||
{
|
|
||||||
_and: [
|
|
||||||
{ scheduled_in: { _lte: $todayplus5 } },
|
|
||||||
{ scheduled_in: { _gte: $today } }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{ inproduction: { _eq: true } }
|
|
||||||
]
|
|
||||||
}) {
|
|
||||||
id
|
|
||||||
ro_number
|
|
||||||
status
|
|
||||||
ownr_fn
|
|
||||||
ownr_ln
|
|
||||||
ownr_co_nm
|
|
||||||
v_vin
|
|
||||||
v_model_yr
|
|
||||||
v_make_desc
|
|
||||||
v_model_desc
|
|
||||||
v_color
|
|
||||||
plate_no
|
|
||||||
ins_co_nm
|
|
||||||
est_ct_fn
|
|
||||||
est_ct_ln
|
|
||||||
rate_mapa
|
|
||||||
rate_lab
|
|
||||||
job_totals
|
|
||||||
vehicle {
|
|
||||||
v_paint_codes
|
|
||||||
}
|
|
||||||
labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }) {
|
|
||||||
aggregate {
|
|
||||||
sum {
|
|
||||||
mod_lb_hrs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }) {
|
|
||||||
aggregate {
|
|
||||||
sum {
|
|
||||||
mod_lb_hrs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const variables = {
|
|
||||||
today: new Date().toISOString(),
|
|
||||||
todayplus5: new Date(Date.now() + 5 * 86400000).toISOString(),
|
|
||||||
shopid: (store.get("app.bodyshop") as any)?.id,
|
shopid: (store.get("app.bodyshop") as any)?.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = (await client.request(query, variables)) as any;
|
const response = await client.request<
|
||||||
|
PpgDataQueryResult,
|
||||||
|
PpgDataQueryVariables
|
||||||
|
>(PPG_DATA_QUERY_TYPED, variables);
|
||||||
const jobs = response.jobs ?? [];
|
const jobs = response.jobs ?? [];
|
||||||
|
|
||||||
const header = {
|
const header = {
|
||||||
@@ -197,18 +217,10 @@ export async function ppgOutputHandler(
|
|||||||
},
|
},
|
||||||
Transaction: {
|
Transaction: {
|
||||||
TransactionID: "",
|
TransactionID: "",
|
||||||
TransactionDate: (() => {
|
TransactionDate: dayjs().format("YYYY-MM-DD:HH:mm"),
|
||||||
const now = new Date();
|
|
||||||
const year = now.getFullYear();
|
|
||||||
const month = ("0" + (now.getMonth() + 1)).slice(-2);
|
|
||||||
const day = ("0" + now.getDate()).slice(-2);
|
|
||||||
const hours = ("0" + now.getHours()).slice(-2);
|
|
||||||
const minutes = ("0" + now.getMinutes()).slice(-2);
|
|
||||||
return `${year}-${month}-${day}:${hours}:${minutes}`;
|
|
||||||
})(),
|
|
||||||
},
|
},
|
||||||
Product: {
|
Product: {
|
||||||
Name: "ImEX Online",
|
Name: import.meta.env.VITE_COMPANY === "IMEX",
|
||||||
Version: "",
|
Version: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -220,7 +232,7 @@ export async function ppgOutputHandler(
|
|||||||
},
|
},
|
||||||
RepairOrders: {
|
RepairOrders: {
|
||||||
ROCount: jobs.length.toString(),
|
ROCount: jobs.length.toString(),
|
||||||
RO: jobs.map((job: any) => ({
|
RO: jobs.map((job) => ({
|
||||||
RONumber: job.ro_number || "",
|
RONumber: job.ro_number || "",
|
||||||
ROStatus: "Open",
|
ROStatus: "Open",
|
||||||
Customer: `${job.ownr_ln || ""}, ${job.ownr_fn || ""}`,
|
Customer: `${job.ownr_ln || ""}, ${job.ownr_fn || ""}`,
|
||||||
@@ -258,4 +270,3 @@ export async function ppgOutputHandler(
|
|||||||
log.error(`Error generating PPG output for config ${config.id}:`, error);
|
log.error(`Error generating PPG output for config ${config.id}:`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const handlePartsPriceChangeRequest = async (
|
|||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
//Route handler here only.
|
//Route handler here only.
|
||||||
|
|
||||||
const { job } = req.body as { job: PpcJob };
|
const job = req.body as PpcJob;
|
||||||
try {
|
try {
|
||||||
await generatePartsPriceChange(job);
|
await generatePartsPriceChange(job);
|
||||||
res.status(200).json({ success: true });
|
res.status(200).json({ success: true });
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export async function setupKeepAliveAgent(): Promise<void> {
|
|||||||
<string>com.convenientbrands.bodyshop-desktop.keepalive</string>
|
<string>com.convenientbrands.bodyshop-desktop.keepalive</string>
|
||||||
<key>ProgramArguments</key>
|
<key>ProgramArguments</key>
|
||||||
<array>
|
<array>
|
||||||
<string>open</string>
|
<string>Shop Partner Keep Alive</string>
|
||||||
<string>imexmedia://keep-alive</string>
|
<string>imexmedia://keep-alive</string>
|
||||||
</array>
|
</array>
|
||||||
<key>RunAtLoad</key>
|
<key>RunAtLoad</key>
|
||||||
@@ -31,7 +31,7 @@ export async function setupKeepAliveAgent(): Promise<void> {
|
|||||||
|
|
||||||
const plistPath = join(
|
const plistPath = join(
|
||||||
homedir(),
|
homedir(),
|
||||||
"Library/LaunchAgents/com.convenientbrands.bodyshop-desktop.keepalive.plist",
|
"/Library/LaunchAgents/com.convenientbrands.bodyshop-desktop.keepalive.plist",
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -40,7 +40,9 @@ export async function setupKeepAliveAgent(): Promise<void> {
|
|||||||
log.info(`Launch agent created and loaded: ${stdout}`);
|
log.info(`Launch agent created and loaded: ${stdout}`);
|
||||||
if (stderr) log.warn(`Launch agent stderr: ${stderr}`);
|
if (stderr) log.warn(`Launch agent stderr: ${stderr}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(`Error setting up launch agent: ${error instanceof Error ? error.message : String(error)}`);
|
log.error(
|
||||||
|
`Error setting up launch agent: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
throw error; // Rethrow to allow caller to handle
|
throw error; // Rethrow to allow caller to handle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -48,7 +50,7 @@ export async function setupKeepAliveAgent(): Promise<void> {
|
|||||||
export async function isKeepAliveAgentInstalled(): Promise<boolean> {
|
export async function isKeepAliveAgentInstalled(): Promise<boolean> {
|
||||||
const plistPath = join(
|
const plistPath = join(
|
||||||
homedir(),
|
homedir(),
|
||||||
"Library/LaunchAgents/com.convenientbrands.bodyshop-desktop.keepalive.plist",
|
"/Library/LaunchAgents/com.convenientbrands.bodyshop-desktop.keepalive.plist",
|
||||||
);
|
);
|
||||||
const maxRetries = 3;
|
const maxRetries = 3;
|
||||||
const retryDelay = 500; // 500ms delay between retries
|
const retryDelay = 500; // 500ms delay between retries
|
||||||
@@ -56,10 +58,14 @@ export async function isKeepAliveAgentInstalled(): Promise<boolean> {
|
|||||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
try {
|
try {
|
||||||
await fs.access(plistPath, fs.constants.F_OK);
|
await fs.access(plistPath, fs.constants.F_OK);
|
||||||
const { stdout } = await execPromise(`launchctl list | grep com.convenientbrands.bodyshop-desktop.keepalive`);
|
const { stdout } = await execPromise(
|
||||||
|
`launchctl list | grep com.convenientbrands.bodyshop-desktop.keepalive`,
|
||||||
|
);
|
||||||
return !!stdout; // Return true if plist exists and agent is loaded
|
return !!stdout; // Return true if plist exists and agent is loaded
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.debug(`Launch agent not found (attempt ${attempt}/${maxRetries}): ${error instanceof Error ? error.message : String(error)}`);
|
log.debug(
|
||||||
|
`Launch agent not found (attempt ${attempt}/${maxRetries}): ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
if (attempt === maxRetries) {
|
if (attempt === maxRetries) {
|
||||||
return false; // Return false after all retries fail
|
return false; // Return false after all retries fail
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ const execPromise = promisify(exec);
|
|||||||
|
|
||||||
// Define the interval as a variable (in minutes)
|
// Define the interval as a variable (in minutes)
|
||||||
const KEEP_ALIVE_INTERVAL_MINUTES = 15;
|
const KEEP_ALIVE_INTERVAL_MINUTES = 15;
|
||||||
|
const taskName = "ShopPartnerKeepAlive";
|
||||||
|
|
||||||
export async function setupKeepAliveTask(): Promise<void> {
|
export async function setupKeepAliveTask(): Promise<void> {
|
||||||
const taskName = "ImEXShopPartnerKeepAlive";
|
|
||||||
const protocolUrl = "imexmedia://keep-alive";
|
const protocolUrl = "imexmedia://keep-alive";
|
||||||
// Use rundll32.exe to silently open the URL as a protocol
|
// Use rundll32.exe to silently open the URL as a protocol
|
||||||
const command = `rundll32.exe url.dll,OpenURL "${protocolUrl}"`;
|
const command = `rundll32.exe url.dll,OpenURL "${protocolUrl}"`;
|
||||||
@@ -30,7 +30,6 @@ export async function setupKeepAliveTask(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function isKeepAliveTaskInstalled(): Promise<boolean> {
|
export async function isKeepAliveTaskInstalled(): Promise<boolean> {
|
||||||
const taskName = "ImEXShopPartnerKeepAlive";
|
|
||||||
const maxRetries = 3;
|
const maxRetries = 3;
|
||||||
const retryDelay = 500; // 500ms delay between retries
|
const retryDelay = 500; // 500ms delay between retries
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const store = new Store({
|
|||||||
emsOutFilePath: null,
|
emsOutFilePath: null,
|
||||||
qbFilePath: "",
|
qbFilePath: "",
|
||||||
runWatcherOnStartup: true,
|
runWatcherOnStartup: true,
|
||||||
|
enableMemDebug: false,
|
||||||
polling: {
|
polling: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
interval: 30000,
|
interval: 30000,
|
||||||
|
|||||||
@@ -1,19 +1,23 @@
|
|||||||
import { autoUpdater } from "electron-updater";
|
import { autoUpdater } from "electron-updater";
|
||||||
|
import { setReleaseChannel } from "../ipc/ipcMainHandler.user";
|
||||||
|
|
||||||
function checkForAppUpdatesContinuously(): void {
|
let continuousUpdatesTriggered = false;
|
||||||
|
|
||||||
|
async function checkForAppUpdatesContinuously(): Promise<void> {
|
||||||
|
if (!continuousUpdatesTriggered) {
|
||||||
|
continuousUpdatesTriggered = true;
|
||||||
checkForAppUpdates();
|
checkForAppUpdates();
|
||||||
setInterval(
|
setInterval(
|
||||||
() => {
|
() => {
|
||||||
checkForAppUpdatesContinuously();
|
checkForAppUpdates();
|
||||||
},
|
},
|
||||||
1000 * 60 * 30,
|
1000 * 60 * 30,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
function checkForAppUpdates(): void {
|
async function checkForAppUpdates(): Promise<void> {
|
||||||
autoUpdater.checkForUpdatesAndNotify({
|
await setReleaseChannel();
|
||||||
title: "Shop Partner Update",
|
autoUpdater.checkForUpdates();
|
||||||
body: "A new version of Shop Partner is available. Click to update.",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { checkForAppUpdatesContinuously, checkForAppUpdates };
|
export { checkForAppUpdates, checkForAppUpdatesContinuously };
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export const ttlFieldLineDescriptors: FieldDescriptor[] = [
|
|||||||
decimalPlaces: 2,
|
decimalPlaces: 2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "N_SUPP_ANT",
|
name: "N_SUPP_AMT",
|
||||||
type: "N",
|
type: "N",
|
||||||
size: 10,
|
size: 10,
|
||||||
decimalPlaces: 2,
|
decimalPlaces: 2,
|
||||||
|
|||||||
109
src/main/util/ensureWindowOnScreen.ts
Normal file
109
src/main/util/ensureWindowOnScreen.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { screen } from "electron";
|
||||||
|
|
||||||
|
function ensureWindowOnScreen(
|
||||||
|
x: number | undefined,
|
||||||
|
y: number | undefined,
|
||||||
|
windowWidth: number,
|
||||||
|
windowHeight: number,
|
||||||
|
): { validX: number | undefined; validY: number | undefined } {
|
||||||
|
// If no coordinates stored, let Electron position window automatically
|
||||||
|
if (x === undefined || y === undefined) {
|
||||||
|
return { validX: undefined, validY: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
const displays = screen.getAllDisplays();
|
||||||
|
|
||||||
|
// Minimum visible pixels required on each edge to be considered "visible enough"
|
||||||
|
const MIN_VISIBLE = 50; // Ensure at least 50px from each edge is visible
|
||||||
|
|
||||||
|
// Try to find a display where the window would be almost fully visible
|
||||||
|
for (const display of displays) {
|
||||||
|
const { bounds } = display;
|
||||||
|
|
||||||
|
// Check if window is mostly within this display
|
||||||
|
if (
|
||||||
|
x + MIN_VISIBLE >= bounds.x &&
|
||||||
|
x + windowWidth - MIN_VISIBLE <= bounds.x + bounds.width &&
|
||||||
|
y + MIN_VISIBLE >= bounds.y &&
|
||||||
|
y + windowHeight - MIN_VISIBLE <= bounds.y + bounds.height
|
||||||
|
) {
|
||||||
|
// Window is adequately visible on this display
|
||||||
|
return { validX: x, validY: y };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If window isn't adequately visible on any display, try to adjust it to fit the closest display
|
||||||
|
const closestDisplay = findClosestDisplay(displays, x, y);
|
||||||
|
const { bounds } = closestDisplay;
|
||||||
|
|
||||||
|
// Adjust position to ensure window is fully on screen
|
||||||
|
let adjustedX = x;
|
||||||
|
let adjustedY = y;
|
||||||
|
|
||||||
|
// Adjust horizontal position if needed
|
||||||
|
if (x < bounds.x) {
|
||||||
|
adjustedX = bounds.x;
|
||||||
|
} else if (x + windowWidth > bounds.x + bounds.width) {
|
||||||
|
adjustedX = bounds.x + bounds.width - windowWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust vertical position if needed
|
||||||
|
if (y < bounds.y) {
|
||||||
|
adjustedY = bounds.y;
|
||||||
|
} else if (y + windowHeight > bounds.y + bounds.height) {
|
||||||
|
adjustedY = bounds.y + bounds.height - windowHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If adjustments keep window on screen, use adjusted position
|
||||||
|
if (
|
||||||
|
adjustedX >= bounds.x &&
|
||||||
|
adjustedX + windowWidth <= bounds.x + bounds.width &&
|
||||||
|
adjustedY >= bounds.y &&
|
||||||
|
adjustedY + windowHeight <= bounds.y + bounds.height
|
||||||
|
) {
|
||||||
|
return { validX: adjustedX, validY: adjustedY };
|
||||||
|
}
|
||||||
|
|
||||||
|
// If all else fails, center on primary display
|
||||||
|
const primaryDisplay = screen.getPrimaryDisplay();
|
||||||
|
const primaryBounds = primaryDisplay.bounds;
|
||||||
|
|
||||||
|
return {
|
||||||
|
validX: Math.floor(
|
||||||
|
primaryBounds.x + (primaryBounds.width - windowWidth) / 2,
|
||||||
|
),
|
||||||
|
validY: Math.floor(
|
||||||
|
primaryBounds.y + (primaryBounds.height - windowHeight) / 2,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to find the closest display to a point
|
||||||
|
function findClosestDisplay(
|
||||||
|
displays: Electron.Display[],
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
): Electron.Display {
|
||||||
|
let closestDisplay = displays[0];
|
||||||
|
let shortestDistance = Number.MAX_VALUE;
|
||||||
|
|
||||||
|
for (const display of displays) {
|
||||||
|
const { bounds } = display;
|
||||||
|
// Calculate distance to center of display
|
||||||
|
const displayCenterX = bounds.x + bounds.width / 2;
|
||||||
|
const displayCenterY = bounds.y + bounds.height / 2;
|
||||||
|
|
||||||
|
const distance = Math.sqrt(
|
||||||
|
Math.pow(x - displayCenterX, 2) + Math.pow(y - displayCenterY, 2),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (distance < shortestDistance) {
|
||||||
|
shortestDistance = distance;
|
||||||
|
closestDisplay = display;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return closestDisplay;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ensureWindowOnScreen;
|
||||||
@@ -7,6 +7,7 @@ import errorTypeCheck from "../../util/errorTypeCheck";
|
|||||||
import ipcTypes from "../../util/ipcTypes.json";
|
import ipcTypes from "../../util/ipcTypes.json";
|
||||||
import ImportJob from "../decoder/decoder";
|
import ImportJob from "../decoder/decoder";
|
||||||
import store from "../store/store";
|
import store from "../store/store";
|
||||||
|
import getMainWindow from "../../util/getMainWindow";
|
||||||
let watcher: FSWatcher | null;
|
let watcher: FSWatcher | null;
|
||||||
|
|
||||||
async function StartWatcher(): Promise<boolean> {
|
async function StartWatcher(): Promise<boolean> {
|
||||||
@@ -42,7 +43,10 @@ async function StartWatcher(): Promise<boolean> {
|
|||||||
watcher = chokidar.watch(filePaths, {
|
watcher = chokidar.watch(filePaths, {
|
||||||
ignored: (filepath, stats) => {
|
ignored: (filepath, stats) => {
|
||||||
const p = path.parse(filepath);
|
const p = path.parse(filepath);
|
||||||
return !stats?.isFile() && p.ext !== "" && p.ext.toUpperCase() !== ".ENV"; //Only watch for .ENV files.
|
return (
|
||||||
|
(!stats?.isFile() && p.ext !== "" && p.ext.toUpperCase() !== ".ENV") ||
|
||||||
|
p.name?.toUpperCase() === ".DS_STORE"
|
||||||
|
); //Only watch for .ENV files.
|
||||||
},
|
},
|
||||||
usePolling: pollingSettings.enabled || false,
|
usePolling: pollingSettings.enabled || false,
|
||||||
interval: pollingSettings.interval || 30000,
|
interval: pollingSettings.interval || 30000,
|
||||||
@@ -104,23 +108,23 @@ function addWatcherPath(path: string | string[]): void {
|
|||||||
|
|
||||||
function onWatcherReady(): void {
|
function onWatcherReady(): void {
|
||||||
if (watcher) {
|
if (watcher) {
|
||||||
const mainWindow = BrowserWindow.getAllWindows()[0]; //TODO: Filter to only main window once a proper key has been set.
|
const mainWindow = getMainWindow();
|
||||||
new Notification({
|
new Notification({
|
||||||
title: "Watcher Started",
|
title: "Watcher Started",
|
||||||
body: "Newly exported estimates will be automatically uploaded.",
|
body: "Newly exported estimates will be automatically uploaded.",
|
||||||
}).show();
|
}).show();
|
||||||
log.info("Confirmed watched paths:", watcher.getWatched());
|
log.info("Confirmed watched paths:", watcher.getWatched());
|
||||||
mainWindow.webContents.send(ipcTypes.toRenderer.watcher.started);
|
mainWindow?.webContents.send(ipcTypes.toRenderer.watcher.started);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function StopWatcher(): Promise<boolean> {
|
async function StopWatcher(): Promise<boolean> {
|
||||||
const mainWindow = BrowserWindow.getAllWindows()[0]; //TODO: Filter to only main window once a proper key has been set.
|
const mainWindow = getMainWindow();
|
||||||
|
|
||||||
if (watcher) {
|
if (watcher) {
|
||||||
await watcher.close();
|
await watcher.close();
|
||||||
log.info("Watcher stopped.");
|
log.info("Watcher stopped.");
|
||||||
mainWindow.webContents.send(ipcTypes.toRenderer.watcher.stopped);
|
mainWindow?.webContents.send(ipcTypes.toRenderer.watcher.stopped);
|
||||||
|
|
||||||
new Notification({
|
new Notification({
|
||||||
title: "Watcher Stopped",
|
title: "Watcher Stopped",
|
||||||
|
|||||||
@@ -1,25 +1,33 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from "react";
|
||||||
import ipcTypes from '../../../../../util/ipcTypes.json';
|
import ipcTypes from "../../../../../util/ipcTypes.json";
|
||||||
import { PaintScaleConfig, PaintScaleType } from '../../../../../util/types/paintScale';
|
import {
|
||||||
|
PaintScaleConfig,
|
||||||
|
PaintScaleType,
|
||||||
|
} from "../../../../../util/types/paintScale";
|
||||||
import { message } from "antd";
|
import { message } from "antd";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
type ConfigType = 'input' | 'output';
|
type ConfigType = "input" | "output";
|
||||||
|
|
||||||
export const usePaintScaleConfig = (configType: ConfigType) => {
|
export const usePaintScaleConfig = (configType: ConfigType) => {
|
||||||
const [paintScaleConfigs, setPaintScaleConfigs] = useState<PaintScaleConfig[]>([]);
|
const [paintScaleConfigs, setPaintScaleConfigs] = useState<
|
||||||
|
PaintScaleConfig[]
|
||||||
|
>([]);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
// Get the appropriate IPC methods based on config type
|
// Get the appropriate IPC methods based on config type
|
||||||
const getConfigsMethod = configType === 'input'
|
const getConfigsMethod =
|
||||||
|
configType === "input"
|
||||||
? ipcTypes.toMain.settings.paintScale.getInputConfigs
|
? ipcTypes.toMain.settings.paintScale.getInputConfigs
|
||||||
: ipcTypes.toMain.settings.paintScale.getOutputConfigs;
|
: ipcTypes.toMain.settings.paintScale.getOutputConfigs;
|
||||||
|
|
||||||
const setConfigsMethod = configType === 'input'
|
const setConfigsMethod =
|
||||||
|
configType === "input"
|
||||||
? ipcTypes.toMain.settings.paintScale.setInputConfigs
|
? ipcTypes.toMain.settings.paintScale.setInputConfigs
|
||||||
: ipcTypes.toMain.settings.paintScale.setOutputConfigs;
|
: ipcTypes.toMain.settings.paintScale.setOutputConfigs;
|
||||||
|
|
||||||
const setPathMethod = configType === 'input'
|
const setPathMethod =
|
||||||
|
configType === "input"
|
||||||
? ipcTypes.toMain.settings.paintScale.setInputPath
|
? ipcTypes.toMain.settings.paintScale.setInputPath
|
||||||
: ipcTypes.toMain.settings.paintScale.setOutputPath;
|
: ipcTypes.toMain.settings.paintScale.setOutputPath;
|
||||||
|
|
||||||
@@ -29,15 +37,19 @@ export const usePaintScaleConfig = (configType: ConfigType) => {
|
|||||||
.invoke(getConfigsMethod)
|
.invoke(getConfigsMethod)
|
||||||
.then((configs: PaintScaleConfig[]) => {
|
.then((configs: PaintScaleConfig[]) => {
|
||||||
// Ensure all configs have a pollingInterval and type (for backward compatibility)
|
// Ensure all configs have a pollingInterval and type (for backward compatibility)
|
||||||
const updatedConfigs = configs.map(config => ({
|
const defaultPolling = configType === "input" ? 1440 : 60;
|
||||||
|
const updatedConfigs = configs.map((config) => ({
|
||||||
...config,
|
...config,
|
||||||
pollingInterval: config.pollingInterval || 1440, // Default to 1440 seconds
|
pollingInterval: config.pollingInterval || defaultPolling, // Default to 1440 for input, 60 for output
|
||||||
type: config.type || PaintScaleType.PPG, // Default type if missing
|
type: config.type || PaintScaleType.PPG, // Default type if missing
|
||||||
}));
|
}));
|
||||||
setPaintScaleConfigs(updatedConfigs || []);
|
setPaintScaleConfigs(updatedConfigs || []);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error(`Failed to load paint scale ${configType} configs:`, error);
|
console.error(
|
||||||
|
`Failed to load paint scale ${configType} configs:`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}, [getConfigsMethod]);
|
}, [getConfigsMethod]);
|
||||||
|
|
||||||
@@ -47,25 +59,40 @@ export const usePaintScaleConfig = (configType: ConfigType) => {
|
|||||||
.invoke(setConfigsMethod, configs)
|
.invoke(setConfigsMethod, configs)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Notify main process to update cron job
|
// Notify main process to update cron job
|
||||||
if (configType === 'input') {
|
if (configType === "input") {
|
||||||
window.electron.ipcRenderer.send(ipcTypes.toMain.settings.paintScale.updateInputCron, configs);
|
window.electron.ipcRenderer.send(
|
||||||
} else if (configType === 'output') {
|
ipcTypes.toMain.settings.paintScale.updateInputCron,
|
||||||
window.electron.ipcRenderer.send(ipcTypes.toMain.settings.paintScale.updateOutputCron, configs);
|
configs,
|
||||||
|
);
|
||||||
|
} else if (configType === "output") {
|
||||||
|
window.electron.ipcRenderer.send(
|
||||||
|
ipcTypes.toMain.settings.paintScale.updateOutputCron,
|
||||||
|
configs,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error(`Failed to save paint scale ${configType} configs:`, error);
|
console.error(
|
||||||
|
`Failed to save paint scale ${configType} configs:`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// New helper to check if a path is unique across input and output configs
|
// New helper to check if a path is unique across input and output configs
|
||||||
const checkPathUnique = async (newPath: string): Promise<boolean> => {
|
const checkPathUnique = async (newPath: string): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
const inputConfigs: PaintScaleConfig[] = await window.electron.ipcRenderer.invoke(ipcTypes.toMain.settings.paintScale.getInputConfigs);
|
const inputConfigs: PaintScaleConfig[] =
|
||||||
const outputConfigs: PaintScaleConfig[] = await window.electron.ipcRenderer.invoke(ipcTypes.toMain.settings.paintScale.getOutputConfigs);
|
await window.electron.ipcRenderer.invoke(
|
||||||
|
ipcTypes.toMain.settings.paintScale.getInputConfigs,
|
||||||
|
);
|
||||||
|
const outputConfigs: PaintScaleConfig[] =
|
||||||
|
await window.electron.ipcRenderer.invoke(
|
||||||
|
ipcTypes.toMain.settings.paintScale.getOutputConfigs,
|
||||||
|
);
|
||||||
const allConfigs = [...inputConfigs, ...outputConfigs];
|
const allConfigs = [...inputConfigs, ...outputConfigs];
|
||||||
// Allow updating the current config even if its current value equals newPath.
|
// Allow updating the current config even if its current value equals newPath.
|
||||||
return !allConfigs.some(config => config.path === newPath);
|
return !allConfigs.some((config) => config.path === newPath);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to check unique path:", error);
|
console.error("Failed to check unique path:", error);
|
||||||
return false;
|
return false;
|
||||||
@@ -74,10 +101,11 @@ export const usePaintScaleConfig = (configType: ConfigType) => {
|
|||||||
|
|
||||||
// Handle adding a new paint scale config
|
// Handle adding a new paint scale config
|
||||||
const handleAddConfig = (type: PaintScaleType) => {
|
const handleAddConfig = (type: PaintScaleType) => {
|
||||||
|
const defaultPolling = configType === "input" ? 1440 : 60;
|
||||||
const newConfig: PaintScaleConfig = {
|
const newConfig: PaintScaleConfig = {
|
||||||
id: Date.now().toString(),
|
id: Date.now().toString(),
|
||||||
type,
|
type,
|
||||||
pollingInterval: 1440, // Default to 1440 seconds
|
pollingInterval: defaultPolling, // Default to 1440 for input, 60 for output
|
||||||
};
|
};
|
||||||
const updatedConfigs = [...paintScaleConfigs, newConfig];
|
const updatedConfigs = [...paintScaleConfigs, newConfig];
|
||||||
setPaintScaleConfigs(updatedConfigs);
|
setPaintScaleConfigs(updatedConfigs);
|
||||||
@@ -86,7 +114,9 @@ export const usePaintScaleConfig = (configType: ConfigType) => {
|
|||||||
|
|
||||||
// Handle removing a config
|
// Handle removing a config
|
||||||
const handleRemoveConfig = (id: string) => {
|
const handleRemoveConfig = (id: string) => {
|
||||||
const updatedConfigs = paintScaleConfigs.filter((config) => config.id !== id);
|
const updatedConfigs = paintScaleConfigs.filter(
|
||||||
|
(config) => config.id !== id,
|
||||||
|
);
|
||||||
setPaintScaleConfigs(updatedConfigs);
|
setPaintScaleConfigs(updatedConfigs);
|
||||||
saveConfigs(updatedConfigs);
|
saveConfigs(updatedConfigs);
|
||||||
};
|
};
|
||||||
@@ -94,7 +124,10 @@ export const usePaintScaleConfig = (configType: ConfigType) => {
|
|||||||
// Handle path selection (modified to check directory uniqueness)
|
// Handle path selection (modified to check directory uniqueness)
|
||||||
const handlePathChange = async (id: string) => {
|
const handlePathChange = async (id: string) => {
|
||||||
try {
|
try {
|
||||||
const path: string | null = await window.electron.ipcRenderer.invoke(setPathMethod, id);
|
const path: string | null = await window.electron.ipcRenderer.invoke(
|
||||||
|
setPathMethod,
|
||||||
|
id,
|
||||||
|
);
|
||||||
if (path) {
|
if (path) {
|
||||||
const isUnique = await checkPathUnique(path);
|
const isUnique = await checkPathUnique(path);
|
||||||
if (!isUnique) {
|
if (!isUnique) {
|
||||||
|
|||||||
@@ -13,9 +13,10 @@ import {
|
|||||||
Space,
|
Space,
|
||||||
Table,
|
Table,
|
||||||
Tag,
|
Tag,
|
||||||
|
theme,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import { FC, useState } from "react";
|
import { JSX, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
PaintScaleConfig,
|
PaintScaleConfig,
|
||||||
@@ -24,15 +25,17 @@ import {
|
|||||||
} from "../../../../util/types/paintScale";
|
} from "../../../../util/types/paintScale";
|
||||||
import { usePaintScaleConfig } from "./PaintScale/usePaintScaleConfig";
|
import { usePaintScaleConfig } from "./PaintScale/usePaintScaleConfig";
|
||||||
|
|
||||||
const SettingsPaintScaleInputPaths: FC = () => {
|
const SettingsPaintScaleInputPaths = (): JSX.Element => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { token } = theme.useToken(); // Access theme tokens
|
||||||
|
|
||||||
const {
|
const {
|
||||||
paintScaleConfigs,
|
paintScaleConfigs,
|
||||||
handleAddConfig,
|
handleAddConfig,
|
||||||
handleRemoveConfig,
|
handleRemoveConfig,
|
||||||
handlePathChange,
|
handlePathChange,
|
||||||
handlePollingIntervalChange,
|
handlePollingIntervalChange,
|
||||||
} = usePaintScaleConfig("input");
|
} = usePaintScaleConfig("output");
|
||||||
|
|
||||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||||
const [selectedType, setSelectedType] = useState<PaintScaleType | null>(null);
|
const [selectedType, setSelectedType] = useState<PaintScaleType | null>(null);
|
||||||
@@ -87,7 +90,7 @@ const SettingsPaintScaleInputPaths: FC = () => {
|
|||||||
placeholder={t("settings.labels.paintScalePath")}
|
placeholder={t("settings.labels.paintScalePath")}
|
||||||
disabled
|
disabled
|
||||||
style={{
|
style={{
|
||||||
borderColor: isValid ? "#52c41a" : "#d9d9d9",
|
borderColor: isValid ? token.colorSuccess : token.colorError, // Use semantic tokens
|
||||||
}}
|
}}
|
||||||
suffix={
|
suffix={
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@@ -98,9 +101,9 @@ const SettingsPaintScaleInputPaths: FC = () => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{isValid ? (
|
{isValid ? (
|
||||||
<CheckCircleFilled style={{ color: "#52c41a" }} />
|
<CheckCircleFilled style={{ color: token.colorSuccess }} />
|
||||||
) : (
|
) : (
|
||||||
<WarningFilled style={{ color: "#faad14" }} />
|
<WarningFilled style={{ color: token.colorError }} />
|
||||||
)}
|
)}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,18 @@ import {
|
|||||||
FolderOpenFilled,
|
FolderOpenFilled,
|
||||||
WarningFilled,
|
WarningFilled,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { Button, Card, Input, Modal, Select, Space, Table, Tag } from "antd";
|
import {
|
||||||
import { FC, useState } from "react";
|
Button,
|
||||||
|
Card,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
Select,
|
||||||
|
Space,
|
||||||
|
Table,
|
||||||
|
Tag,
|
||||||
|
theme,
|
||||||
|
} from "antd";
|
||||||
|
import { JSX, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
PaintScaleConfig,
|
PaintScaleConfig,
|
||||||
@@ -14,7 +24,8 @@ import {
|
|||||||
} from "../../../../util/types/paintScale";
|
} from "../../../../util/types/paintScale";
|
||||||
import { usePaintScaleConfig } from "./PaintScale/usePaintScaleConfig";
|
import { usePaintScaleConfig } from "./PaintScale/usePaintScaleConfig";
|
||||||
|
|
||||||
const SettingsPaintScaleOutputPaths: FC = () => {
|
const SettingsPaintScaleOutputPaths = (): JSX.Element => {
|
||||||
|
const { token } = theme.useToken();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const {
|
const {
|
||||||
paintScaleConfigs,
|
paintScaleConfigs,
|
||||||
@@ -22,7 +33,7 @@ const SettingsPaintScaleOutputPaths: FC = () => {
|
|||||||
handleRemoveConfig,
|
handleRemoveConfig,
|
||||||
handlePathChange,
|
handlePathChange,
|
||||||
handlePollingIntervalChange,
|
handlePollingIntervalChange,
|
||||||
} = usePaintScaleConfig("output");
|
} = usePaintScaleConfig("input");
|
||||||
|
|
||||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||||
const [selectedType, setSelectedType] = useState<PaintScaleType | null>(null);
|
const [selectedType, setSelectedType] = useState<PaintScaleType | null>(null);
|
||||||
@@ -77,13 +88,13 @@ const SettingsPaintScaleOutputPaths: FC = () => {
|
|||||||
placeholder={t("settings.labels.paintScalePath")}
|
placeholder={t("settings.labels.paintScalePath")}
|
||||||
disabled
|
disabled
|
||||||
style={{
|
style={{
|
||||||
borderColor: isValid ? "#52c41a" : "#d9d9d9",
|
borderColor: isValid ? token.colorSuccess : token.colorError,
|
||||||
}}
|
}}
|
||||||
suffix={
|
suffix={
|
||||||
isValid ? (
|
isValid ? (
|
||||||
<CheckCircleFilled style={{ color: "#52c41a" }} />
|
<CheckCircleFilled style={{ color: token.colorSuccess }} />
|
||||||
) : (
|
) : (
|
||||||
<WarningFilled style={{ color: "#faad14" }} />
|
<WarningFilled style={{ color: token.colorError }} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
5
src/util/getMainWindow.ts
Normal file
5
src/util/getMainWindow.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { BrowserWindow } from "electron";
|
||||||
|
|
||||||
|
export default function getMainWindow(): BrowserWindow | null {
|
||||||
|
return BrowserWindow.getAllWindows()[0] || null;
|
||||||
|
}
|
||||||
309
src/util/memUsage.ts
Normal file
309
src/util/memUsage.ts
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
import { BrowserWindow } from "electron";
|
||||||
|
import log from "electron-log/main";
|
||||||
|
import fs from "fs";
|
||||||
|
import os from "os";
|
||||||
|
import path from "path";
|
||||||
|
import Store from "../main/store/store";
|
||||||
|
/**
|
||||||
|
* Human-readable memory/cpu/resource snapshot.
|
||||||
|
*/
|
||||||
|
export type MemoryUsageStats = {
|
||||||
|
timestamp: string;
|
||||||
|
label?: string;
|
||||||
|
uptimeSeconds: number;
|
||||||
|
pid: number;
|
||||||
|
memory: {
|
||||||
|
rss: number;
|
||||||
|
heapTotal: number;
|
||||||
|
heapUsed: number;
|
||||||
|
external: number;
|
||||||
|
arrayBuffers?: number;
|
||||||
|
};
|
||||||
|
memoryPretty: {
|
||||||
|
rss: string;
|
||||||
|
heapTotal: string;
|
||||||
|
heapUsed: string;
|
||||||
|
external: string;
|
||||||
|
arrayBuffers?: string;
|
||||||
|
};
|
||||||
|
os: {
|
||||||
|
totalMem: number;
|
||||||
|
freeMem: number;
|
||||||
|
freeMemPercent: number;
|
||||||
|
};
|
||||||
|
cpuUsage?: NodeJS.CpuUsage;
|
||||||
|
resourceUsage?: NodeJS.ResourceUsage;
|
||||||
|
heapSpaces?: Array<import("v8").HeapSpaceInfo>;
|
||||||
|
heapSnapshotFile?: string;
|
||||||
|
custom?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
// (merged into top import)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for dumpMemoryStats.
|
||||||
|
*/
|
||||||
|
export type DumpOptions = {
|
||||||
|
/**
|
||||||
|
* Call global.gc() before sampling if available (requires node run with --expose-gc).
|
||||||
|
*/
|
||||||
|
runGc?: boolean;
|
||||||
|
/**
|
||||||
|
* Optional label to include in the returned snapshot.
|
||||||
|
*/
|
||||||
|
label?: string;
|
||||||
|
includeHeapSpaces?: boolean;
|
||||||
|
writeHeapSnapshot?: boolean;
|
||||||
|
heapSnapshotDir?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert bytes to a compact human readable string.
|
||||||
|
*/
|
||||||
|
function formatBytes(bytes: number): string {
|
||||||
|
if (!isFinite(bytes)) return String(bytes);
|
||||||
|
const units = ["B", "KB", "MB", "GB", "TB"];
|
||||||
|
let i = 0;
|
||||||
|
let val = bytes;
|
||||||
|
while (val >= 1024 && i < units.length - 1) {
|
||||||
|
val /= 1024;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return `${val.toFixed(2)} ${units[i]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronously produce a memory / cpu / os snapshot.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* const stats = await dumpMemoryStats({ runGc: true, label: 'before-heavy-task' });
|
||||||
|
*/
|
||||||
|
export async function dumpMemoryStats(
|
||||||
|
options: DumpOptions = {},
|
||||||
|
): Promise<MemoryUsageStats> {
|
||||||
|
const {
|
||||||
|
runGc = false,
|
||||||
|
label,
|
||||||
|
includeHeapSpaces = true,
|
||||||
|
writeHeapSnapshot = true,
|
||||||
|
heapSnapshotDir,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
// Allow GC if requested and available to get a cleaner snapshot
|
||||||
|
if (runGc && typeof (global as any).gc === "function") {
|
||||||
|
try {
|
||||||
|
(global as any).gc();
|
||||||
|
} catch {
|
||||||
|
// ignore GC errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let the event loop settle a tick so GC can complete if run
|
||||||
|
await new Promise((resolve) => setImmediate(resolve));
|
||||||
|
|
||||||
|
const mem = process.memoryUsage();
|
||||||
|
const totalMem = os.totalmem();
|
||||||
|
const freeMem = os.freemem();
|
||||||
|
|
||||||
|
const stats: MemoryUsageStats = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
label,
|
||||||
|
uptimeSeconds: Math.floor(process.uptime()),
|
||||||
|
pid: process.pid,
|
||||||
|
memory: {
|
||||||
|
rss: mem.rss,
|
||||||
|
heapTotal: mem.heapTotal,
|
||||||
|
heapUsed: mem.heapUsed,
|
||||||
|
external: mem.external,
|
||||||
|
arrayBuffers: mem.arrayBuffers,
|
||||||
|
},
|
||||||
|
memoryPretty: {
|
||||||
|
rss: formatBytes(mem.rss),
|
||||||
|
heapTotal: formatBytes(mem.heapTotal),
|
||||||
|
heapUsed: formatBytes(mem.heapUsed),
|
||||||
|
external: formatBytes(mem.external),
|
||||||
|
arrayBuffers:
|
||||||
|
mem.arrayBuffers !== undefined
|
||||||
|
? formatBytes(mem.arrayBuffers)
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
os: {
|
||||||
|
totalMem,
|
||||||
|
freeMem,
|
||||||
|
freeMemPercent: Math.round((freeMem / totalMem) * 10000) / 100,
|
||||||
|
},
|
||||||
|
cpuUsage: process.cpuUsage ? process.cpuUsage() : undefined,
|
||||||
|
resourceUsage:
|
||||||
|
typeof process.resourceUsage === "function"
|
||||||
|
? process.resourceUsage()
|
||||||
|
: undefined,
|
||||||
|
custom: {
|
||||||
|
numBrowserWindows: BrowserWindow.getAllWindows().length,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (includeHeapSpaces) {
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const v8: typeof import("v8") = require("v8");
|
||||||
|
if (typeof v8.getHeapSpaceStatistics === "function") {
|
||||||
|
stats.heapSpaces = v8.getHeapSpaceStatistics();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
log.warn("Failed to get heap space stats", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (writeHeapSnapshot) {
|
||||||
|
try {
|
||||||
|
if (!runGc && typeof (global as any).gc === "function") {
|
||||||
|
try {
|
||||||
|
(global as any).gc();
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const v8: typeof import("v8") = require("v8");
|
||||||
|
if (typeof v8.writeHeapSnapshot === "function") {
|
||||||
|
const baseDir =
|
||||||
|
heapSnapshotDir || path.dirname(log.transports.file.getFile().path);
|
||||||
|
|
||||||
|
const dir = path.join(baseDir, "heap-snapshots");
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
const fileName = `heap-${Date.now()}-${process.pid}.heapsnapshot`;
|
||||||
|
const filePath = path.join(dir, fileName);
|
||||||
|
const snapshotPath = v8.writeHeapSnapshot(filePath);
|
||||||
|
stats.heapSnapshotFile = snapshotPath;
|
||||||
|
} else {
|
||||||
|
log.warn("v8.writeHeapSnapshot not available");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
log.warn("Failed to write heap snapshot", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
const memLogger = log.create({ logId: "mem-stat" });
|
||||||
|
memLogger.transports.file.resolvePathFn = () => {
|
||||||
|
const filePath = path.join(
|
||||||
|
path.dirname(log.transports.file.getFile().path),
|
||||||
|
"memory-stats.log",
|
||||||
|
);
|
||||||
|
return filePath;
|
||||||
|
};
|
||||||
|
// Configure memory logger format to include process ID
|
||||||
|
memLogger.transports.file.format =
|
||||||
|
"[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] [PID:{processId}] {text}";
|
||||||
|
memLogger.transports.console.format =
|
||||||
|
"[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] [PID:{processId}] {text}";
|
||||||
|
|
||||||
|
export async function dumpMemoryStatsToFile() {
|
||||||
|
try {
|
||||||
|
const stats = await dumpMemoryStats({ includeHeapSpaces: false });
|
||||||
|
memLogger.debug("[MemStat]:", stats);
|
||||||
|
} catch (error) {
|
||||||
|
log.warn("Unexpected error while writing memory stats log", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ongoingMemoryDump() {
|
||||||
|
console.log(
|
||||||
|
`Memory logging set to ${Store.get("settings.enableMemDebug")}. Log file at ${memLogger.transports.file.getFile().path}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
setInterval(
|
||||||
|
async () => {
|
||||||
|
// Also write each snapshot to a dedicated memory stats log file as JSON lines.
|
||||||
|
try {
|
||||||
|
const loggingEnabled = Store.get("settings.enableMemDebug");
|
||||||
|
log.debug(
|
||||||
|
"Checking if memory stats logging is enabled.",
|
||||||
|
loggingEnabled,
|
||||||
|
);
|
||||||
|
if (loggingEnabled) {
|
||||||
|
// Enforce heap snapshot folder size limit (< 1GB) before writing a new snapshot.
|
||||||
|
const MAX_DIR_BYTES = 5 * 1024 * 1024 * 1024; // 5GB
|
||||||
|
const TARGET_REDUCED_BYTES = Math.floor(MAX_DIR_BYTES * 0.9); // prune down to 90%
|
||||||
|
const baseDir = path.dirname(log.transports.file.getFile().path);
|
||||||
|
const heapDir = path.join(baseDir, "heap-snapshots");
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(heapDir, { recursive: true });
|
||||||
|
const files = fs
|
||||||
|
.readdirSync(heapDir)
|
||||||
|
.filter((f) => f.endsWith(".heapsnapshot"));
|
||||||
|
let totalSize = 0;
|
||||||
|
const fileStats: Array<{
|
||||||
|
file: string;
|
||||||
|
size: number;
|
||||||
|
mtimeMs: number;
|
||||||
|
}> = [];
|
||||||
|
for (const file of files) {
|
||||||
|
try {
|
||||||
|
const stat = fs.statSync(path.join(heapDir, file));
|
||||||
|
if (stat.isFile()) {
|
||||||
|
totalSize += stat.size;
|
||||||
|
fileStats.push({
|
||||||
|
file,
|
||||||
|
size: stat.size,
|
||||||
|
mtimeMs: stat.mtimeMs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log.warn("Failed to stat heap snapshot file", file, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (totalSize > MAX_DIR_BYTES) {
|
||||||
|
// Sort oldest first and delete until below TARGET_REDUCED_BYTES.
|
||||||
|
fileStats.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
||||||
|
let bytesAfter = totalSize;
|
||||||
|
for (const info of fileStats) {
|
||||||
|
if (bytesAfter <= TARGET_REDUCED_BYTES) break;
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(path.join(heapDir, info.file));
|
||||||
|
bytesAfter -= info.size;
|
||||||
|
log.warn(
|
||||||
|
`Pruned heap snapshot '${info.file}' (${formatBytes(info.size)}) to reduce directory size. New size: ${formatBytes(bytesAfter)}.`,
|
||||||
|
);
|
||||||
|
} catch (errDel) {
|
||||||
|
log.warn(
|
||||||
|
"Failed to delete heap snapshot file",
|
||||||
|
info.file,
|
||||||
|
errDel,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bytesAfter > MAX_DIR_BYTES) {
|
||||||
|
// Still above hard cap; skip writing a new snapshot this cycle.
|
||||||
|
log.warn(
|
||||||
|
`Heap snapshot directory still above hard cap (${formatBytes(bytesAfter)} > ${formatBytes(MAX_DIR_BYTES)}). Skipping new heap snapshot this cycle.`,
|
||||||
|
);
|
||||||
|
const stats = await dumpMemoryStats({
|
||||||
|
includeHeapSpaces: false,
|
||||||
|
writeHeapSnapshot: false,
|
||||||
|
});
|
||||||
|
memLogger.debug("[MemStat]:", stats);
|
||||||
|
return; // skip remainder; we already logged stats without snapshot.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (dirErr) {
|
||||||
|
log.warn(
|
||||||
|
"Unexpected error while enforcing heap snapshot directory size limit",
|
||||||
|
dirErr,
|
||||||
|
);
|
||||||
|
// Continue; failure to enforce limit should not stop memory stats.
|
||||||
|
}
|
||||||
|
// Directory is within allowed bounds (or pruning succeeded); proceed normally.
|
||||||
|
const stats = await dumpMemoryStats({ includeHeapSpaces: false });
|
||||||
|
memLogger.debug("[MemStat]:", stats);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
log.warn("Unexpected error while writing memory stats log", err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
15 * 60 * 1000,
|
||||||
|
); // every 15 minutes
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ongoingMemoryDump;
|
||||||
@@ -29,26 +29,26 @@
|
|||||||
"duplicatePath": "The selected directory is already used in another configuration."
|
"duplicatePath": "The selected directory is already used in another configuration."
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
|
"actions": "Actions",
|
||||||
|
"addPaintScalePath": "Add Paint Scale Path",
|
||||||
"emsOutFilePath": "EMS Out File Path (Parts Order, etc.)",
|
"emsOutFilePath": "EMS Out File Path (Parts Order, etc.)",
|
||||||
|
"invalidPath": "Path not set or invalid",
|
||||||
|
"paintScalePath": "Paint Scale Path",
|
||||||
|
"paintScaleSettingsInput": "BSMS To Paint Scale",
|
||||||
|
"paintScaleSettingsOutput": "Paint Scale To BSMS",
|
||||||
|
"paintScaleType": "Paint Scale Type",
|
||||||
|
"pollingInterval": "Polling Interval (m)",
|
||||||
"pollinginterval": "Polling Interval (ms)",
|
"pollinginterval": "Polling Interval (ms)",
|
||||||
"ppcfilepath": "Parts Price Change File Path",
|
"ppcfilepath": "Parts Price Change File Path",
|
||||||
|
"remove": "Remove",
|
||||||
|
"selectPaintScaleType": "Select Paint Scale Type",
|
||||||
"started": "Started",
|
"started": "Started",
|
||||||
"stopped": "Stopped",
|
"stopped": "Stopped",
|
||||||
|
"validPath": "Valid path",
|
||||||
"watchedpaths": "Watched Paths",
|
"watchedpaths": "Watched Paths",
|
||||||
"watchermodepolling": "Polling",
|
"watchermodepolling": "Polling",
|
||||||
"watchermoderealtime": "Real Time",
|
"watchermoderealtime": "Real Time",
|
||||||
"watcherstatus": "Watcher Status",
|
"watcherstatus": "Watcher Status"
|
||||||
"paintScaleSettingsInput": "BSMS To Paint Scale",
|
|
||||||
"paintScaleSettingsOutput": "Paint Scale To BSMS",
|
|
||||||
"paintScalePath": "Paint Scale Path",
|
|
||||||
"paintScaleType": "Paint Scale Type",
|
|
||||||
"addPaintScalePath": "Add Paint Scale Path",
|
|
||||||
"remove": "Remove",
|
|
||||||
"actions": "Actions",
|
|
||||||
"pollingInterval": "Polling Interval (m)",
|
|
||||||
"validPath": "Valid path",
|
|
||||||
"invalidPath": "Path not set or invalid",
|
|
||||||
"selectPaintScaleType": "Select Paint Scale Type"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": {
|
"title": {
|
||||||
@@ -57,6 +57,7 @@
|
|||||||
},
|
},
|
||||||
"updates": {
|
"updates": {
|
||||||
"apply": "Apply Update",
|
"apply": "Apply Update",
|
||||||
|
"applying": "Applying update",
|
||||||
"available": "An update is available.",
|
"available": "An update is available.",
|
||||||
"download": "Download Update",
|
"download": "Download Update",
|
||||||
"downloading": "An update is downloading."
|
"downloading": "An update is downloading."
|
||||||
|
|||||||
62
tests/heapPrune.test.ts
Normal file
62
tests/heapPrune.test.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
// We import the module after setting up a temporary log path by monkey patching electron-log.
|
||||||
|
// Since the project primarily uses Playwright for tests, we leverage its expect assertion library.
|
||||||
|
|
||||||
|
// NOTE: This is a lightweight test that simulates the pruning logic indirectly by invoking the exported ongoingMemoryDump
|
||||||
|
// function and creating artificial heap snapshot files exceeding the threshold.
|
||||||
|
|
||||||
|
// Because ongoingMemoryDump sets an interval, we invoke its internal logic by importing the file and manually calling dumpMemoryStats.
|
||||||
|
// For simplicity and to avoid altering production code for testability, we replicate the size enforcement logic here and assert behavior.
|
||||||
|
|
||||||
|
function createDummySnapshots(dir: string, count: number, sizeBytes: number) {
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const file = path.join(dir, `dummy-${i}.heapsnapshot`);
|
||||||
|
const fd = fs.openSync(file, "w");
|
||||||
|
// Write sizeBytes of zeros
|
||||||
|
const buf = Buffer.alloc(1024 * 1024, 0); // 1MB chunk
|
||||||
|
let written = 0;
|
||||||
|
while (written < sizeBytes) {
|
||||||
|
fs.writeSync(fd, buf, 0, Math.min(buf.length, sizeBytes - written));
|
||||||
|
written += Math.min(buf.length, sizeBytes - written);
|
||||||
|
}
|
||||||
|
fs.closeSync(fd);
|
||||||
|
// Stagger mtime for deterministic pruning ordering
|
||||||
|
const mtime = new Date(Date.now() - (count - i) * 1000);
|
||||||
|
fs.utimesSync(file, mtime, mtime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("heap snapshot directory pruning reduces size below simulated hard cap", async () => {
|
||||||
|
const baseDir = fs.mkdtempSync(path.join(process.cwd(), "heap-test-"));
|
||||||
|
const heapDir = path.join(baseDir, "heap-snapshots");
|
||||||
|
// Simulate oversize: 15 files of 5MB each = 75MB
|
||||||
|
createDummySnapshots(heapDir, 15, 5 * 1024 * 1024);
|
||||||
|
// Use smaller cap to keep test resource usage low.
|
||||||
|
const MAX_DIR_BYTES = 50 * 1024 * 1024; // 50MB simulated cap
|
||||||
|
const TARGET_REDUCED_BYTES = Math.floor(MAX_DIR_BYTES * 0.9);
|
||||||
|
const files = fs
|
||||||
|
.readdirSync(heapDir)
|
||||||
|
.filter((f) => f.endsWith(".heapsnapshot"));
|
||||||
|
let totalSize = 0;
|
||||||
|
const fileStats: Array<{ file: string; size: number; mtimeMs: number }> = [];
|
||||||
|
for (const file of files) {
|
||||||
|
const stat = fs.statSync(path.join(heapDir, file));
|
||||||
|
totalSize += stat.size;
|
||||||
|
fileStats.push({ file, size: stat.size, mtimeMs: stat.mtimeMs });
|
||||||
|
}
|
||||||
|
expect(totalSize).toBeGreaterThan(MAX_DIR_BYTES);
|
||||||
|
fileStats.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
||||||
|
let bytesAfter = totalSize;
|
||||||
|
for (const info of fileStats) {
|
||||||
|
if (bytesAfter <= TARGET_REDUCED_BYTES) break;
|
||||||
|
fs.unlinkSync(path.join(heapDir, info.file));
|
||||||
|
bytesAfter -= info.size;
|
||||||
|
}
|
||||||
|
expect(bytesAfter).toBeLessThanOrEqual(TARGET_REDUCED_BYTES);
|
||||||
|
// Cleanup
|
||||||
|
fs.rmSync(baseDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
@@ -234,9 +234,53 @@
|
|||||||
</concept_node>
|
</concept_node>
|
||||||
</children>
|
</children>
|
||||||
</folder_node>
|
</folder_node>
|
||||||
|
<folder_node>
|
||||||
|
<name>errors</name>
|
||||||
|
<children>
|
||||||
|
<concept_node>
|
||||||
|
<name>duplicatePath</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
</children>
|
||||||
|
</folder_node>
|
||||||
<folder_node>
|
<folder_node>
|
||||||
<name>labels</name>
|
<name>labels</name>
|
||||||
<children>
|
<children>
|
||||||
|
<concept_node>
|
||||||
|
<name>actions</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>addPaintScalePath</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>emsOutFilePath</name>
|
<name>emsOutFilePath</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -250,6 +294,84 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>invalidPath</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>paintScalePath</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>paintScaleSettingsInput</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>paintScaleSettingsOutput</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>paintScaleType</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>pollingInterval</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>pollinginterval</name>
|
<name>pollinginterval</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -276,6 +398,32 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>remove</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>selectPaintScaleType</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>started</name>
|
<name>started</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -302,6 +450,19 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>validPath</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>watchedpaths</name>
|
<name>watchedpaths</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -405,6 +566,19 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>applying</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>available</name>
|
<name>available</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
|
|||||||
Reference in New Issue
Block a user