diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 7f86a42..e800dc8 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -14,22 +14,24 @@ jobs: - name: Verify Node.js run: node -v - - name: Install dependencies + - name: Install electron-app dependencies working-directory: electron-app run: npm ci - - name: Build Windows executable + - name: Create production .env file working-directory: electron-app - env: - WS_URL: ${{ secrets.WS_URL }} - API_URL: ${{ secrets.API_URL }} - RELEASE_URL: ${{ secrets.RELEASE_URL }} - NODE_ENV: production - run: npm run electron:build -- --win + run: | + echo "WS_URL=${{ secrets.WS_URL }}" > .env + echo "API_URL=${{ secrets.API_URL }}" >> .env + echo "NODE_ENV=production" >> .env - - name: Upload Windows artifact + - name: Build Windows portable executable + working-directory: electron-app + run: npm run build:portable + + - name: Upload Windows portable artifact uses: actions/upload-artifact@v4 with: - name: windows-exe - path: electron-app/release/*.exe + name: rmtPocketWatcher-Windows-Portable + path: electron-app/release/rmtPocketWatcher-*.exe retention-days: 7 diff --git a/electron-app/.env.example b/electron-app/.env.example index e8e7d85..e15ef15 100644 --- a/electron-app/.env.example +++ b/electron-app/.env.example @@ -1,5 +1,8 @@ # WebSocket connection URL WS_URL=ws://localhost:3000/ws +# API URL +API_URL=http://localhost:3000 + # Development mode NODE_ENV=development diff --git a/electron-app/package-lock.json b/electron-app/package-lock.json index 48d3b14..65711ba 100644 --- a/electron-app/package-lock.json +++ b/electron-app/package-lock.json @@ -27,6 +27,7 @@ "electron-rebuild": "^3.2.9", "react": "^19.2.1", "react-dom": "^19.2.1", + "rimraf": "^6.1.2", "typescript": "^5.3.0", "vite": "^5.0.0", "wait-on": "^7.2.0" @@ -1054,6 +1055,29 @@ "@hapi/hoek": "^9.0.0" } }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1327,6 +1351,23 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/@npmcli/move-file/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -2744,6 +2785,17 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/cacache/node_modules/glob": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", @@ -2788,6 +2840,58 @@ "node": ">=8" } }, + "node_modules/cacache/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/cacheable-lookup": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", @@ -5943,6 +6047,23 @@ "node-gyp-build-test": "build-test.js" } }, + "node_modules/node-gyp/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/node-gyp/node_modules/semver": { "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", @@ -6562,17 +6683,91 @@ } }, "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.2.tgz", + "integrity": "sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "glob": "^7.1.3" + "glob": "^13.0.0", + "package-json-from-dist": "^1.0.1" }, "bin": { - "rimraf": "bin.js" + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz", + "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "path-scurry": "^2.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/lru-cache": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/rimraf/node_modules/path-scurry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", + "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" diff --git a/electron-app/package.json b/electron-app/package.json index bc2987e..f4a853e 100644 --- a/electron-app/package.json +++ b/electron-app/package.json @@ -9,11 +9,13 @@ "build:main": "npx tsc --project tsconfig.main.json && npx tsc --project tsconfig.preload.json", "build:renderer": "npx vite build", "build": "npm run build:main && npm run build:renderer", + "build:portable": "npm run build && npx electron-builder --win portable", + "clean": "rimraf dist", + "clean:dev": "npm run clean && npm run build:main && concurrently \"npm run dev\" \"wait-on http://localhost:5173 && cross-env NODE_ENV=development electron dist/main/index.js\"", "electron:dev": "concurrently \"npm run dev\" \"wait-on http://localhost:5173 && cross-env NODE_ENV=development electron dist/main/index.js\"", "electron:build": "npm run build && electron-builder", "publish": "npm run build && electron-builder --publish always", "rebuild": "electron-rebuild", - "test": "jest" }, "dependencies": { @@ -36,6 +38,7 @@ "electron-rebuild": "^3.2.9", "react": "^19.2.1", "react-dom": "^19.2.1", + "rimraf": "^6.1.2", "typescript": "^5.3.0", "vite": "^5.0.0", "wait-on": "^7.2.0" diff --git a/electron-app/src/main/database.ts b/electron-app/src/main/database.ts index 8c9df7b..f7a7079 100644 --- a/electron-app/src/main/database.ts +++ b/electron-app/src/main/database.ts @@ -33,6 +33,14 @@ export function initDatabase() { ) `); + // Create settings table for custom AUEC amount + db.exec(` + CREATE TABLE IF NOT EXISTS settings ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ) + `); + console.log('Database initialized at:', dbPath); } @@ -80,6 +88,26 @@ export function deleteAlert(id: string): void { stmt.run(id); } +export function getCustomAuecAmount(): number | null { + if (!db) throw new Error('Database not initialized'); + + const stmt = db.prepare('SELECT value FROM settings WHERE key = ?'); + const row = stmt.get('customAuecAmount') as any; + + return row ? parseFloat(row.value) : null; +} + +export function setCustomAuecAmount(amount: number): void { + if (!db) throw new Error('Database not initialized'); + + const stmt = db.prepare(` + INSERT OR REPLACE INTO settings (key, value) + VALUES (?, ?) + `); + + stmt.run('customAuecAmount', amount.toString()); +} + export function closeDatabase(): void { if (db) { db.close(); diff --git a/electron-app/src/main/ipc-handlers.ts b/electron-app/src/main/ipc-handlers.ts index 3cbb8a7..b3cb4a1 100644 --- a/electron-app/src/main/ipc-handlers.ts +++ b/electron-app/src/main/ipc-handlers.ts @@ -53,6 +53,8 @@ export function setupIpcHandlers(mainWindow: BrowserWindow): void { try { ipcMain.removeHandler('alerts:add'); } catch (e) { /* ignore */ } try { ipcMain.removeHandler('alerts:update'); } catch (e) { /* ignore */ } try { ipcMain.removeHandler('alerts:delete'); } catch (e) { /* ignore */ } + try { ipcMain.removeHandler('settings:getCustomAuecAmount'); } catch (e) { /* ignore */ } + try { ipcMain.removeHandler('settings:setCustomAuecAmount'); } catch (e) { /* ignore */ } // IPC handlers for renderer requests ipcMain.handle('ws:connect', async () => { @@ -155,6 +157,26 @@ export function setupIpcHandlers(mainWindow: BrowserWindow): void { } }); + // Custom AUEC amount handlers + ipcMain.handle('settings:getCustomAuecAmount', async () => { + try { + return db.getCustomAuecAmount(); + } catch (error) { + console.error('Failed to get custom AUEC amount:', error); + return null; + } + }); + + ipcMain.handle('settings:setCustomAuecAmount', async (_event, amount: number) => { + try { + db.setCustomAuecAmount(amount); + return { success: true }; + } catch (error) { + console.error('Failed to set custom AUEC amount:', error); + return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; + } + }); + // Auto-connect on startup wsClient.connect(); } @@ -181,6 +203,8 @@ export function cleanupIpcHandlers(): void { ipcMain.removeHandler('alerts:add'); ipcMain.removeHandler('alerts:update'); ipcMain.removeHandler('alerts:delete'); + ipcMain.removeHandler('settings:getCustomAuecAmount'); + ipcMain.removeHandler('settings:setCustomAuecAmount'); } catch (error) { // Handlers may not exist, ignore } diff --git a/electron-app/src/renderer/App.tsx b/electron-app/src/renderer/App.tsx index f053cc5..afb6f5f 100644 --- a/electron-app/src/renderer/App.tsx +++ b/electron-app/src/renderer/App.tsx @@ -52,6 +52,10 @@ export function App() { const [alertNotification, setAlertNotification] = useState(null); const alertAudioRef = useRef(null); + // Custom AUEC Amount State + const [customAuecAmount, setCustomAuecAmount] = useState(null); + const [customAuecInput, setCustomAuecInput] = useState(''); + // Load alerts from database on mount useEffect(() => { const loadAlerts = async () => { @@ -65,6 +69,22 @@ export function App() { loadAlerts(); }, []); + // Load custom AUEC amount from database on mount + useEffect(() => { + const loadCustomAmount = async () => { + try { + const amount = await window.electron.ipcRenderer.invoke('settings:getCustomAuecAmount'); + if (amount) { + setCustomAuecAmount(amount); + setCustomAuecInput(amount.toString()); + } + } catch (error) { + console.error('Failed to load custom AUEC amount:', error); + } + }; + loadCustomAmount(); + }, []); + // Fetch initial data on mount useEffect(() => { fetchInitialData(); @@ -374,6 +394,21 @@ export function App() { } }; + const saveCustomAuecAmount = async () => { + const amount = parseFloat(customAuecInput); + if (isNaN(amount) || amount <= 0) { + alert('Please enter a valid AUEC amount'); + return; + } + + try { + await window.electron.ipcRenderer.invoke('settings:setCustomAuecAmount', amount); + setCustomAuecAmount(amount); + } catch (error) { + console.error('Failed to save custom AUEC amount:', error); + } + }; + const dismissNotification = () => { setAlertNotification(null); }; @@ -769,7 +804,41 @@ export function App() { {latestPrice && (
-

Current Listings ({latestPrice.allPrices.length})

+
+

Current Listings ({latestPrice.allPrices.length})

+
+ setCustomAuecInput(e.target.value)} + style={{ + padding: '8px 12px', + backgroundColor: '#0a0e27', + border: '1px solid #2a2f4a', + borderRadius: '4px', + color: '#fff', + fontSize: '14px', + width: '180px', + }} + /> + +
+
Platform Seller Price/1M AUEC + {customAuecAmount && ( + + Price for {(customAuecAmount / 1000000).toLocaleString()}M AUEC + + )} - {latestPrice.allPrices.map((price) => ( - - {price.platform} - {price.sellerName} - - ${price.pricePerMillion.toFixed(9)} - - - ))} + {[...latestPrice.allPrices] + .sort((a, b) => a.pricePerMillion - b.pricePerMillion) + .map((price) => ( + + {price.platform} + {price.sellerName} + + ${price.pricePerMillion.toFixed(9)} + + {customAuecAmount && ( + + ${((price.pricePerMillion * customAuecAmount) / 1000000).toFixed(2)} + + )} + + ))}