diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index cafd15d..47fd78d 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -25,7 +25,7 @@ jobs: node-version: [20] # The type of runner that the job will run on - runs-on: macos-12 + runs-on: macos-13 steps: - uses: actions/checkout@v4 diff --git a/.gitignore b/.gitignore index f31fe56..e2b0c27 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ main.js src/**/*.js app/auto-launch/**/*.js +app/process/**/*.js +app/utils/**/*.js *.js.map # dependencies diff --git a/README.md b/README.md index 6a274f3..819941c 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ [![Codacy Badge](https://app.codacy.com/project/badge/Grade/c705f535eebe4ba8b7a5789f6b409933)](https://app.codacy.com/gh/everoddandeven/monerod-gui/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) [![Make a pull request][prs-badge]][prs] [![License][license-badge]](LICENSE.md) +[![Lint Test](https://github.com/everoddandeven/monerod-gui/actions/workflows/lint.yml/badge.svg)](https://github.com/everoddandeven/monerod-gui/actions/workflows/lint.yml) [![Linux - AppImage Build](https://github.com/everoddandeven/monerod-gui/actions/workflows/linux_appimage.yml/badge.svg)](https://github.com/everoddandeven/monerod-gui/actions/workflows/linux_appimage.yml) [![Linux - x64 DEB Build](https://github.com/everoddandeven/monerod-gui/actions/workflows/linux_x64_deb.yml/badge.svg)](https://github.com/everoddandeven/monerod-gui/actions/workflows/linux_x64_deb.yml) @@ -17,7 +18,7 @@ [![Watch on GitHub][github-watch-badge]][github-watch] [![Star on GitHub][github-star-badge]][github-star] -[![XMR Donated](https://img.shields.io/badge/donated-0.188011974017_XMR-blue?logo=monero)](https://github.com/everoddandeven/monerod-gui?tab=readme-ov-file#monero) +[![XMR Donated](https://img.shields.io/badge/donated-8.757300489601_XMR-blue?logo=monero)](https://github.com/everoddandeven/monerod-gui?tab=readme-ov-file#monero) [![BTC Donated][bitcoin-donated-badge]][bitcoin-donated] @@ -74,7 +75,11 @@ https://github.com/user-attachments/assets/c4a50d2f-5bbb-48ac-9425-30ecc20ada7c ## To Do +- [X] Upgrade Electron to latest version +- [ ] Upgrade Angular to latest version +- [ ] Implement e2e tests - [X] Detect wired/Wi-Fi connection +- [X] Detect battery/ac power - [ ] Detect preinstalled `monerod` - [ ] Linux - [ ] Windows @@ -83,14 +88,49 @@ https://github.com/user-attachments/assets/c4a50d2f-5bbb-48ac-9425-30ecc20ada7c - [X] Linux - [X] Windows - [X] MacOS -- [X] Import/export `monerod.conf` node configuration -- [X] Synchronization in a specific time slot - [X] Installers - [X] Linux - [X] Windows - - [X] MacOS + - [X] MacOS +- [ ] Packages + - [X] deb + - [X] rpm + - [X] exe + - [X] msi + - [X] dmg + - [ ] pkg + - [ ] flatpack + - [ ] snap +- [X] Import/export `monerod.conf` node configuration +- [X] Synchronization in a specific time slot +- [ ] Prompt user access control for administration operation +- [ ] Check for new versions of the GUI +- [ ] Wallet RPC management +- [ ] Blockchain tools + - [ ] Private testnet + - [ ] Import blockchain + - [ ] Export blockchain + - [ ] Prune blockchain + - [ ] Blockchain explorer + - [ ] Blockchain ancestry + - [ ] Blockchain stats + - [ ] Blockchain usage + - [ ] Blockchain depth +- [ ] Mining tools + - [ ] Calculator + - [ ] XMRig integration + - [ ] P2Pool integration +- [ ] Network tools + - [ ] Generate SSL certificate + - [ ] Tor integration + - [ ] I2P integration + - [ ] Automatic malicious node detection - [ ] Remote node management - [ ] No CORS connection + - [ ] SSH connection +- [ ] Move to Tauri +- [ ] Port and upgrade `battery-level` dependency +- [ ] Light Wallet Server integration ## Getting Started @@ -108,13 +148,6 @@ npm install There is an issue with `yarn` and `node_modules` when the application is built by the packager. Please use `npm` as dependencies manager. -If you want to generate Angular components with Angular-cli , you **MUST** install `@angular/cli` in npm global context. -Please follow [Angular-cli documentation](https://github.com/angular/angular-cli) if you had installed a previous version of `angular-cli`. - -``` bash -npm install -g @angular/cli -``` - *Install NodeJS dependencies with npm (used by Electron main process):* ``` bash @@ -124,16 +157,6 @@ npm install Why two package.json ? This project follow [Electron Builder two package.json structure](https://www.electron.build/tutorials/two-package-structure) in order to optimize final bundle and be still able to use Angular `ng add` feature. -## To build for development - -- **in a terminal window** -> npm start - -Voila! You can use your Angular + Electron app in a local development environment with hot reload! - -The application code is managed by `app/main.ts`. In this sample, the app runs with a simple Angular App (`http://localhost:4200`), and an Electron window. \ -The Angular component contains an example of Electron and NodeJS native lib import. \ -You can disable "Developer Tools" by commenting `win.webContents.openDevTools();` in `app/main.ts`. - ## Project structure | Folder | Description | @@ -141,19 +164,30 @@ You can disable "Developer Tools" by commenting `win.webContents.openDevTools(); | app | Electron main process folder (NodeJS) | | src | Electron renderer process folder (Web / Angular) | -## Browser mode +## To build for development + +| Command | Description | +|------------------------------|-------------------------------------------------------------------------------------------------------| +| `npm run electron:local:dev` | Builds your application and start electron locally (DEV MODE) | -Maybe you only want to execute the application in the browser with hot reload? Just run `npm run ng:serve:web`. -## Included Commands +If you want to generate Angular components with Angular-cli , you **MUST** install `@angular/cli` in npm global context. +Please follow [Angular-cli documentation](https://github.com/angular/angular-cli) if you had installed a previous version of `angular-cli`. + +``` bash +npm install -g @angular/cli +``` + +## To build for production + +| Command | Description | +|------------------------------|-------------------------------------------------------------------------------------------------------| +| `npm run electron:local` | Builds your application and start electron locally | +| `npm run electron:build` | Builds your application and creates an app and installer for Windows | +| `npm run electron:build:deb` | Builds your application and creates an installer consumable for debian based operating systems | +| `npm run electron:build:rpm` | Builds your application and creates an installer consumable for redhat based operating systems | +| `npm run electron:build:mac` | Builds your application and creates an installer consumable for MacOS | -| Command | Description | -|--------------------------|-------------------------------------------------------------------------------------------------------| -| `npm run ng:serve` | Execute the app in the web browser (DEV mode) | -| `npm run web:build` | Build the app that can be used directly in the web browser. Your built files are in the /dist folder. | -| `npm run electron:local` | Builds your application and start electron locally | -| `npm run electron:local:dev` | Builds your application and start electron locally (DEV MODE) | -| `npm run electron:build` | Builds your application and creates an app consumable based on your operating system | **Your application is optimised. Only /dist folder and NodeJS dependencies are included in the final bundle.** @@ -178,24 +212,6 @@ Finally from VsCode press **Ctrl+Shift+D** and select **Application Debug** and Please note that Hot reload is only available in Renderer process. -[maintained-badge]: https://img.shields.io/badge/maintained-yes-brightgreen -[license-badge]: https://img.shields.io/badge/license-MIT-blue.svg -[prs-badge]: https://img.shields.io/badge/PRs-welcome-red.svg -[prs]: http://makeapullrequest.com - -[macos-build-badge]: https://github.com/everoddandeven/monerod-gui/workflows/MacOS%20Build/badge.svg -[macos-build]: https://github.com/everoddandeven/monerod-gui/actions?query=workflow%3A%22MacOS+Build%22 -[windows-build-badge]: https://github.com/everoddandeven/monerod-gui/workflows/Windows%20Build/badge.svg -[windows-build]: https://github.com/everoddandeven/monerod-gui/actions?query=workflow%3A%22Windows+Build%22 - -[github-watch-badge]: https://img.shields.io/github/watchers/everoddandeven/monerod-gui.svg?style=social -[github-watch]: https://github.com/everoddandeven/monerod-gui/watchers -[github-star-badge]: https://img.shields.io/github/stars/everoddandeven/monerod-gui.svg?style=social -[github-star]: https://github.com/everoddandeven/monerod-gui/stargazers - -[bitcoin-donated]: https://github.com/everoddandeven/monerod-gui?tab=readme-ov-file#bitcoin -[bitcoin-donated-badge]: https://img.shields.io/badge/dynamic/json?url=https://explorer.viawallet.com/res/btc/addresses/bc1qndc2lesy0sse9vj33a35pnfrqz4znlhhs58vfp&query=$.data.balance&suffix=%20BTC&logo=bitcoin&label=donated - ## Donating Please consider donating to support the development of this project. @@ -220,3 +236,21 @@ Please consider donating to support the development of this project. wowQrCode
WW33Zj3xu6EGTyKVWaz8EQZmqsTXKdK5eG7PDRaiPuJ1LyREhGHLCRDX3AaLx4r9NFCThRvsbq99KATbswJaxd3T1iwQLJ3Tw

+ +[maintained-badge]: https://img.shields.io/badge/maintained-yes-brightgreen +[license-badge]: https://img.shields.io/badge/license-MIT-blue.svg +[prs-badge]: https://img.shields.io/badge/PRs-welcome-red.svg +[prs]: http://makeapullrequest.com + +[macos-build-badge]: https://github.com/everoddandeven/monerod-gui/workflows/MacOS%20Build/badge.svg +[macos-build]: https://github.com/everoddandeven/monerod-gui/actions?query=workflow%3A%22MacOS+Build%22 +[windows-build-badge]: https://github.com/everoddandeven/monerod-gui/workflows/Windows%20Build/badge.svg +[windows-build]: https://github.com/everoddandeven/monerod-gui/actions?query=workflow%3A%22Windows+Build%22 + +[github-watch-badge]: https://img.shields.io/github/watchers/everoddandeven/monerod-gui.svg?style=social +[github-watch]: https://github.com/everoddandeven/monerod-gui/watchers +[github-star-badge]: https://img.shields.io/github/stars/everoddandeven/monerod-gui.svg?style=social +[github-star]: https://github.com/everoddandeven/monerod-gui/stargazers + +[bitcoin-donated]: https://github.com/everoddandeven/monerod-gui?tab=readme-ov-file#bitcoin +[bitcoin-donated-badge]: https://img.shields.io/badge/dynamic/json?url=https://explorer.viawallet.com/res/btc/addresses/bc1qndc2lesy0sse9vj33a35pnfrqz4znlhhs58vfp&query=$.data.balance&suffix=%20BTC&logo=bitcoin&label=donated diff --git a/angular.json b/angular.json index f247748..b24aa89 100644 --- a/angular.json +++ b/angular.json @@ -34,7 +34,8 @@ "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", - "src/assets" + "src/assets", + "src/splash.html" ], "scripts": [ "node_modules/jquery/dist/jquery.min.js", diff --git a/app/main.ts b/app/main.ts index 6f56428..85a6b55 100644 --- a/app/main.ts +++ b/app/main.ts @@ -1,94 +1,22 @@ import { app, BrowserWindow, ipcMain, screen, dialog, Tray, Menu, MenuItemConstructorOptions, - IpcMainInvokeEvent, Notification, NotificationConstructorOptions, clipboard, powerMonitor + IpcMainInvokeEvent, Notification, NotificationConstructorOptions, clipboard, powerMonitor, + WebContents, + HandlerDetails, + Event, + WebContentsWillNavigateEventParams } from 'electron'; -import { ChildProcessWithoutNullStreams, exec, ExecException, spawn } from 'child_process'; import * as path from 'path'; import * as fs from 'fs'; -import * as https from 'https'; -import { createHash } from 'crypto'; -import * as tar from 'tar'; import * as os from 'os'; -import AutoLaunch from './auto-launch'; -import AdmZip from 'adm-zip'; - -const pidusage = require('pidusage'); -const batteryLevel = require('battery-level'); -const network = require('network'); - -function isOnBatteryPower(): Promise { - return new Promise((resolve) => { - exec("upower -i $(upower -e | grep 'battery') | grep 'state'", (error, stdout) => { - if (error) { - console.error(`isOnBatteryPower(): ${error.message}`); - resolve(false); // Ritorna false se non riesce a rilevare lo stato della batteria - return; - } - - const isOnBattery = stdout.includes("discharging"); - resolve(isOnBattery); - }); - }); -} - -interface Stats { - /** - * percentage (from 0 to 100*vcore) - */ - cpu: number; - - /** - * bytes - */ - memory: number; - - /** - * PPID - */ - ppid: number; - - /** - * PID - */ - pid: number; - - /** - * ms user + system time - */ - ctime: number; - - /** - * ms since the start of the process - */ - elapsed: number; - - /** - * ms since epoch - */ - timestamp: number; -} - -//import bz2 from 'unbzip2-stream'; -//import * as bz2 from 'unbzip2-stream'; -const bz2 = require('unbzip2-stream'); +import { AppMainProcess, MonerodProcess } from './process'; +import { BatteryUtils, FileUtils, NetworkUtils } from './utils'; app.setName('Monero Daemon'); -let autoLauncher = new AutoLaunch({ - name: 'monerod-gui', - path: process.execPath, - options: { - extraArguments: [ - '--auto-launch' - ], - linux: { - comment: 'Monerod GUI startup script', - version: '1.0.0' - } - } -}); - -const isAutoLaunched: boolean = process.argv.includes('--auto-launch'); -const minimized: boolean = process.argv.includes('--hidden'); +if (process.platform === 'win32') +{ + app.setAppUserModelId(app.name); +} let win: BrowserWindow | null = null; let isHidden: boolean = false; @@ -102,7 +30,9 @@ const dirname = (__dirname.endsWith(appApp) ? __dirname.replace(appApp, appSrc) console.log('dirname: ' + dirname); -let monerodProcess: ChildProcessWithoutNullStreams | null = null; +//let monerodProcess: ChildProcessWithoutNullStreams | null = null; +let monerodProcess: MonerodProcess | null = null; + const iconRelPath: string = 'assets/icons/monero-symbol-on-white-480.png'; //const wdwIcon = `${dirname}/${iconRelPath}`; const wdwIcon = path.join(dirname, iconRelPath); @@ -110,13 +40,6 @@ const wdwIcon = path.join(dirname, iconRelPath); let tray: Tray; let trayMenu: Menu; -const args = process.argv.slice(1), - serve = args.some(val => val === '--serve'); - -const isAppImage: () => boolean = () => { - return (!!process.env.APPIMAGE) || (!!process.env.PORTABLE_EXECUTABLE_DIR); -} - // #region Window function updateTrayMenu(): void { @@ -203,7 +126,7 @@ function createTray(): Tray { return tray; } -function createWindow(): BrowserWindow { +async function createWindow(): Promise { const size = screen.getPrimaryDisplay().workAreaSize; tray = createTray(); @@ -217,21 +140,23 @@ function createWindow(): BrowserWindow { webPreferences: { preload: path.join(__dirname, 'preload.js'), nodeIntegration: false, - allowRunningInsecureContent: (serve), + allowRunningInsecureContent: (AppMainProcess.serve), contextIsolation: true, devTools: !app.isPackaged, - sandbox: true + sandbox: true, + defaultFontSize: process.platform == 'win32' ? 12 : 16, + defaultMonospaceFontSize: process.platform == 'win32' ? 11 : 13 }, show: false, autoHideMenuBar: true, icon: wdwIcon }); - isHidden = minimized; + isHidden = AppMainProcess.startMinized; if (!app.isPackaged) win.webContents.openDevTools(); - if (serve) { + if (AppMainProcess.serve) { const debug = require('electron-debug'); debug(); @@ -249,7 +174,7 @@ function createWindow(): BrowserWindow { const url = new URL(path.join('file:', dirname, pathIndex)); console.log(`Main window url: ${url}`); - win.loadURL(url.href); + await win.loadURL(url.href); } win.on('close', (event) => { @@ -274,12 +199,8 @@ function createWindow(): BrowserWindow { return win; } -const createSplashWindow = async (): Promise => { - return undefined; - - if (os.platform() == 'win32' || isAppImage()) { - return undefined; - } +const createSplashWindow = async (): Promise => { + console.log("createSplashWindow()"); const window = new BrowserWindow({ width: 480, @@ -291,7 +212,12 @@ const createSplashWindow = async (): Promise => { icon: wdwIcon, minimizable: false, maximizable: false, - fullscreen: false + fullscreen: false, + fullscreenable: false, + movable: false, + resizable: false, + closable: true, + center: true }); // Path when running electron executable @@ -302,7 +228,15 @@ const createSplashWindow = async (): Promise => { pathIndex = '../dist/splash.html'; } - const url = new URL(path.join('file:', dirname, pathIndex)); + if (!fs.existsSync(path.join(dirname, pathIndex))) { + console.error("createSplashScreen(): path doesn't exists: " + path.join(dirname, pathIndex)); + window.close(); + return undefined; + } + + const indexPath = path.join('file:', dirname, pathIndex); + + const url = new URL(indexPath); await window.loadURL(url.href); @@ -320,120 +254,49 @@ const createSplashWindow = async (): Promise => { // #region WiFi -function isConnectedToWiFi(): Promise { - try { +async function isWifiConnected() { + let connected: boolean = false; - return new Promise((resolve, reject) => { - network.get_active_interface((err: any | null, obj: { name: string, ip_address: string, mac_address: string, type: string, netmask: string, gateway_ip: string }) => { - if (err) { - console.error(err); - reject(err); - } - else { - resolve(obj.type == 'Wireless'); - } - }) - }); + try { + connected = await NetworkUtils.isConnectedToWiFi(); } - catch(error: any) { - return isConnectedToWiFiV2(); + catch (error: any) { + console.error(error); + connected = false; } -} - -function isConnectedToWiFiV2(): Promise { - return new Promise((resolve, reject) => { - const platform = os.platform(); // Use os to get the platform - - let command = ''; - if (platform === 'win32') { - // Windows: Use 'netsh' command to check the Wi-Fi status - command = 'netsh wlan show interfaces'; - } else if (platform === 'darwin') { - // macOS: Use 'airport' command to check the Wi-Fi status - command = "/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -I | grep 'state: running'"; - } else if (platform === 'linux') { - // Linux: Use 'nmcli' to check for Wi-Fi connectivity - command = 'nmcli dev status'; - } else { - resolve(false); // Unsupported platform - } - - // Execute the platform-specific command - if (command) { - exec(command, (error: ExecException | null, stdout: string, stderr: string) => { - if (error) { - console.error(error); - reject(stderr); - resolve(false); // In case of error, assume not connected to Wi-Fi - } else { - // Check if the output indicates a connected status - if (stdout) { - const components: string[] = stdout.split("\n"); - - components.forEach((component: string) => { - if (component.includes('wifi') && !component.includes('--')) { - resolve(true); - } - }); - - resolve(false); - } else { - resolve(false); - } - } - }); - } - }); -} -function isWifiConnected() { - isConnectedToWiFi().then((connected: boolean) => { - win?.webContents.send('is-wifi-connected-result', connected); - }).catch((error: any) => { - console.error(error); - win?.webContents.send('is-wifi-connected-result', false); - }); + win?.webContents.send('is-wifi-connected-result', connected); } // #endregion // #region monerod -function getMonerodVersion(monerodFilePath: string): void { - const monerodProcess = spawn(monerodFilePath, [ '--version' ]); - - monerodProcess.stdout.on('data', (data) => { - win?.webContents.send('monero-version', `${data}`); - }) - - monerodProcess.stderr.on('data', (data) => { - win?.webContents.send('monero-version-error', `${data}`); - }) -} - -function checkValidMonerodPath(monerodPath: string): void { - let foundUsage: boolean = false; - const monerodProcess = spawn(monerodPath, ['--help']); - - monerodProcess.stderr.on('data', (data) => { - win?.webContents.send('on-check-valid-monerod-path', false); - }); - - monerodProcess.stdout.on('data', (data) => { - if (`${data}`.includes('monerod [options|settings] [daemon_command...]')) { - foundUsage = true; - } +async function getMonerodVersion(monerodFilePath: string): Promise { + const proc = new MonerodProcess({ + monerodCmd: monerodFilePath, + isExe: true }); - monerodProcess.on('close', (code: number) => { - win?.webContents.send('on-check-valid-monerod-path', foundUsage); - }) - + try { + console.log("Before proc.getVersion()"); + const version = await proc.getVersion(); + console.log("After proc.getVersion()"); + win?.webContents.send('monero-version', version); + } + catch(error: any) { + const err = (error instanceof Error) ? error.message : `${error}`; + win?.webContents.send('monero-version-error', err); + } } -let moneroFirstStdout: boolean = true; +async function checkValidMonerodPath(monerodPath: string): Promise { + const valid = await MonerodProcess.isValidMonerodPath(monerodPath); + + win?.webContents.send('on-check-valid-monerod-path', valid); +} -function startMoneroDaemon(commandOptions: string[]): ChildProcessWithoutNullStreams { +async function startMoneroDaemon(commandOptions: string[]): Promise { const monerodPath = commandOptions.shift(); if (!monerodPath) { @@ -447,145 +310,82 @@ function startMoneroDaemon(commandOptions: string[]): ChildProcessWithoutNullStr win?.webContents.send('monero-stderr', error); throw new Error("Monerod already started"); } - - const message: string = "Starting monerod daemon with options: " + commandOptions.join(" "); - - console.log(message); - moneroFirstStdout = true; - commandOptions.push('--non-interactive'); - // Avvia il processo usando spawn - monerodProcess = spawn(monerodPath, commandOptions); - - // Gestisci l'output di stdout in streaming - monerodProcess.stdout.on('data', (data) => { - //console.log(`monerod stdout: ${data}`); - const pattern = '**********************************************************************'; - - if (moneroFirstStdout && data.includes(pattern)) { - win?.webContents.send('monerod-started', true); - moneroFirstStdout = false; - } + const monerodProc = new MonerodProcess({ + monerodCmd: monerodPath, + flags: commandOptions, + isExe: true + }); + monerodProc.onStdOut((data) => { win?.webContents.send('monero-stdout', `${data}`); - // Puoi anche inviare i log all'interfaccia utente tramite IPC }); - // Gestisci gli errori in stderr - monerodProcess.stderr.on('data', (data) => { - console.error(`monerod error: ${data}`); - - if (moneroFirstStdout) { - win?.webContents.send('monerod-started', false); - moneroFirstStdout = false; - } - + monerodProc.onStdErr((data) => { win?.webContents.send('monero-stderr', `${data}`); }); - // Gestisci la chiusura del processo - monerodProcess.on('close', (code: number) => { - console.log(`monerod exited with code: ${code}`); - win?.webContents.send('monero-stdout', `monerod exited with code: ${code}`); + monerodProc.onError((err: Error) => { + win?.webContents.send('monero-stderr', `${err.message}`); + }); + + monerodProc.onClose((_code: number | null) => { + const code = _code != null ? _code : -Number.MAX_SAFE_INTEGER; + const msg = `Process monerod ${monerodProc.pid} exited with code: ${code}`; + console.log(msg); + win?.webContents.send('monero-stdout', msg); win?.webContents.send('monero-close', code); monerodProcess = null; }); + monerodProcess = null; + monerodProcess = monerodProc; + + try { + await monerodProcess.start(); + win?.webContents.send('monerod-started', true); + } + catch(error: any) { + win?.webContents.send('monerod-started', false); + } + return monerodProcess; } -function monitorMonerod(): void { +async function monitorMonerod(): Promise { if (!monerodProcess) { win?.webContents.send('on-monitor-monerod-error', 'Monerod not running'); return; } - if (!monerodProcess.pid) { - win?.webContents.send('on-monitor-monerod-error', 'Unknown monero pid'); - return; + try { + const stats = await monerodProcess.getStats(); + win?.webContents.send('on-monitor-monerod', stats); } + catch(error: any) { + let message: string; - pidusage(monerodProcess.pid, (error: Error | null, stats: Stats) => { - if (error) { - win?.webContents.send('on-monitor-monerod-error', `${error}`); - return; + if (error instanceof Error) { + message = error.message; + } + else { + message = `${error}`; } - win?.webContents.send('on-monitor-monerod', stats); - }); + win?.webContents.send('on-monitor-monerod-error', message); + } } // #endregion // #region Download Utils -const downloadFile = (url: string, destinationDir: string, onProgress: (progress: number) => void): Promise => { - return new Promise((resolve, reject) => { - const request = (url: string) => { - https.get(url, (response) => { - if (response.statusCode === 200) { - const contentDisposition = response.headers['content-disposition']; - let finalFilename = ''; - - // Estrai il nome del file dall'URL o dal content-disposition - if (contentDisposition && contentDisposition.includes('filename')) { - const match = contentDisposition.match(/filename="(.+)"/); - if (match) { - finalFilename = match[1]; - } - } else { - // Se non c'è content-disposition, prendiamo il nome dall'URL - finalFilename = url.split('/').pop() || 'downloaded-file'; - } - - const destination = `${destinationDir}/${finalFilename}`; - const file = fs.createWriteStream(destination); - - const totalBytes = parseInt(response.headers['content-length'] || '0', 10); - let downloadedBytes = 0; - - response.on('data', (chunk) => { - downloadedBytes += chunk.length; - const progress = (downloadedBytes / totalBytes) * 100; - onProgress(progress); // Notifica il progresso - }); - - response.pipe(file); - - file.on('finish', () => { - file.close(() => resolve(finalFilename)); // Restituisci il nome del file finale - }); - } else if (response.statusCode === 301 || response.statusCode === 302) { - // Se è un redirect, effettua una nuova richiesta verso il location header - const newUrl = response.headers.location; - if (newUrl) { - request(newUrl); // Ripeti la richiesta con il nuovo URL - } else { - reject(new Error('Redirection failed without a location header')); - } - } else { - reject(new Error(`Failed to download: ${response.statusCode}`)); - } - }).on('error', (err) => { - reject(err); - }); - }; - - request(url); // Inizia la richiesta - }); -}; - -// Funzione per scaricare e verificare l'hash -const downloadAndVerifyHash = async (hashUrl: string, fileName: string, filePath: string): Promise => { - //const hashFilePath = path.join(app.getPath('temp'), 'monero_hashes.txt'); - - // Scarica il file di hash - const hashFileName = await downloadFile(hashUrl, app.getPath('temp'), () => {}); +async function downloadAndVerifyHash(hashUrl: string, fileName: string, filePath: string): Promise { + const hashFileName = await FileUtils.downloadFile(hashUrl, app.getPath('temp'), () => {}); const hashFilePath = `${app.getPath('temp')}/${hashFileName}`; - // Leggi il file di hash e cerca l'hash corrispondente const hashContent = fs.readFileSync(hashFilePath, 'utf8'); const hashLines = hashContent.split('\n'); let expectedHash: string | null = null; @@ -603,106 +403,10 @@ const downloadAndVerifyHash = async (hashUrl: string, fileName: string, filePath } // Verifica l'hash del file scaricato - const calculatedHash = await verifyFileHash(`${filePath}/${fileName}`); - return calculatedHash === expectedHash; + return await FileUtils.checkFileHash(`${filePath}/${fileName}`, expectedHash); }; // Funzione per verificare l'hash del file -const verifyFileHash = (filePath: string): Promise => { - return new Promise((resolve, reject) => { - const hash = createHash('sha256'); - const fileStream = fs.createReadStream(filePath); - - fileStream.on('data', (data) => { - hash.update(data); - }); - - fileStream.on('end', () => { - resolve(hash.digest('hex')); - }); - - fileStream.on('error', (err) => { - reject(err); - }); - }); -}; - -const extractTarBz2 = (filePath: string, destination: string): Promise => { - return new Promise((resolve, reject) => { - // Crea il file decomprimendo il .bz2 in uno .tar temporaneo - const tarPath = path.join(destination, 'temp.tar'); - const fileStream = fs.createReadStream(filePath); - const decompressedStream = fileStream.pipe(bz2()); - - const writeStream = fs.createWriteStream(tarPath); - - decompressedStream.pipe(writeStream); - - let extractedDir: string = ''; - - writeStream.on('finish', () => { - // Una volta che il file .tar è stato creato, estrailo - tar.extract({ cwd: destination, file: tarPath, onReadEntry: (entry: tar.ReadEntry) => { - if (extractedDir == '') { - const topLevelDir = entry.path.split('/')[0]; - extractedDir = topLevelDir; // Salva la prima directory - } - } }) - .then(() => { - // Elimina il file .tar temporaneo dopo l'estrazione - fs.unlink(tarPath, (err) => { - if (err) reject(err); - else if (extractedDir == '') reject('Extraction failed') - else resolve(extractedDir); - }); - }) - .catch(reject); - }); - - writeStream.on('error', reject); - }); -}; - -const extractZip = (filePath: string, destination: string): Promise => { - return new Promise((resolve, reject) => { - try { - const zip = new AdmZip(filePath); - - // Ensure destination exists - if (!fs.existsSync(destination)) { - fs.mkdirSync(destination, { recursive: true }); - } - - // Extract the ZIP file - zip.extractAllTo(destination, true); - - // Get the name of the extracted folder - const extractedEntries = zip.getEntries(); - const folderName = extractedEntries[0]?.entryName.split('/')[0]; - - // Ensure folder name exists - if (!folderName) { - reject(new Error("Could not determine the extracted folder name")); - return; - } - - resolve(path.join(destination, folderName)); - } catch (error) { - reject(error); - } - }); -}; - -const extract = (filePath: string, destination: string): Promise => { - if (filePath.endsWith('.zip')) { - return extractZip(filePath, destination); - } - else if (filePath.endsWith('.tar.bz2')) { - return extractTarBz2(filePath, destination); - } - - throw new Error("Unknown file type " + filePath); -} // #endregion @@ -718,6 +422,21 @@ function showNotification(options?: NotificationConstructorOptions): void { new Notification(options).show(); } +function showSecurityWarning(msg: string): void { + if (win) { + dialog.showMessageBoxSync(win, { + type: 'warning', + title: 'Security Warning', + message: msg + }); + } + else { + dialog.showErrorBox('Security Warning', msg); + } + + console.warn(msg); +} + try { // This method will be called when Electron has finished // initialization and is ready to create browser windows. @@ -728,20 +447,20 @@ try { const gotInstanceLock = app.requestSingleInstanceLock(); if (!gotInstanceLock) { - dialog.showErrorBox('', 'Another instance of monerod GUI is running'); + dialog.showErrorBox('', 'Another instance of Monerod GUI is running'); app.quit(); return; } setTimeout(async () => { const splash = await createSplashWindow(); - createWindow(); + await createWindow(); await new Promise((resolve, reject) => { try { setTimeout(() => { if (splash) splash.close(); - if (!minimized) { + if (!AppMainProcess.startMinized) { win?.show(); win?.maximize(); } @@ -764,11 +483,11 @@ try { } }); - app.on('activate', () => { + app.on('activate', async () => { // On OS X it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. if (win === null) { - createWindow(); + await createWindow(); } }); @@ -778,59 +497,43 @@ try { // #region Security - app.on('web-contents-created', (event, webContents) => { - webContents.setWindowOpenHandler((details) => { - console.warn("Prevented unsafe window creation"); + app.on('web-contents-created', (event, webContents: WebContents) => { + webContents.setWindowOpenHandler((details: HandlerDetails) => { + const msg = `Prevented unsafe content: ${details.url}`; + showSecurityWarning(msg); console.warn(details); + return { action: 'deny' }; }); - }); - app.on('web-contents-created', (event, contents) => { - contents.on('will-navigate', (event, navigationUrl) => { + webContents.on('will-navigate', (event: Event, navigationUrl: string) => { event.preventDefault(); - console.warn(`Prevented unsage window navigation to ${navigationUrl}`); - /* - const parsedUrl = new URL(navigationUrl) - - if (parsedUrl.origin !== 'https://example.com') { - event.preventDefault() - } - */ - }) - }) - // #endregion - - ipcMain.handle('is-on-battery-power', (event: IpcMainInvokeEvent) => { - const onBattery = powerMonitor.isOnBatteryPower(); - - if (!onBattery && os.platform() == 'linux') { - isOnBatteryPower().then((value) => { - win?.webContents.send('on-is-on-battery-power', value); - }).catch((error: any) => { - console.error(`${error}`); - win?.webContents.send('on-is-on-battery-power', false); - }); + const msg = `Prevented unsage window navigation to ${navigationUrl}`; + showSecurityWarning(msg); + }); + }); - return; - } - else { - win?.webContents.send('on-is-on-battery-power', onBattery); - } + // #endregion + ipcMain.handle('is-on-battery-power', async (event: IpcMainInvokeEvent) => { + const onBattery = await BatteryUtils.isOnBatteryPower(); + win?.webContents.send('on-is-on-battery-power', onBattery); }); powerMonitor.on('on-ac', () => win?.webContents.send('on-ac')); powerMonitor.on('on-battery', () => win?.webContents.send('on-battery')); - ipcMain.handle('is-auto-launched', (event: IpcMainInvokeEvent) => { - console.debug(event); - - win?.webContents.send('on-is-auto-launched', isAutoLaunched); + ipcMain.handle('is-auto-launched', (event: IpcMainInvokeEvent) => { + win?.webContents.send('on-is-auto-launched', AppMainProcess.autoLaunched); }); - ipcMain.handle('quit', (event: IpcMainInvokeEvent) => { + ipcMain.handle('quit', async (event: IpcMainInvokeEvent) => { isQuitting = true; + + if (monerodProcess) { + await monerodProcess.stop(); + } + tray.destroy(); win?.close(); win?.destroy(); @@ -860,7 +563,7 @@ try { }); // Scarica il file Monero - const fileName = await downloadFile(downloadUrl, destination, (progress) => { + const fileName = await FileUtils.downloadFile(downloadUrl, destination, (progress) => { win?.setProgressBar(progress, { mode: 'normal' }); @@ -879,7 +582,7 @@ try { // Estrai il file const fPath = `${destination}/${fileName}`; event.sender.send('download-progress', { progress: 100, status: 'Extracting' }); - const extractedDir = await extract(fPath, destination); + const extractedDir = await FileUtils.extract(fPath, destination); event.sender.send('download-progress', { progress: 100, status: 'Download and extraction completed successfully' }); event.sender.send('download-progress', { progress: 200, status: os.platform() == 'win32' ? extractedDir : `${destination}/${extractedDir}` }); @@ -889,13 +592,37 @@ try { }); } catch (error) { - event.sender.send('download-progress', { progress: 0, status: `Error: ${error}` }); + event.sender.send('download-progress', { progress: 0, status: `${error}` }); win?.setProgressBar(0, { mode: 'error' }); } }); + ipcMain.handle('download-file', async (event: IpcMainInvokeEvent, url: string, destination: string) => { + try { + event.sender.send('download-file-progress', { progress: 0, status: 'Starting download' }); + + const fileName = await FileUtils.downloadFile(url, destination, (progress) => { + win?.setProgressBar(progress, { + mode: 'normal' + }); + + event.sender.send('download-file-progress', { progress, status: 'Downloading' }); + }); + + win?.setProgressBar(0, { + mode: 'none' + }); + + event.sender.send('download-file-complete', `${destination}${separator}${fileName}`); + } + catch(error: any) { + console.error(error); + event.sender.send('download-file-error', `${error}`); + } + }); + ipcMain.handle('read-file', (event: IpcMainInvokeEvent, filePath: string) => { fs.readFile(filePath, 'utf-8', (err, data) => { if (err != null) { @@ -995,91 +722,52 @@ try { // #region Auto Launch - ipcMain.handle('is-auto-launch-enabled', (event: IpcMainInvokeEvent) => { - autoLauncher.isEnabled().then((enabled: boolean) => { - win?.webContents.send('on-is-auto-launch-enabled', enabled); - }).catch((error: any) => { - console.error(error); - win?.webContents.send('on-is-auto-launch-enabled', false); - }); + ipcMain.handle('is-auto-launch-enabled', async (event: IpcMainInvokeEvent) => { + const enabled = await AppMainProcess.isAutoLaunchEnabled(); + win?.webContents.send('on-is-auto-launch-enabled', enabled); }); - ipcMain.handle('enable-auto-launch', (event: IpcMainInvokeEvent, minimized: boolean) => { - autoLauncher.isEnabled().then((enabled: boolean) => { - if (enabled) { - win?.webContents.send('on-enable-auto-launch-error', 'already enabled'); - return; - } - - autoLauncher = new AutoLaunch({ - name: 'monerod-gui', - path: process.execPath, - options: { - launchInBackground: minimized, - extraArguments: [ - '--auto-launch' - ] - } - }); - - autoLauncher.enable().then(() => { - autoLauncher.isEnabled().then((enabled: boolean) => { - if (enabled) { - win?.webContents.send('on-enable-auto-launch-success'); - } - win?.webContents.send('on-enable-auto-launch-error', `Could not enabled auto launch`); - }).catch((error: any) => { - win?.webContents.send('on-enable-auto-launch-error', `${error}`); - }); + ipcMain.handle('enable-auto-launch', async (event: IpcMainInvokeEvent, minimized: boolean) => { + try { + await AppMainProcess.enableAutoLaunch(minimized); + win?.webContents.send('on-enable-auto-launch-success'); + } + catch(error: any) { + const err = (error instanceof Error) ? error.message : `${error}`; - }).catch((error: any) => { - console.error(error); - win?.webContents.send('on-enable-auto-launch-error', `${error}`); - }); - }).catch((error: any) => { - console.error(error); - win?.webContents.send('on-enable-auto-launch-error', `${error}`); - }); + win?.webContents.send('on-enable-auto-launch-error', err); + } }); - ipcMain.handle('get-battery-level', (event: IpcMainInvokeEvent) => { - batteryLevel().then((level: number) => { - win?.webContents.send('on-get-battery-level', level); - }).catch((error: any) => { - console.error(error); - win?.webContents.send('on-get-battery-level', -1); - }) + ipcMain.handle('get-battery-level', async (event: IpcMainInvokeEvent) => { + win?.webContents.send('on-get-battery-level', await BatteryUtils.getLevel()); }); - ipcMain.handle('disable-auto-launch', (event: IpcMainInvokeEvent) => { - autoLauncher.isEnabled().then((enabled: boolean) => { - if (!enabled) { - win?.webContents.send('on-disable-auto-launch-error', 'already disabled'); - return; - } + ipcMain.handle('disable-auto-launch', async (event: IpcMainInvokeEvent) => { + try { + await AppMainProcess.disableAutoLaunch(); + win?.webContents.send('on-disable-auto-launch-success'); + } + catch(error: any) { + const err = (error instanceof Error) ? error.message : `${error}`; + win?.webContents.send('on-disable-auto-launch-error', err); + } + }); - autoLauncher.disable().then(() => { - autoLauncher.isEnabled().then((enabled: boolean) => { - if (!enabled) { - win?.webContents.send('on-disable-auto-launch-success'); - } - win?.webContents.send('on-disable-auto-launch-error', `Could not disable auto launch`); - }).catch((error: any) => { - win?.webContents.send('on-disable-auto-launch-error', `${error}`); - }); + // #endregion - }).catch((error: any) => { - console.error(error); - win?.webContents.send('on-disable-auto-launch-error', `${error}`); + ipcMain.handle('show-error-box', (event: IpcMainInvokeEvent, title: string, content: string) => { + if (win) { + dialog.showMessageBoxSync(win, { + message: content, + type: 'error', + title: title != '' ? title : 'Error' }); - }).catch((error: any) => { - console.error(error); - win?.webContents.send('on-disable-auto-launch-error', `${error}`); - }); + return; + } + dialog.showErrorBox(title, content); }); - // #endregion - ipcMain.handle('set-tray-item-enabled', (event: IpcMainInvokeEvent, id: string, enabled: boolean) => { setTrayItemEnabled(id, enabled); }); @@ -1088,18 +776,20 @@ try { tray.setToolTip(toolTip); }); - ipcMain.handle('is-app-image', (event: IpcMainInvokeEvent) => { - win?.webContents.send('on-is-app-image', isAppImage()); + ipcMain.handle('is-portable', (event: IpcMainInvokeEvent) => { + win?.webContents.send('on-is-portable', AppMainProcess.isPortable); }); ipcMain.handle('copy-to-clipboard', (event: IpcMainInvokeEvent, content: string) => { clipboard.writeText(content, "selection"); }); - -} catch (e) { +} catch (e: any) { // Catch Error console.error(e); - // throw e; + + dialog.showErrorBox('', `${e}`); + + app.quit(); } diff --git a/app/preload.js b/app/preload.js index 52fd7a0..bac4091 100644 --- a/app/preload.js +++ b/app/preload.js @@ -93,10 +93,10 @@ contextBridge.exposeInMainWorld('electronAPI', { ipcRenderer.on('monero-version-error', callback); }, unregisterOnMoneroVersion: () => { - ipcRenderer.removeAllListeners('on-monero-version'); + ipcRenderer.removeAllListeners('monero-version'); }, unregisterOnMoneroVersionError: () => { - ipcRenderer.removeAllListeners('unregister-on-monero-version-error'); + ipcRenderer.removeAllListeners('monero-version-error'); }, downloadMonerod: (downloadUrl, destination) => { ipcRenderer.invoke('download-monerod', downloadUrl, destination); @@ -110,6 +110,9 @@ contextBridge.exposeInMainWorld('electronAPI', { onCheckValidMonerodPath: (callback) => { ipcRenderer.on('on-check-valid-monerod-path', callback); }, + unregisterOnCheckValidMonerodPath: () => { + ipcRenderer.removeAllListeners('on-check-valid-monerod-path'); + }, selectFolder: () => { ipcRenderer.invoke('select-folder') }, @@ -227,14 +230,14 @@ contextBridge.exposeInMainWorld('electronAPI', { unregisterOnDisableAutoLaunchSuccess: () => { ipcRenderer.removeAllListeners('on-disable-auto-launch-success') }, - isAppImage: () => { - ipcRenderer.invoke('is-app-image'); + isPortable: () => { + ipcRenderer.invoke('is-portable'); }, - onIsAppImage: (callback) => { - ipcRenderer.on('on-is-app-image', callback); + onIsPortable: (callback) => { + ipcRenderer.on('on-is-portable', callback); }, - unregisterOnIsAppImage: () => { - ipcRenderer.removeAllListeners('on-is-app-image'); + unregisterIsPortable: () => { + ipcRenderer.removeAllListeners('on-is-portable'); }, isAutoLaunched: () => { ipcRenderer.invoke('is-auto-launched'); @@ -244,5 +247,25 @@ contextBridge.exposeInMainWorld('electronAPI', { }, unregisterOnIsAutoLaunched: () => { ipcRenderer.removeAllListeners('on-is-auto-launched'); + }, + downloadFile: (url, destination) => { + ipcRenderer.invoke('download-file', url, destination); + }, + onDownloadFileProgress: (callback) => { + ipcRenderer.on('download-file-progress', callback); + }, + onDownloadFileError: (callback) => { + ipcRenderer.on('download-file-error', callback); + }, + onDownloadFileComplete: (callback) => { + ipcRenderer.on('download-file-complete', callback); + }, + unregisterOnDownloadFile: () => { + ipcRenderer.removeAllListeners('download-file-progress'); + ipcRenderer.removeAllListeners('download-file-error'); + ipcRenderer.removeAllListeners('download-file-complete'); + }, + showErrorBox: (title, content) => { + ipcRenderer.invoke('show-error-box', title, content); } }); diff --git a/app/process/AppChildProcess.ts b/app/process/AppChildProcess.ts new file mode 100644 index 0000000..370a3a4 --- /dev/null +++ b/app/process/AppChildProcess.ts @@ -0,0 +1,251 @@ +import * as fs from 'fs'; +import { ChildProcessWithoutNullStreams, spawn } from "child_process"; +import { ProcessStats } from './ProcessStats'; + + +const pidusage = require('pidusage'); + +export class AppChildProcess { + + protected _starting: boolean = false; + protected _stopping: boolean = false; + protected _running: boolean = false; + protected _isExe: boolean = true; + + protected _process?: ChildProcessWithoutNullStreams; + + protected _command: string; + protected readonly _args?: string[]; + + public get pid(): number { + if (!this._process || this._process.pid == null) { + return -1; + } + + return this._process.pid; + } + + protected readonly _handlers: { + 'stdout': ((data: string) => void)[], + 'stderr': ((err: string) => void)[], + 'onclose': ((code: number | null) => void)[], + 'onerror': ((error: Error) => void)[], + } = { + 'stdout': [], + 'stderr': [], + 'onclose': [], + 'onerror': [] + }; + + private readonly mOnErrorDefaultHandler: (error: Error) => void = (error: Error) => { + if (!this._process) { + return; + } + + const listeners = this._process.listeners('error'); + + if (listeners.length > 1) { + return; + } + + console.error("Uncaught exeception: "); + console.error(error); + }; + + public get command(): string { + return this._command; + } + + public get args(): string[] { + return this._args ? this._args : []; + } + + public get running(): boolean { + return this._running; + } + + constructor({ command, args, isExe } : { command: string, args?: string[], isExe?: boolean}) { + this._command = command; + this._args = args; + this._isExe = isExe === false ? false : true; + } + + protected static replaceAll(value: string, oldValue: string, newValue: string): string { + let v = value; + + while(v.includes(oldValue)) { + v = v.replace(oldValue, newValue); + } + + return v; + } + + protected static checkExecutable(executablePath: string): void { + const exeComponents: string[] = executablePath.split(" ").filter((c) => c != ''); + console.log("AppProcess.checkExecutable(): " + executablePath); + if (exeComponents.length == 0) { + throw new Error("Invalid command provided"); + } + + const exePath = exeComponents[0]; + + if (!fs.existsSync(exePath)) { + throw new Error("Cannot find executable: " + exePath); + } + } + + protected checkExecutable(): void { + AppChildProcess.checkExecutable(this.command); + } + + public onStdOut(callback: (out: string) => void): void { + const cbk = (chunk: any) => callback(`${chunk}`); + + if (!this._process) { + this._handlers.stdout.push(cbk); + return; + } + + this._process.stdout.on('data', cbk); + } + + public onStdErr(callback: (err: string) => void): void { + const cbk = (chunk: any) => callback(`${chunk}`); + + if (!this._process) { + this._handlers.stderr.push(cbk); + return; + } + + this._process.stderr.on('data', cbk); + } + + public onError(callback: (err: Error) => void): void { + if (!this._process) + { + this._handlers.onerror.push(callback); + return; + } + + this._process.on('error', callback); + } + + public onClose(callback: (code: number | null) => void): void { + if (!this._process) { + this._handlers.onclose.push(callback); + return; + } + + this._process.on('close', callback); + } + + public async start(): Promise { + if (this._starting) { + throw new Error("Process is already starting"); + } + + if (this._stopping) { + throw new Error("Process is stopping"); + } + + if (this._running) { + throw new Error("Process already running"); + } + + if (this._isExe) { + this.checkExecutable(); + } + + this._starting = true; + + const process = spawn(this._command, this._args); + this._process = process; + + const promise = new Promise((resolve, reject) => { + const onSpawnError = (err: Error) => { + this._starting = false; + this._running = false; + reject(err); + }; + + const onSpawn = () => { + this._starting = false; + this._running = true; + process.off('error', onSpawnError); + process.on('error', this.mOnErrorDefaultHandler); + + this._handlers.onclose.forEach((listener) => process.on('close', listener)); + this._handlers.onerror.forEach((listener) => process.on('error', listener)); + this._handlers.stdout.forEach((listener) => process.stdout.on('data', listener)); + this._handlers.stderr.forEach((listener) => process.stderr.on('data', listener)); + + resolve(); + }; + + process.once('error', onSpawnError); + process.once('spawn', onSpawn); + }); + + process.once('close', () => { + if (this._stopping) return; + + this._running = false; + this._process = undefined; + }); + + await promise; + } + + public async stop(): Promise { + if (this._starting) { + throw new Error("Process is starting"); + } + + if (this._stopping) { + throw new Error("Process is already stopping"); + } + + if (!this._running || !this._process) { + throw new Error("Process is not running"); + } + + this._stopping = true; + + const promise = new Promise((resolve) => { + process.on('close', (code: number | null) => { + this._process = undefined; + this._running = false; + this._stopping = false; + resolve(code); + }); + }); + + this._process?.kill(); + + return await promise; + } + + public async getStats(): Promise { + if (!this._process) { + throw new Error("Process not running"); + } + + const pid = this._process.pid; + + if (!pid) { + throw new Error("Process is unknown"); + } + + return await new Promise((resolve, reject) => { + pidusage(pid, (err: Error | null, stats: ProcessStats) => { + if (err) { + reject(err); + } + else { + resolve(stats); + } + }); + }); + } + +} \ No newline at end of file diff --git a/app/process/AppMainProcess.ts b/app/process/AppMainProcess.ts new file mode 100644 index 0000000..c2f2123 --- /dev/null +++ b/app/process/AppMainProcess.ts @@ -0,0 +1,90 @@ +import AutoLaunch from "../auto-launch"; + +export abstract class AppMainProcess { + + private static autoLaunch: AutoLaunch = new AutoLaunch({ + name: 'monerod-gui', + path: process.execPath, + options: { + launchInBackground: process.argv.includes('--hidden'), + extraArguments: [ + '--auto-launch' + ], + linux: { + comment: 'Monerod GUI startup script', + version: '1.0.1' + } + } + }); + + public static get serve(): boolean { + const args = process.argv.slice(1); + + return args.some(val => val === '--serve'); + } + + public static get autoLaunched(): boolean { + return process.argv.includes('--auto-launch'); + } + + public static get startMinized(): boolean { + return process.argv.includes('--hidden'); + } + + public static get isPortable(): boolean { + return (!!process.env.APPIMAGE) || (!!process.env.PORTABLE_EXECUTABLE_DIR); + } + + public static async isAutoLaunchEnabled(): Promise { + try { + return this.autoLaunch.isEnabled(); + } + catch { + return false; + } + } + + public static async enableAutoLaunch(startMinized: boolean): Promise { + let enabled = await this.isAutoLaunchEnabled(); + + if (enabled) { + throw new Error("Auto launch already enabled"); + } + + this.autoLaunch = new AutoLaunch({ + name: 'monerod-gui', + path: process.execPath, + options: { + launchInBackground: startMinized, + extraArguments: [ + '--auto-launch' + ] + } + }); + + await this.autoLaunch.enable(); + + enabled = await this.isAutoLaunchEnabled(); + + if (!enabled) { + throw new Error("Could not enable auto launch due an unkown error"); + } + } + + public static async disableAutoLaunch(): Promise { + let enabled = await this.isAutoLaunchEnabled(); + + if (!enabled) { + throw new Error("Auto launch already disabled"); + } + + await this.autoLaunch.disable(); + + enabled = await this.isAutoLaunchEnabled(); + + if (enabled) { + throw new Error("Could not disable auto launch due an unknown error"); + } + } + +} \ No newline at end of file diff --git a/app/process/MonerodProcess.ts b/app/process/MonerodProcess.ts new file mode 100644 index 0000000..66055a0 --- /dev/null +++ b/app/process/MonerodProcess.ts @@ -0,0 +1,219 @@ +import { AppChildProcess } from "./AppChildProcess"; + +export class MonerodProcess extends AppChildProcess { + + protected static readonly stdoutPattern: string = '**********************************************************************'; + + public get interactive(): boolean { + return this.args ? !this.args.includes('--non-interactive') : true; + } + + public get detached(): boolean { + return this.args ? this.args.includes('--detached') : false; + } + + constructor({ monerodCmd, flags, isExe }: { monerodCmd: string, flags?: string[], isExe?: boolean }) { + super({ + command: monerodCmd, + args: flags, + isExe: isExe + }) + } + + public static async isValidMonerodPath(monerodPath: string): Promise { + console.log(`MonerodProcess.isValidMonerodPath('${monerodPath}')`); + + if (typeof monerodPath !== 'string' || MonerodProcess.replaceAll(monerodPath, " ", "") == "") { + return false; + } + + try { + MonerodProcess.checkExecutable(monerodPath); + } + catch { + return false; + } + + const proc = new AppChildProcess({ + command: monerodPath, + args: [ '--help' ], + isExe: true + }); + + const promise = new Promise((resolve) => { + let foundUsage: boolean = false; + + proc.onError((err: Error) => { + console.log(`MonerodProcess.isValidMonerodPath(): Error: '${err.message}'`); + resolve(false); + }); + + proc.onStdErr((err: string) => { + console.log(`MonerodProcess.isValidMonerodPath(): Std Error: '${err}'`); + resolve(false); + }); + + proc.onStdOut((data: string) => { + if (foundUsage) { + return; + } + + if ( + `${data}`.includes('monerod [options|settings] [daemon_command...]') || + `${data}`.includes('monerod.exe [options|settings] [daemon_command...]') + ) { + foundUsage = true; + } + }); + + proc.onClose((code: number | null) => { + console.log(`MonerodProcess.isValidMonerodPath(): exit code '${code}', found usage: ${foundUsage}`); + resolve(foundUsage && code == 0); + }); + }); + + try { + await proc.start(); + } + catch(error: any) { + console.log(`MonerodProcess.isValidMonerodPath(): exit code '${error}'`); + } + + return await promise; + } + + public override async start(): Promise { + if (this._isExe) { + const validPath = await MonerodProcess.isValidMonerodPath(this._command); + + if (!validPath) { + throw new Error("Invalid monerod path provided: " + this._command); + } + } + + let message: string = "Starting monerod process"; + + message += `\n\t${this._isExe ? 'Path' : 'Command'}: ${this._command}`; + + if (this._args) { + message += `\n\tFlags: ${this._args.join(" ")}` + } + + console.log(message); + + let firstPatternFound = false; + const waitForPattern = this._args ? !this._args.includes('--version') && !this.args.includes('--help') : true; + + const patternPromise = new Promise((resolve, reject) => { + let firstStdout = true; + let timeout: NodeJS.Timeout | undefined = undefined; + + const onStdOut = (out: string) => { + if (firstStdout) { + firstStdout = false; + + if (!waitForPattern) { + return; + } + + timeout = setTimeout(() => { + if (this._process && this._process.exitCode == null) { + this._process.kill(); + } + timeout = undefined; + + reject(new Error("MonerodProcess.start(): Timed out")); + }, 90*1000); + } + + const foundPattern = out.includes(MonerodProcess.stdoutPattern); + + if (!foundPattern) { + return; + } + + if (firstPatternFound) { + if(timeout !== undefined) { + clearTimeout(timeout); + console.log("MonerodProcess.start(): Cleared timeout"); + } + else { + console.log("MonerodProcess.start(): No timeout found"); + } + + resolve(); + } + else { + firstPatternFound = true; + } + }; + + this.onStdOut(onStdOut); + }); + + await super.start(); + + if (!this._process || !this._process.pid) { + throw new Error("Monerod process did not start!"); + } + try { + console.log(`MonerodProcess.start(): wait for pattern: ${waitForPattern}`); + + if (waitForPattern) await patternPromise; + + console.log("Started monerod process pid: " + this._process.pid); + } + catch(error: any) { + this._running = false; + this._starting = false; + this._stopping = false; + + if (error instanceof Error) { + throw error; + } + else { + throw new Error(`${error}`); + } + } + + } + + public async getVersion(): Promise { + const proc = new MonerodProcess({ + monerodCmd: this._command, + flags: [ '--version' ], + isExe: this._isExe + }); + + const promise = new Promise((resolve, reject) => { + proc.onError((err: Error) => { + console.log("MonerodProcess.getVersion(): proc.onError():"); + console.error(err); + reject(err) + }); + + proc.onStdErr((err: string) => { + console.log("MonerodProcess.getVersion(): proc.onStdErr()"); + console.error(err); + reject(new Error(err)); + }); + + proc.onStdOut((version: string) => { + if (version == '') { + return; + } + + console.log("MonerodProcess.getVersion(): proc.onStdOut():"); + console.log(version); + resolve(version); + }); + }); + + console.log("MonerodProcess.getVersion(): Before proc.start()"); + await proc.start(); + console.log("MonerodProcess.getVersion(): After proc.start()"); + + return await promise; + } + +} \ No newline at end of file diff --git a/app/process/ProcessStats.ts b/app/process/ProcessStats.ts new file mode 100644 index 0000000..be794e8 --- /dev/null +++ b/app/process/ProcessStats.ts @@ -0,0 +1,36 @@ +export interface ProcessStats { + /** + * percentage (from 0 to 100*vcore) + */ + cpu: number; + + /** + * bytes + */ + memory: number; + + /** + * PPID + */ + ppid: number; + + /** + * PID + */ + pid: number; + + /** + * ms user + system time + */ + ctime: number; + + /** + * ms since the start of the process + */ + elapsed: number; + + /** + * ms since epoch + */ + timestamp: number; + } \ No newline at end of file diff --git a/app/process/index.ts b/app/process/index.ts new file mode 100644 index 0000000..7658db0 --- /dev/null +++ b/app/process/index.ts @@ -0,0 +1,4 @@ +export { AppMainProcess } from "./AppMainProcess"; +export { ProcessStats } from "./ProcessStats"; +export { AppChildProcess } from "./AppChildProcess"; +export { MonerodProcess } from "./MonerodProcess"; diff --git a/app/utils/BatteryUtils.ts b/app/utils/BatteryUtils.ts new file mode 100644 index 0000000..9f94493 --- /dev/null +++ b/app/utils/BatteryUtils.ts @@ -0,0 +1,39 @@ +import * as os from 'os'; +import { exec, ExecException } from "child_process"; +import { powerMonitor } from "electron"; + +const batteryLevel = require('battery-level'); + +export abstract class BatteryUtils { + + public static async isOnBatteryPower(): Promise { + const onBattery = powerMonitor.isOnBatteryPower(); + + if (!onBattery && os.platform() == 'linux') { + return await new Promise((resolve) => { + exec("upower -i $(upower -e | grep 'battery') | grep 'state'", (error: ExecException | null, stdout: string) => { + if (error) { + console.error(`isOnBatteryPower(): ${error.message}`); + resolve(false); + return; + } + + const isOnBattery = stdout.includes("discharging"); + resolve(isOnBattery); + }); + }); + } + + return onBattery; + } + + public static async getLevel(): Promise { + try { + return batteryLevel(); + } + catch(error: any) { + console.error(error); + return -1; + } + } +} \ No newline at end of file diff --git a/app/utils/FileUtils.ts b/app/utils/FileUtils.ts new file mode 100644 index 0000000..3026c2b --- /dev/null +++ b/app/utils/FileUtils.ts @@ -0,0 +1,185 @@ +import * as fs from 'fs'; +import * as tar from 'tar'; +import * as path from 'path'; +import * as https from 'https'; +import { createHash } from 'crypto'; + +const AdmZip = require('adm-zip'); +const bz2 = require('unbzip2-stream'); + +export abstract class FileUtils { + + public static async downloadFile(url: string, destinationDir: string, onProgress: (progress: number) => void): Promise { + return new Promise((resolve, reject) => { + const request = (url: string) => { + https.get(url, (response) => { + if (response.statusCode === 200) { + const contentDisposition = response.headers['content-disposition']; + let finalFilename = ''; + + // Estrai il nome del file dall'URL o dal content-disposition + if (contentDisposition && contentDisposition.includes('filename')) { + const match = contentDisposition.match(/filename="(.+)"/); + if (match) { + finalFilename = match[1]; + } + } else { + // Se non c'è content-disposition, prendiamo il nome dall'URL + finalFilename = url.split('/').pop() || 'downloaded-file'; + } + + const destination = `${destinationDir}/${finalFilename}`; + let file: fs.WriteStream; + + try { + file = fs.createWriteStream(destination); + file.on('error', (error: Error) => { + console.log("file error: " + error); + reject(error); + }); + } + catch (error: any) { + reject(error); + return; + } + + const totalBytes = parseInt(response.headers['content-length'] || '0', 10); + let downloadedBytes = 0; + + response.on('data', (chunk) => { + downloadedBytes += chunk.length; + const progress = (downloadedBytes / totalBytes) * 100; + onProgress(progress); // Notifica il progresso + }); + + response.pipe(file); + + file.on('finish', () => { + file.close(() => resolve(finalFilename)); // Restituisci il nome del file finale + }); + } else if (response.statusCode === 301 || response.statusCode === 302) { + // Se è un redirect, effettua una nuova richiesta verso il location header + const newUrl = response.headers.location; + if (newUrl) { + request(newUrl); // Ripeti la richiesta con il nuovo URL + } else { + reject(new Error('Redirection failed without a location header')); + } + } else { + reject(new Error(`Failed to download: ${response.statusCode}`)); + } + }).on('error', (err) => { + reject(err); + }); + }; + + request(url); // Inizia la richiesta + }); + }; + + public static async checkFileHash(filePath: string, hash: string): Promise { + const fileHash = await this.calculateFileHash(filePath); + + return fileHash === hash; + } + + public static calculateFileHash(filePath: string): Promise { + return new Promise((resolve, reject) => { + const hash = createHash('sha256'); + const fileStream = fs.createReadStream(filePath); + + fileStream.on('data', (data) => { + hash.update(data); + }); + + fileStream.on('end', () => { + resolve(hash.digest('hex')); + }); + + fileStream.on('error', (err) => { + reject(err); + }); + }); + }; + + //#region Extraction + + public static async extractTarBz2(filePath: string, destination: string): Promise { + return await new Promise((resolve, reject) => { + // Crea il file decomprimendo il .bz2 in uno .tar temporaneo + const tarPath = path.join(destination, 'temp.tar'); + const fileStream = fs.createReadStream(filePath); + const decompressedStream = fileStream.pipe(bz2()); + + const writeStream = fs.createWriteStream(tarPath); + + decompressedStream.pipe(writeStream); + + let extractedDir: string = ''; + + writeStream.on('finish', () => { + // Una volta che il file .tar è stato creato, estrailo + tar.extract({ cwd: destination, file: tarPath, onReadEntry: (entry: tar.ReadEntry) => { + if (extractedDir == '') { + const topLevelDir = entry.path.split('/')[0]; + extractedDir = topLevelDir; // Salva la prima directory + } + } }) + .then(() => { + // Elimina il file .tar temporaneo dopo l'estrazione + fs.unlink(tarPath, (err) => { + if (err) reject(err); + else if (extractedDir == '') reject('Extraction failed') + else resolve(extractedDir); + }); + }) + .catch(reject); + }); + + writeStream.on('error', reject); + }); + }; + + public static async extractZip(filePath: string, destination: string): Promise { + return await new Promise((resolve, reject) => { + try { + const zip = new AdmZip(filePath); + + // Ensure destination exists + if (!fs.existsSync(destination)) { + fs.mkdirSync(destination, { recursive: true }); + } + + // Extract the ZIP file + zip.extractAllTo(destination, true); + + // Get the name of the extracted folder + const extractedEntries = zip.getEntries(); + const folderName = extractedEntries[0]?.entryName.split('/')[0]; + + // Ensure folder name exists + if (!folderName) { + reject(new Error("Could not determine the extracted folder name")); + return; + } + + resolve(path.join(destination, folderName)); + } catch (error) { + reject(error); + } + }); + }; + + public static async extract(filePath: string, destination: string): Promise { + if (filePath.endsWith('.zip')) { + return await this.extractZip(filePath, destination); + } + else if (filePath.endsWith('.tar.bz2')) { + return await this.extractTarBz2(filePath, destination); + } + + throw new Error("Unknown file type " + filePath); + } + + //#endregion +} \ No newline at end of file diff --git a/app/utils/NetworkUtils.ts b/app/utils/NetworkUtils.ts new file mode 100644 index 0000000..e95a044 --- /dev/null +++ b/app/utils/NetworkUtils.ts @@ -0,0 +1,71 @@ +import { exec, ExecException } from 'child_process'; +import * as os from 'os'; +const network = require('network'); + +export abstract class NetworkUtils { + public static isConnectedToWiFi(): Promise { + try { + + return new Promise((resolve, reject) => { + network.get_active_interface((err: any | null, obj: { name: string, ip_address: string, mac_address: string, type: string, netmask: string, gateway_ip: string }) => { + if (err) { + console.error(err); + reject(err); + } + else { + resolve(obj.type == 'Wireless'); + } + }) + }); + } + catch(error: any) { + return this.isConnectedToWiFiNative(); + } + } + + private static isConnectedToWiFiNative(): Promise { + return new Promise((resolve, reject) => { + const platform = os.platform(); // Use os to get the platform + + let command = ''; + if (platform === 'win32') { + // Windows: Use 'netsh' command to check the Wi-Fi status + command = 'netsh wlan show interfaces'; + } else if (platform === 'darwin') { + // macOS: Use 'airport' command to check the Wi-Fi status + command = "/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -I | grep 'state: running'"; + } else if (platform === 'linux') { + // Linux: Use 'nmcli' to check for Wi-Fi connectivity + command = 'nmcli dev status'; + } else { + resolve(false); // Unsupported platform + } + + // Execute the platform-specific command + if (command) { + exec(command, (error: ExecException | null, stdout: string, stderr: string) => { + if (error) { + console.error(error); + reject(stderr); + resolve(false); // In case of error, assume not connected to Wi-Fi + } else { + // Check if the output indicates a connected status + if (stdout) { + const components: string[] = stdout.split("\n"); + + components.forEach((component: string) => { + if (component.includes('wifi') && !component.includes('--')) { + resolve(true); + } + }); + + resolve(false); + } else { + resolve(false); + } + } + }); + } + }); + } +} \ No newline at end of file diff --git a/app/utils/index.ts b/app/utils/index.ts new file mode 100644 index 0000000..753ebf0 --- /dev/null +++ b/app/utils/index.ts @@ -0,0 +1,3 @@ +export { BatteryUtils } from "./BatteryUtils"; +export { FileUtils } from "./FileUtils"; +export { NetworkUtils } from "./NetworkUtils"; diff --git a/package-lock.json b/package-lock.json index 49071c9..4c38b65 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,21 +16,21 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { - "@angular/common": "18.2.11", - "@angular/compiler": "18.2.11", - "@angular/core": "18.2.11", - "@angular/forms": "18.2.11", - "@angular/language-service": "18.2.11", - "@angular/platform-browser": "18.2.11", - "@angular/platform-browser-dynamic": "18.2.11", - "@angular/router": "18.2.11", + "@angular/common": "18.2.12", + "@angular/compiler": "18.2.12", + "@angular/core": "18.2.12", + "@angular/forms": "18.2.12", + "@angular/language-service": "18.2.12", + "@angular/platform-browser": "18.2.12", + "@angular/platform-browser-dynamic": "18.2.12", + "@angular/router": "18.2.12", "@popperjs/core": "2.11.8", "bootstrap": "5.3.3", "bootstrap-icons": "1.11.3", - "bootstrap-table": "1.23.5", - "chart.js": "4.4.6", + "bootstrap-table": "1.24.0", + "chart.js": "4.4.7", "crypto": "1.0.1", - "idb": "8.0.0", + "idb": "8.0.1", "jquery": "3.7.1", "os": "0.1.2", "rxjs": "7.8.1", @@ -46,36 +46,36 @@ "@angular-eslint/eslint-plugin-template": "18.4.0", "@angular-eslint/schematics": "18.4.0", "@angular-eslint/template-parser": "18.4.0", - "@angular/build": "^18.2.11", - "@angular/cli": "18.2.11", - "@angular/compiler-cli": "18.2.11", - "@electron/packager": "18.3.5", + "@angular/build": "^18.2.12", + "@angular/cli": "18.2.12", + "@angular/compiler-cli": "18.2.12", + "@electron/packager": "18.3.6", "@ngx-translate/core": "16.0.3", "@ngx-translate/http-loader": "16.0.0", - "@playwright/test": "1.48.2", + "@playwright/test": "1.49.1", "@types/auto-launch": "5.0.5", "@types/bootstrap": "5.2.10", "@types/chart.js": "2.9.41", "@types/jest": "29.5.14", "@types/jquery": "3.5.32", - "@types/node": "22.9.0", + "@types/node": "22.10.2", "@types/pidusage": "2.0.5", "@types/unbzip2-stream": "1.4.3", - "@typescript-eslint/eslint-plugin": "8.14.0", - "@typescript-eslint/parser": "8.14.0", + "@typescript-eslint/eslint-plugin": "8.18.2", + "@typescript-eslint/parser": "8.18.2", "conventional-changelog-cli": "5.0.0", - "electron": "33.2.0", + "electron": "33.2.1", "electron-builder": "25.1.8", "electron-debug": "4.1.0", "electron-reloader": "1.2.3", - "eslint": "9.14.0", + "eslint": "9.17.0", "eslint-plugin-import": "2.31.0", - "eslint-plugin-jsdoc": "50.5.0", + "eslint-plugin-jsdoc": "50.6.1", "eslint-plugin-prefer-arrow": "1.2.3", "jest": "29.7.0", - "node-polyfill-webpack-plugin": "4.0.0", + "node-polyfill-webpack-plugin": "4.1.0", "npm-run-all": "4.1.5", - "playwright": "1.48.2", + "playwright": "1.49.1", "ts-node": "10.9.2", "typescript": "5.5.4", "wait-on": "8.0.1", @@ -162,12 +162,12 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1802.11", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.11.tgz", - "integrity": "sha512-p+XIc/j51aI83ExNdeZwvkm1F4wkuKMGUUoj0MVUUi5E6NoiMlXYm6uU8+HbRvPBzGy5+3KOiGp3Fks0UmDSAA==", + "version": "0.1802.12", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.12.tgz", + "integrity": "sha512-bepVb2/GtJppYKaeW8yTGE6egmoWZ7zagFDsmBdbF+BYp+HmeoPsclARcdryBPVq68zedyTRdvhWSUTbw1AYuw==", "dev": true, "dependencies": { - "@angular-devkit/core": "18.2.11", + "@angular-devkit/core": "18.2.12", "rxjs": "7.8.1" }, "engines": { @@ -177,16 +177,16 @@ } }, "node_modules/@angular-devkit/build-angular": { - "version": "18.2.11", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-18.2.11.tgz", - "integrity": "sha512-09Ln3NAdlMw/wMLgnwYU5VgWV5TPBEHolZUIvE9D8b6SFWBCowk3B3RWeAMgg7Peuf9SKwqQHBz2b1C7RTP/8g==", + "version": "18.2.12", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-18.2.12.tgz", + "integrity": "sha512-quVUi7eqTq9OHumQFNl9Y8t2opm8miu4rlYnuF6rbujmmBDvdUvR6trFChueRczl2p5HWqTOr6NPoDGQm8AyNw==", "dev": true, "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1802.11", - "@angular-devkit/build-webpack": "0.1802.11", - "@angular-devkit/core": "18.2.11", - "@angular/build": "18.2.11", + "@angular-devkit/architect": "0.1802.12", + "@angular-devkit/build-webpack": "0.1802.12", + "@angular-devkit/core": "18.2.12", + "@angular/build": "18.2.12", "@babel/core": "7.25.2", "@babel/generator": "7.25.0", "@babel/helper-annotate-as-pure": "7.24.7", @@ -197,7 +197,7 @@ "@babel/preset-env": "7.25.3", "@babel/runtime": "7.25.0", "@discoveryjs/json-ext": "0.6.1", - "@ngtools/webpack": "18.2.11", + "@ngtools/webpack": "18.2.12", "@vitejs/plugin-basic-ssl": "1.1.0", "ansi-colors": "4.1.3", "autoprefixer": "10.4.20", @@ -464,12 +464,12 @@ } }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.1802.11", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1802.11.tgz", - "integrity": "sha512-G76rNsyn1iQk7qjyr+K4rnDzfalmEswmwXQorypSDGaHYzIDY1SZXMoP4225WMq5fJNBOJrk82FA0PSfnPE+zQ==", + "version": "0.1802.12", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1802.12.tgz", + "integrity": "sha512-0Z3fdbZVRnjYWE2/VYyfy+uieY+6YZyEp4ylzklVkc+fmLNsnz4Zw6cK1LzzcBqAwKIyh1IdW20Cg7o8b0sONA==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1802.11", + "@angular-devkit/architect": "0.1802.12", "rxjs": "7.8.1" }, "engines": { @@ -483,9 +483,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "18.2.11", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.11.tgz", - "integrity": "sha512-H9P1shRGigORWJHUY2BRa2YurT+DVminrhuaYHsbhXBRsPmgB2Dx/30YLTnC1s5XmR9QIRUCsg/d3kyT1wd5Zg==", + "version": "18.2.12", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.12.tgz", + "integrity": "sha512-NtB6ypsaDyPE6/fqWOdfTmACs+yK5RqfH5tStEzWFeeDsIEDYKsJ06ypuRep7qTjYus5Rmttk0Ds+cFgz8JdUQ==", "dev": true, "dependencies": { "ajv": "8.17.1", @@ -527,12 +527,12 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "18.2.11", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-18.2.11.tgz", - "integrity": "sha512-efRK3FotTFp4KD5u42jWfXpHUALXB9kJNsWiB4wEImKFH6CN+vjBspJQuLqk2oeBFh/7D2qRMc5P+2tZHM5hdw==", + "version": "18.2.12", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-18.2.12.tgz", + "integrity": "sha512-mMea9txHbnCX5lXLHlo0RAgfhFHDio45/jMsREM2PA8UtVf2S8ltXz7ZwUrUyMQRv8vaSfn4ijDstF4hDMnRgQ==", "dev": true, "dependencies": { - "@angular-devkit/core": "18.2.11", + "@angular-devkit/core": "18.2.12", "jsonc-parser": "3.3.1", "magic-string": "0.30.11", "ora": "5.4.1", @@ -639,13 +639,13 @@ } }, "node_modules/@angular/build": { - "version": "18.2.11", - "resolved": "https://registry.npmjs.org/@angular/build/-/build-18.2.11.tgz", - "integrity": "sha512-AgirvSCmqUKiDE3C0rl3JA68OkOqQWDKUvjqRHXCkhxldLVOVoeIl87+jBYK/v9gcmk+K+ju+5wbGEfu1FjhiQ==", + "version": "18.2.12", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-18.2.12.tgz", + "integrity": "sha512-4Ohz+OSILoL+cCAQ4UTiCT5v6pctu3fXNoNpTEUK46OmxELk9jDITO5rNyNS7TxBn9wY69kjX5VcDf7MenquFQ==", "dev": true, "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1802.11", + "@angular-devkit/architect": "0.1802.12", "@babel/core": "7.25.2", "@babel/helper-annotate-as-pure": "7.24.7", "@babel/helper-split-export-declaration": "7.24.7", @@ -971,17 +971,17 @@ } }, "node_modules/@angular/cli": { - "version": "18.2.11", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.2.11.tgz", - "integrity": "sha512-0JI1xjOLRemBPjdT/yVlabxc3Zkjqa/lhvVxxVC1XhKoW7yGxIGwNrQ4pka4CcQtCuktO6KPMmTGIu8YgC3cpw==", + "version": "18.2.12", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.2.12.tgz", + "integrity": "sha512-xhuZ/b7IhqNw1MgXf+arWf4x+GfUSt/IwbdWU4+CO8A7h0Y46zQywouP/KUK3cMQZfVdHdciTBvlpF3vFacA6Q==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1802.11", - "@angular-devkit/core": "18.2.11", - "@angular-devkit/schematics": "18.2.11", + "@angular-devkit/architect": "0.1802.12", + "@angular-devkit/core": "18.2.12", + "@angular-devkit/schematics": "18.2.12", "@inquirer/prompts": "5.3.8", "@listr2/prompt-adapter-inquirer": "2.0.15", - "@schematics/angular": "18.2.11", + "@schematics/angular": "18.2.12", "@yarnpkg/lockfile": "1.1.0", "ini": "4.1.3", "jsonc-parser": "3.3.1", @@ -1004,9 +1004,9 @@ } }, "node_modules/@angular/common": { - "version": "18.2.11", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-18.2.11.tgz", - "integrity": "sha512-bamJeISl2zUlvjPYebQWazUjhjXU9nrot42cQJng94SkvNENT9LTWfPYgc+Bd972Kg+31jG4H41rgFNs7zySmw==", + "version": "18.2.12", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-18.2.12.tgz", + "integrity": "sha512-gI5o8Bccsi8ow8Wk2vG4Tw/Rw9LoHEA9j8+qHKNR/55SCBsz68Syg310dSyxy+sApJO2WiqIadr5VP36dlSUFw==", "dependencies": { "tslib": "^2.3.0" }, @@ -1014,14 +1014,14 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "18.2.11", + "@angular/core": "18.2.12", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "18.2.11", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-18.2.11.tgz", - "integrity": "sha512-PSVL1YXUhTzkgJNYXiWk9eAZxNV6laQJRGdj9++C1q9m2S9/GlehZGzkt5GtC5rlUweJucCNvBC1+2D5FAt9vA==", + "version": "18.2.12", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-18.2.12.tgz", + "integrity": "sha512-D5d5dLrjQal5DbAXJJNSsCC3UxzjOI2wbc+Iv+LOpRM1gpNwuYfZMX5W7cj62Ce4G2++78CJSppdKBp8D4HErQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -1029,7 +1029,7 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "18.2.11" + "@angular/core": "18.2.12" }, "peerDependenciesMeta": { "@angular/core": { @@ -1038,9 +1038,9 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "18.2.11", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.2.11.tgz", - "integrity": "sha512-YJlAOiXZUYP6/RK9isu5AOucmNZhFB9lpY/beMzkkWgDku+va8szm4BZbLJFz176IUteyLWF3IP4aE7P9OBlXw==", + "version": "18.2.12", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.2.12.tgz", + "integrity": "sha512-IWimTNq5Q+i2Wxev6HLqnN4iYbPvLz04W1BBycT1LfGUsHcjFYLuUqbeUzHbk2snmBAzXkixgVpo8SF6P4Y5Pg==", "dev": true, "dependencies": { "@babel/core": "7.25.2", @@ -1061,7 +1061,7 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/compiler": "18.2.11", + "@angular/compiler": "18.2.12", "typescript": ">=5.4 <5.6" } }, @@ -1094,9 +1094,9 @@ } }, "node_modules/@angular/core": { - "version": "18.2.11", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-18.2.11.tgz", - "integrity": "sha512-/AGAFyZN8KR+kW5FUFCCBCj3qHyDDum7G0lJe5otrT9AqF6+g7PjF8yLha/6wPkJG7ri5xGLhini1sEivVeq/g==", + "version": "18.2.12", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-18.2.12.tgz", + "integrity": "sha512-wCf/OObwS6bpM60rk6bpMpCRGp0DlMLB1WNAMtfcaPNyqimVV5Bm98mWRhkOuRyvU3fU7iHhM/10ePVaoyu9+A==", "dependencies": { "tslib": "^2.3.0" }, @@ -1109,9 +1109,9 @@ } }, "node_modules/@angular/forms": { - "version": "18.2.11", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.2.11.tgz", - "integrity": "sha512-QjxayOxDTqsTJGBzfWd3nms1LZIXj2f1+wIPxxUNXyNS5ZaM7hBWkz2BTFYeewlD/HdNj0alNVCYK3M8ElLWYw==", + "version": "18.2.12", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.2.12.tgz", + "integrity": "sha512-FsukBJEU6jfAmht7TrODTkct/o4iwCZvGozuThOp0tYUPD/E1rZZzuKjEyTnT5Azpfkf0Wqx1nmpz80cczELOQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -1119,24 +1119,24 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.2.11", - "@angular/core": "18.2.11", - "@angular/platform-browser": "18.2.11", + "@angular/common": "18.2.12", + "@angular/core": "18.2.12", + "@angular/platform-browser": "18.2.12", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/language-service": { - "version": "18.2.11", - "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-18.2.11.tgz", - "integrity": "sha512-kI36Wfvw3E01Xox/H535/rrSTiDfzQeXATFR5i5vqc94XWUdQG67e4X6ybnqFUrezXoLPTULHp+5Di896YFPzw==", + "version": "18.2.12", + "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-18.2.12.tgz", + "integrity": "sha512-oaiVAnGzmPZvrXdGh8XnosaqfEPbZxO2225MxbbrD49XTqUgpaS2zrz1Uf5j42e8qytA2kj8tckLq7PAMm0D1w==", "engines": { "node": "^18.19.1 || ^20.11.1 || >=22.0.0" } }, "node_modules/@angular/platform-browser": { - "version": "18.2.11", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.2.11.tgz", - "integrity": "sha512-bzcP0QdPT/ncTxOx0t7901z5m0wDmkraTo/es4g8reV6VK9Ptv0QDuD8aDvrHh7sLCX5VgwDF9ohc6S2TpYUCA==", + "version": "18.2.12", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.2.12.tgz", + "integrity": "sha512-DRSMznuxuecrs+v5BRyd60/R4vjkQtuYUEPfzdo+rqxM83Dmr3PGtnqPRgd5oAFUbATxf02hQXijRD27K7rZRg==", "dependencies": { "tslib": "^2.3.0" }, @@ -1144,9 +1144,9 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/animations": "18.2.11", - "@angular/common": "18.2.11", - "@angular/core": "18.2.11" + "@angular/animations": "18.2.12", + "@angular/common": "18.2.12", + "@angular/core": "18.2.12" }, "peerDependenciesMeta": { "@angular/animations": { @@ -1155,9 +1155,9 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "18.2.11", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-18.2.11.tgz", - "integrity": "sha512-a30U4ZdTZSvL17xWwOq6xh9ToCDP2K7/j1HTJFREObbuAtZTa/6IVgBUM6oOMNQ43kHkT6Mr9Emkgf9iGtWwfw==", + "version": "18.2.12", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-18.2.12.tgz", + "integrity": "sha512-dv1QEjYpcFno6+oUeGEDRWpB5g2Ufb0XkUbLJQIgrOk1Qbyzb8tmpDpTjok8jcKdquigMRWolr6Y1EOicfRlLw==", "dependencies": { "tslib": "^2.3.0" }, @@ -1165,16 +1165,16 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.2.11", - "@angular/compiler": "18.2.11", - "@angular/core": "18.2.11", - "@angular/platform-browser": "18.2.11" + "@angular/common": "18.2.12", + "@angular/compiler": "18.2.12", + "@angular/core": "18.2.12", + "@angular/platform-browser": "18.2.12" } }, "node_modules/@angular/router": { - "version": "18.2.11", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-18.2.11.tgz", - "integrity": "sha512-xh4+t4pNBWxeH1a6GIoEGVSRZO4NDKK8q6b+AzB5GBgKsYgOz2lc74RXIPA//pK3aHrS9qD4sJLlodwgE/1+bA==", + "version": "18.2.12", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-18.2.12.tgz", + "integrity": "sha512-cz/1YWOZadAT35PPPYmpK3HSzKOE56nlUHue5bFkw73VSZr2iBn03ALLpd9YKzWgRmx3y7DqnlQtCkDu9JPGKQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -1182,9 +1182,9 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.2.11", - "@angular/core": "18.2.11", - "@angular/platform-browser": "18.2.11", + "@angular/common": "18.2.12", + "@angular/core": "18.2.12", + "@angular/platform-browser": "18.2.12", "rxjs": "^6.5.3 || ^7.4.0" } }, @@ -1401,9 +1401,9 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz", - "integrity": "sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.3.tgz", + "integrity": "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg==", "dev": true, "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", @@ -3303,9 +3303,9 @@ } }, "node_modules/@electron/packager": { - "version": "18.3.5", - "resolved": "https://registry.npmjs.org/@electron/packager/-/packager-18.3.5.tgz", - "integrity": "sha512-ClgTxXTt3MesWAcjIxIkgxELjTcllw1FRoVsihP7uT48kpDMqI71p4XvnMWbq8PvU57TcrKICAaLkxRhbc+/wQ==", + "version": "18.3.6", + "resolved": "https://registry.npmjs.org/@electron/packager/-/packager-18.3.6.tgz", + "integrity": "sha512-1eXHB5t+SQKvUiDpWGpvr90ZSSbXj+isrh3YbjCTjKT4bE4SQrKSBfukEAaBvp67+GXHFtCHjQgN9qSTFIge+Q==", "dev": true, "dependencies": { "@electron/asar": "^3.2.13", @@ -3934,12 +3934,12 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", - "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz", + "integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==", "dev": true, "dependencies": { - "@eslint/object-schema": "^2.1.4", + "@eslint/object-schema": "^2.1.5", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -3948,18 +3948,21 @@ } }, "node_modules/@eslint/core": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.7.0.tgz", - "integrity": "sha512-xp5Jirz5DyPYlPiKat8jaq0EmYvDXKKpzTbxXMpT9eqlRJkRKIz9AGMdlvYjih+im+QlhWrpvVjl8IPC/lHlUw==", + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.1.tgz", + "integrity": "sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q==", "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.15" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/eslintrc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", - "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", + "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", "dev": true, "dependencies": { "ajv": "^6.12.4", @@ -4032,27 +4035,27 @@ "dev": true }, "node_modules/@eslint/js": { - "version": "9.14.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.14.0.tgz", - "integrity": "sha512-pFoEtFWCPyDOl+C6Ift+wC7Ro89otjigCf5vcuWqWgqNSQbRrpjSvdeE6ofLz4dHmyxD5f7gIdGT4+p36L6Twg==", + "version": "9.17.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.17.0.tgz", + "integrity": "sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/object-schema": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", - "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz", + "integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.2.tgz", - "integrity": "sha512-CXtq5nR4Su+2I47WPOlWud98Y5Lv8Kyxp2ukhgFx/eW6Blm18VXJO5WuQylPugRo8nbluoi6GvvxBLqHcvqUUw==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.4.tgz", + "integrity": "sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==", "dev": true, "dependencies": { "levn": "^0.4.1" @@ -5501,9 +5504,9 @@ ] }, "node_modules/@ngtools/webpack": { - "version": "18.2.11", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.2.11.tgz", - "integrity": "sha512-iTdUGJ5O7yMm1DyCzyoMDMxBJ68emUSSXPWbQzEEdcqmtifRebn+VAq4vHN8OmtGM1mtuKeLEsbiZP8ywrw7Ug==", + "version": "18.2.12", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.2.12.tgz", + "integrity": "sha512-FFJAwtWbtpncMOVNuULPBwFJB7GSjiUwO93eGTzRp8O4EPQ8lCQeFbezQm/NP34+T0+GBLGzPSuQT+muob8YKw==", "dev": true, "engines": { "node": "^18.19.1 || ^20.11.1 || >=22.0.0", @@ -6137,12 +6140,12 @@ } }, "node_modules/@playwright/test": { - "version": "1.48.2", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.2.tgz", - "integrity": "sha512-54w1xCWfXuax7dz4W2M9uw0gDyh+ti/0K/MxcCUxChFh37kkdxPdfZDw5QBbuPUJHr1CiHJ1hXgSs+GgeQc5Zw==", + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.1.tgz", + "integrity": "sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==", "dev": true, "dependencies": { - "playwright": "1.48.2" + "playwright": "1.49.1" }, "bin": { "playwright": "cli.js" @@ -6375,13 +6378,13 @@ "dev": true }, "node_modules/@schematics/angular": { - "version": "18.2.11", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-18.2.11.tgz", - "integrity": "sha512-jT54mc9+hPOwie9bji/g2krVuK1kkNh2PNFGwfgCg3Ofmt3hcyOBai1DKuot5uLTX4VCCbvfwiVR/hJniQl2SA==", + "version": "18.2.12", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-18.2.12.tgz", + "integrity": "sha512-sIoeipsisK5eTLW3XuNZYcal83AfslBbgI7LnV+3VrXwpasKPGHwo2ZdwhCd2IXAkuJ02Iyu7MyV0aQRM9i/3g==", "dev": true, "dependencies": { - "@angular-devkit/core": "18.2.11", - "@angular-devkit/schematics": "18.2.11", + "@angular-devkit/core": "18.2.12", + "@angular-devkit/schematics": "18.2.12", "jsonc-parser": "3.3.1" }, "engines": { @@ -7145,12 +7148,12 @@ } }, "node_modules/@types/node": { - "version": "22.9.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz", - "integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==", + "version": "22.10.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", + "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", "devOptional": true, "dependencies": { - "undici-types": "~6.19.8" + "undici-types": "~6.20.0" } }, "node_modules/@types/node-forge": { @@ -7162,6 +7165,12 @@ "@types/node": "*" } }, + "node_modules/@types/node/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "devOptional": true + }, "node_modules/@types/normalize-package-data": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", @@ -7341,16 +7350,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.14.0.tgz", - "integrity": "sha512-tqp8H7UWFaZj0yNO6bycd5YjMwxa6wIHOLZvWPkidwbgLCsBMetQoGj7DPuAlWa2yGO3H48xmPwjhsSPPCGU5w==", + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.2.tgz", + "integrity": "sha512-adig4SzPLjeQ0Tm+jvsozSGiCliI2ajeURDGHjZ2llnA+A67HihCQ+a3amtPhUakd1GlwHxSRvzOZktbEvhPPg==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.14.0", - "@typescript-eslint/type-utils": "8.14.0", - "@typescript-eslint/utils": "8.14.0", - "@typescript-eslint/visitor-keys": "8.14.0", + "@typescript-eslint/scope-manager": "8.18.2", + "@typescript-eslint/type-utils": "8.18.2", + "@typescript-eslint/utils": "8.18.2", + "@typescript-eslint/visitor-keys": "8.18.2", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -7365,24 +7374,20 @@ }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.14.0.tgz", - "integrity": "sha512-2p82Yn9juUJq0XynBXtFCyrBDb6/dJombnz6vbo6mgQEtWHfvHbQuEa9kAOVIt1c9YFwi7H6WxtPj1kg+80+RA==", + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.18.2.tgz", + "integrity": "sha512-y7tcq4StgxQD4mDr9+Jb26dZ+HTZ/SkfqpXSiqeUXZHxOUyjWDKsmwKhJ0/tApR08DgOhrFAoAhyB80/p3ViuA==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.14.0", - "@typescript-eslint/types": "8.14.0", - "@typescript-eslint/typescript-estree": "8.14.0", - "@typescript-eslint/visitor-keys": "8.14.0", + "@typescript-eslint/scope-manager": "8.18.2", + "@typescript-eslint/types": "8.18.2", + "@typescript-eslint/typescript-estree": "8.18.2", + "@typescript-eslint/visitor-keys": "8.18.2", "debug": "^4.3.4" }, "engines": { @@ -7393,22 +7398,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.14.0.tgz", - "integrity": "sha512-aBbBrnW9ARIDn92Zbo7rguLnqQ/pOrUguVpbUwzOhkFg2npFDwTgPGqFqE0H5feXcOoJOfX3SxlJaKEVtq54dw==", + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.18.2.tgz", + "integrity": "sha512-YJFSfbd0CJjy14r/EvWapYgV4R5CHzptssoag2M7y3Ra7XNta6GPAJPPP5KGB9j14viYXyrzRO5GkX7CRfo8/g==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.14.0", - "@typescript-eslint/visitor-keys": "8.14.0" + "@typescript-eslint/types": "8.18.2", + "@typescript-eslint/visitor-keys": "8.18.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7419,13 +7420,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.14.0.tgz", - "integrity": "sha512-Xcz9qOtZuGusVOH5Uk07NGs39wrKkf3AxlkK79RBK6aJC1l03CobXjJbwBPSidetAOV+5rEVuiT1VSBUOAsanQ==", + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.18.2.tgz", + "integrity": "sha512-AB/Wr1Lz31bzHfGm/jgbFR0VB0SML/hd2P1yxzKDM48YmP7vbyJNHRExUE/wZsQj2wUCvbWH8poNHFuxLqCTnA==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "8.14.0", - "@typescript-eslint/utils": "8.14.0", + "@typescript-eslint/typescript-estree": "8.18.2", + "@typescript-eslint/utils": "8.18.2", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -7436,16 +7437,15 @@ "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.14.0.tgz", - "integrity": "sha512-yjeB9fnO/opvLJFAsPNYlKPnEM8+z4og09Pk504dkqonT02AyL5Z9SSqlE0XqezS93v6CXn49VHvB2G7XSsl0g==", + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.18.2.tgz", + "integrity": "sha512-Z/zblEPp8cIvmEn6+tPDIHUbRu/0z5lqZ+NvolL5SvXWT5rQy7+Nch83M0++XzO0XrWRFWECgOAyE8bsJTl1GQ==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7456,13 +7456,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.14.0.tgz", - "integrity": "sha512-OPXPLYKGZi9XS/49rdaCbR5j/S14HazviBlUQFvSKz3npr3NikF+mrgK7CFVur6XEt95DZp/cmke9d5i3vtVnQ==", + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.2.tgz", + "integrity": "sha512-WXAVt595HjpmlfH4crSdM/1bcsqh+1weFRWIa9XMTx/XHZ9TCKMcr725tLYqWOgzKdeDrqVHxFotrvWcEsk2Tg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.14.0", - "@typescript-eslint/visitor-keys": "8.14.0", + "@typescript-eslint/types": "8.18.2", + "@typescript-eslint/visitor-keys": "8.18.2", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -7477,10 +7477,8 @@ "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { @@ -7508,15 +7506,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.14.0.tgz", - "integrity": "sha512-OGqj6uB8THhrHj0Fk27DcHPojW7zKwKkPmHXHvQ58pLYp4hy8CSUdTKykKeh+5vFqTTVmjz0zCOOPKRovdsgHA==", + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.18.2.tgz", + "integrity": "sha512-Cr4A0H7DtVIPkauj4sTSXVl+VBWewE9/o40KcF3TV9aqDEOWoXF3/+oRXNby3DYzZeCATvbdksYsGZzplwnK/Q==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.14.0", - "@typescript-eslint/types": "8.14.0", - "@typescript-eslint/typescript-estree": "8.14.0" + "@typescript-eslint/scope-manager": "8.18.2", + "@typescript-eslint/types": "8.18.2", + "@typescript-eslint/typescript-estree": "8.18.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7526,17 +7524,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.14.0.tgz", - "integrity": "sha512-vG0XZo8AdTH9OE6VFRwAZldNc7qtJ/6NLGWak+BtENuEUXGZgFpihILPiBvKXvJ2nFu27XNGC6rKiwuaoMbYzQ==", + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.2.tgz", + "integrity": "sha512-zORcwn4C3trOWiCqFQP1x6G3xTRyZ1LYydnj51cRnJ6hxBlr/cKPckk+PKPUw/fXmvfKTcw7bwY3w9izgx5jZw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.14.0", - "eslint-visitor-keys": "^3.4.3" + "@typescript-eslint/types": "8.18.2", + "eslint-visitor-keys": "^4.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7546,6 +7545,18 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@vitejs/plugin-basic-ssl": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.1.0.tgz", @@ -7750,18 +7761,6 @@ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "dev": true }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "dev": true, - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -8259,9 +8258,9 @@ "optional": true }, "node_modules/appdmg/node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", "optional": true, "dependencies": { "nice-try": "^1.0.4", @@ -8709,9 +8708,9 @@ } }, "node_modules/asn1.js/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", "dev": true }, "node_modules/assert": { @@ -9012,13 +9011,13 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.11", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz", - "integrity": "sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==", + "version": "0.4.12", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.12.tgz", + "integrity": "sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og==", "dev": true, "dependencies": { "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.6.2", + "@babel/helper-define-polyfill-provider": "^0.6.3", "semver": "^6.3.1" }, "peerDependencies": { @@ -9048,12 +9047,12 @@ } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz", - "integrity": "sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.3.tgz", + "integrity": "sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.2" + "@babel/helper-define-polyfill-provider": "^0.6.3" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -9312,9 +9311,9 @@ ] }, "node_modules/bootstrap-table": { - "version": "1.23.5", - "resolved": "https://registry.npmjs.org/bootstrap-table/-/bootstrap-table-1.23.5.tgz", - "integrity": "sha512-9WByoSpJvA73gi2YYIlX6IWR74oZtBmSixul/Th8FTBtBd/kZRpbKESGTjhA3BA3AYTnfyY8Iy1KeRWPlV2GWQ==", + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/bootstrap-table/-/bootstrap-table-1.24.0.tgz", + "integrity": "sha512-dyRf5PQwTgFHj9yjuPXa+GIf4JpuQhsgD1CJrOqhw40qI2gTb3mJfRdoBc7iF2bqzOl+k0RnbAlhSPbGe4VS+w==", "peerDependencies": { "jquery": "3" } @@ -9356,6 +9355,15 @@ "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", "dev": true }, + "node_modules/browser-resolve": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-2.0.0.tgz", + "integrity": "sha512-7sWsQlYL2rGLy2IWm8WL8DCTJvYLc/qlOnsakDac87SOoCd16WLsaAMdCiAqsTNHIe+SXfaqyxyo6THoWqs8WQ==", + "dev": true, + "dependencies": { + "resolve": "^1.17.0" + } + }, "node_modules/browserify-aes": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", @@ -9870,6 +9878,35 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", + "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -9944,9 +9981,9 @@ "dev": true }, "node_modules/chart.js": { - "version": "4.4.6", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.6.tgz", - "integrity": "sha512-8Y406zevUPbbIBA/HRk33khEmQPk5+cxeflWE/2rx1NJsjVWMPw/9mSP9rxHP5eqi6LNoPBVMfZHxbwLSgldYA==", + "version": "4.4.7", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.7.tgz", + "integrity": "sha512-pwkcKfdzTMAU/+jNosKhNL2bHtJc/sSmYgVbuGTEDhzkrhmyihmP7vUc/5ZK9WopidMDHNe3Wm7jOd/WhuHWuw==", "dependencies": { "@kurkle/color": "^0.3.0" }, @@ -10018,13 +10055,16 @@ } }, "node_modules/cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.6.tgz", + "integrity": "sha512-3Ek9H3X6pj5TgenXYtNWdaBon1tgYCaebd+XPg0keyjEbEfkD4KkmAxkQ/i1vYvxdcT5nscLBfq9VJRmCBcFSw==", "dev": true, "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" } }, "node_modules/cjs-module-lexer": { @@ -10853,9 +10893,9 @@ } }, "node_modules/create-ecdh/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", "dev": true }, "node_modules/create-hash": { @@ -11075,9 +11115,9 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "devOptional": true, "dependencies": { "path-key": "^3.1.0", @@ -11661,9 +11701,9 @@ } }, "node_modules/diffie-hellman/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", "dev": true }, "node_modules/dir-compare": { @@ -11802,12 +11842,12 @@ } }, "node_modules/domain-browser": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-5.7.0.tgz", - "integrity": "sha512-edTFu0M/7wO1pXY6GDxVNVW086uqwWYIHP98txhcPyV995X21JIH2DtYp33sQJOupYoXKe9RwTw2Ya2vWaquTQ==", + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-4.22.0.tgz", + "integrity": "sha512-IGBwjF7tNk3cwypFNH/7bfzBcgSCbaMOD3GsaY1AU/JRrnHnYgEM0+9kQt52iZxjNsjBtJYtao146V+f8jFZNw==", "dev": true, "engines": { - "node": ">=4" + "node": ">=10" }, "funding": { "url": "https://bevry.me/fund" @@ -11917,6 +11957,20 @@ "tn1150": "^0.1.0" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -11961,9 +12015,9 @@ } }, "node_modules/electron": { - "version": "33.2.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-33.2.0.tgz", - "integrity": "sha512-PVw1ICAQDPsnnsmpNFX/b1i/49h67pbSPxuIENd9K9WpGO1tsRaQt+K2bmXqTuoMJsbzIc75Ce8zqtuwBPqawA==", + "version": "33.2.1", + "resolved": "https://registry.npmjs.org/electron/-/electron-33.2.1.tgz", + "integrity": "sha512-SG/nmSsK9Qg1p6wAW+ZfqU+AV8cmXMTIklUL18NnOKfZLlum4ZsDoVdmmmlL39ZmeCaq27dr7CgslRPahfoVJg==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -12965,9 +13019,9 @@ } }, "node_modules/elliptic": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.0.tgz", - "integrity": "sha512-dpwoQcLc/2WLQvJvLRHKZ+f9FgOdjnq11rurqwekGQygGPsYSK29OMMD2WalatiqQ+XGFDglTNixpPfI+lpaAA==", + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", "dev": true, "dependencies": { "bn.js": "^4.11.9", @@ -12980,9 +13034,9 @@ } }, "node_modules/elliptic/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", "dev": true }, "node_modules/emittery": { @@ -13181,13 +13235,10 @@ } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.4" - }, "engines": { "node": ">= 0.4" } @@ -13373,26 +13424,26 @@ } }, "node_modules/eslint": { - "version": "9.14.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.14.0.tgz", - "integrity": "sha512-c2FHsVBr87lnUtjP4Yhvk4yEhKrQavGafRA/Se1ouse8PfbfC/Qh9Mxa00yWsZRlqeUB9raXip0aiiUZkgnr9g==", + "version": "9.17.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.17.0.tgz", + "integrity": "sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.18.0", - "@eslint/core": "^0.7.0", - "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.14.0", - "@eslint/plugin-kit": "^0.2.0", + "@eslint/config-array": "^0.19.0", + "@eslint/core": "^0.9.0", + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "9.17.0", + "@eslint/plugin-kit": "^0.2.3", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.0", + "@humanwhocodes/retry": "^0.4.1", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", + "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.2.0", @@ -13411,8 +13462,7 @@ "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "text-table": "^0.2.0" + "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" @@ -13575,9 +13625,9 @@ } }, "node_modules/eslint-plugin-jsdoc": { - "version": "50.5.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-50.5.0.tgz", - "integrity": "sha512-xTkshfZrUbiSHXBwZ/9d5ulZ2OcHXxSvm/NPo494H/hadLRJwOq5PMV0EUpMqsb9V+kQo+9BAgi6Z7aJtdBp2A==", + "version": "50.6.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-50.6.1.tgz", + "integrity": "sha512-UWyaYi6iURdSfdVVqvfOs2vdCVz0J40O/z/HTsv2sFjdjmdlUI/qlKLOTmwbPQ2tAfQnE5F9vqx+B+poF71DBQ==", "dev": true, "dependencies": { "@es-joy/jsdoccomment": "~0.49.0", @@ -13915,15 +13965,6 @@ "node": ">= 0.6" } }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -14016,9 +14057,9 @@ "dev": true }, "node_modules/express": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "dev": true, "dependencies": { "accepts": "~1.3.8", @@ -14040,7 +14081,7 @@ "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", @@ -14055,6 +14096,10 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express/node_modules/debug": { @@ -14782,16 +14827,21 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz", + "integrity": "sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==", "dev": true, "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "dunder-proto": "^1.0.0", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -15151,12 +15201,12 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.3" + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -15344,9 +15394,9 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, "engines": { "node": ">= 0.4" @@ -15377,16 +15427,16 @@ "dev": true }, "node_modules/hash-base": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", - "integrity": "sha512-EeeoJKjTyt868liAlVmcv2ZsUfGHlE3Q+BICOXcZiwN3osr5Q/zFGYmTJpoIzuaSTAwndFy+GqhEwlU4L3j4Ow==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz", + "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==", "dev": true, "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1" }, "engines": { - "node": ">=4" + "node": ">= 0.10" } }, "node_modules/hash.js": { @@ -15744,9 +15794,9 @@ } }, "node_modules/idb": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/idb/-/idb-8.0.0.tgz", - "integrity": "sha512-l//qvlAKGmQO31Qn7xdzagVPPaHTxXx199MhrAFuVBTPqydcPYBWjkrbv4Y0ktB+GmWOiwHl237UUOrLmQxLvw==" + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-8.0.1.tgz", + "integrity": "sha512-EkBCzUZSdhJV8PxMSbeEV//xguVKZu9hZZulM+2gHXI0t2hGVU3eYE6/XnH77DS6FM2FY8wl17aDcu9vXpvLWQ==" }, "node_modules/ieee754": { "version": "1.2.1", @@ -16001,13 +16051,13 @@ } }, "node_modules/is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -16582,6 +16632,15 @@ "node": ">=0.10.0" } }, + "node_modules/isomorphic-timers-promises": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/isomorphic-timers-promises/-/isomorphic-timers-promises-1.0.1.tgz", + "integrity": "sha512-u4sej9B1LPSxTGKB/HiuzvEQnXH0ECYkSVQU39koSwmFAxhlEAFl9RdTvLv4TOTQUgBS5O3O5fwUxk6byBZ+IQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -19755,6 +19814,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -19887,9 +19955,9 @@ } }, "node_modules/miller-rabin/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", "dev": true }, "node_modules/mime": { @@ -20217,9 +20285,9 @@ "optional": true }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "dev": true, "funding": [ { @@ -20395,99 +20463,139 @@ "dev": true }, "node_modules/node-polyfill-webpack-plugin": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/node-polyfill-webpack-plugin/-/node-polyfill-webpack-plugin-4.0.0.tgz", - "integrity": "sha512-WLk77vLpbcpmTekRj6s6vYxk30XoyaY5MDZ4+9g8OaKoG3Ij+TjOqhpQjVUlfDZBPBgpNATDltaQkzuXSnnkwg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/node-polyfill-webpack-plugin/-/node-polyfill-webpack-plugin-4.1.0.tgz", + "integrity": "sha512-b4ei444EKkOagG/yFqojrD3QTYM5IOU1f8tn9o6uwrG4qL+brI7oVhjPVd0ZL2xy+Z6CP5bu9w8XTvlWgiXHcw==", + "dev": true, + "dependencies": { + "node-stdlib-browser": "^1.3.0", + "type-fest": "^4.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "webpack": ">=5" + } + }, + "node_modules/node-polyfill-webpack-plugin/node_modules/type-fest": { + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.31.0.tgz", + "integrity": "sha512-yCxltHW07Nkhv/1F6wWBr8kz+5BGMfP+RbRSYFnegVb0qV/UMT0G0ElBloPVerqn4M2ZV80Ir1FtCcYv1cT6vQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true + }, + "node_modules/node-stdlib-browser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/node-stdlib-browser/-/node-stdlib-browser-1.3.0.tgz", + "integrity": "sha512-g/koYzOr9Fb1Jc+tHUHlFd5gODjGn48tHexUK8q6iqOVriEgSnd3/1T7myBYc+0KBVze/7F7n65ec9rW6OD7xw==", "dev": true, "dependencies": { - "assert": "^2.1.0", + "assert": "^2.0.0", + "browser-resolve": "^2.0.0", "browserify-zlib": "^0.2.0", - "buffer": "^6.0.3", - "console-browserify": "^1.2.0", + "buffer": "^5.7.1", + "console-browserify": "^1.1.0", "constants-browserify": "^1.0.0", - "crypto-browserify": "^3.12.0", - "domain-browser": "^5.7.0", - "events": "^3.3.0", + "create-require": "^1.1.1", + "crypto-browserify": "^3.11.0", + "domain-browser": "4.22.0", + "events": "^3.0.0", "https-browserify": "^1.0.0", + "isomorphic-timers-promises": "^1.0.1", "os-browserify": "^0.3.0", "path-browserify": "^1.0.1", + "pkg-dir": "^5.0.0", "process": "^0.11.10", - "punycode": "^2.3.1", + "punycode": "^1.4.1", "querystring-es3": "^0.2.1", - "readable-stream": "^4.5.2", + "readable-stream": "^3.6.0", "stream-browserify": "^3.0.0", "stream-http": "^3.2.0", - "string_decoder": "^1.3.0", - "timers-browserify": "^2.0.12", - "tty-browserify": "^0.0.1", - "type-fest": "^4.18.2", - "url": "^0.11.3", - "util": "^0.12.5", - "vm-browserify": "^1.1.2" + "string_decoder": "^1.0.0", + "timers-browserify": "^2.0.4", + "tty-browserify": "0.0.1", + "url": "^0.11.4", + "util": "^0.12.4", + "vm-browserify": "^1.0.1" }, "engines": { - "node": ">=14" - }, - "peerDependencies": { - "webpack": ">=5" + "node": ">=10" } }, - "node_modules/node-polyfill-webpack-plugin/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "node_modules/node-stdlib-browser/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/node-polyfill-webpack-plugin/node_modules/readable-stream": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", - "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "node_modules/node-stdlib-browser/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" + "p-locate": "^5.0.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/node-polyfill-webpack-plugin/node_modules/type-fest": { - "version": "4.26.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.1.tgz", - "integrity": "sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==", + "node_modules/node-stdlib-browser/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, "engines": { - "node": ">=16" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/node-releases": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "node_modules/node-stdlib-browser/node_modules/pkg-dir": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz", + "integrity": "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==", + "dev": true, + "dependencies": { + "find-up": "^5.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-stdlib-browser/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", "dev": true }, "node_modules/nopt": { @@ -20863,9 +20971,9 @@ } }, "node_modules/npm-run-all/node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", "dev": true, "dependencies": { "nice-try": "^1.0.4", @@ -21940,9 +22048,9 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "dev": true }, "node_modules/path-type": { @@ -22175,12 +22283,12 @@ } }, "node_modules/playwright": { - "version": "1.48.2", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.2.tgz", - "integrity": "sha512-NjYvYgp4BPmiwfe31j4gHLa3J7bD2WiBz8Lk2RoSsmX38SVIARZ18VYjxLjAcDsAhA+F4iSEXTSGgjua0rrlgQ==", + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.1.tgz", + "integrity": "sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==", "dev": true, "dependencies": { - "playwright-core": "1.48.2" + "playwright-core": "1.49.1" }, "bin": { "playwright": "cli.js" @@ -22193,9 +22301,9 @@ } }, "node_modules/playwright-core": { - "version": "1.48.2", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.2.tgz", - "integrity": "sha512-sjjw+qrLFlriJo64du+EK0kJgZzoQPsabGF4lBvsid+3CNIZIYLgnMj9V6JY5VhM2Peh20DJWIVpVljLLnlawA==", + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.1.tgz", + "integrity": "sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==", "dev": true, "bin": { "playwright-core": "cli.js" @@ -22319,13 +22427,13 @@ } }, "node_modules/postcss-modules-local-by-default": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz", - "integrity": "sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.1.0.tgz", + "integrity": "sha512-rm0bdSv4jC3BDma3s9H19ZddW0aHX6EoqwDYU2IfZhRN+53QrufTRo2IdkAbRqLx4R2IYbZnbjKKxg4VN5oU9Q==", "dev": true, "dependencies": { "icss-utils": "^5.0.0", - "postcss-selector-parser": "^6.0.2", + "postcss-selector-parser": "^7.0.0", "postcss-value-parser": "^4.1.0" }, "engines": { @@ -22336,12 +22444,12 @@ } }, "node_modules/postcss-modules-scope": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz", - "integrity": "sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", "dev": true, "dependencies": { - "postcss-selector-parser": "^6.0.4" + "postcss-selector-parser": "^7.0.0" }, "engines": { "node": "^10 || ^12 || >= 14" @@ -22366,9 +22474,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", + "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", "dev": true, "dependencies": { "cssesc": "^3.0.0", @@ -22564,9 +22672,9 @@ } }, "node_modules/public-encrypt/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", "dev": true }, "node_modules/pump": { @@ -24880,12 +24988,6 @@ "node": ">=8" } }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true - }, "node_modules/thingies": { "version": "1.21.0", "resolved": "https://registry.npmjs.org/thingies/-/thingies-1.21.0.tgz", @@ -25623,7 +25725,7 @@ "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "devOptional": true + "dev": true }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", diff --git a/package.json b/package.json index e4bb83f..8d8addc 100644 --- a/package.json +++ b/package.json @@ -80,21 +80,21 @@ "lint": "ng lint" }, "dependencies": { - "@angular/common": "18.2.11", - "@angular/compiler": "18.2.11", - "@angular/core": "18.2.11", - "@angular/forms": "18.2.11", - "@angular/language-service": "18.2.11", - "@angular/platform-browser": "18.2.11", - "@angular/platform-browser-dynamic": "18.2.11", - "@angular/router": "18.2.11", + "@angular/common": "18.2.12", + "@angular/compiler": "18.2.12", + "@angular/core": "18.2.12", + "@angular/forms": "18.2.12", + "@angular/language-service": "18.2.12", + "@angular/platform-browser": "18.2.12", + "@angular/platform-browser-dynamic": "18.2.12", + "@angular/router": "18.2.12", "@popperjs/core": "2.11.8", "bootstrap": "5.3.3", "bootstrap-icons": "1.11.3", - "bootstrap-table": "1.23.5", - "chart.js": "4.4.6", + "bootstrap-table": "1.24.0", + "chart.js": "4.4.7", "crypto": "1.0.1", - "idb": "8.0.0", + "idb": "8.0.1", "jquery": "3.7.1", "os": "0.1.2", "rxjs": "7.8.1", @@ -110,36 +110,36 @@ "@angular-eslint/eslint-plugin-template": "18.4.0", "@angular-eslint/schematics": "18.4.0", "@angular-eslint/template-parser": "18.4.0", - "@angular/build": "^18.2.11", - "@angular/cli": "18.2.11", - "@angular/compiler-cli": "18.2.11", - "@electron/packager": "18.3.5", + "@angular/build": "^18.2.12", + "@angular/cli": "18.2.12", + "@angular/compiler-cli": "18.2.12", + "@electron/packager": "18.3.6", "@ngx-translate/core": "16.0.3", "@ngx-translate/http-loader": "16.0.0", - "@playwright/test": "1.48.2", + "@playwright/test": "1.49.1", "@types/auto-launch": "5.0.5", "@types/bootstrap": "5.2.10", "@types/chart.js": "2.9.41", "@types/jest": "29.5.14", "@types/jquery": "3.5.32", - "@types/node": "22.9.0", + "@types/node": "22.10.2", "@types/pidusage": "2.0.5", "@types/unbzip2-stream": "1.4.3", - "@typescript-eslint/eslint-plugin": "8.14.0", - "@typescript-eslint/parser": "8.14.0", + "@typescript-eslint/eslint-plugin": "8.18.2", + "@typescript-eslint/parser": "8.18.2", "conventional-changelog-cli": "5.0.0", - "electron": "33.2.0", + "electron": "33.2.1", "electron-builder": "25.1.8", "electron-debug": "4.1.0", "electron-reloader": "1.2.3", - "eslint": "9.14.0", + "eslint": "9.17.0", "eslint-plugin-import": "2.31.0", - "eslint-plugin-jsdoc": "50.5.0", + "eslint-plugin-jsdoc": "50.6.1", "eslint-plugin-prefer-arrow": "1.2.3", "jest": "29.7.0", - "node-polyfill-webpack-plugin": "4.0.0", + "node-polyfill-webpack-plugin": "4.1.0", "npm-run-all": "4.1.5", - "playwright": "1.48.2", + "playwright": "1.49.1", "ts-node": "10.9.2", "typescript": "5.5.4", "wait-on": "8.0.1", diff --git a/src/app/core/services/daemon/daemon.service.ts b/src/app/core/services/daemon/daemon.service.ts index b32ada8..1e68c03 100644 --- a/src/app/core/services/daemon/daemon.service.ts +++ b/src/app/core/services/daemon/daemon.service.ts @@ -141,6 +141,14 @@ export class DaemonService { window.electronAPI.onMoneroClose((event: any, code: number) => { console.debug(event); console.debug(code); + + if (code != 0) { + window.electronAPI.showNotification({ + title: 'Daemon Error', + body: 'Monero daemon exited with code: ' + code, + closeButtonText: 'Dismiss' + }); + } this.onClose(); }); @@ -220,6 +228,7 @@ export class DaemonService { private onClose(): void { this.daemonRunning = false; + this.starting = false; this.stopping = false; this.onDaemonStatusChanged.emit(false); this.onDaemonStopEnd.emit(); @@ -240,8 +249,19 @@ export class DaemonService { } public async saveSettings(settings: DaemonSettings, restartDaemon: boolean = true): Promise { + settings.assertValid(); + + if (settings.monerodPath != '') { + const valid = await this.checkValidMonerodPath(settings.monerodPath); + + if (!valid) { + throw new Error("Invalid monerod path provided"); + } + } + const db = await this.openDbPromise; await db.put(this.storeName, { id: 1, ...settings }); + this.onSavedSettings.emit(settings); if (restartDaemon) { @@ -267,6 +287,7 @@ export class DaemonService { const checkPromise = new Promise((resolve) => { window.electronAPI.onCheckValidMonerodPath((event: any, valid: boolean) => { + window.electronAPI.unregisterOnCheckValidMonerodPath(); resolve(valid); }); }); @@ -863,6 +884,14 @@ export class DaemonService { } } + public async removeBootstrapDaemon(): Promise { + const response = await this.callRpc(new SetBootstrapDaemonRequest('', '', '', '')); + + if (typeof response.status == 'string' && response.status != 'OK') { + throw new Error(`Could not remove bootstrap daemon: ${response.status}`); + } + } + public async saveBc(): Promise { const response = await this.callRpc(new SaveBcRequest()); @@ -997,7 +1026,7 @@ export class DaemonService { return; } - await this.delay(5000); + await this.delay(15000); } window.electronAPI.showNotification({ @@ -1136,8 +1165,11 @@ export class DaemonService { const destination = settings.downloadUpgradePath; // Aggiorna con il percorso desiderato const moneroFolder = await this.installer.downloadMonero(destination, settings.monerodPath != ''); const { platform } = await this.electronService.getOsType(); - const ext = platform == 'win32' ? '.exe' : ''; - const separator = platform ?? 'win32' ? '\\' : '/'; + const isWin32 = platform == 'win32'; + + const ext = isWin32 ? '.exe' : ''; + const separator = isWin32 ? '\\' : '/'; + settings.monerodPath = `${moneroFolder}${separator}monerod${ext}`; await this.saveSettings(settings); diff --git a/src/app/core/services/electron/electron.service.ts b/src/app/core/services/electron/electron.service.ts index 0aff963..46bd2b4 100644 --- a/src/app/core/services/electron/electron.service.ts +++ b/src/app/core/services/electron/electron.service.ts @@ -12,7 +12,7 @@ export class ElectronService { childProcess!: typeof childProcess; fs!: typeof fs; - private _isAppImage?: boolean; + private _isPortable?: boolean; private _isAutoLaunched?: boolean; private _online: boolean = false; private _isProduction: boolean = false; @@ -124,7 +124,7 @@ export class ElectronService { } public async isAutoLaunchEnabled(): Promise { - if (await this.isAppImage()) { + if (await this.isPortable()) { return false; } @@ -141,7 +141,7 @@ export class ElectronService { } public async enableAutoLaunch(minimized: boolean): Promise { - if (await this.isAppImage()) { + if (await this.isPortable()) { throw new Error("Cannot enable auto launch"); } @@ -174,7 +174,7 @@ export class ElectronService { public async disableAutoLaunch(): Promise { - if (await this.isAppImage()) { + if (await this.isPortable()) { throw new Error("Cannot disable auto launch"); } @@ -205,21 +205,21 @@ export class ElectronService { await promise; } - public async isAppImage(): Promise { - if (this._isAppImage === undefined) { + public async isPortable(): Promise { + if (this._isPortable === undefined) { const promise = new Promise((resolve) => { - window.electronAPI.onIsAppImage((event: any, value: boolean) => { - window.electronAPI.unregisterOnIsAppImage(); + window.electronAPI.onIsPortable((event: any, value: boolean) => { + window.electronAPI.unregisterIsPortable(); resolve(value); }); }); - window.electronAPI.isAppImage(); + window.electronAPI.isPortable(); - this._isAppImage = await promise; + this._isPortable = await promise; } - return this._isAppImage; + return this._isPortable; } public async selectFile(extensions?: string[]): Promise { @@ -311,4 +311,25 @@ export class ElectronService { return await promise; } + public async downloadFile(url: string, destination: string, progressFunction?: (info: { progress: number, status: string }) => void): Promise { + const promise = new Promise((resolve, reject) => { + if (progressFunction) { + window.electronAPI.onDownloadProgress((event: any, prog: { progress: number, status: string }) => progressFunction(prog)); + } + + window.electronAPI.onDownloadFileError((event: any, error: string) => { + window.electronAPI.unregisterOnDownloadFile(); + reject(new Error(error)); + }); + + window.electronAPI.onDownloadFileComplete((event: any, fileName: string) => { + window.electronAPI.unregisterOnDownloadFile(); + resolve(fileName); + }); + }); + + window.electronAPI.downloadFile(url, destination); + return await promise; + } + } diff --git a/src/app/core/utils/StringUtils.ts b/src/app/core/utils/StringUtils.ts new file mode 100644 index 0000000..fffffa5 --- /dev/null +++ b/src/app/core/utils/StringUtils.ts @@ -0,0 +1,11 @@ +export abstract class StringUtils { + public static replaceAll(value: string, oldValue: string, newValue: string): string { + let v = value; + + while(v.includes(oldValue)) { + v = v.replace(oldValue, newValue); + } + + return v; + } +} \ No newline at end of file diff --git a/src/app/core/utils/index.ts b/src/app/core/utils/index.ts new file mode 100644 index 0000000..ac7c00a --- /dev/null +++ b/src/app/core/utils/index.ts @@ -0,0 +1 @@ +export { StringUtils } from "./StringUtils"; diff --git a/src/app/pages/about/about.component.html b/src/app/pages/about/about.component.html index 2fa4c6b..fe2183e 100644 --- a/src/app/pages/about/about.component.html +++ b/src/app/pages/about/about.component.html @@ -11,7 +11,7 @@

About

-
+
diff --git a/src/app/pages/bans/bans.component.html b/src/app/pages/bans/bans.component.html index 59e1b0f..253fd13 100644 --- a/src/app/pages/bans/bans.component.html +++ b/src/app/pages/bans/bans.component.html @@ -11,10 +11,9 @@

Bans

-
+

List of banned IPs

-
List of banned IPs

Ban another node by IP

-
-
+
+
-
+

Overview of current block set queue

@@ -268,8 +268,8 @@
Block Additional Details

-
-
+
+
@@ -398,8 +398,8 @@
Miscellaneous

-
-
+
+
@@ -453,8 +453,8 @@

  
-
-
+
+
@@ -477,7 +477,8 @@

  

Prune Blockchain


-
   -
-
+
+
diff --git a/src/app/pages/outputs/outputs.component.html b/src/app/pages/outputs/outputs.component.html index a5f2543..b7aa600 100644 --- a/src/app/pages/outputs/outputs.component.html +++ b/src/app/pages/outputs/outputs.component.html @@ -10,7 +10,7 @@

Outputs

-
+

Get outputs

@@ -43,8 +43,8 @@

  
-
-
+
+
@@ -59,7 +59,7 @@

   'index': number } ]" - rows="15" cols="15" [(ngModel)]="getOutsJsonString" [ngModelOptions]="{standalone: true}"> + rows="10" cols="15" [(ngModel)]="getOutsJsonString" [ngModelOptions]="{standalone: true}"> Array of outputs
@@ -77,6 +77,8 @@

  
+
+
-
+

Get a histogram of output amounts

+ + + -
- - - - - - - - - -
AmountBaseStart HeightDistributions
-
- -
-
+
+
+ +
+ + + + + + + + + +
AmountBaseStart HeightDistributions
+
-

Get a histogram of output amounts. For all amounts (possibly filtered by parameters), gives the number of outputs on the chain for that amount. RingCT outputs counts as 0 amount.

+
@@ -130,7 +140,7 @@

Get a histogram of output amounts. For all amounts (possibly fi ... , '2346534525' ]" - rows="15" cols="15" [(ngModel)]="getOutHistogramAmountsJsonString" [ngModelOptions]="{standalone: true}"> + rows="10" cols="15" [(ngModel)]="getOutHistogramAmountsJsonString" [ngModelOptions]="{standalone: true}"> Array of unsigned int

@@ -166,6 +176,8 @@

Get a histogram of output amounts. For all amounts (possibly fi

+
+
-
-
+
+
@@ -218,7 +230,7 @@

Get Outputs

... , '2346534525' ]" - rows="15" cols="15" [(ngModel)]="getOutDistributionAmountsJsonString" [ngModelOptions]="{standalone: true}"> + rows="10" cols="15" [(ngModel)]="getOutDistributionAmountsJsonString" [ngModelOptions]="{standalone: true}"> Array of unsigned int, amounts to look for
@@ -248,6 +260,8 @@

Get Outputs

+
+
-
-
+
+

Check if outputs have been spent using the key image associated with the output

+ rows="10" cols="15" > List of key image hex strings to check.
Invalid key images. @@ -303,12 +317,12 @@

Check if outputs have been spent using the key image associated

-
- -
+ +
- - + + +
diff --git a/src/app/pages/peers/peers.component.html b/src/app/pages/peers/peers.component.html index 83c83a1..4dcf3b8 100644 --- a/src/app/pages/peers/peers.component.html +++ b/src/app/pages/peers/peers.component.html @@ -11,7 +11,7 @@

Peers

-
+

List of known peers

@@ -92,8 +92,8 @@

  
-
-
+
+
@@ -129,8 +129,8 @@

  
-
-
+
+
diff --git a/src/app/pages/settings/settings.component.html b/src/app/pages/settings/settings.component.html index 56a0733..5be8448 100644 --- a/src/app/pages/settings/settings.component.html +++ b/src/app/pages/settings/settings.component.html @@ -13,7 +13,7 @@

Settings

-
+