From 7e780e179286a0da5ccbe78dc5a41db640e3fb69 Mon Sep 17 00:00:00 2001 From: "courtmanr@gmail.com" Date: Fri, 28 Feb 2025 20:39:40 +0000 Subject: [PATCH] v1.0.5 --- .gitignore | 3 + Dockerfile | 42 +- README.md | 154 +- connection_log.txt | 114 -- frontend/package-lock.json | 363 +++- frontend/package.json | 4 + frontend/src/App.jsx | 187 +- frontend/src/components/NetworkDisplay.jsx | 1991 ++++++++++++-------- frontend/src/utils/testConnection.js | 37 - frontend/vite.config.js | 7 +- package-lock.json | 323 ++++ package.json | 8 +- plan.md | 285 --- start-dev.sh | 19 - start-pulse.sh | 31 + startup_log.txt | 110 -- test-server.js | 186 -- 17 files changed, 2147 insertions(+), 1717 deletions(-) delete mode 100644 connection_log.txt delete mode 100644 frontend/src/utils/testConnection.js delete mode 100644 plan.md delete mode 100755 start-dev.sh create mode 100755 start-pulse.sh delete mode 100644 startup_log.txt delete mode 100644 test-server.js diff --git a/.gitignore b/.gitignore index 15b28c3a2..6693e10e4 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,9 @@ build/ logs/ *.log +# Reports +reports/ + # IDE and editor files .idea/ .vscode/ diff --git a/Dockerfile b/Dockerfile index eccc3df89..7d0e637a4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,28 +1,30 @@ -FROM node:18-alpine as builder - -WORKDIR /app - -COPY package*.json ./ -RUN npm ci - -COPY . . -RUN npm run build - FROM node:18-alpine WORKDIR /app -COPY --from=builder /app/package*.json ./ -COPY --from=builder /app/dist ./dist +# Copy package files +COPY package*.json ./ +COPY frontend/package*.json ./frontend/ -RUN npm ci --only=production +# Install dependencies +RUN npm ci +RUN cd frontend && npm ci -# Health check -HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ - CMD wget -qO- http://localhost:${PORT:-3000}/api/health || exit 1 +# Copy application files +COPY . . -# Expose the port -EXPOSE ${PORT:-3000} +# Make the startup script executable +RUN chmod +x start-pulse.sh -# Start the application -CMD ["node", "dist/server.js"] \ No newline at end of file +# Set environment variables +ENV NODE_ENV=development +ENV LOG_LEVEL=info +ENV ENABLE_DEV_TOOLS=true +ENV PORT=3000 +ENV DOCKER_CONTAINER=true + +# Expose the backend and frontend ports +EXPOSE 3000 5173 + +# Start both the backend and frontend (using the start-pulse.sh script) +CMD ["./start-pulse.sh"] \ No newline at end of file diff --git a/README.md b/README.md index dc46400bd..64e6a9e38 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,7 @@ A lightweight, responsive ProxMox monitoring application that displays real-time - Real-time monitoring of ProxMox nodes, VMs, and containers - Dashboard with summary cards for nodes, guests, and resources - Responsive design that works on desktop and mobile -- WebSocket connection for real-time updates -- Development tools for easier debugging -- Configurable polling intervals for optimal performance +- WebSocket connection for live updates ## Installation @@ -18,15 +16,50 @@ A lightweight, responsive ProxMox monitoring application that displays real-time ``` npm install ``` -3. Create a `.env` file based on the `.env.example` file -4. Build the application: +3. Install frontend dependencies: ``` - npm run build - ``` -5. Start the application: - ``` - npm start + cd frontend && npm install && cd .. ``` +4. Create a `.env` file based on the `.env.example` file + +## Running the Application + +The simplest way to run the application is to use one of these methods: + +### Using the startup script directly + +```bash +# Make the script executable (only needed once) +chmod +x start-pulse.sh + +# Run the application +./start-pulse.sh +``` + +### Using npm + +```bash +# Run the application +npm run pulse +``` + +### Using Docker + +```bash +# Build the Docker image +docker build -t proxmox-pulse . + +# Run the Docker container +docker run -p 3000:3000 -p 5173:5173 --env-file .env proxmox-pulse +``` + +This will: +1. Kill any existing server processes +2. Start the backend server in development mode +3. Start the frontend Vite server +4. Host the application at http://192.168.0.130:5173/ + +The application will show real-time metrics from your ProxMox servers configured in the .env file. ## Configuration @@ -37,120 +70,35 @@ The application uses environment variables for configuration. You can create a ` PROXMOX_NODE_1_NAME=Proxmox Node 1 PROXMOX_NODE_1_HOST=https://192.168.0.132:8006 PROXMOX_NODE_1_TOKEN_ID=root@pam!pulse -PROXMOX_NODE_1_TOKEN_SECRET=e1850350-6afc-4b8e-ae28-472152af84f9 +PROXMOX_NODE_1_TOKEN_SECRET=your-token-secret # Node 2 PROXMOX_NODE_2_NAME=Proxmox Node 2 PROXMOX_NODE_2_HOST=https://192.168.0.141:8006 PROXMOX_NODE_2_TOKEN_ID=root@pam!pulse -PROXMOX_NODE_2_TOKEN_SECRET=63e21f63-4cfd-4ba0-9b14-fc681c59d932 +PROXMOX_NODE_2_TOKEN_SECRET=your-token-secret # App Configuration PORT=3000 NODE_ENV=development -LOG_LEVEL=debug +LOG_LEVEL=info ENABLE_DEV_TOOLS=true METRICS_HISTORY_MINUTES=60 IGNORE_SSL_ERRORS=true - -# Polling Intervals (in milliseconds) -NODE_POLLING_INTERVAL_MS=10000 -EVENT_POLLING_INTERVAL_MS=2000 ``` -### Polling Intervals - -The application uses polling to fetch data from ProxMox nodes: - -- `NODE_POLLING_INTERVAL_MS`: How often to poll for node status, VMs, and containers (default: 10000ms) -- `EVENT_POLLING_INTERVAL_MS`: How often to poll for events (default: 2000ms) - -The application also implements adaptive polling for events: -- When events are detected, polling frequency temporarily increases -- Quick follow-up polls occur 500ms after receiving events -- Polling frequency remains higher for 10 seconds after activity is detected - -Decreasing these values will make the application more responsive but may increase server load. Increasing them will reduce server load but make updates less frequent. - ### Important Note on API Tokens -If your ProxMox API token ID contains special characters (like `!`, `?`, `&`, etc.), you may encounter issues when using it in shell commands or other contexts. The application handles this internally, but if you're testing the connection manually, make sure to properly escape or quote the token ID. - -For example, when using curl: - -```bash -# INCORRECT - will cause issues with the ! character -curl -k -v -H "Authorization: PVEAPIToken=root@pam!pulse=your-token-secret" https://your-proxmox-host:8006/api2/json/version - -# CORRECT - use single quotes around the token ID -curl -k -v -H "Authorization: PVEAPIToken='root@pam!pulse'=your-token-secret" https://your-proxmox-host:8006/api2/json/version -``` - -## Testing the Connection - -The application includes a script to test the connection to your ProxMox nodes. This is useful for troubleshooting connection issues. - -```bash -npm run test:connection -``` - -This script will: -1. Validate your configuration -2. Test the connection to each configured ProxMox node -3. Check for potential issues with your API tokens -4. Provide helpful error messages and suggestions if the connection fails - -If you encounter connection issues, the script will provide detailed information about what went wrong and how to fix it. - -## Development - -To run the application in development mode: - -```bash -npm run dev -``` - -This will start the application with hot reloading enabled. - -### Development Tools - -When `ENABLE_DEV_TOOLS` is set to `true`, the application provides additional endpoints for debugging: - -- `/dev/api-reference`: API reference documentation -- `/dev/config`: Current configuration (with sensitive information redacted) -- `/dev/state`: Current application state -- `/dev/refresh/:nodeId`: Manually refresh data for a node +If your ProxMox API token ID contains special characters (like `!`, `?`, `&`, etc.), make sure your `.env` file has the correct format. The application handles this internally, but it's important for the configuration to be correct. ## Troubleshooting -### Connection Issues - If you're having trouble connecting to your ProxMox nodes, check the following: -1. **Network Connectivity**: Make sure you can reach the ProxMox host from the machine running the application. - ```bash - ping your-proxmox-host - ``` - -2. **API Access**: Verify that the ProxMox API is accessible on the specified port. - ```bash - curl -k https://your-proxmox-host:8006/api2/json/version - ``` - -3. **API Token**: Ensure that your API token has the correct permissions and is properly formatted. - - If your token ID contains special characters, make sure to properly escape or quote it. - - Run the connection test script to check for issues with your token: - ```bash - npm run test:connection - ``` - -4. **SSL Certificates**: If you're using self-signed certificates, make sure `IGNORE_SSL_ERRORS` is set to `true`. - -### Common Errors - -- **Timeout**: The connection to the ProxMox API is timing out. Check your network configuration and firewall settings. -- **401 Unauthorized**: The API token is invalid or doesn't have the necessary permissions. -- **SSL Certificate Error**: The SSL certificate is not trusted. Set `IGNORE_SSL_ERRORS` to `true` or install a valid certificate. +1. **Network Connectivity**: Make sure you can reach the ProxMox host from your machine +2. **API Access**: Verify that the ProxMox API is accessible on the specified port +3. **API Token**: Ensure that your API token has the correct permissions and is properly formatted +4. **SSL Certificates**: If you're using self-signed certificates, make sure `IGNORE_SSL_ERRORS` is set to `true` ## License diff --git a/connection_log.txt b/connection_log.txt deleted file mode 100644 index 808b99e92..000000000 --- a/connection_log.txt +++ /dev/null @@ -1,114 +0,0 @@ - -> pulse_basic@1.0.0 test:connection -> ts-node src/scripts/test-connection.ts - -2025-02-27 14:52:10 info [TestConnection]: Running comprehensive connection tests... -{ - "service": "proxmox-monitor" -} -2025-02-27 14:52:10 info [StartupCheck]: Running startup checks... -{ - "service": "proxmox-monitor" -} -2025-02-27 14:52:10 info [StartupCheck]: Validating configuration... -{ - "service": "proxmox-monitor" -} -2025-02-27 14:52:10 info [ConfigValidator]: Validating application configuration... -{ - "service": "proxmox-monitor" -} -2025-02-27 14:52:10 info [ConfigValidator]: Validating node configuration: Proxmox Node 1 (node-1) -{ - "service": "proxmox-monitor" -} -2025-02-27 14:52:10 warn [ConfigValidator]: Node node-1: Token ID contains special characters that may cause issues with shell commands: ! -{ - "service": "proxmox-monitor" -} -2025-02-27 14:52:10 warn [ConfigValidator]: When using in curl or similar commands, make sure to properly escape or quote the token ID. -{ - "service": "proxmox-monitor" -} -2025-02-27 14:52:10 info [ConfigValidator]: Node node-1: Successfully connected to ProxMox API version 8.3.4 -{ - "service": "proxmox-monitor" -} -2025-02-27 14:52:10 info [ConfigValidator]: Validating node configuration: Proxmox Node 2 (node-2) -{ - "service": "proxmox-monitor" -} -2025-02-27 14:52:10 warn [ConfigValidator]: Node node-2: Token ID contains special characters that may cause issues with shell commands: ! -{ - "service": "proxmox-monitor" -} -2025-02-27 14:52:10 warn [ConfigValidator]: When using in curl or similar commands, make sure to properly escape or quote the token ID. -{ - "service": "proxmox-monitor" -} -2025-02-27 14:52:10 info [ConfigValidator]: Node node-2: Successfully connected to ProxMox API version 8.3.4 -{ - "service": "proxmox-monitor" -} -2025-02-27 14:52:10 info [StartupCheck]: ✅ Configuration validation passed. -{ - "service": "proxmox-monitor" -} -2025-02-27 14:52:10 info [StartupCheck]: Testing connections to all ProxMox nodes... -{ - "service": "proxmox-monitor" -} -2025-02-27 14:52:10 warn [ProxmoxClient][node-1]: SSL certificate validation is disabled. This is not recommended for production. -{ - "service": "proxmox-monitor" -} -2025-02-27 14:52:10 warn [ProxmoxClient][node-2]: SSL certificate validation is disabled. This is not recommended for production. -{ - "service": "proxmox-monitor" -} -2025-02-27 14:52:10 debug [ProxmoxClient][node-1]: API Request: GET /version -{ - "service": "proxmox-monitor" -} -2025-02-27 14:52:10 debug [ProxmoxClient][node-2]: API Request: GET /version -{ - "service": "proxmox-monitor" -} -2025-02-27 14:52:11 debug [ProxmoxClient][node-2]: API Response: 200 GET /version -{ - "service": "proxmox-monitor", - "data": { - "data": { - "release": "8.3", - "repoid": "65224a0f9cd294a3", - "version": "8.3.4" - } - } -} -2025-02-27 14:52:11 info [ProxmoxClient][node-2]: Connected to ProxMox API version 8.3.4 -{ - "service": "proxmox-monitor" -} -2025-02-27 14:52:11 debug [ProxmoxClient][node-1]: API Response: 200 GET /version -{ - "service": "proxmox-monitor", - "data": { - "data": { - "version": "8.3.4", - "repoid": "65224a0f9cd294a3", - "release": "8.3" - } - } -} -2025-02-27 14:52:11 info [ProxmoxClient][node-1]: Connected to ProxMox API version 8.3.4 -{ - "service": "proxmox-monitor" -} -2025-02-27 14:52:11 info [StartupCheck]: ✅ Successfully connected to all 2 ProxMox nodes. -{ - "service": "proxmox-monitor" -} -2025-02-27 14:52:11 info [TestConnection]: ✅ All checks passed successfully! -{ - "service": "proxmox-monitor" -} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d4cc50d44..15351e042 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13,8 +13,6 @@ "@emotion/styled": "^11.14.0", "@mui/icons-material": "^6.4.6", "@mui/material": "^6.4.6", - "@types/react": "^19.0.10", - "@types/react-dom": "^19.0.4", "@vitejs/plugin-react": "^4.3.4", "axios": "^1.8.1", "react": "^19.0.0", @@ -22,8 +20,11 @@ "react-router-dom": "^7.2.0", "recharts": "^2.15.1", "socket.io-client": "^4.8.1", - "typescript": "^5.7.3", "vite": "^6.2.0" + }, + "devDependencies": { + "concurrently": "^9.1.2", + "kill-port": "^2.0.1" } }, "node_modules/@ampproject/remapping": { @@ -1531,19 +1532,11 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.10.tgz", "integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } }, - "node_modules/@types/react-dom": { - "version": "19.0.4", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.4.tgz", - "integrity": "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.0.0" - } - }, "node_modules/@types/react-transition-group": { "version": "4.4.12", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", @@ -1572,6 +1565,32 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1678,6 +1697,51 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -1687,6 +1751,26 @@ "node": ">=6" } }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -1699,6 +1783,32 @@ "node": ">= 0.8" } }, + "node_modules/concurrently": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.1.2.tgz", + "integrity": "sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, "node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", @@ -1928,6 +2038,13 @@ "integrity": "sha512-dJr1o6yCntRkXElnhsHh1bAV19bo/hKyFf7tCcWgpXbuFIF0Lakjgqv5LRfSDaNzAII8Fnxg2tqgHkgCvxdbxw==", "license": "ISC" }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, "node_modules/engine.io-client": { "version": "6.6.3", "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", @@ -2170,6 +2287,16 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -2207,6 +2334,13 @@ "node": ">= 0.4" } }, + "node_modules/get-them-args": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/get-them-args/-/get-them-args-1.3.2.tgz", + "integrity": "sha512-LRn8Jlk+DwZE4GTlDbT3Hikd1wSHgLMme/+7ddlqKd7ldwR6LjJgTVWzBnR01wnYGe4KgrXjg287RaI22UHmAw==", + "dev": true, + "license": "MIT" + }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -2228,6 +2362,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -2328,6 +2472,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2364,6 +2518,20 @@ "node": ">=6" } }, + "node_modules/kill-port": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/kill-port/-/kill-port-2.0.1.tgz", + "integrity": "sha512-e0SVOV5jFo0mx8r7bS29maVWp17qGqLBZ5ricNSajON6//kmb7qqqNnml4twNE8Dtj97UQD+gNFOaipS/q1zzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-them-args": "1.3.2", + "shell-exec": "1.0.2" + }, + "bin": { + "kill-port": "cli.js" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -2719,6 +2887,16 @@ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", "license": "MIT" }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -2786,6 +2964,16 @@ "fsevents": "~2.3.2" } }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/scheduler": { "version": "0.25.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", @@ -2807,6 +2995,26 @@ "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", "license": "MIT" }, + "node_modules/shell-exec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/shell-exec/-/shell-exec-1.0.2.tgz", + "integrity": "sha512-jyVd+kU2X+mWKMmGhx4fpWbPsjvD53k9ivqetutVW/BQ+WIZoDoP4d8vUMGezV6saZsiNoW2f9GIhg9Dondohg==", + "dev": true, + "license": "MIT" + }, + "node_modules/shell-quote": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz", + "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/socket.io-client": { "version": "4.8.1", "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", @@ -2887,12 +3095,56 @@ "node": ">=0.10.0" } }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/stylis": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", "license": "MIT" }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -2911,25 +3163,29 @@ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "license": "MIT" }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, "node_modules/turbo-stream": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==", "license": "ISC" }, - "node_modules/typescript": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", - "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, "node_modules/update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", @@ -3053,6 +3309,24 @@ } } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/ws": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", @@ -3082,6 +3356,16 @@ "node": ">=0.4.0" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -3101,6 +3385,35 @@ "engines": { "node": ">= 14" } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } } } } diff --git a/frontend/package.json b/frontend/package.json index 2c6af3315..5c3c1279c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -25,5 +25,9 @@ "recharts": "^2.15.1", "socket.io-client": "^4.8.1", "vite": "^6.2.0" + }, + "devDependencies": { + "concurrently": "^9.1.2", + "kill-port": "^2.0.1" } } diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index adbbd37af..1ad4a57e8 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -3,21 +3,117 @@ import { ThemeProvider, createTheme } from '@mui/material/styles'; import CssBaseline from '@mui/material/CssBaseline'; import { Container, Box, Typography, AppBar, Toolbar, Paper } from '@mui/material'; import NetworkDisplay from './components/NetworkDisplay'; -// Import connection test utility -import './utils/testConnection'; -// Create a theme instance +// Create a theme instance with improved colors and styling const theme = createTheme({ palette: { mode: 'light', primary: { - main: '#1976d2', + main: '#3a7bd5', + light: '#5e9cf5', + dark: '#2c5ea3', + contrastText: '#ffffff', }, secondary: { - main: '#dc004e', + main: '#00b8d4', + light: '#62ebff', + dark: '#0088a3', + contrastText: '#ffffff', }, background: { - default: '#f5f5f5', + default: '#f8f9fa', + paper: '#ffffff', + }, + success: { + main: '#4caf50', + light: '#80e27e', + dark: '#087f23', + }, + warning: { + main: '#ff9800', + light: '#ffc947', + dark: '#c66900', + }, + error: { + main: '#f44336', + light: '#ff7961', + dark: '#ba000d', + }, + info: { + main: '#2196f3', + light: '#64b5f6', + dark: '#0069c0', + }, + grey: { + 50: '#fafafa', + 100: '#f5f5f5', + 200: '#eeeeee', + 300: '#e0e0e0', + 400: '#bdbdbd', + 500: '#9e9e9e', + 600: '#757575', + 700: '#616161', + 800: '#424242', + 900: '#212121', + }, + }, + typography: { + fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', + h6: { + fontWeight: 500, + letterSpacing: '0.0075em', + }, + body1: { + fontSize: '0.9rem', + }, + body2: { + fontSize: '0.8rem', + }, + }, + shape: { + borderRadius: 8, + }, + components: { + MuiAppBar: { + styleOverrides: { + root: { + boxShadow: '0 2px 10px rgba(0, 0, 0, 0.05)', + }, + }, + }, + MuiPaper: { + styleOverrides: { + root: { + boxShadow: '0 2px 10px rgba(0, 0, 0, 0.05)', + }, + }, + }, + MuiTableCell: { + styleOverrides: { + root: { + padding: '12px 16px', + }, + head: { + fontWeight: 600, + color: '#424242', + }, + }, + }, + MuiTableRow: { + styleOverrides: { + root: { + '&:last-child td, &:last-child th': { + border: 0, + }, + }, + }, + }, + MuiChip: { + styleOverrides: { + root: { + fontWeight: 500, + }, + }, }, }, }); @@ -26,30 +122,81 @@ function App() { return ( - - - - + + + + + + + System Monitor - - - - Live System Metrics - - - Real-time monitoring of system resources and network activity - - + + {/* Network Display Component */} - + System Monitor © {new Date().getFullYear()} diff --git a/frontend/src/components/NetworkDisplay.jsx b/frontend/src/components/NetworkDisplay.jsx index 83c44b602..46dc79d61 100644 --- a/frontend/src/components/NetworkDisplay.jsx +++ b/frontend/src/components/NetworkDisplay.jsx @@ -31,24 +31,37 @@ import FilterAltIcon from '@mui/icons-material/FilterAlt'; import FilterAltOffIcon from '@mui/icons-material/FilterAltOff'; import MemoryIcon from '@mui/icons-material/Memory'; import StorageIcon from '@mui/icons-material/Storage'; -import CloudDownloadIcon from '@mui/icons-material/CloudDownload'; -import CloudUploadIcon from '@mui/icons-material/CloudUpload'; +import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'; +import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; import SpeedIcon from '@mui/icons-material/Speed'; import SearchIcon from '@mui/icons-material/Search'; import CancelIcon from '@mui/icons-material/Cancel'; import CloseIcon from '@mui/icons-material/Close'; +import PersonIcon from '@mui/icons-material/Person'; import InputBase from '@mui/material/InputBase'; // Define pulse animation const pulseAnimation = keyframes` 0% { - box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.4); + box-shadow: 0 0 0 0 rgba(58, 123, 213, 0.4); } 70% { - box-shadow: 0 0 0 6px rgba(76, 175, 80, 0); + box-shadow: 0 0 0 6px rgba(58, 123, 213, 0); } 100% { - box-shadow: 0 0 0 0 rgba(76, 175, 80, 0); + box-shadow: 0 0 0 0 rgba(58, 123, 213, 0); + } +`; + +// Add fade-in animation +const fadeIn = keyframes` + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); } `; @@ -105,83 +118,89 @@ const networkRateToSliderValue = (bytesPerSecond) => { // Progress bar with label and tooltip const ProgressWithLabel = ({ value, color = "primary", disabled = false, tooltipText }) => { - return ( - - - - theme.palette.grey[200], - boxShadow: '0 1px 2px rgba(0,0,0,0.05) inset', - '& .MuiLinearProgress-bar': { - borderRadius: 2, - boxShadow: '0 1px 0 rgba(255,255,255,0.3) inset' - } - }} - /> - - - - {formatPercentage(value)} - - + const normalizedValue = Math.min(Math.max(0, value), 100); + + const progressBar = ( + + + alpha(theme.palette.grey[300], 0.5), + '& .MuiLinearProgress-bar': { + borderRadius: 4, + transition: 'transform 0.4s cubic-bezier(0.4, 0, 0.2, 1)', + } + }} + /> - + + + {formatPercentage(normalizedValue)} + + + ); + + if (tooltipText) { + return ( + + {progressBar} + + ); + } + + return progressBar; }; // Status indicator circle const StatusIndicator = ({ status }) => { - // Map status to color and display text - const statusMap = { - 'running': { color: '#4caf50', text: 'Running', animate: true }, // Green - 'stopped': { color: '#ff9800', text: 'Stopped', animate: false }, // Amber/Orange - 'paused': { color: '#2196f3', text: 'Paused', animate: false }, // Blue - 'error': { color: '#f44336', text: 'Error', animate: true }, // Red - 'starting': { color: '#9c27b0', text: 'Starting', animate: true }, // Purple - 'stopping': { color: '#795548', text: 'Stopping', animate: true } // Brown - }; + let color = 'grey'; - // Default values for unknown status - const statusInfo = statusMap[status] || { color: '#9e9e9e', text: status, animate: false }; // Gray for unknown + switch (status.toLowerCase()) { + case 'running': + color = '#4caf50'; // success green + break; + case 'stopped': + color = '#f44336'; // error red + break; + case 'paused': + color = '#ff9800'; // warning orange + break; + default: + color = '#9e9e9e'; // grey + } return ( - - - + ); }; @@ -222,6 +241,7 @@ const NetworkDisplay = () => { download: 0, upload: 0 }); + const [activeSlider, setActiveSlider] = useState(null); // Function to reset all filters - wrap in useCallback to prevent infinite renders const resetFilters = useCallback(() => { @@ -236,29 +256,51 @@ const NetworkDisplay = () => { setActiveSearchTerms([]); }, []); + // Function to clear all filters and reset sorting to default + const clearAllFiltersAndSorting = useCallback(() => { + resetFilters(); + + // Reset sorting to default + setSortConfig({ + key: 'name', + direction: 'asc' + }); + }, [resetFilters]); + // Add event listeners for keyboard shortcuts useEffect(() => { const handleKeyDown = (e) => { - // Reset all filters when ESC is pressed + // Handle ESC key behavior if (e.key === 'Escape') { - resetFilters(); + // Reset everything to defaults with a single ESC press + const isSearchInputFocused = document.activeElement === searchInputRef.current; - // If the search input is focused, blur it - if (document.activeElement === searchInputRef.current) { + // If search is focused, blur it + if (isSearchInputFocused) { searchInputRef.current.blur(); } + + // Close filters if open + if (showFilters) { + setShowFilters(false); + } + + // Clear all filters and reset sorting + clearAllFiltersAndSorting(); + + // Prevent default action + e.preventDefault(); return; } - // If filters are visible and user starts typing, focus the search input + // If user starts typing, focus the search input without opening filters // Only if not already in an input field or contentEditable element - if (showFilters && - e.key.length === 1 && + if (e.key.length === 1 && !/^(Control|Alt|Shift|Meta)$/.test(e.key) && !['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement.tagName) && !document.activeElement.isContentEditable) { - // Focus the search input + // Focus the search input without opening filters if (searchInputRef.current) { searchInputRef.current.focus(); @@ -279,7 +321,7 @@ const NetworkDisplay = () => { return () => { document.removeEventListener('keydown', handleKeyDown); }; - }, [showFilters, resetFilters]); // resetFilters is now properly memoized + }, [showFilters, resetFilters, clearAllFiltersAndSorting]); // Removed escRecentlyPressed dependency // Function to add a search term to active filters const addSearchTerm = (term) => { @@ -301,6 +343,21 @@ const NetworkDisplay = () => { })); }; + // Function to handle slider drag start + const handleSliderDragStart = (filterName) => { + setActiveSlider(filterName); + }; + + // Function to handle slider drag end + const handleSliderDragEnd = () => { + setActiveSlider(null); + + // Remove focus from the active element to allow keyboard shortcuts to work + if (document.activeElement) { + document.activeElement.blur(); + } + }; + // Function to clear a specific filter const clearFilter = (filterName) => { setFilters(prev => ({ @@ -533,11 +590,28 @@ const NetworkDisplay = () => { const MAX_DOWNLOAD_RATE = 10485760; // ~10 MB/s const MAX_UPLOAD_RATE = 5242880; // ~5 MB/s + // Create refs for columns to measure their widths for accurate slider positioning + const cpuColumnRef = React.useRef(null); + const memoryColumnRef = React.useRef(null); + const diskColumnRef = React.useRef(null); + const downloadColumnRef = React.useRef(null); + const uploadColumnRef = React.useRef(null); + if (error) { return ( - + - + + Connection Error {error} @@ -548,9 +622,18 @@ const NetworkDisplay = () => { if (!isConnected) { return ( - - - + + + Connecting to server... @@ -558,573 +641,797 @@ const NetworkDisplay = () => { } return ( - - - `1px solid ${theme.palette.grey[200]}` - }}> - {/* Title and status section */} + + + + {/* Header section with search and controls */} - - - - System Metrics - - - `0 0 0 2px ${theme.palette.background.paper}`, - mr: 0.8, - animation: isConnected ? `${pulseAnimation} 3s infinite` : 'none', - }} - /> - - {isConnected ? 'Live Connection' : 'Disconnected'} - - - - - - {/* Debug badge if needed */} - {isDebugMode && ( - - )} - - - - {/* Controls section */} - - {/* Filter controls */} - - - setShowFilters(!showFilters)} - sx={{ - display: 'flex', - alignItems: 'center', - bgcolor: theme => (Object.values(filters).some(val => val > 0) || searchTerm) - ? alpha(theme.palette.primary.main, 0.08) - : alpha(theme.palette.primary.main, 0.04), - borderRadius: 1, - py: 0.5, - px: 1.5, - transition: 'all 0.2s ease-in-out', - cursor: 'pointer', - border: theme => (Object.values(filters).some(val => val > 0) || searchTerm) && !showFilters - ? `1px solid ${alpha(theme.palette.primary.main, 0.3)}` - : 'none', - '&:hover': { - bgcolor: theme => alpha(theme.palette.primary.main, 0.12), + {/* Search box */} + `1px solid ${theme.palette.grey[300]}`, + px: 1.5, + py: 0.5, + width: { xs: '100%', md: 'auto' }, + minWidth: { md: 220 }, + transition: 'all 0.2s ease-in-out', + '&:hover': { + borderColor: theme => theme.palette.primary.main, + boxShadow: '0 1px 3px rgba(0,0,0,0.05)' + }, + '&:focus-within': { + borderColor: theme => theme.palette.primary.main, + boxShadow: theme => `0 0 0 2px ${alpha(theme.palette.primary.main, 0.2)}` + } + }}> + + setSearchTerm(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter' && searchTerm.trim()) { + e.preventDefault(); + // Add current search term to active filters + addSearchTerm(searchTerm.trim()); + // Clear the input field + setSearchTerm(''); + // Keep focus on the input field for the next term + e.target.focus(); + } else if (e.key === 'Escape') { + // Let the global handler handle this completely + // The global handler will detect that search is focused + // and both blur and collapse the filter window + e.preventDefault(); + } + }} + fullWidth + size="small" + inputRef={searchInputRef} + sx={{ + fontSize: '0.875rem', + '& input': { + p: 0.5, + '&::placeholder': { + opacity: 0.7, + fontSize: '0.875rem' } - }} + } + }} + /> + {searchTerm && ( + setSearchTerm('')} + sx={{ p: 0.3, ml: 0.5 }} > - val > 0) || searchTerm) ? "primary" : (showFilters ? "primary" : "action")} - sx={{ mr: 0.8 }} - /> - - {(Object.values(filters).some(val => val > 0) || searchTerm) ? ( - - - {`${getSortedAndFilteredData(guestData).length}/${guestData.length}`} - - - ) : ( - - Filters - - )} - - - - {/* Remove the dropdown and show filter chips directly */} - {(Object.values(filters).some(val => val > 0) || activeSearchTerms.length > 0 || searchTerm) && ( - - {/* Active search term chips */} - {activeSearchTerms.map(term => ( - removeSearchTerm(term)} - color="default" - size="small" - deleteIcon={} - sx={{ height: 24 }} - /> - ))} - - {/* Current search term chip (shown only if not empty and not yet in activeSearchTerms) */} - {searchTerm && !activeSearchTerms.includes(searchTerm) && ( - setSearchTerm('')} - color="default" - size="small" - deleteIcon={} - sx={{ height: 24 }} - /> - )} - - {/* CPU filter chip */} - {filters.cpu > 0 && ( - clearFilter('cpu')} - color="primary" - size="small" - deleteIcon={} - sx={{ height: 24, '& .MuiChip-label': { fontWeight: 500 } }} - /> - )} - - {/* Memory filter chip */} - {filters.memory > 0 && ( - clearFilter('memory')} - color="primary" - size="small" - deleteIcon={} - sx={{ height: 24, '& .MuiChip-label': { fontWeight: 500 } }} - /> - )} - - {/* Disk filter chip */} - {filters.disk > 0 && ( - clearFilter('disk')} - color="primary" - size="small" - deleteIcon={} - sx={{ height: 24, '& .MuiChip-label': { fontWeight: 500 } }} - /> - )} - - {/* Download filter chip */} - {filters.download > 0 && ( - clearFilter('download')} - color="primary" - size="small" - deleteIcon={} - sx={{ height: 24, '& .MuiChip-label': { fontWeight: 500 } }} - /> - )} - - {/* Upload filter chip */} - {filters.upload > 0 && ( - clearFilter('upload')} - color="secondary" - size="small" - deleteIcon={} - sx={{ height: 24, '& .MuiChip-label': { fontWeight: 500 } }} - /> - )} - - {/* Reset all filters button */} - - + + )} + + - {/* Display controls */} - setShowStopped(e.target.checked)} - color="primary" - size="small" - /> - } - label={Show stopped} - sx={{ m: 0, '& .MuiFormControlLabel-label': { ml: 0.5 } }} - /> - - - - {isDebugMode && ( - - Running in debug mode with simulated data. The server connection is unavailable. - - )} - - - theme.palette.grey[50], - }, - '& tbody tr': { - transition: 'background-color 0.15s ease-in-out', - '&:hover': { - backgroundColor: theme => theme.palette.action.hover - } - }, - '& th': { - transition: 'background-color 0.2s ease' - } - }}> - - theme.palette.grey[100], - '& th': { - py: 1.75, - borderBottom: theme => `2px solid ${theme.palette.grey[300]}` - } - }}> - - {showFilters ? ( - + {/* Filter controls */} + + + setShowFilters(!showFilters)} + sx={{ display: 'flex', - alignItems: 'center', - bgcolor: 'background.paper', - border: theme => searchTerm - ? `1px solid ${theme.palette.primary.main}` - : `1px solid ${theme.palette.grey[300]}`, - borderRadius: 1, - pl: 1, - pr: 0.5, - py: 0.5, + alignItems: 'center', + bgcolor: theme => (Object.values(filters).some(val => val > 0) || searchTerm) + ? alpha(theme.palette.primary.main, 0.08) + : alpha(theme.palette.primary.main, 0.04), + borderRadius: 2, + py: 0.7, + px: 1.5, transition: 'all 0.2s ease-in-out', - }}> - - setSearchTerm(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter' && searchTerm.trim()) { - e.preventDefault(); - // Add current search term to active filters - addSearchTerm(searchTerm.trim()); - // Clear the input field - setSearchTerm(''); - // Keep focus on the input field for the next term - e.target.focus(); - } else if (e.key === 'Escape') { - // Simply call resetFilters instead of just clearing the search term - // Don't stop propagation - let the global handler also run - resetFilters(); - e.target.blur(); - } - }} - fullWidth + cursor: 'pointer', + border: theme => (Object.values(filters).some(val => val > 0) || searchTerm) && !showFilters + ? `1px solid ${alpha(theme.palette.primary.main, 0.3)}` + : `1px solid transparent`, + '&:hover': { + bgcolor: theme => alpha(theme.palette.primary.main, 0.12), + } + }} + > + val > 0) || searchTerm) ? "primary" : (showFilters ? "primary" : "action")} + sx={{ mr: 0.8 }} + /> + + {(Object.values(filters).some(val => val > 0) || searchTerm) ? ( + + + {`${getSortedAndFilteredData(guestData).length}/${guestData.length}`} + + + ) : ( + + Filters + + )} + + + + {/* Remove the dropdown and show filter chips directly */} + {(Object.values(filters).some(val => val > 0) || activeSearchTerms.length > 0 || searchTerm) && ( + + {/* Active search term chips */} + {activeSearchTerms.map(term => ( + removeSearchTerm(term)} + color="default" size="small" - autoFocus - inputRef={searchInputRef} - sx={{ - fontSize: '0.875rem', - '& input': { - p: 0, - '&::placeholder': { - opacity: 0.7, - fontSize: '0.875rem' - } - } - }} + deleteIcon={} + sx={{ height: 24 }} /> - {searchTerm && ( - setSearchTerm('')} - sx={{ p: 0.3, ml: 0.5 }} - > - - - )} - - ) : ( + ))} + + {/* Current search term chip (shown only if not empty and not yet in activeSearchTerms) */} + {searchTerm && !activeSearchTerms.includes(searchTerm) && ( + setSearchTerm('')} + color="default" + size="small" + deleteIcon={} + sx={{ height: 24 }} + /> + )} + + {/* CPU filter chip */} + {filters.cpu > 0 && ( + clearFilter('cpu')} + color="primary" + size="small" + deleteIcon={} + sx={{ height: 24, '& .MuiChip-label': { fontWeight: 500 } }} + /> + )} + + {/* Memory filter chip */} + {filters.memory > 0 && ( + clearFilter('memory')} + color="primary" + size="small" + deleteIcon={} + sx={{ height: 24, '& .MuiChip-label': { fontWeight: 500 } }} + /> + )} + + {/* Disk filter chip */} + {filters.disk > 0 && ( + clearFilter('disk')} + color="primary" + size="small" + deleteIcon={} + sx={{ height: 24, '& .MuiChip-label': { fontWeight: 500 } }} + /> + )} + + {/* Download filter chip */} + {filters.download > 0 && ( + clearFilter('download')} + color="primary" + size="small" + deleteIcon={} + sx={{ height: 24, '& .MuiChip-label': { fontWeight: 500 } }} + /> + )} + + {/* Upload filter chip */} + {filters.upload > 0 && ( + clearFilter('upload')} + color="secondary" + size="small" + deleteIcon={} + sx={{ height: 24, '& .MuiChip-label': { fontWeight: 500 } }} + /> + )} + + {/* Reset all filters button */} + + + )} + + + {/* Display controls */} + setShowStopped(e.target.checked)} + color="primary" + size="small" + /> + } + label={Show stopped} + sx={{ + m: 0, + '& .MuiFormControlLabel-label': { ml: 0.5 }, + bgcolor: theme => alpha(theme.palette.grey[100], 0.7), + borderRadius: 2, + px: 1, + py: 0.2, + border: '1px solid', + borderColor: 'grey.200', + }} + /> + + + + {isDebugMode && ( + + Running in debug mode with simulated data. The server connection is unavailable. + + )} + + +
alpha(theme.palette.grey[50], 0.5), + }, + '& tbody tr': { + transition: 'background-color 0.15s ease-in-out', + '&:hover': { + backgroundColor: theme => alpha(theme.palette.primary.light, 0.05) + } + }, + '& th': { + transition: 'background-color 0.2s ease' + } + }}> + + alpha(theme.palette.primary.light, 0.05), + '& th': { + py: showFilters ? 1 : 1.5, + pb: showFilters ? 1 : 1.5, + borderBottom: theme => `2px solid ${alpha(theme.palette.primary.main, 0.1)}`, + verticalAlign: 'middle', + minHeight: showFilters ? '48px' : '40px', + transition: 'padding 0.25s cubic-bezier(0.4, 0, 0.2, 1), min-height 0.25s cubic-bezier(0.4, 0, 0.2, 1)' + } + }}> + requestSort('name')} + sx={{ height: '24px' }} > - Guest Name + + + Guest Name + - )} - - - + + {/* Hidden slider container for Guest Name to match other columns structure */} + + + + {/* CPU Column */} + requestSort('cpu')} + sx={{ height: '24px' }} > - CPU - - - - - alpha(theme.palette.primary.main, 0.1), - boxShadow: 'none', - '& .MuiLinearProgress-bar': { - borderRadius: 4, - boxShadow: 'none', - transition: 'none' - } - }} - /> - updateFilter('cpu', newValue)} - aria-label="CPU filter" - size="small" - sx={{ - position: 'absolute', - top: -12, - width: '100%', - padding: '14px 0', - margin: 0, - '& .MuiSlider-rail': { opacity: 0 }, - '& .MuiSlider-track': { opacity: 0 }, - '& .MuiSlider-thumb': { - width: 16, - height: 16, - bgcolor: '#fff', - border: '2px solid currentColor', - boxShadow: '0 1px 3px 0 rgba(0,0,0,0.2)', - transition: 'width 0.2s ease, height 0.2s ease, box-shadow 0.2s ease', - '&:hover, &.Mui-focusVisible': { - boxShadow: '0 0 0 6px rgba(25, 118, 210, 0.16)', - borderWidth: '2px' - }, - '&:before': { - boxShadow: 'none' - }, - '&:after': { - width: 32, - height: 32 - } - } - }} - /> - - - - {formatPercentage(filters.cpu)} - - + + + CPU - - - - - + + + {/* Always rendered slider container, visibility controlled by CSS */} + + + alpha(theme.palette.primary.main, 0.1), + boxShadow: 'none', + '& .MuiLinearProgress-bar': { + borderRadius: 4, + boxShadow: 'none', + transition: 'none' + } + }} + /> + updateFilter('cpu', newValue)} + onChangeCommitted={handleSliderDragEnd} + onMouseDown={() => handleSliderDragStart('cpu')} + aria-label="CPU filter" + size="small" + sx={{ + position: 'absolute', + top: -5, + width: '100%', + padding: '4px 0', + margin: 0, + opacity: showFilters ? 1 : 0, + visibility: showFilters ? 'visible' : 'hidden', + transition: 'opacity 0.1s ease, visibility 0s', + zIndex: 1, + '& .MuiSlider-rail': { + opacity: 0.3, + background: 'linear-gradient(90deg, rgba(25,118,210,0.1) 0%, rgba(25,118,210,0.3) 100%)', + height: 4, + borderRadius: 2 + }, + '& .MuiSlider-track': { + opacity: 0.8, + background: 'linear-gradient(90deg, rgba(25,118,210,0.6) 0%, rgba(25,118,210,0.9) 100%)', + height: 4, + borderRadius: 2, + border: 'none' + }, + '& .MuiSlider-thumb': { + width: 16, + height: 16, + bgcolor: '#fff', + border: '2px solid #1976d2', + boxShadow: '0 2px 4px 0 rgba(0,0,0,0.2)', + transition: 'all 0.2s ease', + '&:hover, &.Mui-focusVisible': { + boxShadow: '0 0 0 8px rgba(25, 118, 210, 0.16)', + borderWidth: '2px', + width: 18, + height: 18 + }, + '&:before': { + boxShadow: 'none' + }, + '&:after': { + width: 36, + height: 36 + }, + '&.Mui-active': { + boxShadow: '0 0 0 12px rgba(25, 118, 210, 0.16)', + width: 20, + height: 20 + } + }, + '& .MuiSlider-valueLabel': { + backgroundColor: theme => theme.palette.primary.main, + padding: '4px 6px', + borderRadius: 4, + fontSize: '0.75rem', + fontWeight: 'bold', + display: activeSlider === 'cpu' ? 'block' : 'none', + boxShadow: '0 2px 6px rgba(0,0,0,0.2)', + '&:before': { + borderBottom: 'none', + borderRight: 'none' + } + } + }} + valueLabelDisplay="auto" + valueLabelFormat={value => `${formatPercentage(value)}`} + /> + + + + + {/* Memory Column */} + requestSort('memory')} + sx={{ height: '24px' }} > - Memory - - - - - alpha(theme.palette.primary.main, 0.1), - boxShadow: 'none', - '& .MuiLinearProgress-bar': { - borderRadius: 4, - boxShadow: 'none', - transition: 'none' - } - }} - /> - updateFilter('memory', newValue)} - aria-label="Memory filter" - size="small" - sx={{ - position: 'absolute', - top: -12, - width: '100%', - padding: '14px 0', - margin: 0, - '& .MuiSlider-rail': { opacity: 0 }, - '& .MuiSlider-track': { opacity: 0 }, - '& .MuiSlider-thumb': { - width: 16, - height: 16, - bgcolor: '#fff', - border: '2px solid currentColor', - boxShadow: '0 1px 3px 0 rgba(0,0,0,0.2)', - transition: 'width 0.2s ease, height 0.2s ease, box-shadow 0.2s ease', - '&:hover, &.Mui-focusVisible': { - boxShadow: '0 0 0 6px rgba(25, 118, 210, 0.16)', - borderWidth: '2px' - }, - '&:before': { - boxShadow: 'none' - }, - '&:after': { - width: 32, - height: 32 - } - } - }} - /> - - - - {formatPercentage(filters.memory)} - - + + + Memory - - - - - + + + {/* Always rendered slider container, visibility controlled by CSS */} + + + alpha(theme.palette.primary.main, 0.1), + boxShadow: 'none', + '& .MuiLinearProgress-bar': { + borderRadius: 4, + boxShadow: 'none', + transition: 'none' + } + }} + /> + updateFilter('memory', newValue)} + onChangeCommitted={handleSliderDragEnd} + onMouseDown={() => handleSliderDragStart('memory')} + aria-label="Memory filter" + size="small" + sx={{ + position: 'absolute', + top: -5, + width: '100%', + padding: '4px 0', + margin: 0, + opacity: showFilters ? 1 : 0, + visibility: showFilters ? 'visible' : 'hidden', + transition: 'opacity 0.1s ease, visibility 0s', + zIndex: 1, + '& .MuiSlider-rail': { + opacity: 0.3, + background: 'linear-gradient(90deg, rgba(25,118,210,0.1) 0%, rgba(25,118,210,0.3) 100%)', + height: 4, + borderRadius: 2 + }, + '& .MuiSlider-track': { + opacity: 0.8, + background: 'linear-gradient(90deg, rgba(25,118,210,0.6) 0%, rgba(25,118,210,0.9) 100%)', + height: 4, + borderRadius: 2, + border: 'none' + }, + '& .MuiSlider-thumb': { + width: 16, + height: 16, + bgcolor: '#fff', + border: '2px solid #1976d2', + boxShadow: '0 2px 4px 0 rgba(0,0,0,0.2)', + transition: 'all 0.2s ease', + '&:hover, &.Mui-focusVisible': { + boxShadow: '0 0 0 8px rgba(25, 118, 210, 0.16)', + borderWidth: '2px', + width: 18, + height: 18 + }, + '&:before': { + boxShadow: 'none' + }, + '&:after': { + width: 36, + height: 36 + }, + '&.Mui-active': { + boxShadow: '0 0 0 12px rgba(25, 118, 210, 0.16)', + width: 20, + height: 20 + } + }, + '& .MuiSlider-valueLabel': { + backgroundColor: theme => theme.palette.primary.main, + padding: '4px 6px', + borderRadius: 4, + fontSize: '0.75rem', + fontWeight: 'bold', + display: activeSlider === 'memory' ? 'block' : 'none', + boxShadow: '0 2px 6px rgba(0,0,0,0.2)', + '&:before': { + borderBottom: 'none', + borderRight: 'none' + } + } + }} + valueLabelDisplay="auto" + valueLabelFormat={value => `${formatPercentage(value)}`} + /> + + + + + {/* Disk Column */} + requestSort('disk')} + sx={{ height: '24px' }} > - Disk - - - - - alpha(theme.palette.primary.main, 0.1), - boxShadow: 'none', - '& .MuiLinearProgress-bar': { - borderRadius: 4, - boxShadow: 'none', - transition: 'none' - } - }} - /> - updateFilter('disk', newValue)} - aria-label="Disk filter" - size="small" - sx={{ - position: 'absolute', - top: -12, - width: '100%', - padding: '14px 0', - margin: 0, - '& .MuiSlider-rail': { opacity: 0 }, - '& .MuiSlider-track': { opacity: 0 }, - '& .MuiSlider-thumb': { - width: 16, - height: 16, - bgcolor: '#fff', - border: '2px solid currentColor', - boxShadow: '0 1px 3px 0 rgba(0,0,0,0.2)', - transition: 'width 0.2s ease, height 0.2s ease, box-shadow 0.2s ease', - '&:hover, &.Mui-focusVisible': { - boxShadow: '0 0 0 6px rgba(25, 118, 210, 0.16)', - borderWidth: '2px' - }, - '&:before': { - boxShadow: 'none' - }, - '&:after': { - width: 32, - height: 32 - } - } - }} - /> - - - - {formatPercentage(filters.disk)} - - + + + Disk - - - - - + + + {/* Always rendered slider container, visibility controlled by CSS */} + + + alpha(theme.palette.primary.main, 0.1), + boxShadow: 'none', + '& .MuiLinearProgress-bar': { + borderRadius: 4, + boxShadow: 'none', + transition: 'none' + } + }} + /> + updateFilter('disk', newValue)} + onChangeCommitted={handleSliderDragEnd} + onMouseDown={() => handleSliderDragStart('disk')} + aria-label="Disk filter" + size="small" + sx={{ + position: 'absolute', + top: -5, + width: '100%', + padding: '4px 0', + margin: 0, + opacity: showFilters ? 1 : 0, + visibility: showFilters ? 'visible' : 'hidden', + transition: 'opacity 0.1s ease, visibility 0s', + zIndex: 1, + '& .MuiSlider-rail': { + opacity: 0.3, + background: 'linear-gradient(90deg, rgba(25,118,210,0.1) 0%, rgba(25,118,210,0.3) 100%)', + height: 4, + borderRadius: 2 + }, + '& .MuiSlider-track': { + opacity: 0.8, + background: 'linear-gradient(90deg, rgba(25,118,210,0.6) 0%, rgba(25,118,210,0.9) 100%)', + height: 4, + borderRadius: 2, + border: 'none' + }, + '& .MuiSlider-thumb': { + width: 16, + height: 16, + bgcolor: '#fff', + border: '2px solid #1976d2', + boxShadow: '0 2px 4px 0 rgba(0,0,0,0.2)', + transition: 'all 0.2s ease', + '&:hover, &.Mui-focusVisible': { + boxShadow: '0 0 0 8px rgba(25, 118, 210, 0.16)', + borderWidth: '2px', + width: 18, + height: 18 + }, + '&:before': { + boxShadow: 'none' + }, + '&:after': { + width: 36, + height: 36 + }, + '&.Mui-active': { + boxShadow: '0 0 0 12px rgba(25, 118, 210, 0.16)', + width: 20, + height: 20 + } + }, + '& .MuiSlider-valueLabel': { + backgroundColor: theme => theme.palette.primary.main, + padding: '4px 6px', + borderRadius: 4, + fontSize: '0.75rem', + fontWeight: 'bold', + display: activeSlider === 'disk' ? 'block' : 'none', + boxShadow: '0 2px 6px rgba(0,0,0,0.2)', + '&:before': { + borderBottom: 'none', + borderRight: 'none' + } + } + }} + valueLabelDisplay="auto" + valueLabelFormat={value => `${formatPercentage(value)}`} + /> + + + + + {/* Download Column */} + requestSort('download')} + sx={{ height: '24px' }} > - Download + + + Download + - - + + {/* Always rendered slider container, visibility controlled by CSS */} + + alpha(theme.palette.primary.main, 0.1), boxShadow: 'none', @@ -1138,61 +1445,121 @@ const NetworkDisplay = () => { updateFilter('download', newValue)} + onChangeCommitted={handleSliderDragEnd} + onMouseDown={() => handleSliderDragStart('download')} aria-label="Download filter" size="small" sx={{ position: 'absolute', - top: -12, + top: -5, width: '100%', - padding: '14px 0', + padding: '4px 0', margin: 0, - '& .MuiSlider-rail': { opacity: 0 }, - '& .MuiSlider-track': { opacity: 0 }, + opacity: showFilters ? 1 : 0, + visibility: showFilters ? 'visible' : 'hidden', + transition: 'opacity 0.1s ease, visibility 0s', + zIndex: 1, + '& .MuiSlider-rail': { + opacity: 0.3, + background: 'linear-gradient(90deg, rgba(25,118,210,0.1) 0%, rgba(25,118,210,0.3) 100%)', + height: 4, + borderRadius: 2 + }, + '& .MuiSlider-track': { + opacity: 0.8, + background: 'linear-gradient(90deg, rgba(25,118,210,0.6) 0%, rgba(25,118,210,0.9) 100%)', + height: 4, + borderRadius: 2, + border: 'none' + }, '& .MuiSlider-thumb': { - width: 16, - height: 16, + width: 16, + height: 16, bgcolor: '#fff', - border: '2px solid currentColor', - boxShadow: '0 1px 3px 0 rgba(0,0,0,0.2)', - transition: 'width 0.2s ease, height 0.2s ease, box-shadow 0.2s ease', + border: '2px solid #1976d2', + boxShadow: '0 2px 4px 0 rgba(0,0,0,0.2)', + transition: 'all 0.2s ease', '&:hover, &.Mui-focusVisible': { - boxShadow: '0 0 0 6px rgba(25, 118, 210, 0.16)', - borderWidth: '2px' + boxShadow: '0 0 0 8px rgba(25, 118, 210, 0.16)', + borderWidth: '2px', + width: 18, + height: 18 }, '&:before': { boxShadow: 'none' }, '&:after': { - width: 32, - height: 32 + width: 36, + height: 36 + }, + '&.Mui-active': { + boxShadow: '0 0 0 12px rgba(25, 118, 210, 0.16)', + width: 20, + height: 20 + } + }, + '& .MuiSlider-valueLabel': { + backgroundColor: theme => theme.palette.primary.main, + padding: '4px 6px', + borderRadius: 4, + fontSize: '0.75rem', + fontWeight: 'bold', + display: activeSlider === 'download' ? 'block' : 'none', + boxShadow: '0 2px 6px rgba(0,0,0,0.2)', + '&:before': { + borderBottom: 'none', + borderRight: 'none' } } }} + valueLabelDisplay="auto" + valueLabelFormat={value => `${formatNetworkRateForFilter(sliderValueToNetworkRate(value))}`} /> - - ↓ {filters.download > 0 ? formatNetworkRateForFilter(sliderValueToNetworkRate(filters.download)) : 'Any'} - - - - - - + + + + {/* Upload Column */} + requestSort('upload')} + sx={{ height: '24px' }} > - Upload + + + Upload + - - + + {/* Always rendered slider container, visibility controlled by CSS */} + + alpha(theme.palette.secondary.main, 0.1), boxShadow: 'none', @@ -1206,189 +1573,225 @@ const NetworkDisplay = () => { updateFilter('upload', newValue)} + onChangeCommitted={handleSliderDragEnd} + onMouseDown={() => handleSliderDragStart('upload')} aria-label="Upload filter" size="small" sx={{ position: 'absolute', - top: -12, + top: -5, width: '100%', - padding: '14px 0', + padding: '4px 0', margin: 0, - '& .MuiSlider-rail': { opacity: 0 }, - '& .MuiSlider-track': { opacity: 0 }, + opacity: showFilters ? 1 : 0, + visibility: showFilters ? 'visible' : 'hidden', + transition: 'opacity 0.1s ease, visibility 0s', + zIndex: 1, + '& .MuiSlider-rail': { + opacity: 0.3, + background: 'linear-gradient(90deg, rgba(156,39,176,0.1) 0%, rgba(156,39,176,0.3) 100%)', + height: 4, + borderRadius: 2 + }, + '& .MuiSlider-track': { + opacity: 0.8, + background: 'linear-gradient(90deg, rgba(156,39,176,0.6) 0%, rgba(156,39,176,0.9) 100%)', + height: 4, + borderRadius: 2, + border: 'none' + }, '& .MuiSlider-thumb': { - width: 16, - height: 16, + width: 16, + height: 16, bgcolor: '#fff', - border: '2px solid currentColor', - boxShadow: '0 1px 3px 0 rgba(0,0,0,0.2)', - transition: 'width 0.2s ease, height 0.2s ease, box-shadow 0.2s ease', + border: '2px solid #9c27b0', + boxShadow: '0 2px 4px 0 rgba(0,0,0,0.2)', + transition: 'all 0.2s ease', '&:hover, &.Mui-focusVisible': { - boxShadow: '0 0 0 6px rgba(156, 39, 176, 0.16)', - borderWidth: '2px' + boxShadow: '0 0 0 8px rgba(156, 39, 176, 0.16)', + borderWidth: '2px', + width: 18, + height: 18 }, '&:before': { boxShadow: 'none' }, '&:after': { - width: 32, - height: 32 + width: 36, + height: 36 + }, + '&.Mui-active': { + boxShadow: '0 0 0 12px rgba(156, 39, 176, 0.16)', + width: 20, + height: 20 + } + }, + '& .MuiSlider-valueLabel': { + backgroundColor: theme => theme.palette.secondary.main, + padding: '4px 6px', + borderRadius: 4, + fontSize: '0.75rem', + fontWeight: 'bold', + display: activeSlider === 'upload' ? 'block' : 'none', + boxShadow: '0 2px 6px rgba(0,0,0,0.2)', + '&:before': { + borderBottom: 'none', + borderRight: 'none' } } }} + valueLabelDisplay="auto" + valueLabelFormat={value => `${formatNetworkRateForFilter(sliderValueToNetworkRate(value))}`} /> - - ↑ {filters.upload > 0 ? formatNetworkRateForFilter(sliderValueToNetworkRate(filters.upload)) : 'Any'} + + + + + + + {guestData.length > 0 ? ( + getSortedAndFilteredData(guestData) + .map((guest) => { + const metrics = getMetricsForGuest(guest.id); + const networkMetrics = metrics?.metrics?.network; + + // Get resource metrics (CPU, memory, disk) with fallbacks + const cpuUsage = metrics?.metrics?.cpu || 0; + + // Handle either direct percentage or calculated from used/total + const memoryData = metrics?.metrics?.memory || {}; + const memoryUsage = memoryData.percentUsed || + (memoryData.total && memoryData.used ? + (memoryData.used / memoryData.total) * 100 : 0); + + // Same for disk + const diskData = metrics?.metrics?.disk || {}; + const diskUsage = diskData.percentUsed || + (diskData.total && diskData.used ? + (diskData.used / diskData.total) * 100 : 0); + + // Check if system is running + const isRunning = guest.status === 'running'; + + return ( + td': { py: 1.5 }, + transition: 'all 0.2s ease-in-out', + }}> + + + + + {guest.name} + + + + + 80 ? "error" : cpuUsage > 60 ? "warning" : "primary"} + disabled={!isRunning} + tooltipText={isRunning ? `CPU: ${formatPercentage(cpuUsage)} utilized` : "System is not running"} + /> + + + 80 ? "error" : memoryUsage > 60 ? "warning" : "primary"} + disabled={!isRunning} + tooltipText={isRunning ? + memoryData.total ? + `Memory: ${formatBytes(memoryData.used || 0)} / ${formatBytes(memoryData.total)} (${formatPercentage(memoryUsage)})` : + `Memory: ${formatPercentage(memoryUsage)} utilized` : + "System is not running"} + /> + + + 80 ? "error" : diskUsage > 60 ? "warning" : "primary"} + disabled={!isRunning} + tooltipText={isRunning ? + diskData.total ? + `Disk: ${formatBytes(diskData.used || 0)} / ${formatBytes(diskData.total)} (${formatPercentage(diskUsage)})` : + `Disk: ${formatPercentage(diskUsage)} utilized` : + "System is not running"} + /> + + + {isRunning && networkMetrics ? ( + + ↓ {formatNetworkRate(networkMetrics.inRate || 0)} + + ) : ( + + ↓ - + + )} + + + {isRunning && networkMetrics ? ( + + ↑ {formatNetworkRate(networkMetrics.outRate || 0)} + + ) : ( + + ↑ - + + )} + + + ); + }) + ) : ( + + + + + + No Systems Available + + + No guest data has been received from the server - - - - - - - {guestData.length > 0 ? ( - getSortedAndFilteredData(guestData) - .map((guest) => { - const metrics = getMetricsForGuest(guest.id); - const networkMetrics = metrics?.metrics?.network; - - // Get resource metrics (CPU, memory, disk) with fallbacks - const cpuUsage = metrics?.metrics?.cpu || 0; - - // Handle either direct percentage or calculated from used/total - const memoryData = metrics?.metrics?.memory || {}; - const memoryUsage = memoryData.percentUsed || - (memoryData.total && memoryData.used ? - (memoryData.used / memoryData.total) * 100 : 0); - - // Same for disk - const diskData = metrics?.metrics?.disk || {}; - const diskUsage = diskData.percentUsed || - (diskData.total && diskData.used ? - (diskData.used / diskData.total) * 100 : 0); - - // Check if system is running - const isRunning = guest.status === 'running'; - - return ( - td': { py: 1.5 }, - transition: 'all 0.2s ease-in-out', - }}> - - - - - {guest.name} - - - - - 80 ? "error" : cpuUsage > 60 ? "warning" : "primary"} - disabled={!isRunning} - tooltipText={isRunning ? `CPU: ${formatPercentage(cpuUsage)} utilized` : "System is not running"} + + + )} + {guestData.length > 0 && getSortedAndFilteredData(guestData).length === 0 && ( + + + + + + No Matching Systems + + + No systems match the current filters + + - - - 80 ? "error" : memoryUsage > 60 ? "warning" : "primary"} - disabled={!isRunning} - tooltipText={isRunning ? - memoryData.total ? - `Memory: ${formatBytes(memoryData.used || 0)} / ${formatBytes(memoryData.total)} (${formatPercentage(memoryUsage)})` : - `Memory: ${formatPercentage(memoryUsage)} utilized` : - "System is not running"} - /> - - - 80 ? "error" : diskUsage > 60 ? "warning" : "primary"} - disabled={!isRunning} - tooltipText={isRunning ? - diskData.total ? - `Disk: ${formatBytes(diskData.used || 0)} / ${formatBytes(diskData.total)} (${formatPercentage(diskUsage)})` : - `Disk: ${formatPercentage(diskUsage)} utilized` : - "System is not running"} - /> - - - {isRunning && networkMetrics ? ( - - ↓ {formatNetworkRate(networkMetrics.inRate || 0)} - - ) : ( - - ↓ - - - )} - - - {isRunning && networkMetrics ? ( - - ↑ {formatNetworkRate(networkMetrics.outRate || 0)} - - ) : ( - - ↑ - - - )} - - - ); - }) - ) : ( - - - - - - No Systems Available - - - No guest data has been received from the server - - - - - )} - {guestData.length > 0 && getSortedAndFilteredData(guestData).length === 0 && ( - - - - - - No Matching Systems - - - No systems match the current filters - - - - - - )} - -
-
-
-
+
+ + + )} + + + +
+
+
); }; diff --git a/frontend/src/utils/testConnection.js b/frontend/src/utils/testConnection.js deleted file mode 100644 index cb5a718e4..000000000 --- a/frontend/src/utils/testConnection.js +++ /dev/null @@ -1,37 +0,0 @@ -import { io } from 'socket.io-client'; - -// This function can be called from the browser console to help debug WebSocket connections -const testWebSocketConnection = (url = 'http://localhost:3000') => { - console.log(`Testing connection to: ${url}`); - - const socket = io(url, { - transports: ['websocket', 'polling'], - reconnectionAttempts: 5, - reconnectionDelay: 500, - timeout: 3000 - }); - - socket.on('connect', () => { - console.log('✅ CONNECTED SUCCESSFULLY!'); - console.log('Socket ID:', socket.id); - }); - - socket.on('connect_error', (err) => { - console.error('❌ CONNECTION ERROR:', err); - console.error('Message:', err.message); - }); - - socket.on('message', (msg) => { - console.log('📩 RECEIVED MESSAGE:', msg); - }); - - socket.on('disconnect', () => { - console.log('❌ DISCONNECTED'); - }); - - return socket; -}; - -window.testConnection = testWebSocketConnection; - -export default testWebSocketConnection; \ No newline at end of file diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 232e660bd..38628eb69 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -5,19 +5,20 @@ import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [react()], server: { + host: '192.168.0.130', + port: 5173, proxy: { - // Proxy WebSocket connections to the backend + // Proxy WebSocket connections to the real backend '/socket.io': { target: 'http://localhost:3000', ws: true, }, - // Proxy API requests to the backend + // Proxy API requests to the real backend '/api': { target: 'http://localhost:3000', changeOrigin: true, } }, - host: true }, build: { outDir: 'dist', diff --git a/package-lock.json b/package-lock.json index 56ccac37c..61f962f9e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ }, "devDependencies": { "@types/cors": "^2.8.17", + "concurrently": "^9.1.2", "nodemon": "^3.1.9", "ts-node-dev": "^2.0.0" } @@ -287,6 +288,52 @@ "node": ">=0.4.0" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansi-styles/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ansi-styles/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -458,6 +505,46 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -483,6 +570,21 @@ "fsevents": "~2.3.2" } }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/color": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", @@ -547,6 +649,58 @@ "dev": true, "license": "MIT" }, + "node_modules/concurrently": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.1.2.tgz", + "integrity": "sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/concurrently/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -690,6 +844,13 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, "node_modules/enabled": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", @@ -811,6 +972,16 @@ "node": ">= 0.4" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -999,6 +1170,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -1239,6 +1420,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -1280,6 +1471,13 @@ "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", "license": "MIT" }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, "node_modules/logform": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", @@ -1702,6 +1900,16 @@ "node": ">=8.10.0" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -1737,6 +1945,16 @@ "rimraf": "bin.js" } }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1845,6 +2063,19 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/shell-quote": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz", + "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -2097,6 +2328,34 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -2297,6 +2556,13 @@ "strip-json-comments": "^2.0.0" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -2427,6 +2693,24 @@ "node": ">= 12.0.0" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -2465,6 +2749,45 @@ "node": ">=0.4" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/package.json b/package.json index 290e38fb0..2a58fe9a2 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,12 @@ "lint": "eslint . --ext .ts", "test": "echo \"Error: no test specified\" && exit 1", "test:startup": "ts-node src/scripts/startup-check.ts", - "test:api": "ts-node src/scripts/proxmox-api-test.ts" + "test:api": "ts-node src/scripts/proxmox-api-test.ts", + "kill:backend": "npx kill-port 3000", + "kill:frontend": "npx kill-port 5173 5174 5175 5176 5177 5178 5179 5180 5181 5182 5183 5184 5185 5186 5187 5188 5189 5190 5191 5192 5193 5194 5195 5196 5197 5198 5199 5200", + "kill:all": "npm run kill:backend && npm run kill:frontend", + "dev:frontend": "cd frontend && npm run dev", + "pulse": "./start-pulse.sh" }, "keywords": [], "author": "", @@ -31,6 +36,7 @@ }, "devDependencies": { "@types/cors": "^2.8.17", + "concurrently": "^9.1.2", "nodemon": "^3.1.9", "ts-node-dev": "^2.0.0" } diff --git a/plan.md b/plan.md deleted file mode 100644 index c792b7a3c..000000000 --- a/plan.md +++ /dev/null @@ -1,285 +0,0 @@ -# ProxMox Monitoring App Implementation Plan - -## Project Overview - -A lightweight, responsive ProxMox monitoring application that displays real-time metrics for CPU, memory, network, and disk usage across multiple nodes. The application will use the ProxMox API and event subscription for real-time updates. - -## Environment Configuration - -The application will use environment variables for configuration with the following structure: - -``` -# Node 1 -PROXMOX_NODE_1_NAME=Proxmox Node 1 -PROXMOX_NODE_1_HOST=https://192.168.0.132:8006 -PROXMOX_NODE_1_TOKEN_ID=root@pam!pulse -PROXMOX_NODE_1_TOKEN_SECRET=e1850350-6afc-4b8e-ae28-472152af84f9 - -# Node 2 -PROXMOX_NODE_2_NAME=Proxmox Node 2 -PROXMOX_NODE_2_HOST=https://192.168.0.141:8006 -PROXMOX_NODE_2_TOKEN_ID=root@pam!pulse -PROXMOX_NODE_2_TOKEN_SECRET=63e21f63-4cfd-4ba0-9b14-fc681c59d932 - -# App Configuration -PORT=3000 -NODE_ENV=development -LOG_LEVEL=debug -ENABLE_DEV_TOOLS=true -METRICS_HISTORY_MINUTES=60 -``` - -## Technology Stack - -- **Backend**: Node.js with Express, TypeScript, Socket.io for real-time updates -- **Frontend**: Simple HTML/CSS/TypeScript with grid layout -- **Deployment**: Docker container - -## Project Structure - -``` -proxmox-monitor/ -├── src/ -│ ├── config/ -│ ├── types/ -│ ├── api/ -│ ├── services/ -│ ├── utils/ -│ ├── routes/ -│ ├── websocket/ -│ ├── public/ -│ └── server.ts -├── .env -├── .env.example -├── .gitignore -├── tsconfig.json -├── package.json -├── Dockerfile -└── README.md -``` - -## Core Components - -### 1. Configuration Module - -- **Purpose**: Load and validate environment variables -- **Features**: - - Parse environment variables for multiple ProxMox nodes - - Support dynamic number of nodes (PROXMOX_NODE_1, PROXMOX_NODE_2, etc.) - - Validate configuration during startup - - Keep credentials secure - -### 2. ProxMox API Client - -- **Purpose**: Interface with ProxMox API -- **Features**: - - Authentication with API tokens - - Fetch node status information - - Get virtual machines and containers - - Get detailed metrics - - Handle API errors with proper logging - - Support HTTPS with option to ignore certificate validation for lab environments - -### 3. Event Subscription Service - -- **Purpose**: Receive real-time updates from ProxMox -- **Features**: - - Connect to ProxMox event stream - - Process event data - - Emit events to internal event system - - Handle connection errors and auto-reconnect - - Correlate events with specific nodes - -### 4. Node Management Service - -- **Purpose**: Manage multiple ProxMox nodes -- **Features**: - - Create and maintain API clients for each node - - Manage event subscriptions - - Aggregate data from multiple nodes - - Track node connectivity status - - Support dynamic node addition/removal - -### 5. Metrics Collection Service - -- **Purpose**: Collect and process metrics -- **Features**: - - Combine event-based and polling data - - Normalize metrics across different sources - - Calculate derived metrics (percentages, rates) - - Store short-term history (configurable, default 60 minutes) - - Support for different metric types (CPU, memory, network, disk) - -### 6. WebSocket Server - -- **Purpose**: Real-time communication with frontend -- **Features**: - - Push metrics updates to clients - - Handle client connections/disconnections - - Support filtering data sent to clients - - Optimize data transfer (send only changed data) - -### 7. REST API - -- **Purpose**: Provide data to frontend -- **Endpoints**: - - `/api/nodes` - List all nodes - - `/api/nodes/:nodeId` - Get node details - - `/api/nodes/:nodeId/vms` - Get all VMs for a node - - `/api/nodes/:nodeId/containers` - Get all containers for a node - - `/api/status` - Get system status - - `/api/metrics` - Get current metrics - - `/api/metrics/history` - Get historical metrics - -### 8. Development Tools - -- **Purpose**: Assist in development with AI -- **Features**: - - Enhanced logging with context - - API call visualization - - State inspection - - Error handling with suggestions - - Development dashboard - - API response recording/playback - -### 9. Frontend Components - -- **Purpose**: Display monitoring data -- **Features**: - - Grid layout for VMs/containers - - Real-time metric updates - - Node selector - - Responsive design - - Minimal but functional UI - -## Implementation Phases - -### Phase 1: Project Setup - -- Initialize TypeScript project -- Configure environment -- Set up basic project structure -- Install core dependencies - -### Phase 2: Core API Implementation - -- Implement ProxMox API client -- Create type definitions for API responses -- Build node configuration loader -- Implement enhanced logging system - -### Phase 3: Event System - -- Create event subscription service -- Implement event processing -- Set up WebSocket server -- Build real-time data pipeline - -### Phase 4: Metrics Collection - -- Implement metrics normalization -- Create polling fallback for missing metrics -- Build short-term history storage -- Implement derived metrics calculation - -### Phase 5: Node Management - -- Create multi-node support -- Implement node discovery (if in cluster) -- Build node status tracking -- Create aggregated metrics view - -### Phase 6: Frontend Development - -- Create simple grid layout -- Implement WebSocket client -- Build real-time updates -- Create node selector - -### Phase 7: Development Tools - -- Implement enhanced logging -- Create development dashboard -- Add API explorer -- Build state visualization - -### Phase 8: Containerization - -- Create Docker configuration -- Set up environment variable passing -- Test container deployment - -## API Type Definitions - -The application should define TypeScript interfaces for: - -1. **ProxmoxNodeStatus** - Node status information -2. **ProxmoxVM** - Virtual machine information -3. **ProxmoxContainer** - Container information -4. **ProxmoxGuest** - Union type of VM and Container -5. **ProxmoxEvent** - Event data structure -6. **MetricsData** - Normalized metrics format -7. **NodeConfig** - Node configuration structure - -## Security Considerations - -1. Keep API tokens in environment variables -2. Don't commit .env files with credentials -3. Use HTTPS for API communication -4. Consider read-only API tokens with minimal permissions -5. Validate all user inputs - -## Development Context Helpers - -1. **Configuration Validator and API Reference**: - - **Purpose**: Provide a reliable reference for ProxMox API interactions - - **Features**: - - Validate environment configurations during startup - - Test connectivity to all configured nodes - - Sample and store actual API response structures for reference - - Document endpoint patterns with examples - - Create schema validators for request/response bodies - - Generate TypeScript interfaces from actual API responses - - Expose an endpoint that shows API structure for reference - - Store successful API call patterns for the AI to reference - - Cache sample responses for offline development - - Provide helper functions to validate API calls before sending - - Create a "cheat sheet" of common API patterns - - **Implementation**: - - Create a startup validation process that tests all endpoints - - Build an in-memory reference database of API interactions - - Expose `/dev/api-reference` endpoint with interactive documentation - - Log all successful API interactions for pattern recognition - - Create a validation library the AI can use to check its work - -2. **Enhanced Logging**: - - Include node ID, component, operation in logs - - Different log levels (debug, info, warn, error) - - Contextual error information - - Log reference to successful patterns when errors occur - -3. **State Inspection**: - - Endpoint to view current system state - - Connection status visualization - - Event subscription status - - Configuration status and validation results - -4. **API Explorer**: - - Test API calls directly - - View recent API calls and responses - - Response visualization - - Template library of working API calls - - Side-by-side comparison of expected vs. actual responses - -5. **Performance Monitoring**: - - Track memory and CPU usage - - Monitor event processing time - - Connection statistics - -## Docker Configuration - -- Use Node.js Alpine image as base -- Multi-stage build for smaller image -- Expose configuration via environment variables -- Volume mounting for persistent data (if needed) -- Health check endpoint diff --git a/start-dev.sh b/start-dev.sh deleted file mode 100755 index 19ed995b4..000000000 --- a/start-dev.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash - -# Kill any existing test servers -pkill -f "node test-server.js" || true - -# Start the test server -echo "Starting test server..." -node test-server.js & -TEST_SERVER_PID=$! - -# Wait a moment for the server to start -sleep 2 - -# Start the frontend -echo "Starting frontend..." -cd frontend && npm run dev - -# When the frontend exits, also kill the test server -kill $TEST_SERVER_PID \ No newline at end of file diff --git a/start-pulse.sh b/start-pulse.sh new file mode 100755 index 000000000..7e22c8472 --- /dev/null +++ b/start-pulse.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +# Make script executable if it isn't already +chmod +x "$0" + +# Kill any existing servers +echo "Killing any existing servers..." +pkill -f "node dist/server.js" || true +npx kill-port 5173 3000 + +# Start the real backend server +echo "Starting real backend server..." +NODE_ENV=development LOG_LEVEL=info ENABLE_DEV_TOOLS=true npm run dev & +BACKEND_PID=$! + +# Wait a moment for the server to start +sleep 3 + +# Get the host IP (use 0.0.0.0 in Docker, otherwise use 192.168.0.130 or the local IP) +HOST_IP="0.0.0.0" +if [[ -z "${DOCKER_CONTAINER}" ]]; then + # Not in Docker, use the specific IP + HOST_IP="192.168.0.130" +fi + +# Start the frontend Vite dev server +echo "Starting Pulse interface..." +cd frontend && npm run dev -- --host "${HOST_IP}" + +# When the frontend exits, also kill the backend server +kill $BACKEND_PID \ No newline at end of file diff --git a/startup_log.txt b/startup_log.txt deleted file mode 100644 index 5f1fd43e2..000000000 --- a/startup_log.txt +++ /dev/null @@ -1,110 +0,0 @@ - -> pulse_basic@1.0.0 test:startup -> ts-node src/scripts/startup-check.ts - -2025-02-27 14:51:58 info [StartupCheck]: Running startup checks... -{ - "service": "proxmox-monitor" -} -2025-02-27 14:51:58 info [StartupCheck]: Validating configuration... -{ - "service": "proxmox-monitor" -} -2025-02-27 14:51:58 info [ConfigValidator]: Validating application configuration... -{ - "service": "proxmox-monitor" -} -2025-02-27 14:51:58 info [ConfigValidator]: Validating node configuration: Proxmox Node 1 (node-1) -{ - "service": "proxmox-monitor" -} -2025-02-27 14:51:58 warn [ConfigValidator]: Node node-1: Token ID contains special characters that may cause issues with shell commands: ! -{ - "service": "proxmox-monitor" -} -2025-02-27 14:51:58 warn [ConfigValidator]: When using in curl or similar commands, make sure to properly escape or quote the token ID. -{ - "service": "proxmox-monitor" -} -2025-02-27 14:51:58 info [ConfigValidator]: Node node-1: Successfully connected to ProxMox API version 8.3.4 -{ - "service": "proxmox-monitor" -} -2025-02-27 14:51:58 info [ConfigValidator]: Validating node configuration: Proxmox Node 2 (node-2) -{ - "service": "proxmox-monitor" -} -2025-02-27 14:51:58 warn [ConfigValidator]: Node node-2: Token ID contains special characters that may cause issues with shell commands: ! -{ - "service": "proxmox-monitor" -} -2025-02-27 14:51:58 warn [ConfigValidator]: When using in curl or similar commands, make sure to properly escape or quote the token ID. -{ - "service": "proxmox-monitor" -} -2025-02-27 14:51:58 info [ConfigValidator]: Node node-2: Successfully connected to ProxMox API version 8.3.4 -{ - "service": "proxmox-monitor" -} -2025-02-27 14:51:58 info [StartupCheck]: ✅ Configuration validation passed. -{ - "service": "proxmox-monitor" -} -2025-02-27 14:51:58 info [StartupCheck]: Testing connections to all ProxMox nodes... -{ - "service": "proxmox-monitor" -} -2025-02-27 14:51:58 warn [ProxmoxClient][node-1]: SSL certificate validation is disabled. This is not recommended for production. -{ - "service": "proxmox-monitor" -} -2025-02-27 14:51:58 warn [ProxmoxClient][node-2]: SSL certificate validation is disabled. This is not recommended for production. -{ - "service": "proxmox-monitor" -} -2025-02-27 14:51:58 debug [ProxmoxClient][node-1]: API Request: GET /version -{ - "service": "proxmox-monitor" -} -2025-02-27 14:51:58 debug [ProxmoxClient][node-2]: API Request: GET /version -{ - "service": "proxmox-monitor" -} -2025-02-27 14:51:58 debug [ProxmoxClient][node-2]: API Response: 200 GET /version -{ - "service": "proxmox-monitor", - "data": { - "data": { - "version": "8.3.4", - "release": "8.3", - "repoid": "65224a0f9cd294a3" - } - } -} -2025-02-27 14:51:58 info [ProxmoxClient][node-2]: Connected to ProxMox API version 8.3.4 -{ - "service": "proxmox-monitor" -} -2025-02-27 14:51:58 debug [ProxmoxClient][node-1]: API Response: 200 GET /version -{ - "service": "proxmox-monitor", - "data": { - "data": { - "repoid": "65224a0f9cd294a3", - "release": "8.3", - "version": "8.3.4" - } - } -} -2025-02-27 14:51:58 info [ProxmoxClient][node-1]: Connected to ProxMox API version 8.3.4 -{ - "service": "proxmox-monitor" -} -2025-02-27 14:51:58 info [StartupCheck]: ✅ Successfully connected to all 2 ProxMox nodes. -{ - "service": "proxmox-monitor" -} -2025-02-27 14:51:58 info [StartupCheck]: ✅ All startup checks passed. -{ - "service": "proxmox-monitor" -} diff --git a/test-server.js b/test-server.js deleted file mode 100644 index 113659720..000000000 --- a/test-server.js +++ /dev/null @@ -1,186 +0,0 @@ -const express = require('express'); -const http = require('http'); -const { Server } = require('socket.io'); - -const app = express(); -const server = http.createServer(app); -const io = new Server(server, { - cors: { - origin: '*', - methods: ['GET', 'POST'] - } -}); - -const PORT = 3001; - -// Create some sample nodes data -const nodes = [ - { - id: 'node-1', - name: 'Primary Server', - status: 'online', - ip: '192.168.1.100', - uptime: 86400 * 5 // 5 days in seconds - }, - { - id: 'node-2', - name: 'Secondary Server', - status: 'online', - ip: '192.168.1.101', - uptime: 86400 * 3 // 3 days in seconds - } -]; - -// Create some sample guests data -const guests = [ - { - id: 'guest-101', - name: 'Web Server', - status: 'running', - type: 'lxc', - node: 'node-1' - }, - { - id: 'guest-102', - name: 'Database', - status: 'running', - type: 'lxc', - node: 'node-1' - }, - { - id: 'guest-103', - name: 'File Storage', - status: 'stopped', - type: 'qemu', - node: 'node-2' - } -]; - -// Serve a simple message -app.get('/', (req, res) => { - res.send('Socket.IO Test Server'); -}); - -// Socket.IO connection handler -io.on('connection', (socket) => { - console.log('Client connected:', socket.id); - - // Create consistent metrics for each guest to ensure stable data - // This is more reliable than generating random values each time - const initialMetrics = { - 'guest-101': { // Web Server - inRate: 850000, // ~850 KB/s - outRate: 420000 // ~420 KB/s - }, - 'guest-102': { // Database - inRate: 650000, // ~650 KB/s - outRate: 320000 // ~320 KB/s - } - }; - - // Send connection confirmation - socket.emit('message', { - type: 'CONNECTED', - payload: { - timestamp: Date.now(), - message: 'Connected to Test Socket.IO Server' - } - }); - - // Send initial node data - socket.emit('message', { - type: 'NODE_STATUS_UPDATE', - payload: nodes - }); - console.log('Sent initial node data to', socket.id); - - // Send initial guest data - socket.emit('message', { - type: 'GUEST_STATUS_UPDATE', - payload: guests - }); - console.log('Sent initial guest data to', socket.id); - - // Send initial metrics data for running guests - guests.forEach(guest => { - if (guest.status === 'running') { - // Use the consistent initial metrics for this guest - const baseMetrics = initialMetrics[guest.id] || { - inRate: 500000, // Default if not specified - outRate: 250000 - }; - - // Add some randomness (±10%) - const randomFactor = 0.9 + Math.random() * 0.2; - - socket.emit('message', { - type: 'METRICS_UPDATE', - payload: { - guestId: guest.id, - nodeId: guest.node, - timestamp: Date.now(), - metrics: { - network: { - inRate: baseMetrics.inRate * randomFactor, - outRate: baseMetrics.outRate * randomFactor - }, - uptime: Math.floor(Math.random() * 86400) - } - } - }); - - console.log(`Sent initial metrics for ${guest.id} (${guest.name})`); - } - }); - console.log('Sent initial metrics data for running guests to', socket.id); - - // Send test metrics data every 3 seconds - const interval = setInterval(() => { - // Create metrics for each guest - guests.forEach(guest => { - // Only send metrics for running guests - if (guest.status === 'running') { - // Use the consistent initial metrics with some random variation - const baseMetrics = initialMetrics[guest.id] || { - inRate: 500000, - outRate: 250000 - }; - - // Add larger random variation (±30%) - const randomFactor = 0.7 + Math.random() * 0.6; - - const metrics = { - type: 'METRICS_UPDATE', - payload: { - guestId: guest.id, - nodeId: guest.node, - timestamp: Date.now(), - metrics: { - network: { - inRate: baseMetrics.inRate * randomFactor, - outRate: baseMetrics.outRate * randomFactor - }, - uptime: Math.floor(Math.random() * 86400) - } - } - }; - - socket.emit('message', metrics); - console.log(`Sent updated metrics for ${guest.id} (${guest.name})`); - } - }); - - console.log('Sent metrics data to', socket.id); - }, 3000); - - // Clean up on disconnect - socket.on('disconnect', () => { - console.log('Client disconnected:', socket.id); - clearInterval(interval); - }); -}); - -// Start the server -server.listen(PORT, () => { - console.log(`Test server running on http://localhost:${PORT}`); -}); \ No newline at end of file