319 Commits

Author SHA1 Message Date
MacRimi be03035574 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-11-14 19:35:39 +01:00
MacRimi 619f3ca700 Create ProxMenux_offline.png 2025-11-14 19:35:37 +01:00
ProxMenuxBot 8553e63338 Update helpers_cache.json 2025-11-14 18:19:26 +00:00
MacRimi be4d9fe24b Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-11-14 18:54:33 +01:00
MacRimi 497f727b08 Update menu_Helper_Scripts.sh 2025-11-14 18:54:32 +01:00
ProxMenuxBot 74a7569f4c Update helpers_cache.json 2025-11-14 17:18:37 +00:00
MacRimi 66185e3b91 Update cache 2025-11-14 18:17:23 +01:00
MacRimi 1b2beda695 Update workflow 2025-11-14 18:15:56 +01:00
MacRimi feb3b5ef5f Update config_menu.sh 2025-11-14 17:44:19 +01:00
MacRimi b2439331b3 Update install_proxmenux.sh 2025-11-14 17:26:08 +01:00
MacRimi f1000afc27 Delete ProxMenux-1.0.0.AppImage 2025-11-14 17:06:58 +01:00
github-actions[bot] 5fc2a82423 Update AppImage build (2025-11-14 16:02:55) 2025-11-14 16:02:55 +00:00
MacRimi a27f884418 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-11-14 16:59:35 +01:00
MacRimi cae4b73226 Update AppImage 2025-11-14 16:59:18 +01:00
MacRimi 50ed293de2 Merge pull request #76 from c78-contrib/main
Proxmenux offline mode
2025-11-14 16:56:04 +01:00
cod378 616b772a45 chore: remove test installer script
- Delete install_proxmenux_test.sh (1048 lines)
- Test installer no longer needed after validation
2025-11-14 02:40:48 +00:00
cod378 c1d00e21db fix: suppress systemctl output in ProxMenux Monitor uninstaller
- Redirect stdout and stderr to /dev/null for stop, disable, daemon-reload, and reset-failed commands
- Maintain clean console output during uninstallation process
2025-11-14 02:37:43 +00:00
cod378 2e8e2b61d3 fix: use MONITOR_SERVICE constant instead of MONITOR_SERVICE_NAME in uninstall function 2025-11-14 02:30:19 +00:00
cod378 ba595c9719 feat: add test installer script with offline support and ProxMenux Monitor uninstaller 2025-11-14 02:21:00 +00:00
cod378 e392f6a2b7 feat: add cod378 to contributors list in config_menu.sh 2025-11-14 02:20:07 +00:00
cod378 7457e71776 Merge branch 'MacRimi:main' into main 2025-11-13 23:08:13 -03:00
cod378 982d0dd72e Merge branch 'main' of github.com:c78-contrib/ProxMenuxOffline 2025-11-14 01:49:58 +00:00
cod378 d345f96518 feat: add ProxMenux Monitor uninstallation to config menu
- Add uninstall_proxmenux_monitor() function with systemd service cleanup
- Stop and disable monitor service if active
- Remove systemd unit file and reload daemon
- Integrate monitor uninstallation into main uninstall_proxmenu() workflow
- Define MONITOR_UNIT_FILE constant for service file path
2025-11-14 01:49:39 +00:00
ProxMenuxBot 469874e975 Update helpers_cache.json 2025-11-14 01:37:35 +00:00
ProxMenuxBot 6ba817cd43 Update helpers_cache.json 2025-11-14 01:03:21 +00:00
MacRimi 42f2e69e3a Update AppImagen 2025-11-13 21:12:32 +01:00
MacRimi 12442b4bd3 Update settings.tsx 2025-11-13 20:59:36 +01:00
MacRimi 305d37a13b Update settings.tsx 2025-11-13 20:43:13 +01:00
MacRimi 4baf60174f Update settings.tsx 2025-11-13 20:36:35 +01:00
MacRimi 8cd1ac6a4b Update settings.tsx 2025-11-13 20:29:22 +01:00
MacRimi c65fef638e Update settings.tsx 2025-11-13 20:23:08 +01:00
MacRimi a030cd7e28 Update flask_auth_routes.py 2025-11-13 20:16:39 +01:00
MacRimi 59a3b7eac5 Update settings.tsx 2025-11-13 20:10:00 +01:00
MacRimi 2faac48adf Update settings.tsx 2025-11-13 20:01:08 +01:00
MacRimi 3883039764 Update AppImage 2025-11-13 19:51:42 +01:00
MacRimi 307ed0c637 Update AppImage 2025-11-13 19:43:17 +01:00
MacRimi 96ffdb65d0 Update README.md 2025-11-13 19:36:45 +01:00
MacRimi 5ca55798b2 Update README.md 2025-11-13 19:20:06 +01:00
MacRimi cd32e11c6d Update AppImage 2025-11-13 19:11:56 +01:00
MacRimi 774cbe4c9d Update AppImage 2025-11-13 18:32:44 +01:00
MacRimi 1d0bb20506 Update AppImage 2025-11-13 18:21:37 +01:00
MacRimi 8064e107f4 Update AppImage 2025-11-13 17:56:42 +01:00
MacRimi c1d1121ed1 Update AppImage 2025-11-13 17:46:07 +01:00
MacRimi 07603f11db Update api-config.ts 2025-11-13 17:25:51 +01:00
MacRimi ec22c857d5 Update api-config.ts 2025-11-13 17:20:31 +01:00
MacRimi 364e808261 Update api-config.ts 2025-11-13 17:14:47 +01:00
MacRimi 1d47ad0c4b Update AppImage 2025-11-13 16:58:45 +01:00
cod378 c9d0eac6cc Merge branch 'main' of github.com:c78-contrib/ProxMenuxOffline 2025-11-13 03:23:32 +00:00
cod378 97fc72b78a fix: add validation for missing ProxMenux Monitor AppImage
- Check if AppImage exists before attempting installation
- Display clear error message when AppImage is not found
- Update config to track installation failure state
2025-11-13 03:23:11 +00:00
cod378 7b1111430b chore: remove unused offline installer script 2025-11-13 03:22:52 +00:00
cod378 4f3306cd0f Merge branch 'MacRimi:main' into main 2025-11-12 23:56:22 -03:00
cod378 d3f7056ece Merge branch 'main' of github.com:c78-contrib/ProxMenuxOffline 2025-11-13 02:52:26 +00:00
cod378 9f3286c570 feat: migrate to offline installer with enhanced monitor deployment
- Restructured installer to use local repository files instead of remote downloads for improved reliability
- Added comprehensive logging functions (spinner, type_text, msg_* helpers) and dual logo support for SSH/noVNC terminals
- Implemented AppImage version detection, SHA256 verification, and systemd service management for ProxMenux Monitor
- Updated metadata to reflect toolkit positioning and added contributor attribution
2025-11-13 02:50:41 +00:00
ProxMenuxBot 16fc737b2d Update helpers_cache.json 2025-11-12 18:29:07 +00:00
ProxMenuxBot 3e0ae709d9 Update helpers_cache.json 2025-11-12 18:19:51 +00:00
ProxMenuxBot 39ddb7c8f9 Update helpers_cache.json 2025-11-12 12:41:44 +00:00
ProxMenuxBot 3c509ce0e4 Update helpers_cache.json 2025-11-12 12:38:40 +00:00
cod378 048cf2fb8f docs: update project name references from ProxMenuxDotDeb to ProxMenuxOffline 2025-11-12 05:21:33 +00:00
cod378 0a20821c41 refactor: remove verbose cleanup messages from temporary file removal 2025-11-12 05:00:06 +00:00
cod378 e0eaf6267f fix: suppress git clone output to reduce installation noise 2025-11-12 04:53:37 +00:00
cod378 3ddf98277f refactor: update utils script source URL to offline repository 2025-11-12 04:48:29 +00:00
cod378 85294bcd33 fix: correct utils.sh download URL format 2025-11-12 04:40:10 +00:00
cod378 acff4523f3 refactor: simplify utils.sh loading with inline sourcing
- Replaced conditional file check with direct curl sourcing using process substitution
- Streamlined error handling to single-line check
2025-11-12 04:29:40 +00:00
cod378 bf71e1f9b8 refactor: update comment for utils.sh loading 2025-11-12 04:23:43 +00:00
cod378 f0bcdc1c25 refactor: move utils.sh loading to script initialization because this is an installer dependency 2025-11-12 04:22:11 +00:00
cod378 43526c58bd refactor: reorganize installer to use git-based offline installation
- Changed from local script loading to cloning repository into temporary directory
- Added cleanup function with trap to ensure temporary files are removed on exit
- Added git as a required dependency for the installation process
2025-11-12 04:11:41 +00:00
cod378 ce3c7a545e feat: add GitHub authentication script to gitignore 2025-11-12 04:04:10 +00:00
cod378 9498e4e7eb Merge branch 'MacRimi:main' into main 2025-11-11 23:39:28 -03:00
ProxMenuxBot 4ec7c207f4 Update helpers_cache.json 2025-11-12 01:29:08 +00:00
ProxMenuxBot 000479463f Update helpers_cache.json 2025-11-12 01:04:12 +00:00
MacRimi 6b2065e43c Update AppImage 2025-11-11 22:00:44 +01:00
MacRimi e97e1363ae Update release-notes-modal.tsx 2025-11-11 21:37:39 +01:00
MacRimi 697a1f8e31 Update release-notes-modal.tsx 2025-11-11 21:30:57 +01:00
MacRimi 035f43311a Update release-notes-modal.tsx 2025-11-11 21:25:10 +01:00
MacRimi c597f1252e Update release-notes-modal.tsx 2025-11-11 21:12:09 +01:00
MacRimi cc1e7a715c Update settings.tsx 2025-11-11 19:48:02 +01:00
MacRimi 80057e3014 Update AppImage 2025-11-11 19:36:37 +01:00
MacRimi 79ffba873f Update AppImage 2025-11-11 19:20:59 +01:00
MacRimi 673e1cf212 Update virtual-machines.tsx 2025-11-11 18:21:30 +01:00
MacRimi 7e878ecff2 Update virtual-machines.tsx 2025-11-11 18:07:03 +01:00
MacRimi 88cf51a602 Update AppImage 2025-11-11 17:59:36 +01:00
MacRimi 1860fffe07 Update system-logs.tsx 2025-11-11 17:26:47 +01:00
MacRimi fa925543db Update AppImage 2025-11-11 17:12:56 +01:00
MacRimi 825e99c59b Update api-config.ts 2025-11-11 17:04:26 +01:00
MacRimi 955bed80fb Update AppImage 2025-11-11 17:01:25 +01:00
ProxMenuxBot 03b9ac3ec4 Update helpers_cache.json 2025-11-11 12:40:51 +00:00
ProxMenuxBot c255d9a5d8 Update helpers_cache.json 2025-11-11 12:27:59 +00:00
ProxMenuxBot 401d973a51 Update helpers_cache.json 2025-11-11 06:31:46 +00:00
ProxMenuxBot a507d559e1 Update helpers_cache.json 2025-11-11 01:29:31 +00:00
MacRimi 9225982ca5 Create ProxMenux-1.0.1-beta2.AppImage 2025-11-10 19:03:30 +01:00
MacRimi 6f831530cc Update system-logs.tsx 2025-11-10 18:38:33 +01:00
MacRimi e6b4443074 Update virtual-machines.tsx 2025-11-10 18:22:44 +01:00
MacRimi 1c800cbd8f Update storage-overview.tsx 2025-11-10 17:45:40 +01:00
MacRimi a65924799e Update storage-overview.tsx 2025-11-10 17:38:46 +01:00
MacRimi adbfa1e73e Update AppImge 2025-11-10 17:25:22 +01:00
cod378 44a4226ad2 Merge branch 'MacRimi:main' into main 2025-11-10 11:04:47 -03:00
ProxMenuxBot 07ca3f13a0 Update helpers_cache.json 2025-11-10 12:41:30 +00:00
ProxMenuxBot 87a052b89c Update helpers_cache.json 2025-11-10 12:27:40 +00:00
MacRimi 2216543ac3 Update storage-overview.tsx 2025-11-09 23:59:21 +01:00
MacRimi 4254d57d12 Update storage-overview.tsx 2025-11-09 23:46:32 +01:00
MacRimi 30d93898d8 Update storage-overview.tsx 2025-11-09 23:36:50 +01:00
MacRimi 4c7ed2c2c5 Update health_monitor.py 2025-11-09 21:50:10 +01:00
MacRimi 4fb327cef8 Create ProxMenux-1.0.1-beat1.AppImage 2025-11-09 21:37:01 +01:00
MacRimi 588af3613b Update AppImage 2025-11-09 21:20:39 +01:00
MacRimi 5b5f325a4e Update health_monitor.py 2025-11-09 21:03:00 +01:00
MacRimi ae62196dff Update AppImage 2025-11-09 20:52:39 +01:00
MacRimi 27e66ee770 Update health_monitor.py 2025-11-09 20:02:38 +01:00
MacRimi 8fb8134898 Update AppImage 2025-11-09 18:23:27 +01:00
MacRimi a59489f804 Update health_persistence.py 2025-11-09 18:11:55 +01:00
MacRimi cbf3938784 Update AppImage 2025-11-09 18:05:35 +01:00
MacRimi c45ebfe598 Update AppImage 2025-11-09 17:56:37 +01:00
MacRimi a75aad1fdc Update build_appimage.sh 2025-11-09 17:34:11 +01:00
MacRimi a0635a1026 Update AppImage 2025-11-09 17:28:20 +01:00
MacRimi 27353e160f Update AppImage 2025-11-09 16:43:45 +01:00
MacRimi b9619efbbf Update AppImage 2025-11-09 16:30:29 +01:00
MacRimi 1712d32ef7 Update AppImage 2025-11-09 15:44:35 +01:00
MacRimi 014deb2118 Update flask_server.py 2025-11-09 15:35:01 +01:00
MacRimi 6077cf81f2 Update system-overview.tsx 2025-11-09 15:12:32 +01:00
MacRimi 0422c38096 Update system-overview.tsx 2025-11-09 14:48:51 +01:00
MacRimi 8c902ae04d Update system-overview.tsx 2025-11-09 14:39:06 +01:00
MacRimi 0a0b916067 Update system-overview.tsx 2025-11-09 14:34:32 +01:00
MacRimi 6822635a0b Update flask_server.py 2025-11-09 13:55:09 +01:00
MacRimi f9b15fd110 Update node-metrics-charts.tsx 2025-11-09 13:31:16 +01:00
MacRimi 131a458e69 Update node-metrics-charts.tsx 2025-11-09 13:20:30 +01:00
MacRimi 7260807d78 Create use-mobile.tsx 2025-11-09 13:12:51 +01:00
MacRimi df83d8a3e5 Update node-metrics-charts.tsx 2025-11-09 13:07:42 +01:00
MacRimi 0f45424458 Update virtual-machines.tsx 2025-11-09 12:52:10 +01:00
MacRimi 60f92d019b Update virtual-machines.tsx 2025-11-09 12:26:55 +01:00
ProxMenuxBot 2189487982 Update helpers_cache.json 2025-11-08 06:27:04 +00:00
cod378 3a44997795 Merge branch 'MacRimi:main' into main 2025-11-07 21:53:57 -03:00
MacRimi 1f04134aac Update Appimagen 2025-11-07 21:14:56 +01:00
MacRimi ce44538240 Update AppImage 2025-11-07 21:07:33 +01:00
MacRimi 5fd53883be Update two-factor-setup.tsx 2025-11-07 21:02:20 +01:00
MacRimi f064cc89ba Update AppImage 2025-11-07 20:55:00 +01:00
MacRimi 5dd8b3ee36 Update AppImage 2025-11-07 20:36:46 +01:00
MacRimi f2f9c37ee2 Update ppImage 2025-11-07 20:19:55 +01:00
MacRimi 6836777629 Update AppImage 2025-11-07 20:05:29 +01:00
MacRimi beefdd280f Update proxmox-dashboard.tsx 2025-11-07 19:52:45 +01:00
MacRimi 4b9ad0da7a Update AppImage 2025-11-07 19:43:55 +01:00
ProxMenuxBot f9fdd1686c Update helpers_cache.json 2025-11-07 18:28:17 +00:00
MacRimi 25fc3d931e Update AppImage 2025-11-07 19:25:36 +01:00
MacRimi fc7d0f2cd5 Update AppImage 2025-11-07 18:49:37 +01:00
MacRimi 60c91d9fe4 Update AppImage 2025-11-07 17:35:45 +01:00
MacRimi cc2d6849a8 Update AppImage 2025-11-07 17:00:32 +01:00
MacRimi 4a5379ea42 Update flask_server.py 2025-11-07 15:36:51 +01:00
MacRimi ba84c644df Update flask_server.py 2025-11-07 14:33:27 +01:00
MacRimi 37217b4219 Update AppImage 2025-11-07 14:14:43 +01:00
MacRimi 41dab03a5f Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-11-07 13:42:03 +01:00
MacRimi 6e48bf2a71 Update AppImage 2025-11-07 13:41:39 +01:00
ProxMenuxBot 1c1c6f513c Update helpers_cache.json 2025-11-07 12:40:39 +00:00
ProxMenuxBot 49c54f5593 Update helpers_cache.json 2025-11-07 12:27:33 +00:00
MacRimi d083e49d0b Update proxmox-dashboard.tsx 2025-11-07 13:06:47 +01:00
MacRimi 8dc2b833f4 Update AppImage 2025-11-07 12:54:10 +01:00
MacRimi 7d5726be50 Update proxmox-dashboard.tsx 2025-11-07 12:43:31 +01:00
MacRimi 246c1674d1 Uppdate AppImage 2025-11-07 12:37:11 +01:00
MacRimi 06b81f2b64 Update AppImage 2025-11-07 12:21:37 +01:00
MacRimi ee57797890 Updete AppImage 2025-11-07 12:17:10 +01:00
MacRimi a94000e114 Update AppImage 2025-11-07 11:05:57 +01:00
MacRimi e6655b35f3 Update AppImage 2025-11-07 10:58:50 +01:00
MacRimi 696ffde184 Update issue template contact link description to English 2025-11-07 10:16:17 +01:00
MacRimi 9e74e99923 Update feature_request.md 2025-11-07 10:15:09 +01:00
MacRimi bc5c6dadfb Translate bug report template to English 2025-11-07 10:14:37 +01:00
MacRimi 0d173a0bfe Update ISO file name for XigmaNAS version 14.3.0.5 2025-11-06 12:10:12 +01:00
MacRimi cd78920edd Fix ISO URL for XigmaNAS version 14.3.0.5 2025-11-06 09:08:09 +01:00
MacRimi 9a7ec62cf9 Update select_nas_iso.sh 2025-11-06 09:06:52 +01:00
cod378 2b4580cfe8 Merge branch 'MacRimi:main' into main 2025-11-05 23:24:21 -03:00
MacRimi b790c06294 Merge pull request #62 from MrCaringi/main
Add Issue Templates and Configuration for Categorization
2025-11-05 21:21:33 +01:00
JFC 61d87b46d9 Create feature request issue template
Adds a feature request template for GitHub issues.
2025-11-05 12:37:45 -06:00
JFC 143cb4cbab Modify bug report template and assign to MacRimi
Updated bug report template to include mandatory screenshots and assigned to 'MacRimi'.
2025-11-05 12:36:50 -06:00
JFC 22709dac36 Add issue template configuration for GitHub 2025-11-05 12:35:11 -06:00
ProxMenuxBot d97be93449 Update helpers_cache.json 2025-11-05 18:29:25 +00:00
ProxMenuxBot 5864de7dea Update helpers_cache.json 2025-11-05 18:19:51 +00:00
MacRimi 4ea5890e92 Update health-status-modal.tsx 2025-11-05 18:46:19 +01:00
MacRimi 876d51b009 Update health-status-modal.tsx 2025-11-05 18:38:29 +01:00
MacRimi 5b0d55c1a2 Update health_monitor.py 2025-11-05 18:30:31 +01:00
cod378 3ddb1421c3 feat: add offline installer script for ProxMenux
- Clones ProxMenux repository to temporary location and executes installation
- Includes automatic cleanup of temporary files and git dependency check
- Adds colored output and error handling for better user experience
2025-11-04 22:47:52 +00:00
cod378 58f9a7bc02 refactor: simplify utils.sh loading error handling 2025-11-04 22:47:00 +00:00
MacRimi e8e4b728ce Update proxmox-dashboard.tsx 2025-11-04 23:00:37 +01:00
MacRimi 0a4868192d Update proxmox-dashboard.tsx 2025-11-04 22:55:41 +01:00
MacRimi 9d81ffffe8 Update proxmox-dashboard.tsx 2025-11-04 22:47:11 +01:00
MacRimi e6fe4a09e5 Update AppImage 2025-11-04 22:28:42 +01:00
MacRimi 77c5ad7b09 Update AppImage 2025-11-04 21:59:28 +01:00
MacRimi b850e9615a Update proxmox-dashboard.tsx 2025-11-04 21:48:54 +01:00
MacRimi c2ea307821 Update AppImage 2025-11-04 21:42:38 +01:00
MacRimi fb588c0d60 Update flask_auth_routes.py 2025-11-04 21:36:31 +01:00
MacRimi fecbdf6190 Update build_appimage.sh 2025-11-04 21:32:14 +01:00
MacRimi bbbbf6892f Update flask_server.py 2025-11-04 21:27:29 +01:00
MacRimi e1a11053a6 Update flask_server.py 2025-11-04 21:16:16 +01:00
MacRimi f0a62191ea Updae AppImage 2025-11-04 21:02:56 +01:00
MacRimi a8311923fb Update AppImage 2025-11-04 19:58:09 +01:00
MacRimi cd1d88760d Update proxmox-dashboard.tsx 2025-11-04 19:39:50 +01:00
MacRimi 004949d3a0 Update proxmox-dashboard.tsx 2025-11-04 19:21:31 +01:00
MacRimi f6d26042da Update AppImage 2025-11-04 19:13:47 +01:00
MacRimi 270a73a470 Update auth-setup.tsx 2025-11-04 18:48:27 +01:00
MacRimi 018e80e59d Update AppImage 2025-11-04 18:45:54 +01:00
MacRimi cb5cb1e594 Create checkbox.tsx 2025-11-04 18:11:42 +01:00
MacRimi 6c5eb156a1 Update AppImage 2025-11-04 18:07:13 +01:00
MacRimi 8abef33840 Update build_appimage.sh 2025-11-04 17:37:32 +01:00
MacRimi 1d6b8951e8 Update hardware.tsx 2025-11-04 15:28:27 +01:00
MacRimi 711d57d91f Update flask_server.py 2025-11-04 15:09:23 +01:00
MacRimi 65fd847251 Update flask_server.py 2025-11-04 14:24:34 +01:00
MacRimi 73a170a5f1 Update hardware.tsx 2025-11-04 14:00:01 +01:00
MacRimi 9a32d1c0f7 Update flask_server.py 2025-11-04 13:47:00 +01:00
MacRimi 59918032c6 Update hardware.tsx 2025-11-04 13:18:39 +01:00
MacRimi 55394cbf09 Update AppImage 2025-11-04 12:47:26 +01:00
MacRimi 83dcc0c4f2 Update storage-overview.tsx 2025-11-04 12:17:32 +01:00
MacRimi b4b93f0572 Update AppImage 2025-11-04 12:11:08 +01:00
MacRimi ab0e59215c Aupdate version ProxMenux Monitor 2025-11-04 11:34:46 +01:00
MacRimi 5669ce207c Update flask_server.py 2025-11-04 11:03:09 +01:00
MacRimi 37f6cd96a4 Update flask_server.py 2025-11-04 09:46:11 +01:00
MacRimi c0ec74fb12 Update AppImage 2025-11-04 09:14:29 +01:00
cod378 226dc45190 Merge branch 'MacRimi:main' into main 2025-11-03 21:24:30 -03:00
MacRimi 11e3f53a2f Update AppImage 2025-11-03 23:26:04 +01:00
MacRimi 31d7f7e3e9 Update appImage 2025-11-03 23:17:27 +01:00
MacRimi 128edc08e2 Update flask_server.py 2025-11-03 23:13:24 +01:00
MacRimi 5158c5f359 Update flask_server.py 2025-11-03 19:12:07 +01:00
MacRimi a70b33ce13 Update AppImage 2025-11-03 19:02:41 +01:00
MacRimi d787c3caa0 Update AppImage 2025-11-03 18:35:16 +01:00
MacRimi a554af939e Create build-appimage.yml 2025-11-03 18:24:43 +01:00
MacRimi 06604ff0d1 Add manual build workflow for AppImage 2025-11-03 18:15:17 +01:00
MacRimi 9490f79c6d Update build-appimage.yml 2025-11-03 18:14:13 +01:00
MacRimi 311a624698 Update customizable_post_install.sh 2025-11-03 18:08:28 +01:00
cod378 3e2e77f9fb Merge branch 'MacRimi:main' into main 2025-11-03 10:15:41 -03:00
cod378 b2e02cd0e7 refactor: switch from remote URL to local script execution 2025-11-03 12:32:19 +00:00
ProxMenuxBot 87ead71766 Update helpers_cache.json 2025-11-03 12:28:15 +00:00
cod378 b8517a5b3e Merge branch 'ProxMenux-Offline' 2025-11-03 03:54:10 +00:00
cod378 c29cdf44fb refactor: switch from remote URLs to local script execution (unused path) 2025-11-03 02:12:25 +00:00
cod378 4b2ab2894a refactor: switch from remote URLs to local script execution 2025-11-03 02:11:15 +00:00
cod378 c9a01ab5ad refactor: switch from remote URLs to local script execution (unused path) 2025-11-03 02:10:46 +00:00
cod378 90d1046312 refactor: switch from remote URLs to local script execution (unused path) 2025-11-03 02:10:18 +00:00
cod378 14e749a18d refactor: switch from remote URLs to local script execution 2025-11-03 02:09:56 +00:00
cod378 a4be1af0ef refactor: switch from remote URLs to local script execution 2025-11-03 02:08:36 +00:00
cod378 f4185d0a2a refactor: switch from remote URLs to local script execution 2025-11-03 02:07:20 +00:00
cod378 ffb8324b5a refactor: switch from remote URLs to local script execution 2025-11-03 02:06:43 +00:00
cod378 6df44f1632 refactor: switch from remote URLs to local script execution 2025-11-03 02:03:26 +00:00
cod378 9570819f59 refactor: switch from remote URLs to local script execution (unused path) 2025-11-03 01:56:34 +00:00
cod378 f2afc94ed2 refactor: switch from remote URLs to local script execution (unused path) 2025-11-03 01:56:23 +00:00
cod378 050b95946c refactor: switch from remote URLs to local script execution (unused path) 2025-11-03 01:55:45 +00:00
cod378 e33ef92334 refactor: switch from remote URLs to local script execution 2025-11-03 01:54:52 +00:00
cod378 0d7ff46aec refactor: switch from remote URLs to local script execution 2025-11-03 01:53:10 +00:00
cod378 042913e080 refactor: switch from remote URLs to local script execution (unused path) 2025-11-03 01:51:57 +00:00
cod378 98bc8be642 refactor: switch from remote URLs to local script execution (unused path) 2025-11-03 01:51:43 +00:00
cod378 2c6d2f4255 refactor: switch from remote URLs to local script execution (unused path) 2025-11-03 01:51:24 +00:00
cod378 6293556837 refactor: switch from remote URLs to local script execution (unused path) 2025-11-03 01:51:06 +00:00
cod378 641721d199 refactor: switch from remote URLs to local script execution (unused path) 2025-11-03 01:48:35 +00:00
cod378 036a2b9014 refactor: switch from remote URLs to local script execution (unused path) 2025-11-03 01:47:52 +00:00
cod378 9ad092d340 refactor: switch from remote URLs to local script execution (unused path) 2025-11-03 01:47:17 +00:00
cod378 d24884f651 refactor: switch from remote URLs to local script execution (unused path) 2025-11-03 01:46:50 +00:00
cod378 fa1c498716 refactor: switch from remote URLs to local script execution (unused path) 2025-11-03 01:46:19 +00:00
cod378 25635239d4 refactor: switch from remote URLs to local script execution (unused path) 2025-11-03 01:45:43 +00:00
cod378 c816688de3 refactor: switch from remote URLs to local script execution (unused path) 2025-11-03 01:44:48 +00:00
cod378 43d79bd1e9 refactor: switch from remote URLs to local script execution (unused path) 2025-11-03 01:44:06 +00:00
cod378 ba88c7b0f6 refactor: switch from remote URLs to local script execution 2025-11-03 01:41:39 +00:00
cod378 4359d92ffe refactor: switch from remote URLs to local script execution 2025-11-03 01:40:38 +00:00
cod378 16c7513e82 refactor: switch from remote URLs to local script execution 2025-11-03 01:39:15 +00:00
cod378 4572478ad8 refactor: switch from remote URLs to local script execution 2025-11-03 01:38:05 +00:00
cod378 02b5cd61bd refactor: switch from remote URLs to local script execution 2025-11-03 01:36:57 +00:00
cod378 bff07311b2 refactor: switch from remote URLs to local script execution 2025-11-03 01:34:44 +00:00
cod378 44cc89b9d5 refactor: switch from remote URLs to local file paths 2025-11-03 01:33:04 +00:00
cod378 fa1e6c6c64 refactor: switch from remote URLs to local script execution 2025-11-03 01:27:37 +00:00
cod378 4ebbdb284b refactor: switch from remote URLs to local script execution 2025-11-03 01:26:28 +00:00
cod378 51302a7c5a refactor: switch from remote URLs to local script execution 2025-11-03 01:25:19 +00:00
cod378 ba984592ed refactor: replace remote script fetching with local file execution 2025-11-03 01:22:24 +00:00
cod378 60a97e5815 feat: replace remote script fetching with local file execution 2025-11-03 01:20:11 +00:00
cod378 3275a1ecb4 refactor: replace remote script loading with local paths 2025-11-03 01:17:21 +00:00
cod378 af72c7a2d3 refactor: switch from remote URLs to local script paths 2025-11-03 01:16:32 +00:00
cod378 c07ada1fc4 refactor: switch from remote to local script execution 2025-11-03 01:14:54 +00:00
cod378 c19c8f9c5d refactor: switch network menu from remote URL to local scripts 2025-11-03 01:10:44 +00:00
cod378 43fe7ae7db refactor: replace remote script fetching with local file execution 2025-11-03 01:09:12 +00:00
cod378 22916868df feat: switch script paths from remote repo to local directory 2025-11-03 01:06:04 +00:00
cod378 7d00ff8869 feat: switch menu scripts from remote URLs to local paths 2025-11-03 01:03:39 +00:00
cod378 4ea2088485 refactor: replace remote script loading with local file execution
- Changed script loading from curl-based remote fetching to local file execution for improved security and reliability
- Removed dependency on external repository access for core menu functionality
- Fixed missing semicolon in case statement default branch
2025-11-03 00:59:48 +00:00
cod378 e421b40093 refactor: switch from remote scripts to local execution 2025-11-03 00:50:18 +00:00
cod378 a9dd7562ac feat: switch from remote URLs to local script paths
- Changed script sourcing from GitHub URLs to local filesystem paths for improved reliability
- Added error handling for missing script files with descriptive messages
- Removed redundant utility file sourcing and consolidated into single conditional block
- Updated script execution to use direct paths instead of curl commands
- Removed unused start_vm_configuration function that was duplicated elsewhere
2025-11-03 00:46:23 +00:00
cod378 8f62ed67d3 refactor: switch from remote URL to local script paths 2025-11-03 00:22:59 +00:00
cod378 cfd89a14f7 refactor: update script paths to use local resources 2025-11-03 00:16:28 +00:00
cod378 55011842f5 refactor: update script paths to use local resources 2025-11-03 00:15:42 +00:00
cod378 3079a3f51c refactor: update script paths to use local resources 2025-11-03 00:15:08 +00:00
cod378 c4ec390ca0 refactor: update scripts paths to use local references 2025-11-03 00:13:19 +00:00
cod378 f99b7f3589 refactor: switch script to use local paths instead of remote URLs 2025-11-03 00:07:34 +00:00
cod378 887b170c0e refactor: switch script to use local paths instead of remote URLs 2025-11-03 00:06:12 +00:00
cod378 c696cfd8d8 refactor: switch update script to use local file paths 2025-11-03 00:04:47 +00:00
cod378 25966973a2 refactor: update script paths to use local resources 2025-11-03 00:03:21 +00:00
cod378 d0a57d4b7c refactor: update script paths to use local resources 2025-11-03 00:01:36 +00:00
cod378 9341b49fd1 refactor: update script paths to use local references 2025-11-03 00:00:52 +00:00
cod378 bbc3c922a6 refactor: switch from remote repo to local script paths 2025-11-02 23:58:48 +00:00
cod378 17b8d63e6c refactor: switch from remote URLs to local script paths 2025-11-02 23:55:37 +00:00
cod378 c751a8168a refactor: switch backup script from remote URL to local file paths 2025-11-02 23:36:13 +00:00
cod378 f2509dbe5d refactor: switch from remote to local script loading. 2025-11-02 23:26:45 +00:00
cod378 6d44c22982 refactor: switch from remote to local script loading. 2025-11-02 23:25:04 +00:00
cod378 4bed489610 refactor: switch backup scripts from remote URL to local paths 2025-11-02 23:08:34 +00:00
cod378 8edf488636 refactor: update script paths to use local references 2025-11-02 03:32:41 +00:00
cod378 8fe7d249f8 refactor: update script paths to use local resources 2025-11-02 03:32:02 +00:00
cod378 6ed14e1d3c feat: switch from remote to local script loading 2025-11-02 03:29:56 +00:00
cod378 a5459acdaf feat: switch help menu from remote to local script loading 2025-11-02 03:28:14 +00:00
cod378 61cd198d35 feat: switch disk passthrough script to use local scripts 2025-11-02 03:27:18 +00:00
cod378 49ea2b304d feat: switch disk passthrough script to use local scripts 2025-11-02 03:26:27 +00:00
cod378 27231d1764 feat: switch iGPU configuration to use local scripts 2025-11-02 03:25:41 +00:00
cod378 8744620220 feat: improve log2ram installation and system checks
- Enhanced log2ram size calculation to support both MB and GB configurations
- Updated RAM detection to use MB-level precision before converting to GB
- Fixed typos in status messages ("successfull" → "successful")
- Switched from remote repo URL to local scripts directory for better reliability
- Added registration of LVM repair tool after successful header checks
- Improved log2ram monitoring script to properly handle different size units (M
2025-11-02 03:24:26 +00:00
cod378 4590be6d42 fix: replace remote script loading with local file execution 2025-11-02 03:22:54 +00:00
cod378 fa93b43c32 feat: switch telegram notifier to use local scripts
- Changed script source from GitHub repository to local directory (/usr/local/share/proxmenux/scripts)
- Updated path configuration to ensure consistent local file access
- Removed dependency on external repository for improved reliability and security
2025-11-02 02:59:33 +00:00
cod378 3c47f84a24 refactor: switch from remote repo to local scripts path
- Changed repository URL reference from GitHub to local scripts directory (/usr/local/share/proxmenux/scripts)
- Fixed spacing in info2 message formatting by adding space after HOLD variable
- Simplified script dependencies to use local installation instead of remote fetching
2025-11-02 02:55:19 +00:00
cod378 8a371c26de refactor: switch from remote to local script execution
- Changed script loading from remote URL to local directory path for offline usage
- Updated REPO_URL to LOCAL_SCRIPTS path (/usr/local/share/proxmenux/scripts)
- Disabled check_updates function since it's not applicable for local version
- Added comments explaining update functionality will be handled via .deb package in future
2025-11-02 02:51:24 +00:00
ProxMenuxBot 088a594468 Update helpers_cache.json 2025-11-02 01:06:57 +00:00
MacRimi c551913551 Update uninstall-tools.sh 2025-11-02 00:53:04 +01:00
code78 05e81053e0 feat: switch to local file installation and improve monitor setup
- Replaced remote file downloads with local file copying for more reliable installation
- Added proper cleanup of existing monitor service before reinstallation
- Enhanced error handling and logging for monitor service startup
- Improved SHA256 verification for monitor AppImage
- Added copying of install script and all utility scripts to base directory
- Updated progress messages to be more descriptive and accurate
- Increased monitor
2025-11-01 23:47:45 +00:00
MacRimi 981c0ab980 Update remove-banner-pve-v3.sh 2025-11-02 00:42:22 +01:00
MacRimi 1f083b335f Update remove banner v3 2025-11-02 00:37:59 +01:00
code78 10603900df update: Progress status 2025-11-01 23:33:25 +00:00
MacRimi c22e36d219 Update script PVE 9 2025-11-01 17:22:51 +01:00
MacRimi 26fc2ae9db Update install_proxmenux.sh 2025-11-01 17:09:38 +01:00
code78 2a0b298ae5 feat: add comprehensive project documentation and analysis
- Created detailed documentation covering ProxMenux project structure, installation flow, and core components
- Added in-depth analysis of script architecture, execution patterns, and key functionalities
- Documented system configuration, translation mechanism, and component interactions
- Included detailed breakdown of file organization, menu system, and installation processes
- Added technical specifications for ProxMenux Monitor web dashboar
2025-11-01 03:01:51 +00:00
ProxMenuxBot 96f0a9bc5d Update helpers_cache.json 2025-11-01 01:06:00 +00:00
MacRimi 5054e78864 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-11-01 00:11:05 +01:00
MacRimi f1fa6b03d5 change license CC-BY-NC-4.0 2025-11-01 00:10:46 +01:00
MacRimi 8f15bf9668 change license to CC-BY-NC-4.0 2025-10-31 23:48:46 +01:00
MacRimi 4b8e7b19a3 change licente to CC-BY-NC-4.0 2025-10-31 23:47:17 +01:00
MacRimi 67bba1dd09 Update version.txt 2025-10-31 23:38:45 +01:00
MacRimi b826dec79d Update CHANGELOG.md 2025-10-31 23:34:21 +01:00
156 changed files with 18696 additions and 2096 deletions
+29
View File
@@ -0,0 +1,29 @@
---
name: Bug Report
about: Report a problem in the project
title: "[BUG] Describe the issue"
labels: bug
assignees: 'MacRimi'
---
## Description
Describe the bug clearly and concisely.
## Steps to Reproduce
1. ...
2. ...
3. ...
## Expected Behavior
What should happen?
## Screenshots (Required)
Add images to help illustrate the issue.
## Environment
- Operating system:
- Software version:
- Other relevant details:
## Additional Information
Add any other context about the problem here.
+5
View File
@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Soporte General
url: https://github.com/MacRimi/ProxMenux/discussions
about: If your request is neither a bug nor a feature, please use Discussions.
+19
View File
@@ -0,0 +1,19 @@
---
name: Feature Request
about: Suggest a new feature or improvement
title: "[FEATURE] Describe your proposal"
labels: enhancement
assignees: 'MacRimi'
---
## Description
Explain the feature you are proposing.
## Motivation
Why is this improvement important? What problem does it solve?
## Alternatives Considered
Are there other solutions you have thought about?
## Additional Information
Add any extra details that help understand your proposal.
+163 -62
View File
@@ -1,76 +1,177 @@
import requests, json
#!/usr/bin/env python3
import json
import re
import sys
from pathlib import Path
# GitHub API URL to fetch all .json files describing scripts
API_URL = "https://api.github.com/repos/community-scripts/ProxmoxVE/contents/frontend/public/json"
import requests
# Base path to build the full URL for the installable scripts
# ---------- Config ----------
API_URL = "https://api.github.com/repos/community-scripts/ProxmoxVE/contents/frontend/public/json"
SCRIPT_BASE = "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
# Output file where the consolidated helper scripts cache will be stored
OUTPUT_FILE = Path("json/helpers_cache.json")
# Escribimos siempre en <raiz_repo>/json/helpers_cache.json, independientemente del cwd
REPO_ROOT = Path(__file__).resolve().parents[2]
OUTPUT_FILE = REPO_ROOT / "json" / "helpers_cache.json"
OUTPUT_FILE.parent.mkdir(parents=True, exist_ok=True)
# ----------------------------
res = requests.get(API_URL)
data = res.json()
cache = []
# Loop over each file in the JSON directory
for item in data:
url = item.get("download_url")
if not url or not url.endswith(".json"):
continue
def to_mirror_url(raw_url: str) -> str:
"""
Convierte una URL raw de GitHub al raw del mirror.
GH : https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/docker.sh
MIR: https://git.community-scripts.org/community-scripts/ProxmoxVE/raw/branch/main/ct/docker.sh
"""
m = re.match(r"^https://raw\.githubusercontent\.com/([^/]+)/([^/]+)/([^/]+)/(.+)$", raw_url or "")
if not m:
return ""
org, repo, branch, path = m.groups()
if org.lower() != "community-scripts" or repo != "ProxmoxVE":
return ""
return f"https://git.community-scripts.org/community-scripts/ProxmoxVE/raw/branch/{branch}/{path}"
def guess_os_from_script_path(script_path: str) -> str | None:
"""
Heurística suave cuando el JSON no publica resources.os:
- tools/pve/* -> proxmox
- ct/alpine-* -> alpine
- tools/addon/* -> generic (suele ejecutarse sobre LXC existente)
- ct/* -> debian (por defecto para CTs)
"""
if not script_path:
return None
if script_path.startswith("tools/pve/") or script_path == "tools/pve/host-backup.sh" or script_path.startswith("vm/"):
return "proxmox"
if "/alpine-" in script_path or script_path.startswith("ct/alpine-"):
return "alpine"
if script_path.startswith("tools/addon/"):
return "generic"
if script_path.startswith("ct/"):
return "debian"
return None
def fetch_directory_json(api_url: str) -> list[dict]:
r = requests.get(api_url, timeout=30)
r.raise_for_status()
data = r.json()
if not isinstance(data, list):
raise RuntimeError("GitHub API no devolvió una lista.")
return data
def main() -> int:
try:
raw = requests.get(url).json()
if not isinstance(raw, dict):
directory = fetch_directory_json(API_URL)
except Exception as e:
print(f"ERROR: No se pudo leer el índice de JSONs: {e}", file=sys.stderr)
return 1
cache: list[dict] = []
seen: set[tuple[str, str]] = set() # (slug, script) para evitar duplicados
total_items = len(directory)
processed = 0
kept = 0
for item in directory:
url = item.get("download_url")
name_in_dir = item.get("name", "")
if not url or not url.endswith(".json"):
continue
except:
continue
# Extract fields required to identify a valid helper script
name = raw.get("name", "")
slug = raw.get("slug")
type_ = raw.get("type", "")
script = raw.get("install_methods", [{}])[0].get("script", "")
if not slug or not script:
continue # Skip if it's not a valid script
try:
raw = requests.get(url, timeout=30).json()
if not isinstance(raw, dict):
continue
except Exception:
print(f"❌ Error al obtener/parsing {name_in_dir}", file=sys.stderr)
continue
desc = raw.get("description", "")
categories = raw.get("categories", [])
notes = [note.get("text", "") for note in raw.get("notes", []) if isinstance(note, dict)]
full_script_url = f"{SCRIPT_BASE}/{script}"
processed += 1
name = raw.get("name", "")
slug = raw.get("slug")
type_ = raw.get("type", "")
desc = raw.get("description", "")
categories = raw.get("categories", [])
notes = [n.get("text", "") for n in raw.get("notes", []) if isinstance(n, dict)]
# Credenciales (si existen, se copian tal cual)
credentials = raw.get("default_credentials", {})
cred_username = credentials.get("username") if isinstance(credentials, dict) else None
cred_password = credentials.get("password") if isinstance(credentials, dict) else None
add_credentials = any([
cred_username not in (None, ""),
cred_password not in (None, "")
])
install_methods = raw.get("install_methods", [])
if not isinstance(install_methods, list) or not install_methods:
# Sin install_methods válidos -> continuamos
continue
for im in install_methods:
if not isinstance(im, dict):
continue
script = im.get("script", "")
if not script:
continue
# OS desde resources u heurística
resources = im.get("resources", {}) if isinstance(im, dict) else {}
os_name = resources.get("os") if isinstance(resources, dict) else None
if not os_name:
os_name = guess_os_from_script_path(script)
if isinstance(os_name, str):
os_name = os_name.strip().lower()
full_script_url = f"{SCRIPT_BASE}/{script}"
script_url_mirror = to_mirror_url(full_script_url)
key = (slug or "", script)
if key in seen:
continue
seen.add(key)
entry = {
"name": name,
"slug": slug,
"desc": desc,
"script": script,
"script_url": full_script_url,
"script_url_mirror": script_url_mirror, # nuevo
"os": os_name, # nuevo
"categories": categories,
"notes": notes,
"type": type_,
}
if add_credentials:
entry["default_credentials"] = {
"username": cred_username,
"password": cred_password,
}
cache.append(entry)
kept += 1
# Progreso ligero
print(f"[{kept:03d}] {slug or name:<24}{script:<28} os={os_name or 'n/a'} src={'GH+MR' if script_url_mirror else 'GH'}")
# Orden estable para commits reproducibles
cache.sort(key=lambda x: (x.get("slug") or "", x.get("script") or ""))
with OUTPUT_FILE.open("w", encoding="utf-8") as f:
json.dump(cache, f, ensure_ascii=False, indent=2)
print(f"\n✅ helpers_cache.json → {OUTPUT_FILE}")
print(f" Total JSON en índice: {total_items}")
print(f" Procesados: {processed} | Guardados: {kept} | Únicos (slug,script): {len(seen)}")
return 0
credentials = raw.get("default_credentials", {})
cred_username = credentials.get("username")
cred_password = credentials.get("password")
add_credentials = (
(cred_username is not None and str(cred_username).strip() != "") or
(cred_password is not None and str(cred_password).strip() != "")
)
entry = {
"name": name,
"slug": slug,
"desc": desc,
"script": script,
"script_url": full_script_url,
"categories": categories,
"notes": notes,
"type": type_
}
if add_credentials:
entry["default_credentials"] = {
"username": cred_username,
"password": cred_password
}
cache.append(entry)
# Write the JSON cache to disk
with open(OUTPUT_FILE, "w", encoding="utf-8") as f:
json.dump(cache, f, indent=2)
print(f"✅ helpers_cache.json created at {OUTPUT_FILE} with {len(cache)} valid scripts.")
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,76 @@
import requests, json
from pathlib import Path
# GitHub API URL to fetch all .json files describing scripts
API_URL = "https://api.github.com/repos/community-scripts/ProxmoxVE/contents/frontend/public/json"
# Base path to build the full URL for the installable scripts
SCRIPT_BASE = "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
# Output file where the consolidated helper scripts cache will be stored
OUTPUT_FILE = Path("json/helpers_cache.json")
OUTPUT_FILE.parent.mkdir(parents=True, exist_ok=True)
res = requests.get(API_URL)
data = res.json()
cache = []
# Loop over each file in the JSON directory
for item in data:
url = item.get("download_url")
if not url or not url.endswith(".json"):
continue
try:
raw = requests.get(url).json()
if not isinstance(raw, dict):
continue
except:
continue
# Extract fields required to identify a valid helper script
name = raw.get("name", "")
slug = raw.get("slug")
type_ = raw.get("type", "")
script = raw.get("install_methods", [{}])[0].get("script", "")
if not slug or not script:
continue # Skip if it's not a valid script
desc = raw.get("description", "")
categories = raw.get("categories", [])
notes = [note.get("text", "") for note in raw.get("notes", []) if isinstance(note, dict)]
full_script_url = f"{SCRIPT_BASE}/{script}"
credentials = raw.get("default_credentials", {})
cred_username = credentials.get("username")
cred_password = credentials.get("password")
add_credentials = (
(cred_username is not None and str(cred_username).strip() != "") or
(cred_password is not None and str(cred_password).strip() != "")
)
entry = {
"name": name,
"slug": slug,
"desc": desc,
"script": script,
"script_url": full_script_url,
"categories": categories,
"notes": notes,
"type": type_
}
if add_credentials:
entry["default_credentials"] = {
"username": cred_username,
"password": cred_password
}
cache.append(entry)
# Write the JSON cache to disk
with open(OUTPUT_FILE, "w", encoding="utf-8") as f:
json.dump(cache, f, indent=2)
print(f"✅ helpers_cache.json created at {OUTPUT_FILE} with {len(cache)} valid scripts.")
@@ -0,0 +1,81 @@
name: Build ProxMenux Monitor AppImage
on:
workflow_dispatch:
permissions:
contents: write
jobs:
build:
runs-on: ubuntu-22.04
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
working-directory: AppImage
run: npm install --legacy-peer-deps
- name: Build Next.js app
working-directory: AppImage
run: npm run build
- name: Install Python dependencies
run: |
sudo apt-get update
sudo apt-get install -y python3 python3-pip python3-venv
- name: Make build script executable
working-directory: AppImage
run: chmod +x scripts/build_appimage.sh
- name: Build AppImage
working-directory: AppImage
run: ./scripts/build_appimage.sh
- name: Get version from package.json
id: version
working-directory: AppImage
run: echo "VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
- name: Upload AppImage artifact
uses: actions/upload-artifact@v4
with:
name: ProxMenux-${{ steps.version.outputs.VERSION }}-AppImage
path: AppImage/dist/*.AppImage
retention-days: 30
- name: Generate SHA256 checksum
run: |
cd AppImage/dist
sha256sum *.AppImage > ProxMenux-Monitor.AppImage.sha256
echo "Generated SHA256:"
cat ProxMenux-Monitor.AppImage.sha256
- name: Upload AppImage and checksum to /AppImage folder in main
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git fetch origin main
git checkout main
rm -f AppImage/*.AppImage AppImage/*.sha256 || true
# Copy new files
cp AppImage/dist/*.AppImage AppImage/
cp AppImage/dist/ProxMenux-Monitor.AppImage.sha256 AppImage/
git add AppImage/*.AppImage AppImage/*.sha256
git commit -m "Update AppImage build ($(date +'%Y-%m-%d %H:%M:%S'))" || echo "No changes to commit"
git push origin main
+1 -31
View File
@@ -8,10 +8,7 @@ on:
branches: [ main ]
paths: [ 'AppImage/**' ]
workflow_dispatch:
permissions:
contents: write
jobs:
build:
runs-on: ubuntu-22.04
@@ -57,30 +54,3 @@ jobs:
name: ProxMenux-${{ steps.version.outputs.VERSION }}-AppImage
path: AppImage/dist/*.AppImage
retention-days: 30
- name: Generate SHA256 checksum
run: |
cd AppImage/dist
sha256sum *.AppImage > ProxMenux-Monitor.AppImage.sha256
echo "Generated SHA256:"
cat ProxMenux-Monitor.AppImage.sha256
- name: Upload AppImage and checksum to /AppImage folder in main
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git fetch origin main
git checkout main
rm -f AppImage/*.AppImage AppImage/*.sha256 || true
# Copy new files
cp AppImage/dist/*.AppImage AppImage/
cp AppImage/dist/ProxMenux-Monitor.AppImage.sha256 AppImage/
git add AppImage/*.AppImage AppImage/*.sha256
git commit -m "Update AppImage build ($(date +'%Y-%m-%d %H:%M:%S'))" || echo "No changes to commit"
git push origin main
+2
View File
@@ -51,3 +51,5 @@ Thumbs.db
!guides/
!web/
# GitHub authentication
.github/auth.sh
+1 -1
View File
@@ -1 +1 @@
e896eb10de4bf990d31c1d8357289f64cbce481921647f2be53efb850d0b73b2 ProxMenux-1.0.0.AppImage
f35de512c1a19843d15a9a3263a5104759d041ffc9d01249450babe0b0c3f889 ProxMenux-1.0.1.AppImage
+794 -23
View File
@@ -2,40 +2,811 @@
A modern, responsive dashboard for monitoring Proxmox VE systems built with Next.js and React.
---
## Table of Contents
- [Overview](#overview)
- [Features](#features)
- [Technology Stack](#technology-stack)
- [Installation](#installation)
- [Authentication & Security](#authentication--security)
- [Setup Authentication](#setup-authentication)
- [Two-Factor Authentication (2FA)](#two-factor-authentication-2fa)
- [Security Best Practices for API Tokens](#security-best-practices-for-api-tokens)
- [API Documentation](#api-documentation)
- [API Authentication](#api-authentication)
- [Generating API Tokens](#generating-api-tokens)
- [Available Endpoints](#available-endpoints)
- [Integration Examples](#integration-examples)
- [Homepage Integration](#homepage-integration)
- [Home Assistant Integration](#home-assistant-integration)
- [Contributing](#contributing)
- [License](#license)
---
## Overview
**ProxMenux Monitor** is a comprehensive, real-time monitoring dashboard for Proxmox VE environments. Built with modern web technologies, it provides an intuitive interface to monitor system resources, virtual machines, containers, storage, network traffic, and system logs.
The application runs as a standalone AppImage on your Proxmox server and serves a web interface accessible from any device on your network.
## Screenshots
Get a quick overview of ProxMenux Monitor's main features:
<p align="center">
<img src="public/images/onboarding/imagen1.png" alt="Overview Dashboard" width="800"/>
<br/>
<em>System Overview - Monitor CPU, memory, temperature, and uptime in real-time</em>
</p>
<p align="center">
<img src="public/images/onboarding/imagen2.png" alt="Storage Management" width="800"/>
<br/>
<em>Storage Management - Visual representation of disk usage and health</em>
</p>
<p align="center">
<img src="public/images/onboarding/imagen3.png" alt="Network Monitoring" width="800"/>
<br/>
<em>Network Monitoring - Real-time traffic graphs and interface statistics</em>
</p>
<p align="center">
<img src="public/images/onboarding/imagen4.png" alt="Virtual Machines & LXC" width="800"/>
<br/>
<em>VMs & LXC Containers - Comprehensive view with resource usage and controls</em>
</p>
<p align="center">
<img src="public/images/onboarding/imagen5.png" alt="Hardware Information" width="800"/>
<br/>
<em>Hardware Information - Detailed specs for CPU, GPU, and PCIe devices</em>
</p>
<p align="center">
<img src="public/images/onboarding/imagen6.png" alt="System Logs" width="800"/>
<br/>
<em>System Logs - Real-time monitoring with filtering and search</em>
</p>
---
## Features
- **System Overview**: Real-time monitoring of CPU, memory, temperature, and active VMs/LXC containers
- **Storage Management**: Visual representation of storage distribution and disk performance metrics
- **Network Monitoring**: Network interface statistics and performance graphs
- **Virtual Machines**: Comprehensive view of VMs and LXC containers with resource usage
- **System Logs**: Real-time system log monitoring and filtering
- **System Overview**: Real-time monitoring of CPU, memory, temperature, and system uptime
- **Storage Management**: Visual representation of storage distribution, disk health, and SMART data
- **Network Monitoring**: Network interface statistics, real-time traffic graphs, and bandwidth usage
- **Virtual Machines & LXC**: Comprehensive view of all VMs and containers with resource usage and controls
- **Hardware Information**: Detailed hardware specifications including CPU, GPU, PCIe devices, and disks
- **System Logs**: Real-time system log monitoring with filtering and search capabilities
- **Health Monitoring**: Proactive system health checks with persistent error tracking
- **Authentication & 2FA**: Optional password protection with TOTP-based two-factor authentication
- **RESTful API**: Complete API access for integrations with Homepage, Home Assistant, and custom dashboards
- **Dark/Light Theme**: Toggle between themes with Proxmox-inspired design
- **Responsive Design**: Works seamlessly on desktop and mobile devices
- **Onboarding Experience**: Interactive welcome carousel for first-time users
- **Responsive Design**: Works seamlessly on desktop, tablet, and mobile devices
- **Release Notes**: Automatic notifications of new features and improvements
## Technology Stack
- **Frontend**: Next.js 15, React 19, TypeScript
- **Styling**: Tailwind CSS with custom Proxmox-inspired theme
- **Styling**: Tailwind CSS v4 with custom Proxmox-inspired theme
- **Charts**: Recharts for data visualization
- **UI Components**: Radix UI primitives with shadcn/ui
- **Backend**: Flask server for system data collection
- **Packaging**: AppImage for easy distribution
- **Backend**: Flask (Python) server for system data collection
- **Packaging**: AppImage for easy distribution and deployment
## Onboarding Images
## Installation
To customize the onboarding experience, place your screenshot images in `public/images/onboarding/`:
**ProxMenux Monitor is integrated into [ProxMenux](https://proxmenux.com) and comes enabled by default.** No manual installation is required if you're using ProxMenux.
- `imagen1.png` - Overview section screenshot
- `imagen2.png` - Storage section screenshot
- `imagen3.png` - Network section screenshot
- `imagen4.png` - VMs & LXCs section screenshot
- `imagen5.png` - Hardware section screenshot
- `imagen6.png` - System Logs section screenshot
The monitor automatically starts when ProxMenux is installed and runs as a systemd service on your Proxmox server.
**Recommended image specifications:**
- Format: PNG or JPG
- Size: 1200x800px or similar 3:2 aspect ratio
- Quality: High-quality screenshots with representative data
### Accessing the Dashboard
The onboarding carousel will automatically show on first visit and can be dismissed or marked as "Don't show again".
You can access ProxMenux Monitor in two ways:
1. **Direct Access**: `http://your-proxmox-ip:8008`
2. **Via Proxy** (Recommended): `https://your-domain.com/proxmenux-monitor/`
**Note**: All API endpoints work seamlessly with both direct access and proxy configurations. When using a reverse proxy, the application automatically detects and adapts to the proxied environment.
### Proxy Configuration
ProxMenux Monitor includes built-in support for reverse proxy configurations. If you're using Nginx, Caddy, or Traefik, the application will automatically:
- Detect the proxy headers (`X-Forwarded-For`, `X-Forwarded-Proto`, `X-Forwarded-Host`)
- Adjust API endpoints to work correctly through the proxy
- Maintain full functionality for all features including authentication and API access
**Example Nginx configuration:**
```nginx
location /proxmenux-monitor/ {
proxy_pass http://localhost:8008/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
}
```
## Authentication & Security
ProxMenux Monitor includes an optional authentication system to protect your dashboard with a password and two-factor authentication.
### Setup Authentication
On first launch, you'll be presented with three options:
1. **Set up authentication** - Create a username and password to protect your dashboard
2. **Enable 2FA** - Add TOTP-based two-factor authentication for enhanced security
3. **Skip** - Continue without authentication (not recommended for production environments)
![Authentication Setup](AppImage/public/images/docs/auth-setup.png)
### Two-Factor Authentication (2FA)
After setting up your password, you can enable 2FA using any TOTP authenticator app (Google Authenticator, Authy, 1Password, etc.):
1. Navigate to **Settings > Authentication**
2. Click **Enable 2FA**
3. Scan the QR code with your authenticator app
4. Enter the 6-digit code to verify
5. Save your backup codes in a secure location
![2FA Setup](AppImage/public/images/docs/2fa-setup.png)
### Security Best Practices for API Tokens
**IMPORTANT**: Never hardcode your API tokens directly in configuration files or scripts. Instead, use environment variables or secrets management.
**Option 1: Environment Variables**
Store your token in an environment variable:
```bash
# Linux/macOS - Add to ~/.bashrc or ~/.zshrc
export PROXMENUX_API_TOKEN="your_actual_token_here"
# Windows PowerShell - Add to profile
$env:PROXMENUX_API_TOKEN = "your_actual_token_here"
```
Then reference it in your scripts:
```bash
# Linux/macOS
curl -H "Authorization: Bearer $PROXMENUX_API_TOKEN" \
http://your-proxmox-ip:8008/api/system
# Windows PowerShell
curl -H "Authorization: Bearer $env:PROXMENUX_API_TOKEN" `
http://your-proxmox-ip:8008/api/system
```
**Option 2: Secrets File**
Create a dedicated secrets file (make sure to add it to `.gitignore`):
```bash
# Create secrets file
echo "PROXMENUX_API_TOKEN=your_actual_token_here" > ~/.proxmenux_secrets
# Secure the file (Linux/macOS only)
chmod 600 ~/.proxmenux_secrets
# Load in your script
source ~/.proxmenux_secrets
```
**Option 3: Homepage Secrets (Recommended)**
Homepage supports secrets management. Create a `secrets.yaml` file:
```yaml
# secrets.yaml (add to .gitignore!)
proxmenux_token: "your_actual_token_here"
```
Then reference it in your `services.yaml`:
```yaml
- ProxMenux Monitor:
widget:
type: customapi
url: http://proxmox.example.tld:8008/api/system
headers:
Authorization: Bearer {{HOMEPAGE_VAR_PROXMENUX_TOKEN}}
```
**Option 4: Home Assistant Secrets**
Home Assistant has built-in secrets support. Edit `secrets.yaml`:
```yaml
# secrets.yaml
proxmenux_api_token: "your_actual_token_here"
```
Then reference it in `configuration.yaml`:
```yaml
sensor:
- platform: rest
name: ProxMenux CPU
resource: http://proxmox.example.tld:8008/api/system
headers:
Authorization: !secret proxmenux_api_token
```
**Token Security Checklist:**
- ✅ Store tokens in environment variables or secrets files
- ✅ Add secrets files to `.gitignore`
- ✅ Set proper file permissions (chmod 600 on Linux/macOS)
- ✅ Rotate tokens periodically (every 3-6 months)
- ✅ Use different tokens for different integrations
- ✅ Delete tokens you no longer use
- ❌ Never commit tokens to version control
- ❌ Never share tokens in screenshots or logs
- ❌ Never hardcode tokens in configuration files
---
## API Documentation
ProxMenux Monitor provides a comprehensive RESTful API for integrating with external services like Homepage, Home Assistant, or custom dashboards.
### API Authentication
When authentication is enabled on ProxMenux Monitor, all API endpoints (except `/api/health` and `/api/auth/*`) require a valid JWT token in the `Authorization` header.
### API Endpoint Base URL
**Direct Access:**
```
http://your-proxmox-ip:8008/api/
```
**Via Proxy:**
```
https://your-domain.com/proxmenux-monitor/api/
```
**Note**: All API examples in this documentation work with both direct and proxied URLs. Simply replace the base URL with your preferred access method.
### Generating API Tokens
To use the API with authentication enabled, you need to generate a long-lived API token.
#### Option 1: Generate via Web Panel (Recommended)
The easiest way to generate an API token is through the ProxMenux Monitor web interface:
1. Navigate to **Settings** tab in the dashboard
2. Scroll to the **API Access Tokens** section
3. Enter your password
4. If 2FA is enabled, enter your 6-digit code
5. Provide a name for the token (e.g., "Homepage Integration")
6. Click **Generate Token**
7. Copy the token immediately - it will not be shown again
![Generate API Token](AppImage/public/images/docs/generate-api-token.png)
The token will be valid for **365 days** (1 year) and can be used for integrations with Homepage, Home Assistant, or any custom application.
#### Option 2: Generate via API Call
For advanced users or automation, you can generate tokens programmatically:
```bash
curl -X POST http://your-proxmox-ip:8008/api/auth/generate-api-token \
-H "Content-Type: application/json" \
-d '{
"username": "your-username",
"password": "your-password",
"totp_token": "123456",
"token_name": "Homepage Integration"
}'
```
**Response:**
```json
{
"success": true,
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_name": "Homepage Integration",
"expires_in": "365 days",
"message": "API token generated successfully. Store this token securely, it will not be shown again."
}
```
**Notes:**
- If 2FA is enabled, include the `totp_token` field with your 6-digit code
- If 2FA is not enabled, omit the `totp_token` field
- The token is valid for **365 days** (1 year)
- Store the token securely - it cannot be retrieved again
#### Option 3: Generate via cURL (without 2FA)
```bash
# Without 2FA
curl -X POST http://your-proxmox-ip:8008/api/auth/generate-api-token \
-H "Content-Type: application/json" \
-d '{"username":"pedro","password":"your-password","token_name":"Homepage"}'
```
### Using API Tokens
Once you have your API token, include it in the `Authorization` header of all API requests:
```bash
curl -H "Authorization: Bearer YOUR_API_TOKEN_HERE" \
http://your-proxmox-ip:8008/api/system
```
---
### Available Endpoints
Below is a complete list of all API endpoints with descriptions and example responses.
#### System & Metrics
| Endpoint | Method | Auth Required | Description |
|----------|--------|---------------|-------------|
| `/api/system` | GET | Yes | Complete system information (CPU, memory, temperature, uptime) |
| `/api/system-info` | GET | No | Lightweight system info for header (hostname, uptime, health) |
| `/api/node/metrics` | GET | Yes | Historical metrics data (RRD) for CPU, memory, disk I/O |
| `/api/prometheus` | GET | Yes | Export metrics in Prometheus format |
**Example `/api/system` Response:**
```json
{
"hostname": "pve",
"cpu_usage": 15.2,
"memory_usage": 45.8,
"temperature": 42.5,
"uptime": 345600,
"kernel": "6.2.16-3-pve",
"pve_version": "8.0.3"
}
```
#### Storage
| Endpoint | Method | Auth Required | Description |
|----------|--------|---------------|-------------|
| `/api/storage` | GET | Yes | Complete storage information with SMART data |
| `/api/storage/summary` | GET | Yes | Optimized storage summary (without SMART) |
| `/api/proxmox-storage` | GET | Yes | Proxmox storage pools information |
| `/api/backups` | GET | Yes | List of all backup files |
**Example `/api/storage/summary` Response:**
```json
{
"total_capacity": 1431894917120,
"used_space": 197414092800,
"free_space": 1234480824320,
"usage_percentage": 13.8,
"disks": [
{
"device": "/dev/sda",
"model": "Samsung SSD 970",
"size": "476.94 GB",
"type": "SSD"
}
]
}
```
#### Network
| Endpoint | Method | Auth Required | Description |
|----------|--------|---------------|-------------|
| `/api/network` | GET | Yes | Complete network information for all interfaces |
| `/api/network/summary` | GET | Yes | Optimized network summary |
| `/api/network/<interface>/metrics` | GET | Yes | Historical metrics (RRD) for specific interface |
**Example `/api/network/summary` Response:**
```json
{
"interfaces": [
{
"name": "vmbr0",
"ip": "192.168.1.100",
"state": "up",
"rx_bytes": 1234567890,
"tx_bytes": 987654321
}
]
}
```
#### Virtual Machines & Containers
| Endpoint | Method | Auth Required | Description |
|----------|--------|---------------|-------------|
| `/api/vms` | GET | Yes | List of all VMs and LXC containers |
| `/api/vms/<vmid>` | GET | Yes | Detailed configuration for specific VM/LXC |
| `/api/vms/<vmid>/metrics` | GET | Yes | Historical metrics (RRD) for specific VM/LXC |
| `/api/vms/<vmid>/logs` | GET | Yes | Download real logs for specific VM/LXC |
| `/api/vms/<vmid>/control` | POST | Yes | Control VM/LXC (start, stop, shutdown, reboot) |
| `/api/vms/<vmid>/config` | PUT | Yes | Update VM/LXC configuration (description/notes) |
**Example `/api/vms` Response:**
```json
{
"vms": [
{
"vmid": "100",
"name": "ubuntu-server",
"type": "qemu",
"status": "running",
"cpu": 2,
"maxcpu": 4,
"mem": 2147483648,
"maxmem": 4294967296,
"uptime": 86400
}
]
}
```
#### Hardware
| Endpoint | Method | Auth Required | Description |
|----------|--------|---------------|-------------|
| `/api/hardware` | GET | Yes | Complete hardware information (CPU, GPU, PCIe, disks) |
| `/api/gpu/<slot>/realtime` | GET | Yes | Real-time monitoring for specific GPU |
**Example `/api/hardware` Response:**
```json
{
"cpu": {
"model": "AMD Ryzen 9 5950X",
"cores": 16,
"threads": 32,
"frequency": "3.4 GHz"
},
"gpus": [
{
"slot": "0000:01:00.0",
"vendor": "NVIDIA",
"model": "GeForce RTX 3080",
"driver": "nvidia"
}
]
}
```
#### Logs, Events & Notifications
| Endpoint | Method | Auth Required | Description |
|----------|--------|---------------|-------------|
| `/api/logs` | GET | Yes | System logs (journalctl) with filters |
| `/api/logs/download` | GET | Yes | Download logs as text file |
| `/api/notifications` | GET | Yes | Proxmox notification history |
| `/api/notifications/download` | GET | Yes | Download full notification log |
| `/api/events` | GET | Yes | Recent Proxmox tasks and events |
| `/api/task-log/<upid>` | GET | Yes | Full log for specific task using UPID |
**Example `/api/logs` Query Parameters:**
```
/api/logs?severity=error&since=1h&search=failed
```
#### Health Monitoring
| Endpoint | Method | Auth Required | Description |
|----------|--------|---------------|-------------|
| `/api/health` | GET | No | Basic health check (for external monitoring) |
| `/api/health/status` | GET | Yes | Summary of system health status |
| `/api/health/details` | GET | Yes | Detailed health check results |
| `/api/health/acknowledge` | POST | Yes | Dismiss/acknowledge health warnings |
| `/api/health/active-errors` | GET | Yes | Get active persistent errors |
#### ProxMenux Optimizations
| Endpoint | Method | Auth Required | Description |
|----------|--------|---------------|-------------|
| `/api/proxmenux/installed-tools` | GET | Yes | List of installed ProxMenux optimizations |
#### Authentication
| Endpoint | Method | Auth Required | Description |
|----------|--------|---------------|-------------|
| `/api/auth/status` | GET | No | Current authentication status |
| `/api/auth/login` | POST | No | Authenticate and receive JWT token |
| `/api/auth/generate-api-token` | POST | No | Generate long-lived API token (365 days) |
| `/api/auth/setup` | POST | No | Initial setup of username/password |
| `/api/auth/enable` | POST | No | Enable authentication |
| `/api/auth/disable` | POST | Yes | Disable authentication |
| `/api/auth/change-password` | POST | No | Change password |
| `/api/auth/totp/setup` | POST | Yes | Initialize 2FA setup |
| `/api/auth/totp/enable` | POST | Yes | Enable 2FA after verification |
| `/api/auth/totp/disable` | POST | Yes | Disable 2FA |
---
## Integration Examples
### Homepage Integration
[Homepage](https://gethomepage.dev/) is a modern, fully static, fast, secure fully proxied, highly customizable application dashboard.
#### Basic Configuration (No Authentication)
```yaml
- ProxMenux Monitor:
href: http://proxmox.example.tld:8008/
icon: lucide:flask-round
widget:
type: customapi
url: http://proxmox.example.tld:8008/api/system
refreshInterval: 10000
mappings:
- field: uptime
label: Uptime
icon: lucide:clock-4
format: text
- field: cpu_usage
label: CPU
icon: lucide:cpu
format: percent
- field: memory_usage
label: RAM
icon: lucide:memory-stick
format: percent
- field: temperature
label: Temp
icon: lucide:thermometer-sun
format: number
suffix: °C
```
#### With Authentication Enabled (Using Secrets)
First, generate an API token via the web interface (Settings > API Access Tokens) or via API.
Then, store your token securely in Homepage's `secrets.yaml`:
```yaml
# secrets.yaml (add to .gitignore!)
proxmenux_token: "your_actual_api_token_here"
```
Finally, reference the secret in your `services.yaml`:
```yaml
- ProxMenux Monitor:
href: http://proxmox.example.tld:8008/
icon: lucide:flask-round
widget:
type: customapi
url: http://proxmox.example.tld:8008/api/system
headers:
Authorization: Bearer {{HOMEPAGE_VAR_PROXMENUX_TOKEN}}
refreshInterval: 10000
mappings:
- field: uptime
label: Uptime
icon: lucide:clock-4
format: text
- field: cpu_usage
label: CPU
icon: lucide:cpu
format: percent
- field: memory_usage
label: RAM
icon: lucide:memory-stick
format: percent
- field: temperature
label: Temp
icon: lucide:thermometer-sun
format: number
suffix: °C
```
#### Advanced Multi-Widget Configuration
```yaml
# Store token in secrets.yaml
# proxmenux_token: "your_actual_api_token_here"
- ProxMenux System:
href: http://proxmox.example.tld:8008/
icon: lucide:server
description: Proxmox VE Host
widget:
type: customapi
url: http://proxmox.example.tld:8008/api/system
headers:
Authorization: Bearer {{HOMEPAGE_VAR_PROXMENUX_TOKEN}}
refreshInterval: 5000
mappings:
- field: cpu_usage
label: CPU
icon: lucide:cpu
format: percent
- field: memory_usage
label: RAM
icon: lucide:memory-stick
format: percent
- field: temperature
label: Temp
icon: lucide:thermometer-sun
format: number
suffix: °C
- ProxMenux Storage:
href: http://proxmox.example.tld:8008/#/storage
icon: lucide:hard-drive
description: Storage Overview
widget:
type: customapi
url: http://proxmox.example.tld:8008/api/storage/summary
headers:
Authorization: Bearer {{HOMEPAGE_VAR_PROXMENUX_TOKEN}}
refreshInterval: 30000
mappings:
- field: usage_percentage
label: Used
icon: lucide:database
format: percent
- field: used_space
label: Space
icon: lucide:folder
format: bytes
- ProxMenux Network:
href: http://proxmox.example.tld:8008/#/network
icon: lucide:network
description: Network Stats
widget:
type: customapi
url: http://proxmox.example.tld:8008/api/network/summary
headers:
Authorization: Bearer {{HOMEPAGE_VAR_PROXMENUX_TOKEN}}
refreshInterval: 5000
mappings:
- field: interfaces[0].rx_bytes
label: Received
icon: lucide:download
format: bytes
- field: interfaces[0].tx_bytes
label: Sent
icon: lucide:upload
format: bytes
```
![Homepage Integration Example](AppImage/public/images/docs/homepage-integration.png)
### Home Assistant Integration
[Home Assistant](https://www.home-assistant.io/) is an open-source home automation platform.
#### Store Token Securely
First, add your API token to Home Assistant's `secrets.yaml`:
```yaml
# secrets.yaml
proxmenux_api_token: "Bearer your_actual_api_token_here"
```
**Note**: Include "Bearer " prefix in the secrets file for Home Assistant.
#### Configuration.yaml
```yaml
# ProxMenux Monitor Sensors
sensor:
- platform: rest
name: ProxMenux CPU
resource: http://proxmox.example.tld:8008/api/system
headers:
Authorization: !secret proxmenux_api_token
value_template: "{{ value_json.cpu_usage }}"
unit_of_measurement: "%"
scan_interval: 30
- platform: rest
name: ProxMenux Memory
resource: http://proxmox.example.tld:8008/api/system
headers:
Authorization: !secret proxmenux_api_token
value_template: "{{ value_json.memory_usage }}"
unit_of_measurement: "%"
scan_interval: 30
- platform: rest
name: ProxMenux Temperature
resource: http://proxmox.example.tld:8008/api/system
headers:
Authorization: !secret proxmenux_api_token
value_template: "{{ value_json.temperature }}"
unit_of_measurement: "°C"
device_class: temperature
scan_interval: 30
- platform: rest
name: ProxMenux Uptime
resource: http://proxmox.example.tld:8008/api/system
headers:
Authorization: !secret proxmenux_api_token
value_template: >
{% set uptime_seconds = value_json.uptime | int %}
{% set days = (uptime_seconds / 86400) | int %}
{% set hours = ((uptime_seconds % 86400) / 3600) | int %}
{% set minutes = ((uptime_seconds % 3600) / 60) | int %}
{{ days }}d {{ hours }}h {{ minutes }}m
scan_interval: 60
```
#### Lovelace Card Example
```yaml
type: entities
title: Proxmox Monitor
entities:
- entity: sensor.proxmenux_cpu
name: CPU Usage
icon: mdi:cpu-64-bit
- entity: sensor.proxmenux_memory
name: Memory Usage
icon: mdi:memory
- entity: sensor.proxmenux_temperature
name: Temperature
icon: mdi:thermometer
- entity: sensor.proxmenux_uptime
name: Uptime
icon: mdi:clock-outline
```
![Home Assistant Integration Example](AppImage/public/images/docs/homeassistant-integration.png)
---
## Contributing
Contributions are welcome! Please feel free to submit issues, feature requests, or pull requests.
### Development Setup
1. Clone the repository
2. Install dependencies: `npm install`
3. Run development server: `npm run dev`
4. Build AppImage: `./build_appimage.sh`
---
## License
This project is licensed under the **Creative Commons Attribution-NonCommercial 4.0 International License (CC BY-NC 4.0)**.
You are free to:
- Share — copy and redistribute the material in any medium or format
- Adapt — remix, transform, and build upon the material
Under the following terms:
- Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made
- NonCommercial — You may not use the material for commercial purposes
For more details, see the [full license](https://creativecommons.org/licenses/by-nc/4.0/).
---
## Support
For support, feature requests, or bug reports, please visit:
- GitHub Issues: [github.com/your-repo/issues](https://github.com/your-repo/issues)
- Documentation: [github.com/your-repo/wiki](https://github.com/your-repo/wiki)
---
**ProxMenux Monitor** - Made with ❤️ for the Proxmox community
+79 -1
View File
@@ -1,7 +1,85 @@
"use client"
import { useState, useEffect } from "react"
import { ProxmoxDashboard } from "../components/proxmox-dashboard"
import { Login } from "../components/login"
import { AuthSetup } from "../components/auth-setup"
import { getApiUrl } from "../lib/api-config"
export default function Home() {
return <ProxmoxDashboard />
const [authStatus, setAuthStatus] = useState<{
loading: boolean
authEnabled: boolean
authConfigured: boolean
authenticated: boolean
}>({
loading: true,
authEnabled: false,
authConfigured: false,
authenticated: false,
})
useEffect(() => {
checkAuthStatus()
}, [])
const checkAuthStatus = async () => {
try {
const token = localStorage.getItem("proxmenux-auth-token")
const response = await fetch(getApiUrl("/api/auth/status"), {
headers: token ? { Authorization: `Bearer ${token}` } : {},
})
const data = await response.json()
console.log("[v0] Auth status:", data)
const authenticated = data.auth_enabled ? data.authenticated : true
setAuthStatus({
loading: false,
authEnabled: data.auth_enabled,
authConfigured: data.auth_configured,
authenticated,
})
} catch (error) {
console.error("[v0] Failed to check auth status:", error)
setAuthStatus({
loading: false,
authEnabled: false,
authConfigured: false,
authenticated: true,
})
}
}
const handleAuthComplete = () => {
checkAuthStatus()
}
const handleLoginSuccess = () => {
checkAuthStatus()
}
if (authStatus.loading) {
return (
<div className="min-h-screen bg-background flex items-center justify-center">
<div className="text-center space-y-4">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto"></div>
<p className="text-muted-foreground">Loading...</p>
</div>
</div>
)
}
if (authStatus.authEnabled && !authStatus.authenticated) {
return <Login onLogin={handleLoginSuccess} />
}
// Show dashboard in all other cases
return (
<>
{!authStatus.authConfigured && <AuthSetup onComplete={handleAuthComplete} />}
<ProxmoxDashboard />
</>
)
}
+257
View File
@@ -0,0 +1,257 @@
"use client"
import { useState, useEffect } from "react"
import { Button } from "./ui/button"
import { Dialog, DialogContent } from "./ui/dialog"
import { Input } from "./ui/input"
import { Label } from "./ui/label"
import { Shield, Lock, User, AlertCircle } from "lucide-react"
import { getApiUrl } from "../lib/api-config"
interface AuthSetupProps {
onComplete: () => void
}
export function AuthSetup({ onComplete }: AuthSetupProps) {
const [open, setOpen] = useState(false)
const [step, setStep] = useState<"choice" | "setup">("choice")
const [username, setUsername] = useState("")
const [password, setPassword] = useState("")
const [confirmPassword, setConfirmPassword] = useState("")
const [error, setError] = useState("")
const [loading, setLoading] = useState(false)
useEffect(() => {
const checkOnboardingStatus = async () => {
try {
const response = await fetch(getApiUrl("/api/auth/status"))
const data = await response.json()
console.log("[v0] Auth status for modal check:", data)
// Show modal if auth is not configured and not declined
if (!data.auth_configured) {
setTimeout(() => setOpen(true), 500)
}
} catch (error) {
console.error("[v0] Failed to check auth status:", error)
// Fail-safe: show modal if we can't check status
setTimeout(() => setOpen(true), 500)
}
}
checkOnboardingStatus()
}, [])
const handleSkipAuth = async () => {
setLoading(true)
setError("")
try {
console.log("[v0] Skipping authentication setup...")
const response = await fetch(getApiUrl("/api/auth/skip"), {
method: "POST",
headers: { "Content-Type": "application/json" },
})
const data = await response.json()
console.log("[v0] Auth skip response:", data)
if (!response.ok) {
throw new Error(data.error || "Failed to skip authentication")
}
if (data.auth_declined) {
console.log("[v0] Authentication skipped successfully - APIs should be accessible without token")
}
console.log("[v0] Authentication skipped successfully")
localStorage.setItem("proxmenux-auth-declined", "true")
localStorage.removeItem("proxmenux-auth-token") // Remove any old token
setOpen(false)
onComplete()
} catch (err) {
console.error("[v0] Auth skip error:", err)
setError(err instanceof Error ? err.message : "Failed to save preference")
} finally {
setLoading(false)
}
}
const handleSetupAuth = async () => {
setError("")
if (!username || !password) {
setError("Please fill in all fields")
return
}
if (password !== confirmPassword) {
setError("Passwords do not match")
return
}
if (password.length < 6) {
setError("Password must be at least 6 characters")
return
}
setLoading(true)
try {
console.log("[v0] Setting up authentication...")
const response = await fetch(getApiUrl("/api/auth/setup"), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
username,
password,
}),
})
const data = await response.json()
console.log("[v0] Auth setup response:", data)
if (!response.ok) {
throw new Error(data.error || "Failed to setup authentication")
}
if (data.token) {
localStorage.setItem("proxmenux-auth-token", data.token)
localStorage.removeItem("proxmenux-auth-declined")
console.log("[v0] Authentication setup successful")
}
setOpen(false)
onComplete()
} catch (err) {
console.error("[v0] Auth setup error:", err)
setError(err instanceof Error ? err.message : "Failed to setup authentication")
} finally {
setLoading(false)
}
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-w-md max-h-[90vh] overflow-y-auto">
{step === "choice" ? (
<div className="space-y-6 py-2">
<div className="text-center space-y-2">
<div className="mx-auto w-16 h-16 bg-blue-500/10 rounded-full flex items-center justify-center">
<Shield className="h-8 w-8 text-blue-500" />
</div>
<h2 className="text-2xl font-bold">Protect Your Dashboard?</h2>
<p className="text-muted-foreground text-sm">
Add an extra layer of security to protect your Proxmox data when accessing from non-private networks.
</p>
</div>
<div className="space-y-3">
<Button onClick={() => setStep("setup")} className="w-full bg-blue-500 hover:bg-blue-600" size="lg">
<Lock className="h-4 w-4 mr-2" />
Yes, Setup Password
</Button>
<Button
onClick={handleSkipAuth}
variant="outline"
className="w-full bg-transparent"
size="lg"
disabled={loading}
>
No, Continue Without Protection
</Button>
</div>
<p className="text-xs text-center text-muted-foreground">You can always enable this later in Settings</p>
</div>
) : (
<div className="space-y-6 py-2">
<div className="text-center space-y-2">
<div className="mx-auto w-16 h-16 bg-blue-500/10 rounded-full flex items-center justify-center">
<Lock className="h-8 w-8 text-blue-500" />
</div>
<h2 className="text-2xl font-bold">Setup Authentication</h2>
<p className="text-muted-foreground text-sm">Create a username and password to protect your dashboard</p>
</div>
{error && (
<div className="bg-red-500/10 border border-red-500/20 rounded-lg p-3 flex items-start gap-2">
<AlertCircle className="h-5 w-5 text-red-500 flex-shrink-0 mt-0.5" />
<p className="text-sm text-red-500">{error}</p>
</div>
)}
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="username" className="text-sm">
Username
</Label>
<div className="relative">
<User className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
id="username"
type="text"
placeholder="Enter username"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="pl-10 text-base"
disabled={loading}
autoComplete="username"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="password" className="text-sm">
Password
</Label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
id="password"
type="password"
placeholder="Enter password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="pl-10 text-base"
disabled={loading}
autoComplete="new-password"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="confirm-password" className="text-sm">
Confirm Password
</Label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
id="confirm-password"
type="password"
placeholder="Confirm password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="pl-10 text-base"
disabled={loading}
autoComplete="new-password"
/>
</div>
</div>
</div>
<div className="space-y-2">
<Button onClick={handleSetupAuth} className="w-full bg-blue-500 hover:bg-blue-600" disabled={loading}>
{loading ? "Setting up..." : "Setup Authentication"}
</Button>
<Button onClick={() => setStep("choice")} variant="ghost" className="w-full" disabled={loading}>
Back
</Button>
</div>
</div>
)}
</DialogContent>
</Dialog>
)
}
+354 -158
View File
@@ -20,7 +20,14 @@ import {
} from "lucide-react"
import useSWR from "swr"
import { useState, useEffect } from "react"
import { type HardwareData, type GPU, type PCIDevice, type StorageDevice, fetcher } from "../types/hardware"
import {
type HardwareData,
type GPU,
type PCIDevice,
type StorageDevice,
fetcher as swrFetcher,
} from "../types/hardware"
import { fetchApi } from "@/lib/api-config"
const parseLsblkSize = (sizeStr: string | undefined): number => {
if (!sizeStr) return 0
@@ -64,9 +71,12 @@ const formatMemory = (memoryKB: number | string): string => {
return `${tb.toFixed(1)} TB`
}
// Convert to GB if >= 1024 MB
if (mb >= 1024) {
const gb = mb / 1024
// If GB value is greater than 999, convert to TB
if (gb > 999) {
return `${(gb / 1024).toFixed(2)} TB`
}
return `${gb.toFixed(1)} GB`
}
@@ -160,14 +170,65 @@ const groupAndSortTemperatures = (temperatures: any[]) => {
}
export default function Hardware() {
// Static data - load once without refresh
const {
data: hardwareData,
error,
isLoading,
} = useSWR<HardwareData>("/api/hardware", fetcher, {
refreshInterval: 5000,
data: staticHardwareData,
error: staticError,
isLoading: staticLoading,
} = useSWR<HardwareData>("/api/hardware", swrFetcher, {
revalidateOnFocus: false,
revalidateOnReconnect: false,
refreshInterval: 0, // No auto-refresh for static data
})
// Dynamic data - refresh every 5 seconds for temperatures, fans, power, ups
const {
data: dynamicHardwareData,
error: dynamicError,
isLoading: dynamicLoading,
} = useSWR<HardwareData>("/api/hardware", swrFetcher, {
refreshInterval: 7000,
})
// Merge static and dynamic data, preferring static for CPU/memory/PCI/disks
const hardwareData = staticHardwareData
? {
...dynamicHardwareData,
// Keep static data from initial load
cpu: staticHardwareData.cpu,
motherboard: staticHardwareData.motherboard,
memory_modules: staticHardwareData.memory_modules,
pci_devices: staticHardwareData.pci_devices,
storage_devices: staticHardwareData.storage_devices,
gpus: staticHardwareData.gpus,
// Use dynamic data for these
temperatures: dynamicHardwareData?.temperatures,
fans: dynamicHardwareData?.fans,
power_meter: dynamicHardwareData?.power_meter,
power_supplies: dynamicHardwareData?.power_supplies,
ups: dynamicHardwareData?.ups,
}
: undefined
const error = staticError || dynamicError
const isLoading = staticLoading
useEffect(() => {
if (hardwareData?.storage_devices) {
console.log("[v0] Storage devices data from backend:", hardwareData.storage_devices)
hardwareData.storage_devices.forEach((device) => {
if (device.name.startsWith("nvme")) {
console.log(`[v0] NVMe device ${device.name}:`, {
pcie_gen: device.pcie_gen,
pcie_width: device.pcie_width,
pcie_max_gen: device.pcie_max_gen,
pcie_max_width: device.pcie_max_width,
})
}
})
}
}, [hardwareData])
const [selectedGPU, setSelectedGPU] = useState<GPU | null>(null)
const [realtimeGPUData, setRealtimeGPUData] = useState<any>(null)
const [detailsLoading, setDetailsLoading] = useState(false)
@@ -176,6 +237,21 @@ export default function Hardware() {
const [selectedNetwork, setSelectedNetwork] = useState<PCIDevice | null>(null)
const [selectedUPS, setSelectedUPS] = useState<any>(null)
const fetcher = async (url: string) => {
const data = await fetchApi(url)
return data
}
const {
data: hardwareDataSWR,
error: swrError,
isLoading: swrLoading,
mutate,
} = useSWR<HardwareData>("/api/hardware", fetcher, {
refreshInterval: 30000,
revalidateOnFocus: false,
})
useEffect(() => {
if (!selectedGPU) return
@@ -188,25 +264,10 @@ export default function Hardware() {
const fetchRealtimeData = async () => {
try {
const apiUrl = `http://${window.location.hostname}:8008/api/gpu/${fullSlot}/realtime`
const response = await fetch(apiUrl, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
signal: abortController.signal,
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const data = await response.json()
const data = await fetchApi(`/api/gpu/${fullSlot}/realtime`)
setRealtimeGPUData(data)
setDetailsLoading(false)
} catch (error) {
// Only log non-abort errors
if (error instanceof Error && error.name !== "AbortError") {
console.error("[v0] Error fetching GPU realtime data:", error)
}
@@ -215,10 +276,7 @@ export default function Hardware() {
}
}
// Initial fetch
fetchRealtimeData()
// Poll every 3 seconds
const interval = setInterval(fetchRealtimeData, 3000)
return () => {
@@ -234,14 +292,14 @@ export default function Hardware() {
}
const findPCIDeviceForGPU = (gpu: GPU): PCIDevice | null => {
if (!hardwareData?.pci_devices || !gpu.slot) return null
if (!hardwareDataSWR?.pci_devices || !gpu.slot) return null
// Try to find exact match first (e.g., "00:02.0")
let pciDevice = hardwareData.pci_devices.find((d) => d.slot === gpu.slot)
let pciDevice = hardwareDataSWR.pci_devices.find((d) => d.slot === gpu.slot)
// If not found, try to match by partial slot (e.g., "00" matches "00:02.0")
if (!pciDevice && gpu.slot.length <= 2) {
pciDevice = hardwareData.pci_devices.find(
pciDevice = hardwareDataSWR.pci_devices.find(
(d) =>
d.slot.startsWith(gpu.slot + ":") &&
(d.type.toLowerCase().includes("vga") ||
@@ -260,7 +318,7 @@ export default function Hardware() {
return realtimeGPUData.has_monitoring_tool === true
}
if (isLoading) {
if (swrLoading) {
return (
<div className="space-y-6">
<div className="text-center py-8">
@@ -273,7 +331,7 @@ export default function Hardware() {
return (
<div className="space-y-6">
{/* System Information - CPU & Motherboard */}
{(hardwareData?.cpu || hardwareData?.motherboard) && (
{(hardwareDataSWR?.cpu || hardwareDataSWR?.motherboard) && (
<Card className="border-border/50 bg-card/50 p-6">
<div className="mb-4 flex items-center gap-2">
<Cpu className="h-5 w-5 text-primary" />
@@ -282,44 +340,44 @@ export default function Hardware() {
<div className="grid gap-6 md:grid-cols-2">
{/* CPU Info */}
{hardwareData?.cpu && Object.keys(hardwareData.cpu).length > 0 && (
{hardwareDataSWR?.cpu && Object.keys(hardwareDataSWR.cpu).length > 0 && (
<div>
<div className="mb-2 flex items-center gap-2">
<CpuIcon className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-semibold">CPU</h3>
</div>
<div className="space-y-2">
{hardwareData.cpu.model && (
{hardwareDataSWR.cpu.model && (
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Model</span>
<span className="font-medium text-right">{hardwareData.cpu.model}</span>
<span className="font-medium text-right">{hardwareDataSWR.cpu.model}</span>
</div>
)}
{hardwareData.cpu.cores_per_socket && hardwareData.cpu.sockets && (
{hardwareDataSWR.cpu.cores_per_socket && hardwareDataSWR.cpu.sockets && (
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Cores</span>
<span className="font-medium">
{hardwareData.cpu.sockets} × {hardwareData.cpu.cores_per_socket} ={" "}
{hardwareData.cpu.sockets * hardwareData.cpu.cores_per_socket} cores
{hardwareDataSWR.cpu.sockets} × {hardwareDataSWR.cpu.cores_per_socket} ={" "}
{hardwareDataSWR.cpu.sockets * hardwareDataSWR.cpu.cores_per_socket} cores
</span>
</div>
)}
{hardwareData.cpu.total_threads && (
{hardwareDataSWR.cpu.total_threads && (
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Threads</span>
<span className="font-medium">{hardwareData.cpu.total_threads}</span>
<span className="font-medium">{hardwareDataSWR.cpu.total_threads}</span>
</div>
)}
{hardwareData.cpu.l3_cache && (
{hardwareDataSWR.cpu.l3_cache && (
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">L3 Cache</span>
<span className="font-medium">{hardwareData.cpu.l3_cache}</span>
<span className="font-medium">{hardwareDataSWR.cpu.l3_cache}</span>
</div>
)}
{hardwareData.cpu.virtualization && (
{hardwareDataSWR.cpu.virtualization && (
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Virtualization</span>
<span className="font-medium">{hardwareData.cpu.virtualization}</span>
<span className="font-medium">{hardwareDataSWR.cpu.virtualization}</span>
</div>
)}
</div>
@@ -327,41 +385,41 @@ export default function Hardware() {
)}
{/* Motherboard Info */}
{hardwareData?.motherboard && Object.keys(hardwareData.motherboard).length > 0 && (
{hardwareDataSWR?.motherboard && Object.keys(hardwareDataSWR.motherboard).length > 0 && (
<div>
<div className="mb-2 flex items-center gap-2">
<Cpu className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-semibold">Motherboard</h3>
</div>
<div className="space-y-2">
{hardwareData.motherboard.manufacturer && (
{hardwareDataSWR.motherboard.manufacturer && (
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Manufacturer</span>
<span className="font-medium text-right">{hardwareData.motherboard.manufacturer}</span>
<span className="font-medium text-right">{hardwareDataSWR.motherboard.manufacturer}</span>
</div>
)}
{hardwareData.motherboard.model && (
{hardwareDataSWR.motherboard.model && (
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Model</span>
<span className="font-medium text-right">{hardwareData.motherboard.model}</span>
<span className="font-medium text-right">{hardwareDataSWR.motherboard.model}</span>
</div>
)}
{hardwareData.motherboard.bios?.vendor && (
{hardwareDataSWR.motherboard.bios?.vendor && (
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">BIOS</span>
<span className="font-medium text-right">{hardwareData.motherboard.bios.vendor}</span>
<span className="font-medium text-right">{hardwareDataSWR.motherboard.bios.vendor}</span>
</div>
)}
{hardwareData.motherboard.bios?.version && (
{hardwareDataSWR.motherboard.bios?.version && (
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Version</span>
<span className="font-medium">{hardwareData.motherboard.bios.version}</span>
<span className="font-medium">{hardwareDataSWR.motherboard.bios.version}</span>
</div>
)}
{hardwareData.motherboard.bios?.date && (
{hardwareDataSWR.motherboard.bios?.date && (
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Date</span>
<span className="font-medium">{hardwareData.motherboard.bios.date}</span>
<span className="font-medium">{hardwareDataSWR.motherboard.bios.date}</span>
</div>
)}
</div>
@@ -372,18 +430,18 @@ export default function Hardware() {
)}
{/* Memory Modules */}
{hardwareData?.memory_modules && hardwareData.memory_modules.length > 0 && (
{hardwareDataSWR?.memory_modules && hardwareDataSWR.memory_modules.length > 0 && (
<Card className="border-border/50 bg-card/50 p-6">
<div className="mb-4 flex items-center gap-2">
<MemoryStick className="h-5 w-5 text-primary" />
<h2 className="text-lg font-semibold">Memory Modules</h2>
<Badge variant="outline" className="ml-auto">
{hardwareData.memory_modules.length} installed
{hardwareDataSWR.memory_modules.length} installed
</Badge>
</div>
<div className="grid gap-3 md:grid-cols-2 lg:grid-cols-3">
{hardwareData.memory_modules.map((module, index) => (
{hardwareDataSWR.memory_modules.map((module, index) => (
<div key={index} className="rounded-lg border border-border/30 bg-background/60 p-4">
<div className="mb-2 font-medium text-sm">{module.slot}</div>
<div className="space-y-1">
@@ -419,29 +477,29 @@ export default function Hardware() {
)}
{/* Thermal Monitoring */}
{hardwareData?.temperatures && hardwareData.temperatures.length > 0 && (
{hardwareDataSWR?.temperatures && hardwareDataSWR.temperatures.length > 0 && (
<Card className="border-border/50 bg-card/50 p-6">
<div className="mb-4 flex items-center gap-2">
<Thermometer className="h-5 w-5 text-primary" />
<h2 className="text-lg font-semibold">Thermal Monitoring</h2>
<Badge variant="outline" className="ml-auto">
{hardwareData.temperatures.length} sensors
{hardwareDataSWR.temperatures.length} sensors
</Badge>
</div>
<div className="grid gap-6 md:grid-cols-2">
{/* CPU Sensors */}
{groupAndSortTemperatures(hardwareData.temperatures).CPU.length > 0 && (
{groupAndSortTemperatures(hardwareDataSWR.temperatures).CPU.length > 0 && (
<div className="md:col-span-2">
<div className="mb-3 flex items-center gap-2">
<CpuIcon className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-semibold">CPU</h3>
<Badge variant="outline" className="text-xs">
{groupAndSortTemperatures(hardwareData.temperatures).CPU.length}
{groupAndSortTemperatures(hardwareDataSWR.temperatures).CPU.length}
</Badge>
</div>
<div className="grid gap-4 md:grid-cols-2">
{groupAndSortTemperatures(hardwareData.temperatures).CPU.map((temp, index) => {
{groupAndSortTemperatures(hardwareDataSWR.temperatures).CPU.map((temp, index) => {
const percentage =
temp.critical > 0 ? (temp.current / temp.critical) * 100 : (temp.current / 100) * 100
const isHot = temp.current > (temp.high || 80)
@@ -472,21 +530,21 @@ export default function Hardware() {
)}
{/* GPU Sensors */}
{groupAndSortTemperatures(hardwareData.temperatures).GPU.length > 0 && (
{groupAndSortTemperatures(hardwareDataSWR.temperatures).GPU.length > 0 && (
<div
className={groupAndSortTemperatures(hardwareData.temperatures).GPU.length > 1 ? "md:col-span-2" : ""}
className={groupAndSortTemperatures(hardwareDataSWR.temperatures).GPU.length > 1 ? "md:col-span-2" : ""}
>
<div className="mb-3 flex items-center gap-2">
<Gpu className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-semibold">GPU</h3>
<Badge variant="outline" className="text-xs">
{groupAndSortTemperatures(hardwareData.temperatures).GPU.length}
{groupAndSortTemperatures(hardwareDataSWR.temperatures).GPU.length}
</Badge>
</div>
<div
className={`grid gap-4 ${groupAndSortTemperatures(hardwareData.temperatures).GPU.length > 1 ? "md:grid-cols-2" : ""}`}
className={`grid gap-4 ${groupAndSortTemperatures(hardwareDataSWR.temperatures).GPU.length > 1 ? "md:grid-cols-2" : ""}`}
>
{groupAndSortTemperatures(hardwareData.temperatures).GPU.map((temp, index) => {
{groupAndSortTemperatures(hardwareDataSWR.temperatures).GPU.map((temp, index) => {
const percentage =
temp.critical > 0 ? (temp.current / temp.critical) * 100 : (temp.current / 100) * 100
const isHot = temp.current > (temp.high || 80)
@@ -517,21 +575,23 @@ export default function Hardware() {
)}
{/* NVME Sensors */}
{groupAndSortTemperatures(hardwareData.temperatures).NVME.length > 0 && (
{groupAndSortTemperatures(hardwareDataSWR.temperatures).NVME.length > 0 && (
<div
className={groupAndSortTemperatures(hardwareData.temperatures).NVME.length > 1 ? "md:col-span-2" : ""}
className={
groupAndSortTemperatures(hardwareDataSWR.temperatures).NVME.length > 1 ? "md:col-span-2" : ""
}
>
<div className="mb-3 flex items-center gap-2">
<HardDrive className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-semibold">NVME</h3>
<Badge variant="outline" className="text-xs">
{groupAndSortTemperatures(hardwareData.temperatures).NVME.length}
{groupAndSortTemperatures(hardwareDataSWR.temperatures).NVME.length}
</Badge>
</div>
<div
className={`grid gap-4 ${groupAndSortTemperatures(hardwareData.temperatures).NVME.length > 1 ? "md:grid-cols-2" : ""}`}
className={`grid gap-4 ${groupAndSortTemperatures(hardwareDataSWR.temperatures).NVME.length > 1 ? "md:grid-cols-2" : ""}`}
>
{groupAndSortTemperatures(hardwareData.temperatures).NVME.map((temp, index) => {
{groupAndSortTemperatures(hardwareDataSWR.temperatures).NVME.map((temp, index) => {
const percentage =
temp.critical > 0 ? (temp.current / temp.critical) * 100 : (temp.current / 100) * 100
const isHot = temp.current > (temp.high || 80)
@@ -562,21 +622,21 @@ export default function Hardware() {
)}
{/* PCI Sensors */}
{groupAndSortTemperatures(hardwareData.temperatures).PCI.length > 0 && (
{groupAndSortTemperatures(hardwareDataSWR.temperatures).PCI.length > 0 && (
<div
className={groupAndSortTemperatures(hardwareData.temperatures).PCI.length > 1 ? "md:col-span-2" : ""}
className={groupAndSortTemperatures(hardwareDataSWR.temperatures).PCI.length > 1 ? "md:col-span-2" : ""}
>
<div className="mb-3 flex items-center gap-2">
<CpuIcon className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-semibold">PCI</h3>
<Badge variant="outline" className="text-xs">
{groupAndSortTemperatures(hardwareData.temperatures).PCI.length}
{groupAndSortTemperatures(hardwareDataSWR.temperatures).PCI.length}
</Badge>
</div>
<div
className={`grid gap-4 ${groupAndSortTemperatures(hardwareData.temperatures).PCI.length > 1 ? "md:grid-cols-2" : ""}`}
className={`grid gap-4 ${groupAndSortTemperatures(hardwareDataSWR.temperatures).PCI.length > 1 ? "md:grid-cols-2" : ""}`}
>
{groupAndSortTemperatures(hardwareData.temperatures).PCI.map((temp, index) => {
{groupAndSortTemperatures(hardwareDataSWR.temperatures).PCI.map((temp, index) => {
const percentage =
temp.critical > 0 ? (temp.current / temp.critical) * 100 : (temp.current / 100) * 100
const isHot = temp.current > (temp.high || 80)
@@ -607,21 +667,23 @@ export default function Hardware() {
)}
{/* OTHER Sensors */}
{groupAndSortTemperatures(hardwareData.temperatures).OTHER.length > 0 && (
{groupAndSortTemperatures(hardwareDataSWR.temperatures).OTHER.length > 0 && (
<div
className={groupAndSortTemperatures(hardwareData.temperatures).OTHER.length > 1 ? "md:col-span-2" : ""}
className={
groupAndSortTemperatures(hardwareDataSWR.temperatures).OTHER.length > 1 ? "md:col-span-2" : ""
}
>
<div className="mb-3 flex items-center gap-2">
<Thermometer className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-semibold">OTHER</h3>
<Badge variant="outline" className="text-xs">
{groupAndSortTemperatures(hardwareData.temperatures).OTHER.length}
{groupAndSortTemperatures(hardwareDataSWR.temperatures).OTHER.length}
</Badge>
</div>
<div
className={`grid gap-4 ${groupAndSortTemperatures(hardwareData.temperatures).OTHER.length > 1 ? "md:grid-cols-2" : ""}`}
className={`grid gap-4 ${groupAndSortTemperatures(hardwareDataSWR.temperatures).OTHER.length > 1 ? "md:grid-cols-2" : ""}`}
>
{groupAndSortTemperatures(hardwareData.temperatures).OTHER.map((temp, index) => {
{groupAndSortTemperatures(hardwareDataSWR.temperatures).OTHER.map((temp, index) => {
const percentage =
temp.critical > 0 ? (temp.current / temp.critical) * 100 : (temp.current / 100) * 100
const isHot = temp.current > (temp.high || 80)
@@ -655,18 +717,18 @@ export default function Hardware() {
)}
{/* GPU Information - Enhanced with on-demand data fetching */}
{hardwareData?.gpus && hardwareData.gpus.length > 0 && (
{hardwareDataSWR?.gpus && hardwareDataSWR.gpus.length > 0 && (
<Card className="border-border/50 bg-card/50 p-6">
<div className="mb-4 flex items-center gap-2">
<Gpu className="h-5 w-5 text-primary" />
<h2 className="text-lg font-semibold">Graphics Cards</h2>
<Badge variant="outline" className="ml-auto">
{hardwareData.gpus.length} GPU{hardwareData.gpus.length > 1 ? "s" : ""}
{hardwareDataSWR.gpus.length} GPU{hardwareDataSWR.gpus.length > 1 ? "s" : ""}
</Badge>
</div>
<div className="grid gap-4 sm:grid-cols-2">
{hardwareData.gpus.map((gpu, index) => {
{hardwareDataSWR.gpus.map((gpu, index) => {
const pciDevice = findPCIDeviceForGPU(gpu)
const fullSlot = pciDevice?.slot || gpu.slot
@@ -1044,18 +1106,18 @@ export default function Hardware() {
</Dialog>
{/* PCI Devices - Changed to modal */}
{hardwareData?.pci_devices && hardwareData.pci_devices.length > 0 && (
{hardwareDataSWR?.pci_devices && hardwareDataSWR.pci_devices.length > 0 && (
<Card className="border-border/50 bg-card/50 p-6">
<div className="mb-4 flex items-center gap-2">
<CpuIcon className="h-5 w-5 text-primary" />
<h2 className="text-lg font-semibold">PCI Devices</h2>
<Badge variant="outline" className="ml-auto">
{hardwareData.pci_devices.length} devices
{hardwareDataSWR.pci_devices.length} devices
</Badge>
</div>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{hardwareData.pci_devices.map((device, index) => (
{hardwareDataSWR.pci_devices.map((device, index) => (
<div
key={index}
onClick={() => setSelectedPCIDevice(device)}
@@ -1130,7 +1192,7 @@ export default function Hardware() {
</Dialog>
{/* Power Consumption */}
{hardwareData?.power_meter && (
{hardwareDataSWR?.power_meter && (
<Card className="border-border/50 bg-card/50 p-6">
<div className="mb-4 flex items-center gap-2">
<Zap className="h-5 w-5 text-blue-500" />
@@ -1140,13 +1202,13 @@ export default function Hardware() {
<div className="space-y-4">
<div className="flex items-center justify-between rounded-lg border border-border/30 bg-background/60 p-4">
<div className="space-y-1">
<p className="text-sm font-medium">{hardwareData.power_meter.name}</p>
{hardwareData.power_meter.adapter && (
<p className="text-xs text-muted-foreground">{hardwareData.power_meter.adapter}</p>
<p className="text-sm font-medium">{hardwareDataSWR.power_meter.name}</p>
{hardwareDataSWR.power_meter.adapter && (
<p className="text-xs text-muted-foreground">{hardwareDataSWR.power_meter.adapter}</p>
)}
</div>
<div className="text-right">
<p className="text-2xl font-bold text-blue-500">{hardwareData.power_meter.watts.toFixed(1)} W</p>
<p className="text-2xl font-bold text-blue-500">{hardwareDataSWR.power_meter.watts.toFixed(1)} W</p>
<p className="text-xs text-muted-foreground">Current Draw</p>
</div>
</div>
@@ -1155,18 +1217,18 @@ export default function Hardware() {
)}
{/* Power Supplies */}
{hardwareData?.power_supplies && hardwareData.power_supplies.length > 0 && (
{hardwareDataSWR?.power_supplies && hardwareDataSWR.power_supplies.length > 0 && (
<Card className="border-border/50 bg-card/50 p-6">
<div className="mb-4 flex items-center gap-2">
<PowerIcon className="h-5 w-5 text-green-500" />
<h2 className="text-lg font-semibold">Power Supplies</h2>
<Badge variant="outline" className="ml-auto">
{hardwareData.power_supplies.length} PSUs
{hardwareDataSWR.power_supplies.length} PSUs
</Badge>
</div>
<div className="grid gap-3 md:grid-cols-2">
{hardwareData.power_supplies.map((psu, index) => (
{hardwareDataSWR.power_supplies.map((psu, index) => (
<div key={index} className="rounded-lg border border-border/30 bg-background/60 p-4">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">{psu.name}</span>
@@ -1183,18 +1245,18 @@ export default function Hardware() {
)}
{/* Fans */}
{hardwareData?.fans && hardwareData.fans.length > 0 && (
{hardwareDataSWR?.fans && hardwareDataSWR.fans.length > 0 && (
<Card className="border-border/50 bg-card/50 p-6">
<div className="mb-4 flex items-center gap-2">
<FanIcon className="h-5 w-5 text-primary" />
<h2 className="text-lg font-semibold">System Fans</h2>
<Badge variant="outline" className="ml-auto">
{hardwareData.fans.length} fans
{hardwareDataSWR.fans.length} fans
</Badge>
</div>
<div className="grid gap-4 md:grid-cols-2">
{hardwareData.fans.map((fan, index) => {
{hardwareDataSWR.fans.map((fan, index) => {
const isPercentage = fan.unit === "percent" || fan.unit === "%"
const percentage = isPercentage ? fan.speed : Math.min((fan.speed / 5000) * 100, 100)
@@ -1218,18 +1280,18 @@ export default function Hardware() {
)}
{/* UPS */}
{hardwareData?.ups && Array.isArray(hardwareData.ups) && hardwareData.ups.length > 0 && (
{hardwareDataSWR?.ups && Array.isArray(hardwareDataSWR.ups) && hardwareDataSWR.ups.length > 0 && (
<Card className="border-border/50 bg-card/50 p-6">
<div className="mb-4 flex items-center gap-2">
<Battery className="h-5 w-5 text-primary" />
<h2 className="text-lg font-semibold">UPS Status</h2>
<Badge variant="outline" className="ml-auto">
{hardwareData.ups.length} UPS
{hardwareDataSWR.ups.length} UPS
</Badge>
</div>
<div className="grid gap-4 md:grid-cols-2">
{hardwareData.ups.map((ups: any, index: number) => {
{hardwareDataSWR.ups.map((ups: any, index: number) => {
const batteryCharge =
ups.battery_charge_raw || Number.parseFloat(ups.battery_charge?.replace("%", "") || "0")
const loadPercent = ups.load_percent_raw || Number.parseFloat(ups.load_percent?.replace("%", "") || "0")
@@ -1500,19 +1562,19 @@ export default function Hardware() {
</Dialog>
{/* Network Summary - Clickable */}
{hardwareData?.pci_devices &&
hardwareData.pci_devices.filter((d) => d.type.toLowerCase().includes("network")).length > 0 && (
{hardwareDataSWR?.pci_devices &&
hardwareDataSWR.pci_devices.filter((d) => d.type.toLowerCase().includes("network")).length > 0 && (
<Card className="border-border/50 bg-card/50 p-6">
<div className="mb-4 flex items-center gap-2">
<Network className="h-5 w-5 text-primary" />
<h2 className="text-lg font-semibold">Network Summary</h2>
<Badge variant="outline" className="ml-auto">
{hardwareData.pci_devices.filter((d) => d.type.toLowerCase().includes("network")).length} interfaces
{hardwareDataSWR.pci_devices.filter((d) => d.type.toLowerCase().includes("network")).length} interfaces
</Badge>
</div>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{hardwareData.pci_devices
{hardwareDataSWR.pci_devices
.filter((d) => d.type.toLowerCase().includes("network"))
.map((device, index) => (
<div
@@ -1592,14 +1654,14 @@ export default function Hardware() {
</Dialog>
{/* Storage Summary - Clickable */}
{hardwareData?.storage_devices && hardwareData.storage_devices.length > 0 && (
{hardwareDataSWR?.storage_devices && hardwareDataSWR.storage_devices.length > 0 && (
<Card className="border-border/50 bg-card/50 p-6">
<div className="mb-4 flex items-center gap-2">
<HardDrive className="h-5 w-5 text-primary" />
<h2 className="text-lg font-semibold">Storage Summary</h2>
<Badge variant="outline" className="ml-auto">
{
hardwareData.storage_devices.filter(
hardwareDataSWR.storage_devices.filter(
(device) =>
device.type === "disk" && !device.name.startsWith("zd") && !device.name.startsWith("loop"),
).length
@@ -1609,7 +1671,7 @@ export default function Hardware() {
</div>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{hardwareData.storage_devices
{hardwareDataSWR.storage_devices
.filter(
(device) => device.type === "disk" && !device.name.startsWith("zd") && !device.name.startsWith("loop"),
)
@@ -1653,6 +1715,61 @@ export default function Hardware() {
const diskBadge = getDiskTypeBadge(device.name, device.rotation_rate)
const getLinkSpeedInfo = (device: StorageDevice) => {
// NVMe PCIe information
if (device.name.startsWith("nvme") && (device.pcie_gen || device.pcie_width)) {
const current = `${device.pcie_gen || ""} ${device.pcie_width || ""}`.trim()
const max =
device.pcie_max_gen && device.pcie_max_width
? `${device.pcie_max_gen} ${device.pcie_max_width}`.trim()
: null
const isLowerSpeed = max && current !== max
return {
text: current || null,
maxText: max,
isWarning: isLowerSpeed,
color: isLowerSpeed ? "text-orange-500" : "text-blue-500",
}
}
// SATA information
if (device.sata_version) {
return {
text: device.sata_version,
maxText: null,
isWarning: false,
color: "text-blue-500",
}
}
// SAS information
if (device.sas_version || device.sas_speed) {
const text = [device.sas_version, device.sas_speed].filter(Boolean).join(" ")
return {
text: text || null,
maxText: null,
isWarning: false,
color: "text-blue-500",
}
}
// Generic link speed
if (device.link_speed) {
return {
text: device.link_speed,
maxText: null,
isWarning: false,
color: "text-blue-500",
}
}
return null
}
const linkSpeed = getLinkSpeedInfo(device)
return (
<div
key={index}
@@ -1667,6 +1784,14 @@ export default function Hardware() {
{device.model && (
<p className="text-xs text-muted-foreground line-clamp-2 break-words">{device.model}</p>
)}
{linkSpeed && (
<div className="mt-1 flex items-center gap-1">
<span className={`text-xs font-medium ${linkSpeed.color}`}>{linkSpeed.text}</span>
{linkSpeed.maxText && linkSpeed.isWarning && (
<span className="text-xs font-medium text-blue-500">(max: {linkSpeed.maxText})</span>
)}
</div>
)}
</div>
)
})}
@@ -1690,46 +1815,44 @@ export default function Hardware() {
<span className="font-mono text-sm">{selectedDisk.name}</span>
</div>
{selectedDisk.name && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">Type</span>
{(() => {
const getDiskTypeBadge = (diskName: string, rotationRate: number | string | undefined) => {
let diskType = "HDD"
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">Type</span>
{(() => {
const getDiskTypeBadge = (diskName: string, rotationRate: number | string | undefined) => {
let diskType = "HDD"
if (diskName.startsWith("nvme")) {
diskType = "NVMe"
} else if (rotationRate !== undefined && rotationRate !== null) {
const rateNum = typeof rotationRate === "string" ? Number.parseInt(rotationRate) : rotationRate
if (rateNum === 0 || isNaN(rateNum)) {
diskType = "SSD"
}
} else if (typeof rotationRate === "string" && rotationRate.includes("Solid State")) {
if (diskName.startsWith("nvme")) {
diskType = "NVMe"
} else if (rotationRate !== undefined && rotationRate !== null) {
const rateNum = typeof rotationRate === "string" ? Number.parseInt(rotationRate) : rotationRate
if (rateNum === 0 || isNaN(rateNum)) {
diskType = "SSD"
}
const badgeStyles: Record<string, { className: string; label: string }> = {
NVMe: {
className: "bg-purple-500/10 text-purple-500 border-purple-500/20",
label: "NVMe SSD",
},
SSD: {
className: "bg-cyan-500/10 text-cyan-500 border-cyan-500/20",
label: "SSD",
},
HDD: {
className: "bg-blue-500/10 text-blue-500 border-blue-500/20",
label: "HDD",
},
}
return badgeStyles[diskType]
} else if (typeof rotationRate === "string" && rotationRate.includes("Solid State")) {
diskType = "SSD"
}
const diskBadge = getDiskTypeBadge(selectedDisk.name, selectedDisk.rotation_rate)
return <Badge className={diskBadge.className}>{diskBadge.label}</Badge>
})()}
</div>
)}
const badgeStyles: Record<string, { className: string; label: string }> = {
NVMe: {
className: "bg-purple-500/10 text-purple-500 border-purple-500/20",
label: "NVMe SSD",
},
SSD: {
className: "bg-cyan-500/10 text-cyan-500 border-cyan-500/20",
label: "SSD",
},
HDD: {
className: "bg-blue-500/10 text-blue-500 border-blue-500/20",
label: "HDD",
},
}
return badgeStyles[diskType]
}
const diskBadge = getDiskTypeBadge(selectedDisk.name, selectedDisk.rotation_rate)
return <Badge className={diskBadge.className}>{diskBadge.label}</Badge>
})()}
</div>
{selectedDisk.size && (
<div className="flex justify-between border-b border-border/50 pb-2">
@@ -1738,6 +1861,84 @@ export default function Hardware() {
</div>
)}
<div className="pt-2">
<h3 className="text-sm font-semibold text-muted-foreground mb-2 uppercase tracking-wide">
Interface Information
</h3>
</div>
{/* NVMe PCIe Information */}
{selectedDisk.name.startsWith("nvme") && (
<>
{selectedDisk.pcie_gen || selectedDisk.pcie_width ? (
<>
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">Current Link Speed</span>
<span
className={`text-sm font-medium ${
selectedDisk.pcie_max_gen &&
selectedDisk.pcie_max_width &&
`${selectedDisk.pcie_gen} ${selectedDisk.pcie_width}` !==
`${selectedDisk.pcie_max_gen} ${selectedDisk.pcie_max_width}`
? "text-orange-500"
: "text-blue-500"
}`}
>
{selectedDisk.pcie_gen || "PCIe"} {selectedDisk.pcie_width || ""}
</span>
</div>
{selectedDisk.pcie_max_gen && selectedDisk.pcie_max_width && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">Maximum Link Speed</span>
<span className="text-sm font-medium text-blue-500">
{selectedDisk.pcie_max_gen} {selectedDisk.pcie_max_width}
</span>
</div>
)}
</>
) : (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">PCIe Link Speed</span>
<span className="text-sm text-muted-foreground italic">Detecting...</span>
</div>
)}
</>
)}
{/* SATA Information */}
{!selectedDisk.name.startsWith("nvme") && selectedDisk.sata_version && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">SATA Version</span>
<span className="text-sm font-medium text-blue-500">{selectedDisk.sata_version}</span>
</div>
)}
{/* SAS Information */}
{!selectedDisk.name.startsWith("nvme") && selectedDisk.sas_version && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">SAS Version</span>
<span className="text-sm font-medium text-blue-500">{selectedDisk.sas_version}</span>
</div>
)}
{!selectedDisk.name.startsWith("nvme") && selectedDisk.sas_speed && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">SAS Speed</span>
<span className="text-sm font-medium text-blue-500">{selectedDisk.sas_speed}</span>
</div>
)}
{/* Generic Link Speed - only show if no specific interface info */}
{!selectedDisk.name.startsWith("nvme") &&
selectedDisk.link_speed &&
!selectedDisk.pcie_gen &&
!selectedDisk.sata_version &&
!selectedDisk.sas_version && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">Link Speed</span>
<span className="text-sm font-medium text-blue-500">{selectedDisk.link_speed}</span>
</div>
)}
{selectedDisk.model && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">Model</span>
@@ -1783,13 +1984,15 @@ export default function Hardware() {
{selectedDisk.rotation_rate !== undefined && selectedDisk.rotation_rate !== null && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">Rotation Rate</span>
<span className="text-sm">
{typeof selectedDisk.rotation_rate === "number" && selectedDisk.rotation_rate > 0
? `${selectedDisk.rotation_rate} rpm`
: typeof selectedDisk.rotation_rate === "string"
? selectedDisk.rotation_rate
: "Solid State Device"}
</span>
<div className="text-sm">
{typeof selectedDisk.rotation_rate === "number" && selectedDisk.rotation_rate === -1
? "N/A"
: typeof selectedDisk.rotation_rate === "number" && selectedDisk.rotation_rate > 0
? `${selectedDisk.rotation_rate} rpm`
: typeof selectedDisk.rotation_rate === "string"
? selectedDisk.rotation_rate
: "Solid State Device"}
</div>
</div>
)}
@@ -1799,13 +2002,6 @@ export default function Hardware() {
<span className="text-sm">{selectedDisk.form_factor}</span>
</div>
)}
{selectedDisk.sata_version && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">SATA Version</span>
<span className="text-sm">{selectedDisk.sata_version}</span>
</div>
)}
</div>
)}
</DialogContent>
+363
View File
@@ -0,0 +1,363 @@
"use client"
import type React from "react"
import { useState, useEffect } from "react"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
Loader2,
CheckCircle2,
AlertTriangle,
XCircle,
Activity,
Cpu,
MemoryStick,
HardDrive,
Disc,
Network,
Box,
Settings,
FileText,
RefreshCw,
Shield,
X,
} from "lucide-react"
interface CategoryCheck {
status: string
reason?: string
details?: any
[key: string]: any
}
interface HealthDetails {
overall: string
summary: string
details: {
cpu: CategoryCheck
memory: CategoryCheck
storage: CategoryCheck
disks: CategoryCheck
network: CategoryCheck
vms: CategoryCheck
services: CategoryCheck
logs: CategoryCheck
updates: CategoryCheck
security: CategoryCheck
}
timestamp: string
}
interface HealthStatusModalProps {
open: boolean
onOpenChange: (open: boolean) => void
getApiUrl: (path: string) => string
}
const CATEGORIES = [
{ key: "cpu", label: "CPU Usage & Temperature", Icon: Cpu },
{ key: "memory", label: "Memory & Swap", Icon: MemoryStick },
{ key: "storage", label: "Storage Mounts & Space", Icon: HardDrive },
{ key: "disks", label: "Disk I/O & Errors", Icon: Disc },
{ key: "network", label: "Network Interfaces", Icon: Network },
{ key: "vms", label: "VMs & Containers", Icon: Box },
{ key: "services", label: "PVE Services", Icon: Settings },
{ key: "logs", label: "System Logs", Icon: FileText },
{ key: "updates", label: "System Updates", Icon: RefreshCw },
{ key: "security", label: "Security & Certificates", Icon: Shield },
]
export function HealthStatusModal({ open, onOpenChange, getApiUrl }: HealthStatusModalProps) {
const [loading, setLoading] = useState(true)
const [healthData, setHealthData] = useState<HealthDetails | null>(null)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
if (open) {
fetchHealthDetails()
}
}, [open])
const fetchHealthDetails = async () => {
setLoading(true)
setError(null)
try {
const response = await fetch(getApiUrl("/api/health/details"))
if (!response.ok) {
throw new Error("Failed to fetch health details")
}
const data = await response.json()
console.log("[v0] Health data received:", data)
setHealthData(data)
const event = new CustomEvent("healthStatusUpdated", {
detail: { status: data.overall },
})
window.dispatchEvent(event)
} catch (err) {
console.error("[v0] Error fetching health data:", err)
setError(err instanceof Error ? err.message : "Unknown error")
} finally {
setLoading(false)
}
}
const getStatusIcon = (status: string) => {
const statusUpper = status?.toUpperCase()
switch (statusUpper) {
case "OK":
return <CheckCircle2 className="h-5 w-5 text-green-500" />
case "WARNING":
return <AlertTriangle className="h-5 w-5 text-yellow-500" />
case "CRITICAL":
return <XCircle className="h-5 w-5 text-red-500" />
default:
return <Activity className="h-5 w-5 text-gray-500" />
}
}
const getStatusBadge = (status: string) => {
const statusUpper = status?.toUpperCase()
switch (statusUpper) {
case "OK":
return <Badge className="bg-green-500 text-white hover:bg-green-500">OK</Badge>
case "WARNING":
return <Badge className="bg-yellow-500 text-white hover:bg-yellow-500">Warning</Badge>
case "CRITICAL":
return <Badge className="bg-red-500 text-white hover:bg-red-500">Critical</Badge>
default:
return <Badge>Unknown</Badge>
}
}
const getHealthStats = () => {
if (!healthData?.details) {
return { total: 0, healthy: 0, warnings: 0, critical: 0 }
}
let healthy = 0
let warnings = 0
let critical = 0
CATEGORIES.forEach(({ key }) => {
const categoryData = healthData.details[key as keyof typeof healthData.details]
if (categoryData) {
const status = categoryData.status?.toUpperCase()
if (status === "OK") healthy++
else if (status === "WARNING") warnings++
else if (status === "CRITICAL") critical++
}
})
return { total: CATEGORIES.length, healthy, warnings, critical }
}
const stats = getHealthStats()
const handleCategoryClick = (categoryKey: string, status: string) => {
if (status === "OK") return // No navegar si está OK
onOpenChange(false) // Cerrar el modal
// Mapear categorías a tabs
const categoryToTab: Record<string, string> = {
storage: "storage",
disks: "storage",
network: "network",
vms: "vms",
logs: "logs",
hardware: "hardware",
services: "hardware",
}
const targetTab = categoryToTab[categoryKey]
if (targetTab) {
// Disparar evento para cambiar tab
const event = new CustomEvent("changeTab", { detail: { tab: targetTab } })
window.dispatchEvent(event)
}
}
const handleAcknowledge = async (errorKey: string, e: React.MouseEvent) => {
e.stopPropagation() // Prevent navigation
console.log("[v0] Dismissing error:", errorKey)
try {
const response = await fetch(getApiUrl("/api/health/acknowledge"), {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ error_key: errorKey }),
})
if (!response.ok) {
const errorData = await response.json()
console.error("[v0] Acknowledge failed:", errorData)
throw new Error(errorData.error || "Failed to acknowledge error")
}
const result = await response.json()
console.log("[v0] Acknowledge success:", result)
// Refresh health data
await fetchHealthDetails()
} catch (err) {
console.error("[v0] Error acknowledging:", err)
alert("Failed to dismiss error. Please try again.")
}
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-3xl max-h-[85vh] overflow-y-auto">
<DialogHeader>
<div className="flex items-center justify-between gap-3">
<DialogTitle className="flex items-center gap-2 flex-1">
<Activity className="h-6 w-6" />
System Health Status
{healthData && <div className="ml-2">{getStatusBadge(healthData.overall)}</div>}
</DialogTitle>
</div>
<DialogDescription>Detailed health checks for all system components</DialogDescription>
</DialogHeader>
{loading && (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>
)}
{error && (
<div className="rounded-lg border border-red-200 bg-red-50 p-4 text-red-800 dark:bg-red-950 dark:border-red-800 dark:text-red-200">
<p className="font-medium">Error loading health status</p>
<p className="text-sm">{error}</p>
</div>
)}
{healthData && !loading && (
<div className="space-y-4">
{/* Overall Stats Summary */}
<div className="grid grid-cols-4 gap-3 p-4 rounded-lg bg-muted/30 border">
<div className="text-center">
<div className="text-2xl font-bold">{stats.total}</div>
<div className="text-xs text-muted-foreground">Total Checks</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-green-500">{stats.healthy}</div>
<div className="text-xs text-muted-foreground">Healthy</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-yellow-500">{stats.warnings}</div>
<div className="text-xs text-muted-foreground">Warnings</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-red-500">{stats.critical}</div>
<div className="text-xs text-muted-foreground">Critical</div>
</div>
</div>
{healthData.summary && healthData.summary !== "All systems operational" && (
<div className="text-sm p-3 rounded-lg bg-muted/20 border">
<span className="font-medium text-foreground">{healthData.summary}</span>
</div>
)}
<div className="space-y-2">
{CATEGORIES.map(({ key, label, Icon }) => {
const categoryData = healthData.details[key as keyof typeof healthData.details]
const status = categoryData?.status || "UNKNOWN"
const reason = categoryData?.reason
const details = categoryData?.details
return (
<div
key={key}
onClick={() => handleCategoryClick(key, status)}
className={`flex items-start gap-3 p-3 rounded-lg border transition-colors ${
status === "OK"
? "bg-card border-border hover:bg-muted/30"
: status === "WARNING"
? "bg-yellow-500/5 border-yellow-500/20 hover:bg-yellow-500/10 cursor-pointer"
: status === "CRITICAL"
? "bg-red-500/5 border-red-500/20 hover:bg-red-500/10 cursor-pointer"
: "bg-muted/30 hover:bg-muted/50"
}`}
>
<div className="mt-0.5 flex-shrink-0 flex items-center gap-2">
<Icon className="h-4 w-4 text-blue-500" />
{getStatusIcon(status)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2 mb-1">
<p className="font-medium text-sm">{label}</p>
<Badge
variant="outline"
className={`shrink-0 text-xs ${
status === "OK"
? "border-green-500 text-green-500 bg-transparent"
: status === "WARNING"
? "border-yellow-500 text-yellow-500 bg-yellow-500/5"
: status === "CRITICAL"
? "border-red-500 text-red-500 bg-red-500/5"
: ""
}`}
>
{status}
</Badge>
</div>
{reason && <p className="text-xs text-muted-foreground mt-1">{reason}</p>}
{details && typeof details === "object" && (
<div className="mt-2 space-y-1">
{Object.entries(details).map(([detailKey, detailValue]: [string, any]) => {
if (typeof detailValue === "object" && detailValue !== null) {
return (
<div
key={detailKey}
className="flex items-start justify-between gap-2 text-xs pl-3 border-l-2 border-muted py-1"
>
<div className="flex-1">
<span className="font-medium">{detailKey}:</span>
{detailValue.reason && (
<span className="ml-1 text-muted-foreground">{detailValue.reason}</span>
)}
</div>
{(status === "WARNING" || status === "CRITICAL") && (
<Button
size="sm"
variant="outline"
className="h-6 px-2 shrink-0 hover:bg-red-500/10 hover:border-red-500/50 bg-transparent"
onClick={(e) => handleAcknowledge(detailKey, e)}
>
<X className="h-3 w-3 mr-1" />
<span className="text-xs">Dismiss</span>
</Button>
)}
</div>
)
}
return null
})}
</div>
)}
</div>
</div>
)
})}
</div>
{healthData.timestamp && (
<div className="text-xs text-muted-foreground text-center pt-2">
Last updated: {new Date(healthData.timestamp).toLocaleString()}
</div>
)}
</div>
)}
</DialogContent>
</Dialog>
)
}
+244
View File
@@ -0,0 +1,244 @@
"use client"
import type React from "react"
import { useState, useEffect } from "react"
import { Button } from "./ui/button"
import { Input } from "./ui/input"
import { Label } from "./ui/label"
import { Checkbox } from "./ui/checkbox"
import { Lock, User, AlertCircle, Server, Shield } from "lucide-react"
import { getApiUrl } from "../lib/api-config"
import Image from "next/image"
interface LoginProps {
onLogin: () => void
}
export function Login({ onLogin }: LoginProps) {
const [username, setUsername] = useState("")
const [password, setPassword] = useState("")
const [totpCode, setTotpCode] = useState("")
const [requiresTotp, setRequiresTotp] = useState(false)
const [rememberMe, setRememberMe] = useState(false)
const [error, setError] = useState("")
const [loading, setLoading] = useState(false)
useEffect(() => {
const savedUsername = localStorage.getItem("proxmenux-saved-username")
const savedPassword = localStorage.getItem("proxmenux-saved-password")
if (savedUsername && savedPassword) {
setUsername(savedUsername)
setPassword(savedPassword)
setRememberMe(true)
}
}, [])
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault()
setError("")
if (!username || !password) {
setError("Please enter username and password")
return
}
if (requiresTotp && !totpCode) {
setError("Please enter your 2FA code")
return
}
setLoading(true)
try {
const response = await fetch(getApiUrl("/api/auth/login"), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
username,
password,
totp_token: totpCode || undefined, // Include 2FA code if provided
}),
})
const data = await response.json()
if (data.requires_totp) {
setRequiresTotp(true)
setLoading(false)
return
}
if (!response.ok) {
throw new Error(data.message || "Login failed")
}
localStorage.setItem("proxmenux-auth-token", data.token)
if (rememberMe) {
localStorage.setItem("proxmenux-saved-username", username)
localStorage.setItem("proxmenux-saved-password", password)
} else {
localStorage.removeItem("proxmenux-saved-username")
localStorage.removeItem("proxmenux-saved-password")
}
onLogin()
} catch (err) {
setError(err instanceof Error ? err.message : "Login failed")
} finally {
setLoading(false)
}
}
return (
<div className="min-h-screen bg-background flex items-center justify-center p-4">
<div className="w-full max-w-md space-y-8">
<div className="text-center space-y-4">
<div className="flex justify-center">
<div className="w-20 h-20 relative flex items-center justify-center bg-primary/10 rounded-lg">
<Image
src="/images/proxmenux-logo.png"
alt="ProxMenux Logo"
width={80}
height={80}
className="object-contain"
priority
onError={(e) => {
const target = e.target as HTMLImageElement
target.style.display = "none"
const fallback = target.parentElement?.querySelector(".fallback-icon")
if (fallback) {
fallback.classList.remove("hidden")
}
}}
/>
<Server className="h-12 w-12 text-primary absolute fallback-icon hidden" />
</div>
</div>
<div>
<h1 className="text-3xl font-bold">ProxMenux Monitor</h1>
<p className="text-muted-foreground mt-2">Sign in to access your dashboard</p>
</div>
</div>
<div className="bg-card border border-border rounded-lg p-6 shadow-lg">
<form onSubmit={handleLogin} className="space-y-4">
{error && (
<div className="bg-red-500/10 border border-red-500/20 rounded-lg p-3 flex items-start gap-2">
<AlertCircle className="h-5 w-5 text-red-500 flex-shrink-0 mt-0.5" />
<p className="text-sm text-red-500">{error}</p>
</div>
)}
{!requiresTotp ? (
<>
<div className="space-y-2">
<Label htmlFor="login-username" className="text-sm">
Username
</Label>
<div className="relative">
<User className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
id="login-username"
type="text"
placeholder="Enter your username"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="pl-10 text-base"
disabled={loading}
autoComplete="username"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="login-password" className="text-sm">
Password
</Label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
id="login-password"
type="password"
placeholder="Enter your password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="pl-10 text-base"
disabled={loading}
autoComplete="current-password"
/>
</div>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="remember-me"
checked={rememberMe}
onCheckedChange={(checked) => setRememberMe(checked as boolean)}
disabled={loading}
/>
<Label htmlFor="remember-me" className="text-sm font-normal cursor-pointer select-none">
Remember me
</Label>
</div>
</>
) : (
<div className="space-y-4">
<div className="bg-blue-500/10 border border-blue-500/20 rounded-lg p-3 flex items-start gap-2">
<Shield className="h-5 w-5 text-blue-500 flex-shrink-0 mt-0.5" />
<div>
<p className="text-sm font-medium text-blue-500">Two-Factor Authentication</p>
<p className="text-xs text-blue-500 mt-1">Enter the 6-digit code from your authentication app</p>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="totp-code" className="text-sm">
Authentication Code
</Label>
<Input
id="totp-code"
type="text"
placeholder="000000"
value={totpCode}
onChange={(e) => setTotpCode(e.target.value.replace(/\D/g, "").slice(0, 6))}
className="text-center text-lg tracking-widest font-mono text-base"
maxLength={6}
disabled={loading}
autoComplete="one-time-code"
autoFocus
/>
<p className="text-xs text-muted-foreground text-center">
You can also use a backup code (format: XXXX-XXXX)
</p>
</div>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => {
setRequiresTotp(false)
setTotpCode("")
setError("")
}}
className="w-full"
>
Back to login
</Button>
</div>
)}
<Button type="submit" className="w-full bg-blue-500 hover:bg-blue-600" disabled={loading}>
{loading ? "Signing in..." : requiresTotp ? "Verify Code" : "Sign In"}
</Button>
</form>
</div>
<p className="text-center text-sm text-muted-foreground">ProxMenux Monitor v1.0.1</p>
</div>
</div>
)
}
+2 -12
View File
@@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { ArrowLeft, Loader2 } from "lucide-react"
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts"
import { fetchApi } from "@/lib/api-config"
interface MetricsViewProps {
vmid: number
@@ -118,18 +119,7 @@ export function MetricsView({ vmid, vmName, vmType, onBack }: MetricsViewProps)
setError(null)
try {
const baseUrl =
typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : ""
const apiUrl = `${baseUrl}/api/vms/${vmid}/metrics?timeframe=${timeframe}`
const response = await fetch(apiUrl)
if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.error || "Failed to fetch metrics")
}
const result = await response.json()
const result = await fetchApi<any>(`/api/vms/${vmid}/metrics?timeframe=${timeframe}`)
const transformedData = result.data.map((item: any) => {
const date = new Date(item.time * 1000)
+2 -18
View File
@@ -4,6 +4,7 @@ import { Card, CardContent } from "./ui/card"
import { Badge } from "./ui/badge"
import { Wifi, Zap } from "lucide-react"
import { useState, useEffect } from "react"
import { fetchApi } from "../lib/api-config"
interface NetworkCardProps {
interface_: {
@@ -94,26 +95,12 @@ export function NetworkCard({ interface_, timeframe, onClick }: NetworkCardProps
useEffect(() => {
const fetchTrafficData = async () => {
try {
const response = await fetch(`/api/network/${interface_.name}/metrics?timeframe=${timeframe}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
signal: AbortSignal.timeout(5000),
})
const data = await fetchApi(`/api/network/${interface_.name}/metrics?timeframe=${timeframe}`)
if (!response.ok) {
throw new Error(`Failed to fetch traffic data: ${response.status}`)
}
const data = await response.json()
// Calculate totals from the data points
if (data.data && data.data.length > 0) {
const lastPoint = data.data[data.data.length - 1]
const firstPoint = data.data[0]
// Calculate the difference between last and first data points
const receivedGB = Math.max(0, (lastPoint.netin || 0) - (firstPoint.netin || 0))
const sentGB = Math.max(0, (lastPoint.netout || 0) - (firstPoint.netout || 0))
@@ -124,16 +111,13 @@ export function NetworkCard({ interface_, timeframe, onClick }: NetworkCardProps
}
} catch (error) {
console.error("[v0] Failed to fetch traffic data for card:", error)
// Keep showing 0 values on error
setTrafficData({ received: 0, sent: 0 })
}
}
// Only fetch if interface is up and not a VM
if (interface_.status.toLowerCase() === "up" && interface_.vm_type !== "vm") {
fetchTrafficData()
// Refresh every 60 seconds
const interval = setInterval(fetchTrafficData, 60000)
return () => clearInterval(interval)
}
+9 -17
View File
@@ -3,11 +3,12 @@
import { useState } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
import { Badge } from "./ui/badge"
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./ui/dialog"
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "./ui/dialog"
import { Wifi, Activity, Network, Router, AlertCircle, Zap } from "lucide-react"
import useSWR from "swr"
import { NetworkTrafficChart } from "./network-traffic-chart"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
import { fetchApi } from "../lib/api-config"
interface NetworkData {
interfaces: NetworkInterface[]
@@ -128,19 +129,7 @@ const formatSpeed = (speed: number): string => {
}
const fetcher = async (url: string): Promise<NetworkData> => {
const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
signal: AbortSignal.timeout(5000),
})
if (!response.ok) {
throw new Error(`Flask server responded with status: ${response.status}`)
}
return response.json()
return fetchApi<NetworkData>(url)
}
export function NetworkMetrics() {
@@ -149,7 +138,7 @@ export function NetworkMetrics() {
error,
isLoading,
} = useSWR<NetworkData>("/api/network", fetcher, {
refreshInterval: 60000, // Refresh every 60 seconds
refreshInterval: 53000,
revalidateOnFocus: false,
revalidateOnReconnect: true,
})
@@ -161,13 +150,13 @@ export function NetworkMetrics() {
const [interfaceTotals, setInterfaceTotals] = useState<{ received: number; sent: number }>({ received: 0, sent: 0 })
const { data: modalNetworkData } = useSWR<NetworkData>(selectedInterface ? "/api/network" : null, fetcher, {
refreshInterval: 15000, // Refresh every 15 seconds when modal is open
refreshInterval: 17000,
revalidateOnFocus: false,
revalidateOnReconnect: true,
})
const { data: interfaceHistoricalData } = useSWR<any>(`/api/node/metrics?timeframe=${timeframe}`, fetcher, {
refreshInterval: 30000,
refreshInterval: 29000,
revalidateOnFocus: false,
})
@@ -688,6 +677,9 @@ export function NetworkMetrics() {
<Router className="h-5 w-5" />
{selectedInterface?.name} - Interface Details
</DialogTitle>
<DialogDescription>
View detailed information and network traffic statistics for this interface
</DialogDescription>
{selectedInterface?.status.toLowerCase() === "up" && selectedInterface?.vm_type !== "vm" && (
<div className="flex justify-end pt-2">
<Select value={modalTimeframe} onValueChange={(value: any) => setModalTimeframe(value)}>
+6 -14
View File
@@ -3,6 +3,7 @@
import { useState, useEffect } from "react"
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts"
import { Loader2 } from "lucide-react"
import { fetchApi } from "@/lib/api-config"
interface NetworkMetricsData {
time: string
@@ -75,22 +76,13 @@ export function NetworkTrafficChart({
setError(null)
try {
const baseUrl =
typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : ""
const apiPath = interfaceName
? `/api/network/${interfaceName}/metrics?timeframe=${timeframe}`
: `/api/node/metrics?timeframe=${timeframe}`
const apiUrl = interfaceName
? `${baseUrl}/api/network/${interfaceName}/metrics?timeframe=${timeframe}`
: `${baseUrl}/api/node/metrics?timeframe=${timeframe}`
console.log("[v0] Fetching network metrics from:", apiPath)
console.log("[v0] Fetching network metrics from:", apiUrl)
const response = await fetch(apiUrl)
if (!response.ok) {
throw new Error(`Failed to fetch network metrics: ${response.status}`)
}
const result = await response.json()
const result = await fetchApi<any>(apiPath)
if (!result.data || !Array.isArray(result.data)) {
throw new Error("Invalid data format received from server")
+19 -26
View File
@@ -5,6 +5,8 @@ import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts"
import { Loader2, TrendingUp, MemoryStick } from "lucide-react"
import { useIsMobile } from "../hooks/use-mobile"
import { fetchApi } from "@/lib/api-config"
const TIMEFRAME_OPTIONS = [
{ value: "hour", label: "1 Hour" },
@@ -69,6 +71,7 @@ export function NodeMetricsCharts() {
const [data, setData] = useState<NodeMetricsData[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const isMobile = useIsMobile()
const [visibleLines, setVisibleLines] = useState({
cpu: { cpu: true, load: true },
@@ -86,24 +89,8 @@ export function NodeMetricsCharts() {
setError(null)
try {
const baseUrl =
typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : ""
const apiUrl = `${baseUrl}/api/node/metrics?timeframe=${timeframe}`
const result = await fetchApi<any>(`/api/node/metrics?timeframe=${timeframe}`)
console.log("[v0] Fetching node metrics from:", apiUrl)
const response = await fetch(apiUrl)
console.log("[v0] Response status:", response.status)
console.log("[v0] Response ok:", response.ok)
if (!response.ok) {
const errorText = await response.text()
console.log("[v0] Error response text:", errorText)
throw new Error(`Failed to fetch node metrics: ${response.status}`)
}
const result = await response.json()
console.log("[v0] Node metrics result:", result)
console.log("[v0] Result keys:", Object.keys(result))
console.log("[v0] Data array length:", result.data?.length || 0)
@@ -318,15 +305,15 @@ export function NodeMetricsCharts() {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* CPU Usage + Load Average Chart */}
<Card className="bg-card border-border">
<CardHeader>
<CardHeader className="px-4 md:px-6">
<CardTitle className="text-foreground flex items-center">
<TrendingUp className="h-5 w-5 mr-2" />
CPU Usage & Load Average
</CardTitle>
</CardHeader>
<CardContent>
<CardContent className="px-0 md:px-6">
<ResponsiveContainer width="100%" height={300}>
<AreaChart data={data} margin={{ bottom: 60, left: 30, right: 10 }}>
<AreaChart data={data} margin={{ bottom: 60, left: 0, right: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="currentColor" className="text-border" />
<XAxis
dataKey="time"
@@ -343,7 +330,9 @@ export function NodeMetricsCharts() {
stroke="currentColor"
className="text-foreground"
tick={{ fill: "currentColor", fontSize: 12 }}
label={{ value: "CPU %", angle: -90, position: "insideLeft", fill: "currentColor" }}
label={
isMobile ? undefined : { value: "CPU %", angle: -90, position: "insideLeft", fill: "currentColor" }
}
domain={[0, "dataMax"]}
/>
<YAxis
@@ -352,7 +341,9 @@ export function NodeMetricsCharts() {
stroke="currentColor"
className="text-foreground"
tick={{ fill: "currentColor", fontSize: 12 }}
label={{ value: "Load", angle: 90, position: "insideRight", fill: "currentColor" }}
label={
isMobile ? undefined : { value: "Load", angle: 90, position: "insideRight", fill: "currentColor" }
}
domain={[0, "dataMax"]}
/>
<Tooltip content={<CustomCpuTooltip />} />
@@ -386,15 +377,15 @@ export function NodeMetricsCharts() {
{/* Memory Usage Chart */}
<Card className="bg-card border-border">
<CardHeader>
<CardHeader className="px-4 md:px-6">
<CardTitle className="text-foreground flex items-center">
<MemoryStick className="h-5 w-5 mr-2" />
Memory Usage
</CardTitle>
</CardHeader>
<CardContent>
<CardContent className="px-0 pr-2 md:px-6">
<ResponsiveContainer width="100%" height={300}>
<AreaChart data={data} margin={{ bottom: 60, left: 30, right: 10 }}>
<AreaChart data={data} margin={{ bottom: 60, left: 0, right: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="currentColor" className="text-border" />
<XAxis
dataKey="time"
@@ -410,7 +401,9 @@ export function NodeMetricsCharts() {
stroke="currentColor"
className="text-foreground"
tick={{ fill: "currentColor", fontSize: 12 }}
label={{ value: "GB", angle: -90, position: "insideLeft", fill: "currentColor" }}
label={
isMobile ? undefined : { value: "GB", angle: -90, position: "insideLeft", fill: "currentColor" }
}
domain={[0, "dataMax"]}
/>
<Tooltip content={<CustomMemoryTooltip />} />
+42 -27
View File
@@ -19,6 +19,7 @@ import {
Rocket,
} from "lucide-react"
import Image from "next/image"
import { Checkbox } from "./ui/checkbox"
interface OnboardingSlide {
id: number
@@ -106,6 +107,7 @@ export function OnboardingCarousel() {
const [open, setOpen] = useState(false)
const [currentSlide, setCurrentSlide] = useState(0)
const [direction, setDirection] = useState<"next" | "prev">("next")
const [dontShowAgain, setDontShowAgain] = useState(false)
useEffect(() => {
const hasSeenOnboarding = localStorage.getItem("proxmenux-onboarding-seen")
@@ -119,6 +121,9 @@ export function OnboardingCarousel() {
setDirection("next")
setCurrentSlide(currentSlide + 1)
} else {
if (dontShowAgain) {
localStorage.setItem("proxmenux-onboarding-seen", "true")
}
setOpen(false)
}
}
@@ -131,11 +136,16 @@ export function OnboardingCarousel() {
}
const handleSkip = () => {
if (dontShowAgain) {
localStorage.setItem("proxmenux-onboarding-seen", "true")
}
setOpen(false)
}
const handleDontShowAgain = () => {
localStorage.setItem("proxmenux-onboarding-seen", "true")
const handleClose = () => {
if (dontShowAgain) {
localStorage.setItem("proxmenux-onboarding-seen", "true")
}
setOpen(false)
}
@@ -147,15 +157,14 @@ export function OnboardingCarousel() {
const slide = slides[currentSlide]
return (
<Dialog open={open} onOpenChange={setOpen}>
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="max-w-4xl p-0 gap-0 overflow-hidden border-0 bg-transparent">
<div className="relative bg-card rounded-lg overflow-hidden shadow-2xl">
{/* Close button */}
<Button
variant="ghost"
size="icon"
className="absolute top-4 right-4 z-50 h-8 w-8 rounded-full bg-background/80 backdrop-blur-sm hover:bg-background"
onClick={handleSkip}
onClick={handleClose}
>
<X className="h-4 w-4" />
</Button>
@@ -166,7 +175,6 @@ export function OnboardingCarousel() {
<div className="absolute inset-0 bg-black/10" />
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_120%,rgba(255,255,255,0.1),transparent)]" />
{/* Icon or Image */}
<div className="relative z-10 text-white">
{slide.image ? (
<div className="relative w-full h-36 md:h-48 flex items-center justify-center px-4">
@@ -192,20 +200,18 @@ export function OnboardingCarousel() {
)}
</div>
{/* Decorative elements */}
<div className="absolute top-10 left-10 w-20 h-20 bg-white/10 rounded-full blur-2xl" />
<div className="absolute bottom-10 right-10 w-32 h-32 bg-white/10 rounded-full blur-3xl" />
</div>
<div className="p-4 md:p-8 space-y-4 md:space-y-6">
<div className="p-4 md:p-8 space-y-3 md:space-y-6 max-h-[60vh] md:max-h-none overflow-y-auto">
<div className="space-y-2 md:space-y-3">
<h2 className="text-2xl md:text-3xl font-bold text-foreground text-balance">{slide.title}</h2>
<p className="text-base md:text-lg text-muted-foreground leading-relaxed text-pretty">
<h2 className="text-xl md:text-3xl font-bold text-foreground text-balance">{slide.title}</h2>
<p className="text-sm md:text-lg text-muted-foreground leading-relaxed text-pretty">
{slide.description}
</p>
</div>
{/* Progress dots */}
<div className="flex items-center justify-center gap-2 py-2 md:py-4">
{slides.map((_, index) => (
<button
@@ -221,12 +227,12 @@ export function OnboardingCarousel() {
))}
</div>
<div className="flex flex-col sm:flex-row items-center justify-between gap-3 md:gap-4">
<div className="flex flex-col sm:flex-row items-center justify-between gap-2 md:gap-4">
<Button
variant="ghost"
onClick={handlePrev}
disabled={currentSlide === 0}
className="gap-2 w-full sm:w-auto"
className="gap-2 w-full sm:w-auto text-sm"
>
<ChevronLeft className="h-4 w-4" />
Previous
@@ -235,10 +241,17 @@ export function OnboardingCarousel() {
<div className="flex gap-2 w-full sm:w-auto">
{currentSlide < slides.length - 1 ? (
<>
<Button variant="outline" onClick={handleSkip} className="flex-1 sm:flex-none bg-transparent">
<Button
variant="outline"
onClick={handleSkip}
className="flex-1 sm:flex-none bg-transparent text-sm"
>
Skip
</Button>
<Button onClick={handleNext} className="gap-2 bg-blue-500 hover:bg-blue-600 flex-1 sm:flex-none">
<Button
onClick={handleNext}
className="gap-2 bg-blue-500 hover:bg-blue-600 flex-1 sm:flex-none text-sm"
>
Next
<ChevronRight className="h-4 w-4" />
</Button>
@@ -246,7 +259,7 @@ export function OnboardingCarousel() {
) : (
<Button
onClick={handleNext}
className="gap-2 bg-gradient-to-r from-blue-500 to-purple-500 hover:from-blue-600 hover:to-purple-600 w-full sm:w-auto"
className="gap-2 bg-gradient-to-r from-blue-500 to-purple-500 hover:from-blue-600 hover:to-purple-600 w-full sm:w-auto text-sm"
>
Get Started!
<Sparkles className="h-4 w-4" />
@@ -255,17 +268,19 @@ export function OnboardingCarousel() {
</div>
</div>
{/* Don't show again */}
{currentSlide === slides.length - 1 && (
<div className="text-center pt-2">
<button
onClick={handleDontShowAgain}
className="text-sm text-muted-foreground hover:text-foreground transition-colors underline"
>
Don't show again
</button>
</div>
)}
<div className="flex items-center justify-center gap-2 pt-2 pb-1">
<Checkbox
id="dont-show-again"
checked={dontShowAgain}
onCheckedChange={(checked) => setDontShowAgain(checked as boolean)}
/>
<label
htmlFor="dont-show-again"
className="text-xs md:text-sm text-muted-foreground hover:text-foreground transition-colors cursor-pointer select-none"
>
Don't show this again
</label>
</div>
</div>
</div>
</DialogContent>
+150 -57
View File
@@ -10,7 +10,11 @@ import { NetworkMetrics } from "./network-metrics"
import { VirtualMachines } from "./virtual-machines"
import Hardware from "./hardware"
import { SystemLogs } from "./system-logs"
import { Settings } from "./settings"
import { OnboardingCarousel } from "./onboarding-carousel"
import { HealthStatusModal } from "./health-status-modal"
import { ReleaseNotesModal, useVersionCheck } from "./release-notes-modal"
import { getApiUrl, fetchApi } from "../lib/api-config"
import {
RefreshCw,
AlertTriangle,
@@ -24,6 +28,7 @@ import {
Box,
Cpu,
FileText,
SettingsIcon,
} from "lucide-react"
import Image from "next/image"
import { ThemeToggle } from "./theme-toggle"
@@ -47,11 +52,20 @@ interface FlaskSystemData {
load_average: number[]
}
interface FlaskSystemInfo {
hostname: string
node_id: string
uptime: string
health: {
status: "healthy" | "warning" | "critical"
}
}
export function ProxmoxDashboard() {
const [systemStatus, setSystemStatus] = useState<SystemStatus>({
status: "healthy",
uptime: "Loading...",
lastUpdate: new Date().toLocaleTimeString(),
lastUpdate: new Date().toLocaleTimeString("en-US", { hour12: false }),
serverName: "Loading...",
nodeId: "Loading...",
})
@@ -62,55 +76,37 @@ export function ProxmoxDashboard() {
const [activeTab, setActiveTab] = useState("overview")
const [showNavigation, setShowNavigation] = useState(true)
const [lastScrollY, setLastScrollY] = useState(0)
const [showHealthModal, setShowHealthModal] = useState(false)
const { showReleaseNotes, setShowReleaseNotes } = useVersionCheck()
const fetchSystemData = useCallback(async () => {
console.log("[v0] Fetching system data from Flask server...")
console.log("[v0] Current window location:", window.location.href)
const baseUrl = typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : ""
const apiUrl = `${baseUrl}/api/system`
console.log("[v0] API URL:", apiUrl)
try {
const response = await fetch(apiUrl, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
cache: "no-store",
})
console.log("[v0] Response status:", response.status)
const data: FlaskSystemInfo = await fetchApi("/api/system-info")
if (!response.ok) {
throw new Error(`Server responded with status: ${response.status}`)
}
const uptimeValue =
data.uptime && typeof data.uptime === "string" && data.uptime.trim() !== "" ? data.uptime : "N/A"
const data: FlaskSystemData = await response.json()
console.log("[v0] System data received:", data)
const backendStatus = data.health?.status?.toUpperCase() || "OK"
let healthStatus: "healthy" | "warning" | "critical"
let status: "healthy" | "warning" | "critical" = "healthy"
if (data.cpu_usage > 90 || data.memory_usage > 90) {
status = "critical"
} else if (data.cpu_usage > 75 || data.memory_usage > 75) {
status = "warning"
if (backendStatus === "CRITICAL") {
healthStatus = "critical"
} else if (backendStatus === "WARNING") {
healthStatus = "warning"
} else {
healthStatus = "healthy"
}
setSystemStatus({
status,
uptime: data.uptime,
lastUpdate: new Date().toLocaleTimeString(),
serverName: data.hostname,
nodeId: data.node_id,
status: healthStatus,
uptime: uptimeValue,
lastUpdate: new Date().toLocaleTimeString("en-US", { hour12: false }),
serverName: data.hostname || "Unknown",
nodeId: data.node_id || "Unknown",
})
setIsServerConnected(true)
} catch (error) {
console.error("[v0] Failed to fetch system data from Flask server:", error)
console.error("[v0] Error details:", {
message: error instanceof Error ? error.message : "Unknown error",
apiUrl,
windowLocation: window.location.href,
})
setIsServerConnected(false)
setSystemStatus((prev) => ({
@@ -119,16 +115,67 @@ export function ProxmoxDashboard() {
serverName: "Server Offline",
nodeId: "Server Offline",
uptime: "N/A",
lastUpdate: new Date().toLocaleTimeString(),
lastUpdate: new Date().toLocaleTimeString("en-US", { hour12: false }),
}))
}
}, [])
useEffect(() => {
// Siempre fetch inicial
fetchSystemData()
const interval = setInterval(fetchSystemData, 10000)
return () => clearInterval(interval)
}, [fetchSystemData])
// En overview: cada 30 segundos para actualización frecuente del estado de salud
// En otras tabs: cada 60 segundos para reducir carga
let interval: ReturnType<typeof setInterval> | null = null
if (activeTab === "overview") {
interval = setInterval(fetchSystemData, 30000) // 30 segundos
} else {
interval = setInterval(fetchSystemData, 60000) // 60 segundos
}
return () => {
if (interval) clearInterval(interval)
}
}, [fetchSystemData, activeTab])
useEffect(() => {
const handleChangeTab = (event: CustomEvent) => {
const { tab } = event.detail
if (tab) {
setActiveTab(tab)
}
}
window.addEventListener("changeTab", handleChangeTab as EventListener)
return () => {
window.removeEventListener("changeTab", handleChangeTab as EventListener)
}
}, [])
useEffect(() => {
const handleHealthStatusUpdate = (event: CustomEvent) => {
const { status } = event.detail
let healthStatus: "healthy" | "warning" | "critical"
if (status === "CRITICAL") {
healthStatus = "critical"
} else if (status === "WARNING") {
healthStatus = "warning"
} else {
healthStatus = "healthy"
}
setSystemStatus((prev) => ({
...prev,
status: healthStatus,
}))
}
window.addEventListener("healthStatusUpdated", handleHealthStatusUpdate as EventListener)
return () => {
window.removeEventListener("healthStatusUpdated", handleHealthStatusUpdate as EventListener)
}
}, [])
useEffect(() => {
if (
@@ -214,6 +261,8 @@ export function ProxmoxDashboard() {
return "Hardware"
case "logs":
return "System Logs"
case "settings":
return "Settings"
default:
return "Navigation Menu"
}
@@ -222,6 +271,7 @@ export function ProxmoxDashboard() {
return (
<div className="min-h-screen bg-background">
<OnboardingCarousel />
<ReleaseNotesModal open={showReleaseNotes} onClose={() => setShowReleaseNotes(false)} />
{!isServerConnected && (
<div className="bg-red-500/10 border-b border-red-500/20 px-6 py-3">
@@ -235,13 +285,8 @@ export function ProxmoxDashboard() {
<p> The ProxMenux server should start automatically on port 8008</p>
<p>
Try accessing:{" "}
<a
href={`http://${typeof window !== "undefined" ? window.location.host : "localhost:8008"}/api/health`}
target="_blank"
rel="noopener noreferrer"
className="underline"
>
http://{typeof window !== "undefined" ? window.location.host : "localhost:8008"}/api/health
<a href={getApiUrl("/api/health")} target="_blank" rel="noopener noreferrer" className="underline">
{getApiUrl("/api/health")}
</a>
</p>
</div>
@@ -249,7 +294,10 @@ export function ProxmoxDashboard() {
</div>
)}
<header className="border-b border-border bg-card sticky top-0 z-50 shadow-sm">
<header
className="border-b border-border bg-card sticky top-0 z-50 shadow-sm cursor-pointer hover:bg-accent/5 transition-colors"
onClick={() => setShowHealthModal(true)}
>
<div className="container mx-auto px-4 md:px-6 py-4 md:py-4">
{/* Logo and Title */}
<div className="flex items-start justify-between gap-3">
@@ -299,12 +347,17 @@ export function ProxmoxDashboard() {
<span className="ml-1 capitalize">{systemStatus.status}</span>
</Badge>
<div className="text-sm text-muted-foreground whitespace-nowrap">Uptime: {systemStatus.uptime}</div>
<div className="text-sm text-muted-foreground whitespace-nowrap">
Uptime: {systemStatus.uptime || "N/A"}
</div>
<Button
variant="outline"
size="sm"
onClick={refreshData}
onClick={(e) => {
e.stopPropagation()
refreshData()
}}
disabled={isRefreshing}
className="border-border/50 bg-transparent hover:bg-secondary"
>
@@ -312,7 +365,9 @@ export function ProxmoxDashboard() {
Refresh
</Button>
<ThemeToggle />
<div onClick={(e) => e.stopPropagation()}>
<ThemeToggle />
</div>
</div>
{/* Mobile Actions */}
@@ -322,17 +377,28 @@ export function ProxmoxDashboard() {
<span className="ml-1 capitalize hidden sm:inline">{systemStatus.status}</span>
</Badge>
<Button variant="ghost" size="sm" onClick={refreshData} disabled={isRefreshing} className="h-8 w-8 p-0">
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation()
refreshData()
}}
disabled={isRefreshing}
className="h-8 w-8 p-0"
>
<RefreshCw className={`h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`} />
</Button>
<ThemeToggle />
<div onClick={(e) => e.stopPropagation()}>
<ThemeToggle />
</div>
</div>
</div>
{/* Mobile Server Info */}
<div className="lg:hidden mt-2 flex items-center justify-end text-xs text-muted-foreground">
<span className="whitespace-nowrap">Uptime: {systemStatus.uptime}</span>
<span className="whitespace-nowrap">Uptime: {systemStatus.uptime || "N/A"}</span>
</div>
</div>
</header>
@@ -346,7 +412,7 @@ export function ProxmoxDashboard() {
>
<div className="container mx-auto px-4 md:px-6 pt-4 md:pt-6">
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-0">
<TabsList className="hidden md:grid w-full grid-cols-6 bg-card border border-border">
<TabsList className="hidden md:grid w-full grid-cols-7 bg-card border border-border">
<TabsTrigger
value="overview"
className="data-[state=active]:bg-blue-500 data-[state=active]:text-white data-[state=active]:rounded-md"
@@ -383,6 +449,12 @@ export function ProxmoxDashboard() {
>
System Logs
</TabsTrigger>
<TabsTrigger
value="settings"
className="data-[state=active]:bg-blue-500 data-[state=active]:text-white data-[state=active]:rounded-md"
>
Settings
</TabsTrigger>
</TabsList>
<Sheet open={mobileMenuOpen} onOpenChange={setMobileMenuOpen}>
@@ -491,6 +563,21 @@ export function ProxmoxDashboard() {
<FileText className="h-5 w-5" />
<span>System Logs</span>
</Button>
<Button
variant="ghost"
onClick={() => {
setActiveTab("settings")
setMobileMenuOpen(false)
}}
className={`w-full justify-start gap-3 ${
activeTab === "settings"
? "bg-blue-500/10 text-blue-500 border-l-4 border-blue-500 rounded-l-none"
: ""
}`}
>
<SettingsIcon className="h-5 w-5" />
<span>Settings</span>
</Button>
</div>
</SheetContent>
</Sheet>
@@ -523,10 +610,14 @@ export function ProxmoxDashboard() {
<TabsContent value="logs" className="space-y-4 md:space-y-6 mt-0">
<SystemLogs key={`logs-${componentKey}`} />
</TabsContent>
<TabsContent value="settings" className="space-y-4 md:space-y-6 mt-0">
<Settings />
</TabsContent>
</Tabs>
<footer className="mt-8 md:mt-12 pt-4 md:pt-6 border-t border-border text-center text-xs md:text-sm text-muted-foreground">
<p className="font-medium mb-2">ProxMenux Monitor v1.0.0</p>
<p className="font-medium mb-2">ProxMenux Monitor v1.0.1</p>
<p>
<a
href="https://ko-fi.com/macrimi"
@@ -539,6 +630,8 @@ export function ProxmoxDashboard() {
</p>
</footer>
</div>
<HealthStatusModal open={showHealthModal} onOpenChange={setShowHealthModal} getApiUrl={getApiUrl} />
</div>
)
}
+203
View File
@@ -0,0 +1,203 @@
"use client"
import { useState, useEffect } from "react"
import { Button } from "./ui/button"
import { Dialog, DialogContent } from "./ui/dialog"
import { X, Sparkles, Link2, Shield, Zap, HardDrive, Gauge, Wrench, Settings } from "lucide-react"
import { Checkbox } from "./ui/checkbox"
const APP_VERSION = "1.0.1" // Sync with AppImage/package.json
interface ReleaseNote {
date: string
changes: {
added?: string[]
changed?: string[]
fixed?: string[]
}
}
export const CHANGELOG: Record<string, ReleaseNote> = {
"1.0.1": {
date: "November 11, 2025",
changes: {
added: [
"Proxy Support - Access ProxMenux through reverse proxies with full functionality",
"Authentication System - Secure your dashboard with password protection",
"PCIe Link Speed Detection - View NVMe drive connection speeds and detect performance issues",
"Enhanced Storage Display - Better formatting for disk sizes (auto-converts GB to TB when needed)",
"SATA/SAS Information - View detailed interface information for all storage devices",
"Two-Factor Authentication (2FA) - Enhanced security with TOTP support",
"Health Monitoring System - Comprehensive system health checks with dismissible warnings",
"Release Notes Modal - Automatic notification of new features and improvements",
],
changed: [
"Optimized VM & LXC page - Reduced CPU usage by 85% through intelligent caching",
"Storage metrics now separate local and remote storage for clarity",
"Update warnings now appear only after 365 days instead of 30 days",
"API intervals staggered to distribute server load (23s and 37s)",
],
fixed: [
"Fixed dark mode text contrast issues in various components",
"Corrected storage calculation discrepancies between Overview and Storage pages",
"Resolved JSON stringify error in VM control actions",
"Improved IP address fetching for LXC containers",
],
},
},
"1.0.0": {
date: "October 15, 2025",
changes: {
added: [
"Initial release of ProxMenux Monitor",
"Real-time system monitoring dashboard",
"Storage management with SMART health monitoring",
"Network metrics and bandwidth tracking",
"VM & LXC container management",
"Hardware information display",
"System logs viewer with filtering",
],
},
},
}
const CURRENT_VERSION_FEATURES = [
{
icon: <Link2 className="h-5 w-5" />,
text: "Proxy Support - Access ProxMenux through reverse proxies with full functionality",
},
{
icon: <Shield className="h-5 w-5" />,
text: "Two-Factor Authentication (2FA) - Enhanced security with TOTP support for login protection",
},
{
icon: <Zap className="h-5 w-5" />,
text: "Performance Improvements - Optimized loading times and reduced CPU usage across the application",
},
{
icon: <HardDrive className="h-5 w-5" />,
text: "Storage Enhancements - Improved disk space consumption display with local and remote storage separation",
},
{
icon: <Gauge className="h-5 w-5" />,
text: "PCIe Link Speed Detection - View NVMe drive connection speeds and identify performance bottlenecks",
},
{
icon: <Wrench className="h-5 w-5" />,
text: "Hardware Page Improvements - Enhanced hardware information display with detailed PCIe and interface data",
},
{
icon: <Settings className="h-5 w-5" />,
text: "New Settings Page - Centralized configuration for authentication, optimizations, and system preferences",
},
]
interface ReleaseNotesModalProps {
open: boolean
onClose: () => void
}
export function ReleaseNotesModal({ open, onClose }: ReleaseNotesModalProps) {
const [dontShowAgain, setDontShowAgain] = useState(false)
const handleClose = () => {
if (dontShowAgain) {
localStorage.setItem("proxmenux-last-seen-version", APP_VERSION)
}
onClose()
}
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="max-w-2xl max-h-[85vh] p-0 gap-0 border-0 bg-transparent">
<div className="relative bg-card rounded-lg shadow-2xl h-full flex flex-col max-h-[85vh]">
<Button
variant="ghost"
size="icon"
className="absolute top-4 right-4 z-50 h-8 w-8 rounded-full bg-background/80 backdrop-blur-sm hover:bg-background"
onClick={handleClose}
>
<X className="h-4 w-4" />
</Button>
<div className="relative h-32 md:h-40 bg-gradient-to-br from-amber-500 via-orange-500 to-red-500 flex items-center justify-center overflow-hidden flex-shrink-0">
<div className="absolute inset-0 bg-black/10" />
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_120%,rgba(255,255,255,0.1),transparent)]" />
<div className="relative z-10 text-white animate-pulse">
<Sparkles className="h-12 w-12 md:h-14 md:w-14" />
</div>
<div className="absolute top-10 left-10 w-20 h-20 bg-white/10 rounded-full blur-2xl" />
<div className="absolute bottom-10 right-10 w-32 h-32 bg-white/10 rounded-full blur-3xl" />
</div>
<div className="flex-1 overflow-y-auto p-6 md:p-8 space-y-4 md:space-y-6 min-h-0">
<div className="space-y-2">
<h2 className="text-xl md:text-2xl font-bold text-foreground text-balance">
What's New in Version {APP_VERSION}
</h2>
<p className="text-sm text-muted-foreground leading-relaxed">
We've added exciting new features and improvements to make ProxMenux Monitor even better!
</p>
</div>
<div className="space-y-2">
{CURRENT_VERSION_FEATURES.map((feature, index) => (
<div
key={index}
className="flex items-start gap-2 md:gap-3 p-3 rounded-lg bg-muted/50 border border-border/50 hover:bg-muted/70 transition-colors"
>
<div className="text-orange-500 mt-0.5 flex-shrink-0">{feature.icon}</div>
<p className="text-xs md:text-sm text-foreground leading-relaxed">{feature.text}</p>
</div>
))}
</div>
</div>
<div className="flex-shrink-0 p-6 md:p-8 pt-4 border-t border-border/50 bg-card">
<div className="flex flex-col gap-3">
<Button
onClick={handleClose}
className="w-full bg-gradient-to-r from-amber-500 to-orange-500 hover:from-amber-600 hover:to-orange-600"
>
<Sparkles className="h-4 w-4 mr-2" />
Got it!
</Button>
<div className="flex items-center justify-center gap-2">
<Checkbox
id="dont-show-version-again"
checked={dontShowAgain}
onCheckedChange={(checked) => setDontShowAgain(checked as boolean)}
/>
<label
htmlFor="dont-show-version-again"
className="text-xs md:text-sm text-muted-foreground hover:text-foreground transition-colors cursor-pointer select-none"
>
Don't show again for this version
</label>
</div>
</div>
</div>
</div>
</DialogContent>
</Dialog>
)
}
export function useVersionCheck() {
const [showReleaseNotes, setShowReleaseNotes] = useState(false)
useEffect(() => {
const lastSeenVersion = localStorage.getItem("proxmenux-last-seen-version")
if (lastSeenVersion !== APP_VERSION) {
setShowReleaseNotes(true)
}
}, [])
return { showReleaseNotes, setShowReleaseNotes }
}
export { APP_VERSION }
+910
View File
@@ -0,0 +1,910 @@
"use client"
import { useState, useEffect } from "react"
import { Button } from "./ui/button"
import { Input } from "./ui/input"
import { Label } from "./ui/label"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card"
import {
Shield,
Lock,
User,
AlertCircle,
CheckCircle,
Info,
LogOut,
Wrench,
Package,
Key,
Copy,
Eye,
EyeOff,
} from "lucide-react"
import { APP_VERSION } from "./release-notes-modal"
import { getApiUrl, fetchApi } from "../lib/api-config"
import { TwoFactorSetup } from "./two-factor-setup"
interface ProxMenuxTool {
key: string
name: string
enabled: boolean
}
export function Settings() {
const [authEnabled, setAuthEnabled] = useState(false)
const [totpEnabled, setTotpEnabled] = useState(false)
const [loading, setLoading] = useState(false)
const [error, setError] = useState("")
const [success, setSuccess] = useState("")
// Setup form state
const [showSetupForm, setShowSetupForm] = useState(false)
const [username, setUsername] = useState("")
const [password, setPassword] = useState("")
const [confirmPassword, setConfirmPassword] = useState("")
// Change password form state
const [showChangePassword, setShowChangePassword] = useState(false)
const [currentPassword, setCurrentPassword] = useState("")
const [newPassword, setNewPassword] = useState("")
const [confirmNewPassword, setConfirmNewPassword] = useState("")
const [show2FASetup, setShow2FASetup] = useState(false)
const [show2FADisable, setShow2FADisable] = useState(false)
const [disable2FAPassword, setDisable2FAPassword] = useState("")
const [proxmenuxTools, setProxmenuxTools] = useState<ProxMenuxTool[]>([])
const [loadingTools, setLoadingTools] = useState(true)
const [expandedVersions, setExpandedVersions] = useState<Record<string, boolean>>({
[APP_VERSION]: true, // Current version expanded by default
})
// API Token state management
const [showApiTokenSection, setShowApiTokenSection] = useState(false)
const [apiToken, setApiToken] = useState("")
const [apiTokenVisible, setApiTokenVisible] = useState(false)
const [tokenPassword, setTokenPassword] = useState("")
const [tokenTotpCode, setTokenTotpCode] = useState("")
const [generatingToken, setGeneratingToken] = useState(false)
const [tokenCopied, setTokenCopied] = useState(false)
useEffect(() => {
checkAuthStatus()
loadProxmenuxTools()
}, [])
const checkAuthStatus = async () => {
try {
const response = await fetch(getApiUrl("/api/auth/status"))
const data = await response.json()
setAuthEnabled(data.auth_enabled || false)
setTotpEnabled(data.totp_enabled || false) // Get 2FA status
} catch (err) {
console.error("Failed to check auth status:", err)
}
}
const loadProxmenuxTools = async () => {
try {
const response = await fetch(getApiUrl("/api/proxmenux/installed-tools"))
const data = await response.json()
if (data.success) {
setProxmenuxTools(data.installed_tools || [])
}
} catch (err) {
console.error("Failed to load ProxMenux tools:", err)
} finally {
setLoadingTools(false)
}
}
const handleEnableAuth = async () => {
setError("")
setSuccess("")
if (!username || !password) {
setError("Please fill in all fields")
return
}
if (password !== confirmPassword) {
setError("Passwords do not match")
return
}
if (password.length < 6) {
setError("Password must be at least 6 characters")
return
}
setLoading(true)
try {
const response = await fetch(getApiUrl("/api/auth/setup"), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
username,
password,
enable_auth: true,
}),
})
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || "Failed to enable authentication")
}
// Save token
localStorage.setItem("proxmenux-auth-token", data.token)
localStorage.setItem("proxmenux-auth-setup-complete", "true")
setSuccess("Authentication enabled successfully!")
setAuthEnabled(true)
setShowSetupForm(false)
setUsername("")
setPassword("")
setConfirmPassword("")
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to enable authentication")
} finally {
setLoading(false)
}
}
const handleDisableAuth = async () => {
if (
!confirm(
"Are you sure you want to disable authentication? This will remove password protection from your dashboard.",
)
) {
return
}
setLoading(true)
setError("")
setSuccess("")
try {
const token = localStorage.getItem("proxmenux-auth-token")
const response = await fetch(getApiUrl("/api/auth/disable"), {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
})
const data = await response.json()
if (!response.ok) {
throw new Error(data.message || "Failed to disable authentication")
}
localStorage.removeItem("proxmenux-auth-token")
localStorage.removeItem("proxmenux-auth-setup-complete")
setSuccess("Authentication disabled successfully! Reloading...")
setTimeout(() => {
window.location.reload()
}, 1000)
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to disable authentication. Please try again.")
} finally {
setLoading(false)
}
}
const handleChangePassword = async () => {
setError("")
setSuccess("")
if (!currentPassword || !newPassword) {
setError("Please fill in all fields")
return
}
if (newPassword !== confirmNewPassword) {
setError("New passwords do not match")
return
}
if (newPassword.length < 6) {
setError("Password must be at least 6 characters")
return
}
setLoading(true)
try {
const response = await fetch(getApiUrl("/api/auth/change-password"), {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("proxmenux-auth-token")}`,
},
body: JSON.stringify({
current_password: currentPassword,
new_password: newPassword,
}),
})
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || "Failed to change password")
}
// Update token if provided
if (data.token) {
localStorage.setItem("proxmenux-auth-token", data.token)
}
setSuccess("Password changed successfully!")
setShowChangePassword(false)
setCurrentPassword("")
setNewPassword("")
setConfirmNewPassword("")
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to change password")
} finally {
setLoading(false)
}
}
const handleDisable2FA = async () => {
setError("")
setSuccess("")
if (!disable2FAPassword) {
setError("Please enter your password")
return
}
setLoading(true)
try {
const token = localStorage.getItem("proxmenux-auth-token")
const response = await fetch(getApiUrl("/api/auth/totp/disable"), {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ password: disable2FAPassword }),
})
const data = await response.json()
if (!response.ok) {
throw new Error(data.message || "Failed to disable 2FA")
}
setSuccess("2FA disabled successfully!")
setTotpEnabled(false)
setShow2FADisable(false)
setDisable2FAPassword("")
checkAuthStatus()
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to disable 2FA")
} finally {
setLoading(false)
}
}
const handleLogout = () => {
localStorage.removeItem("proxmenux-auth-token")
localStorage.removeItem("proxmenux-auth-setup-complete")
window.location.reload()
}
const handleGenerateApiToken = async () => {
setError("")
setSuccess("")
if (!tokenPassword) {
setError("Please enter your password")
return
}
if (totpEnabled && !tokenTotpCode) {
setError("Please enter your 2FA code")
return
}
setGeneratingToken(true)
try {
const data = await fetchApi("/api/auth/generate-api-token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
password: tokenPassword,
totp_token: totpEnabled ? tokenTotpCode : undefined,
}),
})
if (!data.success) {
setError(data.message || data.error || "Failed to generate API token")
return
}
if (!data.token) {
setError("No token received from server")
return
}
setApiToken(data.token)
setSuccess("API token generated successfully! Make sure to copy it now as you won't be able to see it again.")
setTokenPassword("")
setTokenTotpCode("")
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to generate API token. Please try again.")
} finally {
setGeneratingToken(false)
}
}
const copyApiToken = () => {
navigator.clipboard.writeText(apiToken)
setTokenCopied(true)
setTimeout(() => setTokenCopied(false), 2000)
}
const toggleVersion = (version: string) => {
setExpandedVersions((prev) => ({
...prev,
[version]: !prev[version],
}))
}
return (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold">Settings</h1>
<p className="text-muted-foreground mt-2">Manage your dashboard security and preferences</p>
</div>
{/* Authentication Settings */}
<Card>
<CardHeader>
<div className="flex items-center gap-2">
<Shield className="h-5 w-5 text-blue-500" />
<CardTitle>Authentication</CardTitle>
</div>
<CardDescription>Protect your dashboard with username and password authentication</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{error && (
<div className="bg-red-500/10 border border-red-500/20 rounded-lg p-3 flex items-start gap-2">
<AlertCircle className="h-5 w-5 text-red-500 flex-shrink-0 mt-0.5" />
<p className="text-sm text-red-500">{error}</p>
</div>
)}
{success && (
<div className="bg-green-500/10 border border-green-500/20 rounded-lg p-3 flex items-start gap-2">
<CheckCircle className="h-5 w-5 text-green-500 flex-shrink-0 mt-0.5" />
<p className="text-sm text-green-500">{success}</p>
</div>
)}
<div className="flex items-center justify-between p-4 bg-muted/50 rounded-lg">
<div className="flex items-center gap-3">
<div
className={`w-10 h-10 rounded-full flex items-center justify-center ${authEnabled ? "bg-green-500/10" : "bg-gray-500/10"}`}
>
<Lock className={`h-5 w-5 ${authEnabled ? "text-green-500" : "text-gray-500"}`} />
</div>
<div>
<p className="font-medium">Authentication Status</p>
<p className="text-sm text-muted-foreground">
{authEnabled ? "Password protection is enabled" : "No password protection"}
</p>
</div>
</div>
<div
className={`px-3 py-1 rounded-full text-sm font-medium ${authEnabled ? "bg-green-500/10 text-green-500" : "bg-gray-500/10 text-gray-500"}`}
>
{authEnabled ? "Enabled" : "Disabled"}
</div>
</div>
{!authEnabled && !showSetupForm && (
<div className="space-y-3">
<div className="bg-blue-500/10 border border-blue-500/20 rounded-lg p-3 flex items-start gap-2">
<Info className="h-5 w-5 text-blue-500 flex-shrink-0 mt-0.5" />
<p className="text-sm text-blue-500">
Enable authentication to protect your dashboard when accessing from non-private networks.
</p>
</div>
<Button onClick={() => setShowSetupForm(true)} className="w-full bg-blue-500 hover:bg-blue-600">
<Shield className="h-4 w-4 mr-2" />
Enable Authentication
</Button>
</div>
)}
{!authEnabled && showSetupForm && (
<div className="space-y-4 border border-border rounded-lg p-4">
<h3 className="font-semibold">Setup Authentication</h3>
<div className="space-y-2">
<Label htmlFor="setup-username">Username</Label>
<div className="relative">
<User className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
id="setup-username"
type="text"
placeholder="Enter username"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="pl-10"
disabled={loading}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="setup-password">Password</Label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
id="setup-password"
type="password"
placeholder="Enter password (min 6 characters)"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="pl-10"
disabled={loading}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="setup-confirm-password">Confirm Password</Label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
id="setup-confirm-password"
type="password"
placeholder="Confirm password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="pl-10"
disabled={loading}
/>
</div>
</div>
<div className="flex gap-2">
<Button onClick={handleEnableAuth} className="flex-1 bg-blue-500 hover:bg-blue-600" disabled={loading}>
{loading ? "Enabling..." : "Enable"}
</Button>
<Button onClick={() => setShowSetupForm(false)} variant="outline" className="flex-1" disabled={loading}>
Cancel
</Button>
</div>
</div>
)}
{authEnabled && (
<div className="space-y-3">
<Button onClick={handleLogout} variant="outline" className="w-full bg-transparent">
<LogOut className="h-4 w-4 mr-2" />
Logout
</Button>
{!showChangePassword && (
<Button onClick={() => setShowChangePassword(true)} variant="outline" className="w-full">
<Lock className="h-4 w-4 mr-2" />
Change Password
</Button>
)}
{showChangePassword && (
<div className="space-y-4 border border-border rounded-lg p-4">
<h3 className="font-semibold">Change Password</h3>
<div className="space-y-2">
<Label htmlFor="current-password">Current Password</Label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
id="current-password"
type="password"
placeholder="Enter current password"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
className="pl-10"
disabled={loading}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="new-password">New Password</Label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
id="new-password"
type="password"
placeholder="Enter new password (min 6 characters)"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
className="pl-10"
disabled={loading}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="confirm-new-password">Confirm New Password</Label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
id="confirm-new-password"
type="password"
placeholder="Confirm new password"
value={confirmNewPassword}
onChange={(e) => setConfirmNewPassword(e.target.value)}
className="pl-10"
disabled={loading}
/>
</div>
</div>
<div className="flex gap-2">
<Button
onClick={handleChangePassword}
className="flex-1 bg-blue-500 hover:bg-blue-600"
disabled={loading}
>
{loading ? "Changing..." : "Change Password"}
</Button>
<Button
onClick={() => setShowChangePassword(false)}
variant="outline"
className="flex-1"
disabled={loading}
>
Cancel
</Button>
</div>
</div>
)}
{!totpEnabled && (
<div className="space-y-3">
<div className="bg-blue-500/10 border border-blue-500/20 rounded-lg p-3 flex items-start gap-2">
<Info className="h-5 w-5 text-blue-500 flex-shrink-0 mt-0.5" />
<div className="text-sm text-blue-400">
<p className="font-medium mb-1">Two-Factor Authentication (2FA)</p>
<p className="text-blue-300">
Add an extra layer of security by requiring a code from your authenticator app in addition to
your password.
</p>
</div>
</div>
<Button onClick={() => setShow2FASetup(true)} variant="outline" className="w-full">
<Shield className="h-4 w-4 mr-2" />
Enable Two-Factor Authentication
</Button>
</div>
)}
{totpEnabled && (
<div className="space-y-3">
<div className="bg-green-500/10 border border-green-500/20 rounded-lg p-3 flex items-center gap-2">
<CheckCircle className="h-5 w-5 text-green-500" />
<p className="text-sm text-green-500 font-medium">2FA is enabled</p>
</div>
{!show2FADisable && (
<Button onClick={() => setShow2FADisable(true)} variant="outline" className="w-full">
<Shield className="h-4 w-4 mr-2" />
Disable 2FA
</Button>
)}
{show2FADisable && (
<div className="space-y-4 border border-border rounded-lg p-4">
<h3 className="font-semibold">Disable Two-Factor Authentication</h3>
<p className="text-sm text-muted-foreground">Enter your password to confirm</p>
<div className="space-y-2">
<Label htmlFor="disable-2fa-password">Password</Label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
id="disable-2fa-password"
type="password"
placeholder="Enter your password"
value={disable2FAPassword}
onChange={(e) => setDisable2FAPassword(e.target.value)}
className="pl-10"
disabled={loading}
/>
</div>
</div>
<div className="flex gap-2">
<Button onClick={handleDisable2FA} variant="destructive" className="flex-1" disabled={loading}>
{loading ? "Disabling..." : "Disable 2FA"}
</Button>
<Button
onClick={() => {
setShow2FADisable(false)
setDisable2FAPassword("")
setError("")
}}
variant="outline"
className="flex-1"
disabled={loading}
>
Cancel
</Button>
</div>
</div>
)}
</div>
)}
<Button onClick={handleDisableAuth} variant="destructive" className="w-full" disabled={loading}>
Disable Authentication
</Button>
</div>
)}
</CardContent>
</Card>
{/* API Access Tokens */}
{authEnabled && (
<Card>
<CardHeader>
<div className="flex items-center gap-2">
<Key className="h-5 w-5 text-purple-500" />
<CardTitle>API Access Tokens</CardTitle>
</div>
<CardDescription>
Generate long-lived API tokens for external integrations like Homepage and Home Assistant
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{error && (
<div className="bg-red-500/10 border border-red-500/20 rounded-lg p-3 flex items-start gap-2">
<AlertCircle className="h-5 w-5 text-red-500 flex-shrink-0 mt-0.5" />
<p className="text-sm text-red-500">{error}</p>
</div>
)}
{success && (
<div className="bg-green-500/10 border border-green-500/20 rounded-lg p-3 flex items-start gap-2">
<CheckCircle className="h-5 w-5 text-green-500 flex-shrink-0 mt-0.5" />
<p className="text-sm text-green-500">{success}</p>
</div>
)}
<div className="bg-blue-500/10 border border-blue-500/20 rounded-lg p-4">
<div className="flex items-start gap-3">
<Info className="h-5 w-5 text-blue-500 flex-shrink-0 mt-0.5" />
<div className="space-y-2 text-sm text-blue-400">
<p className="font-medium">About API Tokens</p>
<ul className="list-disc list-inside space-y-1 text-blue-300">
<li>Tokens are valid for 1 year</li>
<li>Use them to access APIs from external services</li>
<li>Include in Authorization header: Bearer YOUR_TOKEN</li>
<li>See README.md for complete integration examples</li>
</ul>
</div>
</div>
</div>
{!showApiTokenSection && !apiToken && (
<Button onClick={() => setShowApiTokenSection(true)} className="w-full bg-purple-500 hover:bg-purple-600">
<Key className="h-4 w-4 mr-2" />
Generate New API Token
</Button>
)}
{showApiTokenSection && !apiToken && (
<div className="space-y-4 border border-border rounded-lg p-4">
<h3 className="font-semibold">Generate API Token</h3>
<p className="text-sm text-muted-foreground">
Enter your credentials to generate a new long-lived API token
</p>
<div className="space-y-2">
<Label htmlFor="token-password">Password</Label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
id="token-password"
type="password"
placeholder="Enter your password"
value={tokenPassword}
onChange={(e) => setTokenPassword(e.target.value)}
className="pl-10"
disabled={generatingToken}
/>
</div>
</div>
{totpEnabled && (
<div className="space-y-2">
<Label htmlFor="token-totp">2FA Code</Label>
<div className="relative">
<Shield className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
id="token-totp"
type="text"
placeholder="Enter 6-digit code"
value={tokenTotpCode}
onChange={(e) => setTokenTotpCode(e.target.value)}
className="pl-10"
maxLength={6}
disabled={generatingToken}
/>
</div>
</div>
)}
<div className="flex gap-2">
<Button
onClick={handleGenerateApiToken}
className="flex-1 bg-purple-500 hover:bg-purple-600"
disabled={generatingToken}
>
{generatingToken ? "Generating..." : "Generate Token"}
</Button>
<Button
onClick={() => {
setShowApiTokenSection(false)
setTokenPassword("")
setTokenTotpCode("")
setError("")
}}
variant="outline"
className="flex-1"
disabled={generatingToken}
>
Cancel
</Button>
</div>
</div>
)}
{apiToken && (
<div className="space-y-4 border border-green-500/20 bg-green-500/5 rounded-lg p-4">
<div className="flex items-center gap-2 text-green-500">
<CheckCircle className="h-5 w-5" />
<h3 className="font-semibold">Your API Token</h3>
</div>
<div className="bg-amber-500/10 border border-amber-500/30 rounded-lg p-3 flex items-start gap-2">
<AlertCircle className="h-5 w-5 text-amber-500 flex-shrink-0 mt-0.5" />
<div className="space-y-1">
<p className="text-sm text-amber-600 dark:text-amber-400 font-semibold">
Important: Save this token now!
</p>
<p className="text-xs text-amber-600/80 dark:text-amber-400/80">
You won't be able to see it again. Store it securely.
</p>
</div>
</div>
<div className="space-y-2">
<Label>Token</Label>
<div className="relative">
<Input
value={apiToken}
readOnly
type={apiTokenVisible ? "text" : "password"}
className="pr-20 font-mono text-sm"
/>
<div className="absolute right-2 top-1/2 -translate-y-1/2 flex gap-1">
<Button
size="sm"
variant="ghost"
onClick={() => setApiTokenVisible(!apiTokenVisible)}
className="h-7 w-7 p-0"
>
{apiTokenVisible ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</Button>
<Button size="sm" variant="ghost" onClick={copyApiToken} className="h-7 w-7 p-0">
<Copy className={`h-4 w-4 ${tokenCopied ? "text-green-500" : ""}`} />
</Button>
</div>
</div>
{tokenCopied && (
<p className="text-xs text-green-500 flex items-center gap-1">
<CheckCircle className="h-3 w-3" />
Copied to clipboard!
</p>
)}
</div>
<div className="space-y-2">
<p className="text-sm font-medium">How to use this token:</p>
<div className="bg-muted/50 rounded p-3 text-xs font-mono">
<p className="text-muted-foreground mb-2"># Add to request headers:</p>
<p>Authorization: Bearer YOUR_TOKEN_HERE</p>
</div>
<p className="text-xs text-muted-foreground">
See the README documentation for complete integration examples with Homepage and Home Assistant.
</p>
</div>
<Button
onClick={() => {
setApiToken("")
setShowApiTokenSection(false)
}}
variant="outline"
className="w-full"
>
Done
</Button>
</div>
)}
</CardContent>
</Card>
)}
{/* ProxMenux Optimizations */}
<Card>
<CardHeader>
<div className="flex items-center gap-2">
<Wrench className="h-5 w-5 text-orange-500" />
<CardTitle>ProxMenux Optimizations</CardTitle>
</div>
<CardDescription>System optimizations and utilities installed via ProxMenux</CardDescription>
</CardHeader>
<CardContent>
{loadingTools ? (
<div className="flex items-center justify-center py-8">
<div className="animate-spin h-8 w-8 border-4 border-orange-500 border-t-transparent rounded-full" />
</div>
) : proxmenuxTools.length === 0 ? (
<div className="text-center py-8">
<Package className="h-12 w-12 text-muted-foreground mx-auto mb-3 opacity-50" />
<p className="text-muted-foreground">No ProxMenux optimizations installed yet</p>
<p className="text-sm text-muted-foreground mt-1">Run ProxMenux to configure system optimizations</p>
</div>
) : (
<div className="space-y-2">
<div className="flex items-center justify-between mb-4 pb-2 border-b border-border">
<span className="text-sm font-medium text-muted-foreground">Installed Tools</span>
<span className="text-sm font-semibold text-orange-500">{proxmenuxTools.length} active</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
{proxmenuxTools.map((tool) => (
<div
key={tool.key}
className="flex items-center gap-2 p-3 bg-muted/50 rounded-lg border border-border hover:bg-muted transition-colors"
>
<div className="w-2 h-2 rounded-full bg-green-500 flex-shrink-0" />
<span className="text-sm font-medium">{tool.name}</span>
</div>
))}
</div>
</div>
)}
</CardContent>
</Card>
<TwoFactorSetup
open={show2FASetup}
onClose={() => setShow2FASetup(false)}
onSuccess={() => {
setSuccess("2FA enabled successfully!")
checkAuthStatus()
}}
/>
</div>
)
}
+5 -2
View File
@@ -1,10 +1,13 @@
import { LayoutDashboard, HardDrive, Network, Server, Cpu, FileText } from "path-to-icons"
import { LayoutDashboard, HardDrive, Network, Server, Cpu, FileText, SettingsIcon } from "lucide-react"
const menuItems = [
{ name: "Overview", href: "/", icon: LayoutDashboard },
{ name: "Storage", href: "/storage", icon: HardDrive },
{ name: "Network", href: "/network", icon: Network },
{ name: "Virtual Machines", href: "/virtual-machines", icon: Server },
{ name: "Hardware", href: "/hardware", icon: Cpu }, // New Hardware section
{ name: "Hardware", href: "/hardware", icon: Cpu },
{ name: "System Logs", href: "/logs", icon: FileText },
{ name: "Settings", href: "/settings", icon: SettingsIcon },
]
// ... existing code ...
+6 -5
View File
@@ -5,6 +5,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
import { Progress } from "./ui/progress"
import { Badge } from "./ui/badge"
import { HardDrive, Database, Archive, AlertTriangle, CheckCircle, Activity, AlertCircle } from "lucide-react"
import { formatStorage } from "@/lib/utils"
interface StorageData {
total: number
@@ -116,10 +117,10 @@ export function StorageMetrics() {
<HardDrive className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-xl lg:text-2xl font-bold text-foreground">{storageData.total.toFixed(1)} GB</div>
<div className="text-xl lg:text-2xl font-bold text-foreground">{formatStorage(storageData.total)}</div>
<Progress value={usagePercent} className="mt-2" />
<p className="text-xs text-muted-foreground mt-2">
{storageData.used.toFixed(1)} GB used {storageData.available.toFixed(1)} GB available
{formatStorage(storageData.used)} used {formatStorage(storageData.available)} available
</p>
</CardContent>
</Card>
@@ -130,7 +131,7 @@ export function StorageMetrics() {
<Database className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-xl lg:text-2xl font-bold text-foreground">{storageData.used.toFixed(1)} GB</div>
<div className="text-xl lg:text-2xl font-bold text-foreground">{formatStorage(storageData.used)}</div>
<Progress value={usagePercent} className="mt-2" />
<p className="text-xs text-muted-foreground mt-2">{usagePercent.toFixed(1)}% of total space</p>
</CardContent>
@@ -144,7 +145,7 @@ export function StorageMetrics() {
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-xl lg:text-2xl font-bold text-foreground">{storageData.available.toFixed(1)} GB</div>
<div className="text-xl lg:text-2xl font-bold text-foreground">{formatStorage(storageData.available)}</div>
<div className="flex items-center mt-2">
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20">
{((storageData.available / storageData.total) * 100).toFixed(1)}% Free
@@ -201,7 +202,7 @@ export function StorageMetrics() {
<div className="flex items-center space-x-6">
<div className="text-right">
<div className="text-sm font-medium text-foreground">
{disk.used.toFixed(1)} GB / {disk.total.toFixed(1)} GB
{formatStorage(disk.used)} / {formatStorage(disk.total)}
</div>
<Progress value={disk.usage_percent} className="w-24 mt-1" />
</div>
+152 -66
View File
@@ -2,10 +2,11 @@
import { useEffect, useState } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { HardDrive, Database, AlertTriangle, CheckCircle2, XCircle, Square, Thermometer } from "lucide-react"
import { HardDrive, Database, AlertTriangle, CheckCircle2, XCircle, Square, Thermometer, Archive } from "lucide-react"
import { Badge } from "@/components/ui/badge"
import { Progress } from "@/components/ui/progress"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { fetchApi } from "../lib/api-config"
interface DiskInfo {
name: string
@@ -64,6 +65,7 @@ interface ProxmoxStorage {
used: number
available: number
percent: number
node: string // Added node property for detailed debug logging
}
interface ProxmoxStorageData {
@@ -75,12 +77,11 @@ const formatStorage = (sizeInGB: number): string => {
if (sizeInGB < 1) {
// Less than 1 GB, show in MB
return `${(sizeInGB * 1024).toFixed(1)} MB`
} else if (sizeInGB < 1024) {
// Less than 1024 GB, show in GB
return `${sizeInGB.toFixed(1)} GB`
} else if (sizeInGB > 999) {
return `${(sizeInGB / 1024).toFixed(2)} TB`
} else {
// 1024 GB or more, show in TB
return `${(sizeInGB / 1024).toFixed(1)} TB`
// Between 1 and 999 GB, show in GB
return `${sizeInGB.toFixed(2)} GB`
}
}
@@ -93,20 +94,11 @@ export function StorageOverview() {
const fetchStorageData = async () => {
try {
const baseUrl =
typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : ""
const [storageResponse, proxmoxResponse] = await Promise.all([
fetch(`${baseUrl}/api/storage`),
fetch(`${baseUrl}/api/proxmox-storage`),
const [data, proxmoxData] = await Promise.all([
fetchApi<StorageData>("/api/storage"),
fetchApi<ProxmoxStorageData>("/api/proxmox-storage"),
])
const data = await storageResponse.json()
const proxmoxData = await proxmoxResponse.json()
console.log("[v0] Storage data received:", data)
console.log("[v0] Proxmox storage data received:", proxmoxData)
setStorageData(data)
setProxmoxStorage(proxmoxData)
} catch (error) {
@@ -211,6 +203,12 @@ export function StorageOverview() {
if (diskName.startsWith("nvme")) {
return "NVMe"
}
// rotation_rate = -1 means HDD but RPM is unknown (detected via kernel rotational flag)
// rotation_rate = 0 or undefined means SSD
// rotation_rate > 0 means HDD with known RPM
if (rotationRate === -1) {
return "HDD"
}
if (!rotationRate || rotationRate === 0) {
return "SSD"
}
@@ -393,20 +391,88 @@ export function StorageOverview() {
return "[&>div]:bg-red-500"
}
const getUsageColor = (percent: number): string => {
if (percent < 70) return "text-blue-500"
if (percent < 85) return "text-yellow-500"
if (percent < 95) return "text-orange-500"
return "text-red-500"
}
const diskHealthBreakdown = getDiskHealthBreakdown()
const diskTypesBreakdown = getDiskTypesBreakdown()
const totalProxmoxUsed =
proxmoxStorage && proxmoxStorage.storage
? proxmoxStorage.storage
.filter(
(storage) => storage && storage.total > 0 && storage.status && storage.status.toLowerCase() === "active",
)
.reduce((sum, storage) => sum + storage.used, 0)
: 0
const localStorageTypes = ["dir", "lvmthin", "lvm", "zfspool", "btrfs"]
const remoteStorageTypes = ["pbs", "nfs", "cifs", "smb", "glusterfs", "iscsi", "iscsidirect", "rbd", "cephfs"]
const usagePercent =
storageData && storageData.total > 0 ? ((totalProxmoxUsed / (storageData.total * 1024)) * 100).toFixed(2) : "0.00"
const totalLocalUsed =
proxmoxStorage?.storage
.filter(
(storage) =>
storage &&
storage.name &&
storage.status === "active" &&
storage.total > 0 &&
storage.used >= 0 &&
storage.available >= 0 &&
localStorageTypes.includes(storage.type.toLowerCase()),
)
.reduce((sum, storage) => sum + storage.used, 0) || 0
const totalLocalCapacity =
proxmoxStorage?.storage
.filter(
(storage) =>
storage &&
storage.name &&
storage.status === "active" &&
storage.total > 0 &&
storage.used >= 0 &&
storage.available >= 0 &&
localStorageTypes.includes(storage.type.toLowerCase()),
)
.reduce((sum, storage) => sum + storage.total, 0) || 0
const localUsagePercent = totalLocalCapacity > 0 ? ((totalLocalUsed / totalLocalCapacity) * 100).toFixed(2) : "0.00"
const totalRemoteUsed =
proxmoxStorage?.storage
.filter(
(storage) =>
storage &&
storage.name &&
storage.status === "active" &&
storage.total > 0 &&
storage.used >= 0 &&
storage.available >= 0 &&
remoteStorageTypes.includes(storage.type.toLowerCase()),
)
.reduce((sum, storage) => sum + storage.used, 0) || 0
const totalRemoteCapacity =
proxmoxStorage?.storage
.filter(
(storage) =>
storage &&
storage.name &&
storage.status === "active" &&
storage.total > 0 &&
storage.used >= 0 &&
storage.available >= 0 &&
remoteStorageTypes.includes(storage.type.toLowerCase()),
)
.reduce((sum, storage) => sum + storage.total, 0) || 0
const remoteUsagePercent =
totalRemoteCapacity > 0 ? ((totalRemoteUsed / totalRemoteCapacity) * 100).toFixed(2) : "0.00"
const remoteStorageCount =
proxmoxStorage?.storage.filter(
(storage) =>
storage &&
storage.name &&
storage.status === "active" &&
remoteStorageTypes.includes(storage.type.toLowerCase()),
).length || 0
if (loading) {
return (
@@ -441,64 +507,81 @@ export function StorageOverview() {
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Used Storage</CardTitle>
<CardTitle className="text-sm font-medium">Local Used</CardTitle>
<Database className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-xl lg:text-2xl font-bold">{formatStorage(totalProxmoxUsed)}</div>
<p className="text-xs text-muted-foreground mt-1">{usagePercent}% used</p>
<div className="text-xl lg:text-2xl font-bold">{formatStorage(totalLocalUsed)}</div>
<p className="text-xs mt-1">
<span className={getUsageColor(Number.parseFloat(localUsagePercent))}>{localUsagePercent}%</span>
<span className="text-muted-foreground"> of </span>
<span className="text-green-500">{formatStorage(totalLocalCapacity)}</span>
</p>
</CardContent>
</Card>
{/* Disk Health */}
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Disk Health</CardTitle>
<CheckCircle2 className="h-4 w-4 text-muted-foreground" />
<CardTitle className="text-sm font-medium">Remote Used</CardTitle>
<Archive className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-xl lg:text-2xl font-bold">{storageData.disk_count} disks</div>
<div className="text-xl lg:text-2xl font-bold">
{remoteStorageCount > 0 ? formatStorage(totalRemoteUsed) : "None"}
</div>
<p className="text-xs mt-1">
<span className="text-green-500">{diskHealthBreakdown.normal} normal</span>
{diskHealthBreakdown.warning > 0 && (
{remoteStorageCount > 0 ? (
<>
{", "}
<span className="text-yellow-500">{diskHealthBreakdown.warning} warning</span>
</>
)}
{diskHealthBreakdown.critical > 0 && (
<>
{", "}
<span className="text-red-500">{diskHealthBreakdown.critical} critical</span>
<span className={getUsageColor(Number.parseFloat(remoteUsagePercent))}>{remoteUsagePercent}%</span>
<span className="text-muted-foreground"> of </span>
<span className="text-green-500">{formatStorage(totalRemoteCapacity)}</span>
</>
) : (
<span className="text-muted-foreground">No remote storage</span>
)}
</p>
</CardContent>
</Card>
{/* Disk Types */}
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Disk Types</CardTitle>
<CardTitle className="text-sm font-medium">Physical Disks</CardTitle>
<HardDrive className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-xl lg:text-2xl font-bold">{storageData.disk_count} disks</div>
<p className="text-xs mt-1">
{diskTypesBreakdown.nvme > 0 && <span className="text-purple-500">{diskTypesBreakdown.nvme} NVMe</span>}
{diskTypesBreakdown.ssd > 0 && (
<>
{diskTypesBreakdown.nvme > 0 && ", "}
<span className="text-cyan-500">{diskTypesBreakdown.ssd} SSD</span>
</>
)}
{diskTypesBreakdown.hdd > 0 && (
<>
{(diskTypesBreakdown.nvme > 0 || diskTypesBreakdown.ssd > 0) && ", "}
<span className="text-blue-500">{diskTypesBreakdown.hdd} HDD</span>
</>
)}
</p>
<div className="space-y-1 mt-1">
<p className="text-xs">
{diskTypesBreakdown.nvme > 0 && <span className="text-purple-500">{diskTypesBreakdown.nvme} NVMe</span>}
{diskTypesBreakdown.ssd > 0 && (
<>
{diskTypesBreakdown.nvme > 0 && ", "}
<span className="text-cyan-500">{diskTypesBreakdown.ssd} SSD</span>
</>
)}
{diskTypesBreakdown.hdd > 0 && (
<>
{(diskTypesBreakdown.nvme > 0 || diskTypesBreakdown.ssd > 0) && ", "}
<span className="text-blue-500">{diskTypesBreakdown.hdd} HDD</span>
</>
)}
</p>
<p className="text-xs">
<span className="text-green-500">{diskHealthBreakdown.normal} normal</span>
{diskHealthBreakdown.warning > 0 && (
<>
{", "}
<span className="text-yellow-500">{diskHealthBreakdown.warning} warning</span>
</>
)}
{diskHealthBreakdown.critical > 0 && (
<>
{", "}
<span className="text-red-500">{diskHealthBreakdown.critical} critical</span>
</>
)}
</p>
</div>
</CardContent>
</Card>
</div>
@@ -514,7 +597,10 @@ export function StorageOverview() {
<CardContent>
<div className="space-y-4">
{proxmoxStorage.storage
.filter((storage) => storage && storage.name && storage.total > 0)
.filter(
(storage) =>
storage && storage.name && storage.total > 0 && storage.used >= 0 && storage.available >= 0,
)
.sort((a, b) => a.name.localeCompare(b.name))
.map((storage) => (
<div key={storage.name} className="border rounded-lg p-4">
@@ -562,7 +648,7 @@ export function StorageOverview() {
<div className="grid grid-cols-3 gap-4 text-sm">
<div>
<p className="text-muted-foreground">Total</p>
<p className="font-medium">{storage.total.toLocaleString()} GB</p>
<p className="font-medium">{formatStorage(storage.total)}</p>
</div>
<div>
<p className="text-muted-foreground">Used</p>
@@ -575,12 +661,12 @@ export function StorageOverview() {
: "text-blue-400"
}`}
>
{storage.used.toLocaleString()} GB
{formatStorage(storage.used)}
</p>
</div>
<div>
<p className="text-muted-foreground">Available</p>
<p className="font-medium text-green-400">{storage.available.toLocaleString()} GB</p>
<p className="font-medium text-green-400">{formatStorage(storage.available)}</p>
</div>
</div>
</div>
+258 -222
View File
@@ -27,7 +27,8 @@ import {
Menu,
Terminal,
} from "lucide-react"
import { useState, useEffect } from "react"
import { useState, useEffect, useMemo } from "react"
import { API_PORT, fetchApi } from "@/lib/api-config"
interface Log {
timestamp: string
@@ -125,9 +126,20 @@ export function SystemLogs() {
const getApiUrl = (endpoint: string) => {
if (typeof window !== "undefined") {
return `${window.location.protocol}//${window.location.hostname}:8008${endpoint}`
const { protocol, hostname, port } = window.location
const isStandardPort = port === "" || port === "80" || port === "443"
if (isStandardPort) {
return endpoint
} else {
return `${protocol}//${hostname}:${API_PORT}${endpoint}`
}
}
return `http://localhost:8008${endpoint}`
// This part might not be strictly necessary if only running client-side, but good for SSR safety
// In a real SSR scenario, you'd need to handle API_PORT differently
const protocol = typeof window !== "undefined" ? window.location.protocol : "http:" // Defaulting to http for SSR safety
const hostname = typeof window !== "undefined" ? window.location.hostname : "localhost" // Defaulting to localhost for SSR safety
return `${protocol}//${hostname}:${API_PORT}${endpoint}`
}
useEffect(() => {
@@ -186,27 +198,15 @@ export function SystemLogs() {
const [logsRes, backupsRes, eventsRes, notificationsRes] = await Promise.all([
fetchSystemLogs(),
fetch(getApiUrl("/api/backups")),
fetch(getApiUrl("/api/events?limit=50")),
fetch(getApiUrl("/api/notifications")),
fetchApi("/api/backups"),
fetchApi("/api/events?limit=50"),
fetchApi("/api/notifications"),
])
setLogs(logsRes)
if (backupsRes.ok) {
const backupsData = await backupsRes.json()
setBackups(backupsData.backups || [])
}
if (eventsRes.ok) {
const eventsData = await eventsRes.json()
setEvents(eventsData.events || [])
}
if (notificationsRes.ok) {
const notificationsData = await notificationsRes.json()
setNotifications(notificationsData.notifications || [])
}
setBackups(backupsRes.backups || [])
setEvents(eventsRes.events || [])
setNotifications(notificationsRes.notifications || [])
} catch (err) {
console.error("[v0] Error fetching system logs data:", err)
setError("Failed to connect to server")
@@ -217,7 +217,7 @@ export function SystemLogs() {
const fetchSystemLogs = async (): Promise<SystemLog[]> => {
try {
let apiUrl = getApiUrl("/api/logs")
let apiUrl = "/api/logs"
const params = new URLSearchParams()
// CHANGE: Always add since_days parameter (no more "now" option)
@@ -250,22 +250,7 @@ export function SystemLogs() {
}
console.log("[v0] Making fetch request to:", apiUrl)
const response = await fetch(apiUrl, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
cache: "no-store",
signal: AbortSignal.timeout(30000), // 30 second timeout
})
console.log("[v0] Response status:", response.status, "OK:", response.ok)
if (!response.ok) {
throw new Error(`Flask server responded with status: ${response.status}`)
}
const data = await response.json()
const data = await fetchApi(apiUrl)
console.log("[v0] Received logs data, count:", data.logs?.length || 0)
const logsArray = Array.isArray(data) ? data : data.logs || []
@@ -356,37 +341,33 @@ export function SystemLogs() {
if (upid) {
// Try to fetch the complete task log from Proxmox
try {
const response = await fetch(getApiUrl(`/api/task-log/${encodeURIComponent(upid)}`))
const taskLog = await fetchApi(`/api/task-log/${encodeURIComponent(upid)}`, {}, "text")
if (response.ok) {
const taskLog = await response.text()
// Download the complete task log
const blob = new Blob(
[
`Proxmox Task Log\n`,
`================\n\n`,
`UPID: ${upid}\n`,
`Timestamp: ${notification.timestamp}\n`,
`Service: ${notification.service}\n`,
`Source: ${notification.source}\n\n`,
`Complete Task Log:\n`,
`${"-".repeat(80)}\n`,
`${taskLog}\n`,
],
{ type: "text/plain" },
)
// Download the complete task log
const blob = new Blob(
[
`Proxmox Task Log\n`,
`================\n\n`,
`UPID: ${upid}\n`,
`Timestamp: ${notification.timestamp}\n`,
`Service: ${notification.service}\n`,
`Source: ${notification.source}\n\n`,
`Complete Task Log:\n`,
`${"-".repeat(80)}\n`,
`${taskLog}\n`,
],
{ type: "text/plain" },
)
const url = window.URL.createObjectURL(blob)
const a = document.createElement("a")
a.href = url
a.download = `task_log_${upid.replace(/:/g, "_")}_${notification.timestamp.replace(/[:\s]/g, "_")}.txt`
document.body.appendChild(a)
a.click()
window.URL.revokeObjectURL(url)
document.body.removeChild(a)
return
}
const url = window.URL.createObjectURL(blob)
const a = document.createElement("a")
a.href = url
a.download = `task_log_${upid.replace(/:/g, "_")}_${notification.timestamp.replace(/[:\s]/g, "_")}.txt`
document.body.appendChild(a)
a.click()
window.URL.revokeObjectURL(url)
document.body.removeChild(a)
return
} catch (error) {
console.error("[v0] Failed to fetch task log from Proxmox:", error)
// Fall through to download notification message
@@ -421,39 +402,61 @@ export function SystemLogs() {
}
}
const logsOnly: CombinedLogEntry[] = logs
.map((log) => ({ ...log, isEvent: false, sortTimestamp: new Date(log.timestamp).getTime() }))
.sort((a, b) => b.sortTimestamp - a.sortTimestamp)
const safeToLowerCase = (value: any): string => {
if (value === null || value === undefined) return ""
return String(value).toLowerCase()
}
const eventsOnly: CombinedLogEntry[] = events
.map((event) => ({
timestamp: event.starttime,
level: event.level,
service: event.type,
message: `${event.type}${event.vmid ? ` (VM/CT ${event.vmid})` : ""} - ${event.status}`,
source: `Node: ${event.node} • User: ${event.user}`,
isEvent: true,
eventData: event,
sortTimestamp: new Date(event.starttime).getTime(),
}))
.sort((a, b) => b.sortTimestamp - a.sortTimestamp)
const memoizedLogs = useMemo(() => logs, [logs])
const memoizedEvents = useMemo(() => events, [events])
const memoizedBackups = useMemo(() => backups, [backups])
const memoizedNotifications = useMemo(() => notifications, [notifications])
const logsOnly: CombinedLogEntry[] = useMemo(
() =>
memoizedLogs
.map((log) => ({ ...log, isEvent: false, sortTimestamp: new Date(log.timestamp).getTime() }))
.sort((a, b) => b.sortTimestamp - a.sortTimestamp),
[memoizedLogs],
)
const eventsOnly: CombinedLogEntry[] = useMemo(
() =>
memoizedEvents
.map((event) => ({
timestamp: event.starttime,
level: event.level,
service: event.type,
message: `${event.type}${event.vmid ? ` (VM/CT ${event.vmid})` : ""} - ${event.status}`,
source: `Node: ${event.node} • User: ${event.user}`,
isEvent: true,
eventData: event,
sortTimestamp: new Date(event.starttime).getTime(),
}))
.sort((a, b) => b.sortTimestamp - a.sortTimestamp),
[memoizedEvents],
)
// Filter logs only
const filteredLogsOnly = logsOnly.filter((log) => {
const message = log.message || ""
const service = log.service || ""
const searchTermLower = safeToLowerCase(searchTerm)
const matchesSearch =
log.message.toLowerCase().includes(searchTerm.toLowerCase()) ||
log.service.toLowerCase().includes(searchTerm.toLowerCase())
safeToLowerCase(message).includes(searchTermLower) || safeToLowerCase(service).includes(searchTermLower)
const matchesLevel = levelFilter === "all" || log.level === levelFilter
const matchesService = serviceFilter === "all" || log.service === serviceFilter
return matchesSearch && matchesLevel && matchesService
})
// Filter events only
const filteredEventsOnly = eventsOnly.filter((event) => {
const message = event.message || ""
const service = event.service || ""
const searchTermLower = safeToLowerCase(searchTerm)
const matchesSearch =
event.message.toLowerCase().includes(searchTerm.toLowerCase()) ||
event.service.toLowerCase().includes(searchTerm.toLowerCase())
safeToLowerCase(message).includes(searchTermLower) || safeToLowerCase(service).includes(searchTermLower)
const matchesLevel = levelFilter === "all" || event.level === levelFilter
const matchesService = serviceFilter === "all" || event.service === serviceFilter
@@ -463,30 +466,40 @@ export function SystemLogs() {
const displayedLogsOnly = filteredLogsOnly.slice(0, displayedLogsCount)
const displayedEventsOnly = filteredEventsOnly.slice(0, displayedLogsCount)
const combinedLogs: CombinedLogEntry[] = [
...logs.map((log) => ({ ...log, isEvent: false, sortTimestamp: new Date(log.timestamp).getTime() })),
...events.map((event) => ({
timestamp: event.starttime,
level: event.level,
service: event.type,
message: `${event.type}${event.vmid ? ` (VM/CT ${event.vmid})` : ""} - ${event.status}`,
source: `Node: ${event.node} • User: ${event.user}`,
isEvent: true,
eventData: event,
sortTimestamp: new Date(event.starttime).getTime(),
})),
].sort((a, b) => b.sortTimestamp - a.sortTimestamp) // Sort by timestamp descending
const combinedLogs: CombinedLogEntry[] = useMemo(
() =>
[
...memoizedLogs.map((log) => ({ ...log, isEvent: false, sortTimestamp: new Date(log.timestamp).getTime() })),
...memoizedEvents.map((event) => ({
timestamp: event.starttime,
level: event.level,
service: event.type,
message: `${event.type}${event.vmid ? ` (VM/CT ${event.vmid})` : ""} - ${event.status}`,
source: `Node: ${event.node} • User: ${event.user}`,
isEvent: true,
eventData: event,
sortTimestamp: new Date(event.starttime).getTime(),
})),
].sort((a, b) => b.sortTimestamp - a.sortTimestamp),
[memoizedLogs, memoizedEvents],
)
// Filter combined logs
const filteredCombinedLogs = combinedLogs.filter((log) => {
const matchesSearch =
log.message.toLowerCase().includes(searchTerm.toLowerCase()) ||
log.service.toLowerCase().includes(searchTerm.toLowerCase())
const matchesLevel = levelFilter === "all" || log.level === levelFilter
const matchesService = serviceFilter === "all" || log.service === serviceFilter
const filteredCombinedLogs = useMemo(
() =>
combinedLogs.filter((log) => {
const message = log.message || ""
const service = log.service || ""
const searchTermLower = safeToLowerCase(searchTerm)
return matchesSearch && matchesLevel && matchesService
})
const matchesSearch =
safeToLowerCase(message).includes(searchTermLower) || safeToLowerCase(service).includes(searchTermLower)
const matchesLevel = levelFilter === "all" || log.level === levelFilter
const matchesService = serviceFilter === "all" || log.service === serviceFilter
return matchesSearch && matchesLevel && matchesService
}),
[combinedLogs, searchTerm, levelFilter, serviceFilter],
)
// CHANGE: Re-assigning displayedLogs to use the filteredCombinedLogs
const displayedLogs = filteredCombinedLogs.slice(0, displayedLogsCount)
@@ -548,7 +561,9 @@ export function SystemLogs() {
}
const getNotificationTypeColor = (type: string) => {
switch (type.toLowerCase()) {
if (!type) return "bg-gray-500/10 text-gray-500 border-gray-500/20"
switch (safeToLowerCase(type)) {
case "error":
return "bg-red-500/10 text-red-500 border-red-500/20"
case "warning":
@@ -564,7 +579,9 @@ export function SystemLogs() {
// ADDED: New function for notification source colors
const getNotificationSourceColor = (source: string) => {
switch (source.toLowerCase()) {
if (!source) return "bg-gray-500/10 text-gray-500 border-gray-500/20"
switch (safeToLowerCase(source)) {
case "task-log":
return "bg-purple-500/10 text-purple-500 border-purple-500/20"
case "journal":
@@ -583,7 +600,7 @@ export function SystemLogs() {
info: logs.filter((log) => ["info", "notice", "debug"].includes(log.level)).length,
}
const uniqueServices = [...new Set(logs.map((log) => log.service))]
const uniqueServices = useMemo(() => [...new Set(memoizedLogs.map((log) => log.service))], [memoizedLogs])
const getBackupType = (volid: string): "vm" | "lxc" => {
if (volid.includes("/vm/") || volid.includes("vzdump-qemu")) {
@@ -908,9 +925,11 @@ export function SystemLogs() {
<SelectValue placeholder="Filter by service" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Services</SelectItem>
{uniqueServices.slice(0, 20).map((service) => (
<SelectItem key={service} value={service}>
<SelectItem key="service-all" value="all">
All Services
</SelectItem>
{uniqueServices.slice(0, 20).map((service, idx) => (
<SelectItem key={`service-${service}-${idx}`} value={service}>
{service}
</SelectItem>
))}
@@ -925,51 +944,59 @@ export function SystemLogs() {
<ScrollArea className="h-[600px] w-full rounded-md border border-border overflow-x-hidden">
<div className="space-y-2 p-4 w-full box-border">
{displayedLogs.map((log, index) => (
<div
key={index}
className="flex flex-col md:flex-row md:items-start space-y-2 md:space-y-0 md:space-x-4 p-3 rounded-lg border border-white/10 sm:border-border bg-white/5 sm:bg-card sm:hover:bg-white/5 transition-colors cursor-pointer overflow-hidden box-border"
onClick={() => {
if (log.eventData) {
setSelectedEvent(log.eventData)
setIsEventModalOpen(true)
} else {
setSelectedLog(log as SystemLog)
setIsLogModalOpen(true)
}
}}
>
<div className="flex-shrink-0 flex gap-2 flex-wrap">
<Badge variant="outline" className={getLevelColor(log.level)}>
{getLevelIcon(log.level)}
{log.level.toUpperCase()}
</Badge>
{log.eventData && (
<Badge variant="outline" className="bg-purple-500/10 text-purple-500 border-purple-500/20">
<Activity className="h-3 w-3 mr-1" />
EVENT
</Badge>
)}
</div>
{displayedLogs.map((log, index) => {
// Generate a more stable unique key
const timestampMs = new Date(log.timestamp).getTime()
const uniqueKey = log.eventData
? `event-${log.eventData.upid.replace(/:/g, "-")}-${timestampMs}`
: `log-${timestampMs}-${log.service?.substring(0, 10) || "unknown"}-${log.pid || "nopid"}-${index}`
<div className="flex-1 min-w-0 overflow-hidden box-border">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-1 gap-1">
<div className="text-sm font-medium text-foreground truncate min-w-0">{log.service}</div>
<div className="text-xs text-muted-foreground font-mono truncate sm:ml-2 sm:flex-shrink-0">
{log.timestamp}
return (
<div
key={uniqueKey}
className="flex flex-col md:flex-row md:items-start space-y-2 md:space-y-0 md:space-x-4 p-3 rounded-lg border border-white/10 sm:border-border bg-white/5 sm:bg-card sm:hover:bg-white/5 transition-colors cursor-pointer overflow-hidden box-border"
onClick={() => {
if (log.eventData) {
setSelectedEvent(log.eventData)
setIsEventModalOpen(true)
} else {
setSelectedLog(log as SystemLog)
setIsLogModalOpen(true)
}
}}
>
<div className="flex-shrink-0 flex gap-2 flex-wrap">
<Badge variant="outline" className={getLevelColor(log.level)}>
{getLevelIcon(log.level)}
{log.level.toUpperCase()}
</Badge>
{log.eventData && (
<Badge variant="outline" className="bg-purple-500/10 text-purple-500 border-purple-500/20">
<Activity className="h-3 w-3 mr-1" />
EVENT
</Badge>
)}
</div>
<div className="flex-1 min-w-0 overflow-hidden box-border">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-1 gap-1">
<div className="text-sm font-medium text-foreground truncate min-w-0">{log.service}</div>
<div className="text-xs text-muted-foreground font-mono truncate sm:ml-2 sm:flex-shrink-0">
{log.timestamp}
</div>
</div>
<div className="text-sm text-foreground mb-1 line-clamp-2 break-all overflow-hidden">
{log.message}
</div>
<div className="text-xs text-muted-foreground truncate break-all overflow-hidden">
{log.source}
{log.pid && ` • PID: ${log.pid}`}
{log.hostname && ` • Host: ${log.hostname}`}
</div>
</div>
<div className="text-sm text-foreground mb-1 line-clamp-2 break-all overflow-hidden">
{log.message}
</div>
<div className="text-xs text-muted-foreground truncate break-all overflow-hidden">
{log.source}
{log.pid && ` • PID: ${log.pid}`}
{log.hostname && ` • Host: ${log.hostname}`}
</div>
</div>
</div>
))}
)
})}
{displayedLogs.length === 0 && (
<div className="text-center py-8 text-muted-foreground">
@@ -1030,44 +1057,48 @@ export function SystemLogs() {
<ScrollArea className="h-[500px] w-full rounded-md border border-border">
<div className="space-y-2 p-4">
{backups.map((backup, index) => (
<div
key={index}
className="flex items-start space-x-4 p-3 rounded-lg border border-white/10 sm:border-border bg-white/5 sm:bg-card sm:hover:bg-white/5 transition-colors cursor-pointer"
onClick={() => {
setSelectedBackup(backup)
setIsBackupModalOpen(true)
}}
>
<div className="flex-shrink-0">
<HardDrive className="h-5 w-5 text-blue-500" />
</div>
{memoizedBackups.map((backup, index) => {
const uniqueKey = `backup-${backup.volid.replace(/[/:]/g, "-")}-${backup.timestamp || index}`
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-1 gap-2 flex-wrap">
<div className="flex items-center gap-2 flex-wrap">
<Badge variant="outline" className={getBackupTypeColor(backup.volid)}>
{getBackupTypeLabel(backup.volid)}
</Badge>
<Badge variant="outline" className={getBackupStorageColor(backup.volid)}>
{getBackupStorageLabel(backup.volid)}
return (
<div
key={uniqueKey}
className="flex items-start space-x-4 p-3 rounded-lg border border-white/10 sm:border-border bg-white/5 sm:bg-card sm:hover:bg-white/5 transition-colors cursor-pointer"
onClick={() => {
setSelectedBackup(backup)
setIsBackupModalOpen(true)
}}
>
<div className="flex-shrink-0">
<HardDrive className="h-5 w-5 text-blue-500" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-1 gap-2 flex-wrap">
<div className="flex items-center gap-2 flex-wrap">
<Badge variant="outline" className={getBackupTypeColor(backup.volid)}>
{getBackupTypeLabel(backup.volid)}
</Badge>
<Badge variant="outline" className={getBackupStorageColor(backup.volid)}>
{getBackupStorageLabel(backup.volid)}
</Badge>
</div>
<Badge
variant="outline"
className="bg-green-500/10 text-green-500 border-green-500/20 whitespace-nowrap"
>
{backup.size_human}
</Badge>
</div>
<Badge
variant="outline"
className="bg-green-500/10 text-green-500 border-green-500/20 whitespace-nowrap"
>
{backup.size_human}
</Badge>
</div>
<div className="text-xs text-muted-foreground mb-1 truncate">Storage: {backup.storage}</div>
<div className="text-xs text-muted-foreground flex items-center">
<Calendar className="h-3 w-3 mr-1 flex-shrink-0" />
<span className="truncate">{backup.created}</span>
<div className="text-xs text-muted-foreground mb-1 truncate">Storage: {backup.storage}</div>
<div className="text-xs text-muted-foreground flex items-center">
<Calendar className="h-3 w-3 mr-1 flex-shrink-0" />
<span className="truncate">{backup.created}</span>
</div>
</div>
</div>
</div>
))}
)
})}
{backups.length === 0 && (
<div className="text-center py-8 text-muted-foreground">
@@ -1083,42 +1114,47 @@ export function SystemLogs() {
<TabsContent value="notifications" className="space-y-4">
<ScrollArea className="h-[600px] w-full rounded-md border border-border">
<div className="space-y-2 p-4">
{notifications.map((notification, index) => (
<div
key={index}
className="flex flex-col md:flex-row md:items-start space-y-2 md:space-y-0 md:space-x-4 p-3 rounded-lg border border-white/10 sm:border-border bg-white/5 sm:bg-card sm:hover:bg-white/5 transition-colors cursor-pointer overflow-hidden w-full"
onClick={() => {
setSelectedNotification(notification)
setIsNotificationModalOpen(true)
}}
>
<div className="flex-shrink-0 flex gap-2 flex-wrap">
<Badge variant="outline" className={getNotificationTypeColor(notification.type)}>
{notification.type.toUpperCase()}
</Badge>
<Badge variant="outline" className={getNotificationSourceColor(notification.source)}>
{notification.source === "task-log" && <Activity className="h-3 w-3 mr-1" />}
{notification.source === "journal" && <FileText className="h-3 w-3 mr-1" />}
{notification.source.toUpperCase()}
</Badge>
</div>
{memoizedNotifications.map((notification, index) => {
const timestampMs = new Date(notification.timestamp).getTime()
const uniqueKey = `notification-${timestampMs}-${notification.service?.substring(0, 10) || "unknown"}-${notification.source?.substring(0, 10) || "unknown"}-${index}`
<div className="flex-1 min-w-0 overflow-hidden">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-1 gap-1">
<div className="text-sm font-medium text-foreground truncate">{notification.service}</div>
<div className="text-xs text-muted-foreground font-mono truncate">
{notification.timestamp}
return (
<div
key={uniqueKey}
className="flex flex-col md:flex-row md:items-start space-y-2 md:space-y-0 md:space-x-4 p-3 rounded-lg border border-white/10 sm:border-border bg-white/5 sm:bg-card sm:hover:bg-white/5 transition-colors cursor-pointer overflow-hidden w-full"
onClick={() => {
setSelectedNotification(notification)
setIsNotificationModalOpen(true)
}}
>
<div className="flex-shrink-0 flex gap-2 flex-wrap">
<Badge variant="outline" className={getNotificationTypeColor(notification.type)}>
{notification.type.toUpperCase()}
</Badge>
<Badge variant="outline" className={getNotificationSourceColor(notification.source)}>
{notification.source === "task-log" && <Activity className="h-3 w-3 mr-1" />}
{notification.source === "journal" && <FileText className="h-3 w-3 mr-1" />}
{notification.source.toUpperCase()}
</Badge>
</div>
<div className="flex-1 min-w-0 overflow-hidden">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-1 gap-1">
<div className="text-sm font-medium text-foreground truncate">{notification.service}</div>
<div className="text-xs text-muted-foreground font-mono truncate">
{notification.timestamp}
</div>
</div>
<div className="text-sm text-foreground mb-1 line-clamp-2 break-all overflow-hidden">
{notification.message}
</div>
<div className="text-xs text-muted-foreground break-words overflow-hidden">
Service: {notification.service} Source: {notification.source}
</div>
</div>
<div className="text-sm text-foreground mb-1 line-clamp-2 break-all overflow-hidden">
{notification.message}
</div>
<div className="text-xs text-muted-foreground break-words overflow-hidden">
Service: {notification.service} Source: {notification.source}
</div>
</div>
</div>
))}
)
})}
{notifications.length === 0 && (
<div className="text-center py-8 text-muted-foreground">
+142 -189
View File
@@ -8,6 +8,7 @@ import { Cpu, MemoryStick, Thermometer, Server, Zap, AlertCircle, HardDrive, Net
import { NodeMetricsCharts } from "./node-metrics-charts"
import { NetworkTrafficChart } from "./network-traffic-chart"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
import { fetchApi } from "../lib/api-config"
interface SystemData {
cpu_usage: number
@@ -97,22 +98,7 @@ interface ProxmoxStorageData {
const fetchSystemData = async (): Promise<SystemData | null> => {
try {
const baseUrl = typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : ""
const apiUrl = `${baseUrl}/api/system`
const response = await fetch(apiUrl, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
cache: "no-store",
})
if (!response.ok) {
throw new Error(`Flask server responded with status: ${response.status}`)
}
const data = await response.json()
const data = await fetchApi<SystemData>("/api/system")
return data
} catch (error) {
console.error("[v0] Failed to fetch system data:", error)
@@ -122,22 +108,7 @@ const fetchSystemData = async (): Promise<SystemData | null> => {
const fetchVMData = async (): Promise<VMData[]> => {
try {
const baseUrl = typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : ""
const apiUrl = `${baseUrl}/api/vms`
const response = await fetch(apiUrl, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
cache: "no-store",
})
if (!response.ok) {
throw new Error(`Flask server responded with status: ${response.status}`)
}
const data = await response.json()
const data = await fetchApi<any>("/api/vms")
return Array.isArray(data) ? data : data.vms || []
} catch (error) {
console.error("[v0] Failed to fetch VM data:", error)
@@ -147,78 +118,30 @@ const fetchVMData = async (): Promise<VMData[]> => {
const fetchStorageData = async (): Promise<StorageData | null> => {
try {
const baseUrl = typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : ""
const apiUrl = `${baseUrl}/api/storage/summary`
const response = await fetch(apiUrl, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
cache: "no-store",
})
if (!response.ok) {
console.log("[v0] Storage API not available (this is normal if not configured)")
return null
}
const data = await response.json()
const data = await fetchApi<StorageData>("/api/storage/summary")
return data
} catch (error) {
console.log("[v0] Storage data unavailable:", error instanceof Error ? error.message : "Unknown error")
console.log("[v0] Storage API not available (this is normal if not configured)")
return null
}
}
const fetchNetworkData = async (): Promise<NetworkData | null> => {
try {
const baseUrl = typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : ""
const apiUrl = `${baseUrl}/api/network/summary`
const response = await fetch(apiUrl, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
cache: "no-store",
})
if (!response.ok) {
console.log("[v0] Network API not available (this is normal if not configured)")
return null
}
const data = await response.json()
const data = await fetchApi<NetworkData>("/api/network/summary")
return data
} catch (error) {
console.log("[v0] Network data unavailable:", error instanceof Error ? error.message : "Unknown error")
console.log("[v0] Network API not available (this is normal if not configured)")
return null
}
}
const fetchProxmoxStorageData = async (): Promise<ProxmoxStorageData | null> => {
try {
const baseUrl = typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : ""
const apiUrl = `${baseUrl}/api/proxmox-storage`
const response = await fetch(apiUrl, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
cache: "no-store",
})
if (!response.ok) {
console.log("[v0] Proxmox storage API not available")
return null
}
const data = await response.json()
const data = await fetchApi<ProxmoxStorageData>("/api/proxmox-storage")
return data
} catch (error) {
console.log("[v0] Proxmox storage data unavailable:", error instanceof Error ? error.message : "Unknown error")
console.log("[v0] Proxmox storage API not available")
return null
}
}
@@ -229,100 +152,87 @@ export function SystemOverview() {
const [storageData, setStorageData] = useState<StorageData | null>(null)
const [proxmoxStorageData, setProxmoxStorageData] = useState<ProxmoxStorageData | null>(null)
const [networkData, setNetworkData] = useState<NetworkData | null>(null)
const [loading, setLoading] = useState(true)
const [loadingStates, setLoadingStates] = useState({
system: true,
vms: true,
storage: true,
network: true,
})
const [error, setError] = useState<string | null>(null)
const [networkTimeframe, setNetworkTimeframe] = useState("day")
const [networkTotals, setNetworkTotals] = useState<{ received: number; sent: number }>({ received: 0, sent: 0 })
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true)
setError(null)
const fetchAllData = async () => {
const [systemResult, vmResult, storageResults, networkResult] = await Promise.all([
fetchSystemData().finally(() => setLoadingStates((prev) => ({ ...prev, system: false }))),
fetchVMData().finally(() => setLoadingStates((prev) => ({ ...prev, vms: false }))),
Promise.all([fetchStorageData(), fetchProxmoxStorageData()]).finally(() =>
setLoadingStates((prev) => ({ ...prev, storage: false })),
),
fetchNetworkData().finally(() => setLoadingStates((prev) => ({ ...prev, network: false }))),
])
const systemResult = await fetchSystemData()
if (!systemResult) {
setError("Flask server not available. Please ensure the server is running.")
setLoading(false)
return
}
setSystemData(systemResult)
} catch (err) {
console.error("[v0] Error fetching system data:", err)
setError("Failed to connect to Flask server. Please check your connection.")
} finally {
setLoading(false)
if (!systemResult) {
setError("Flask server not available. Please ensure the server is running.")
return
}
setSystemData(systemResult)
setVmData(vmResult)
setStorageData(storageResults[0])
setProxmoxStorageData(storageResults[1])
setNetworkData(networkResult)
setTimeout(async () => {
const refreshedSystemData = await fetchSystemData()
if (refreshedSystemData) {
setSystemData(refreshedSystemData)
}
}, 2000)
}
fetchData()
fetchAllData()
const systemInterval = setInterval(() => {
fetchSystemData().then((data) => {
if (data) setSystemData(data)
})
}, 10000)
const systemInterval = setInterval(async () => {
const data = await fetchSystemData()
if (data) setSystemData(data)
}, 9000)
const vmInterval = setInterval(async () => {
const data = await fetchVMData()
setVmData(data)
}, 59000)
const storageInterval = setInterval(async () => {
const [storage, proxmoxStorage] = await Promise.all([fetchStorageData(), fetchProxmoxStorageData()])
if (storage) setStorageData(storage)
if (proxmoxStorage) setProxmoxStorageData(proxmoxStorage)
}, 59000)
const networkInterval = setInterval(async () => {
const data = await fetchNetworkData()
if (data) setNetworkData(data)
}, 59000)
return () => {
clearInterval(systemInterval)
}
}, [])
useEffect(() => {
const fetchVMs = async () => {
const vmResult = await fetchVMData()
setVmData(vmResult)
}
fetchVMs()
const vmInterval = setInterval(fetchVMs, 60000)
return () => {
clearInterval(vmInterval)
}
}, [])
useEffect(() => {
const fetchStorage = async () => {
const storageResult = await fetchStorageData()
setStorageData(storageResult)
const proxmoxStorageResult = await fetchProxmoxStorageData()
setProxmoxStorageData(proxmoxStorageResult)
}
fetchStorage()
const storageInterval = setInterval(fetchStorage, 60000)
return () => {
clearInterval(storageInterval)
}
}, [])
useEffect(() => {
const fetchNetwork = async () => {
const networkResult = await fetchNetworkData()
setNetworkData(networkResult)
}
fetchNetwork()
const networkInterval = setInterval(fetchNetwork, 60000)
return () => {
clearInterval(networkInterval)
}
}, [])
if (loading) {
const isInitialLoading = loadingStates.system && !systemData
if (isInitialLoading) {
return (
<div className="space-y-6">
<div className="text-center py-8">
<div className="text-lg font-medium text-foreground mb-2">Connecting to ProxMenux Monitor...</div>
<div className="text-sm text-muted-foreground">Fetching real-time system data</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 lg:gap-6">
{[...Array(4)].map((_, i) => (
<Card key={i} className="bg-card border-border animate-pulse">
<CardContent className="p-6">
@@ -390,14 +300,11 @@ export function SystemOverview() {
const formatStorage = (sizeInGB: number): string => {
if (sizeInGB < 1) {
// Less than 1 GB, show in MB
return `${(sizeInGB * 1024).toFixed(1)} MB`
} else if (sizeInGB < 1024) {
// Less than 1024 GB, show in GB
return `${sizeInGB.toFixed(1)} GB`
} else {
// 1024 GB or more, show in TB
} else if (sizeInGB > 999) {
return `${(sizeInGB / 1024).toFixed(2)} TB`
} else {
return `${sizeInGB.toFixed(2)} GB`
}
}
@@ -407,13 +314,10 @@ export function SystemOverview() {
const vmLxcStorages = proxmoxStorageData?.storage.filter(
(s) =>
// Include only local storage types that can host VMs/LXCs
(s.type === "lvm" || s.type === "lvmthin" || s.type === "zfspool" || s.type === "btrfs" || s.type === "dir") &&
// Exclude network storage
s.type !== "nfs" &&
s.type !== "cifs" &&
s.type !== "iscsi" &&
// Exclude the "local" storage (used for ISOs/templates)
s.name !== "local",
)
@@ -479,7 +383,6 @@ export function SystemOverview() {
return (
<div className="space-y-6">
{/* Key Metrics Cards */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 lg:gap-6">
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
@@ -529,34 +432,44 @@ export function SystemOverview() {
</Card>
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Active VM & LXC</CardTitle>
<Server className="h-4 w-4 text-muted-foreground" />
<CardHeader>
<CardTitle className="text-foreground flex items-center">
<Server className="h-5 w-5 mr-2" />
Active VM & LXC
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-xl lg:text-2xl font-bold text-foreground">{vmStats.running}</div>
<div className="mt-2 flex flex-wrap gap-1">
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20">
{vmStats.running} Running
</Badge>
{vmStats.stopped > 0 && (
<Badge variant="outline" className="bg-red-500/10 text-red-500 border-red-500/20">
{vmStats.stopped} Stopped
</Badge>
)}
</div>
<p className="text-xs text-muted-foreground mt-2">
Total: {vmStats.vms} VMs, {vmStats.lxc} LXC
</p>
{loadingStates.vms ? (
<div className="space-y-2 animate-pulse">
<div className="h-8 bg-muted rounded w-12"></div>
<div className="h-5 bg-muted rounded w-24"></div>
<div className="h-4 bg-muted rounded w-32"></div>
</div>
) : (
<>
<div className="text-xl lg:text-2xl font-bold text-foreground">{vmStats.running}</div>
<div className="mt-2 flex flex-wrap gap-1">
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20">
{vmStats.running} Running
</Badge>
{vmStats.stopped > 0 && (
<Badge variant="outline" className="bg-red-500/10 text-red-500 border-red-500/20">
{vmStats.stopped} Stopped
</Badge>
)}
</div>
<p className="text-xs text-muted-foreground mt-2">
Total: {vmStats.vms} VMs, {vmStats.lxc} LXC
</p>
</>
)}
</CardContent>
</Card>
</div>
{/* Node Metrics Charts */}
<NodeMetricsCharts />
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Storage Summary */}
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground flex items-center">
@@ -565,8 +478,45 @@ export function SystemOverview() {
</CardTitle>
</CardHeader>
<CardContent>
{storageData ? (
{loadingStates.storage ? (
<div className="space-y-4 animate-pulse">
<div className="h-6 bg-muted rounded w-full"></div>
<div className="h-4 bg-muted rounded w-3/4"></div>
<div className="h-4 bg-muted rounded w-2/3"></div>
</div>
) : storageData ? (
<div className="space-y-4">
{(() => {
const totalCapacity = (vmLxcStorageTotal || 0) + (localStorage?.total || 0)
const totalUsed = (vmLxcStorageUsed || 0) + (localStorage?.used || 0)
const totalAvailable = (vmLxcStorageAvailable || 0) + (localStorage?.available || 0)
const totalPercent = totalCapacity > 0 ? (totalUsed / totalCapacity) * 100 : 0
return totalCapacity > 0 ? (
<div className="space-y-2 pb-4 border-b-2 border-border">
<div className="flex justify-between items-center">
<span className="text-sm font-medium text-foreground">Total Node Capacity:</span>
<span className="text-lg font-bold text-foreground">{formatStorage(totalCapacity)}</span>
</div>
<Progress
value={totalPercent}
className="mt-2 h-3 [&>div]:bg-gradient-to-r [&>div]:from-blue-500 [&>div]:to-purple-500"
/>
<div className="flex justify-between items-center mt-1">
<div className="flex items-center gap-3">
<span className="text-xs text-muted-foreground">
Used: <span className="font-semibold text-foreground">{formatStorage(totalUsed)}</span>
</span>
<span className="text-xs text-muted-foreground">
Free: <span className="font-semibold text-green-500">{formatStorage(totalAvailable)}</span>
</span>
</div>
<span className="text-xs font-semibold text-muted-foreground">{totalPercent.toFixed(1)}%</span>
</div>
</div>
) : null
})()}
<div className="space-y-2 pb-3 border-b border-border">
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Total Capacity:</span>
@@ -642,7 +592,6 @@ export function SystemOverview() {
</CardContent>
</Card>
{/* Network Summary */}
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground flex items-center justify-between">
@@ -665,7 +614,13 @@ export function SystemOverview() {
</CardTitle>
</CardHeader>
<CardContent>
{networkData ? (
{loadingStates.network ? (
<div className="space-y-4 animate-pulse">
<div className="h-6 bg-muted rounded w-full"></div>
<div className="h-4 bg-muted rounded w-3/4"></div>
<div className="h-4 bg-muted rounded w-2/3"></div>
</div>
) : networkData ? (
<div className="space-y-4">
<div className="flex justify-between items-center pb-3 border-b border-border">
<span className="text-sm text-muted-foreground">Active Interfaces:</span>
@@ -736,7 +691,6 @@ export function SystemOverview() {
</Card>
</div>
{/* System Information */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card className="bg-card border-border">
<CardHeader>
@@ -769,7 +723,6 @@ export function SystemOverview() {
</CardContent>
</Card>
{/* System Health & Alerts */}
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground flex items-center">
+261
View File
@@ -0,0 +1,261 @@
"use client"
import { useState } from "react"
import { Button } from "./ui/button"
import { Input } from "./ui/input"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "./ui/dialog"
import { AlertCircle, CheckCircle, Copy, Shield, Check } from "lucide-react"
import { getApiUrl } from "../lib/api-config"
interface TwoFactorSetupProps {
open: boolean
onClose: () => void
onSuccess: () => void
}
export function TwoFactorSetup({ open, onClose, onSuccess }: TwoFactorSetupProps) {
const [step, setStep] = useState(1)
const [qrCode, setQrCode] = useState("")
const [secret, setSecret] = useState("")
const [backupCodes, setBackupCodes] = useState<string[]>([])
const [verificationCode, setVerificationCode] = useState("")
const [error, setError] = useState("")
const [loading, setLoading] = useState(false)
const [copiedSecret, setCopiedSecret] = useState(false)
const [copiedCodes, setCopiedCodes] = useState(false)
const handleSetupStart = async () => {
setError("")
setLoading(true)
try {
const token = localStorage.getItem("proxmenux-auth-token")
const response = await fetch(getApiUrl("/api/auth/totp/setup"), {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
})
const data = await response.json()
if (!response.ok) {
throw new Error(data.message || "Failed to setup 2FA")
}
setQrCode(data.qr_code)
setSecret(data.secret)
setBackupCodes(data.backup_codes)
setStep(2)
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to setup 2FA")
} finally {
setLoading(false)
}
}
const handleVerify = async () => {
if (!verificationCode || verificationCode.length !== 6) {
setError("Please enter a 6-digit code")
return
}
setError("")
setLoading(true)
try {
const token = localStorage.getItem("proxmenux-auth-token")
const response = await fetch(getApiUrl("/api/auth/totp/enable"), {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ token: verificationCode }),
})
const data = await response.json()
if (!response.ok) {
throw new Error(data.message || "Invalid verification code")
}
setStep(3)
} catch (err) {
setError(err instanceof Error ? err.message : "Verification failed")
} finally {
setLoading(false)
}
}
const copyToClipboard = (text: string, type: "secret" | "codes") => {
navigator.clipboard.writeText(text)
if (type === "secret") {
setCopiedSecret(true)
setTimeout(() => setCopiedSecret(false), 2000)
} else {
setCopiedCodes(true)
setTimeout(() => setCopiedCodes(false), 2000)
}
}
const handleClose = () => {
setStep(1)
setQrCode("")
setSecret("")
setBackupCodes([])
setVerificationCode("")
setError("")
onClose()
}
const handleFinish = () => {
handleClose()
onSuccess()
}
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="max-w-md max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Shield className="h-5 w-5 text-blue-500" />
Setup Two-Factor Authentication
</DialogTitle>
<DialogDescription>Add an extra layer of security to your account</DialogDescription>
</DialogHeader>
{error && (
<div className="bg-red-500/10 border border-red-500/20 rounded-lg p-3 flex items-start gap-2">
<AlertCircle className="h-5 w-5 text-red-500 flex-shrink-0 mt-0.5" />
<p className="text-sm text-red-500">{error}</p>
</div>
)}
{step === 1 && (
<div className="space-y-4">
<div className="bg-blue-500/10 border border-blue-500/20 rounded-lg p-4">
<p className="text-sm text-blue-500">
Two-factor authentication (2FA) adds an extra layer of security by requiring a code from your
authentication app in addition to your password.
</p>
</div>
<div className="space-y-2">
<h4 className="font-medium">You will need:</h4>
<ul className="text-sm text-muted-foreground space-y-1 list-disc list-inside">
<li>An authentication app (Google Authenticator, Authy, etc.)</li>
<li>Scan a QR code or enter a key manually</li>
<li>Store backup codes securely</li>
</ul>
</div>
<Button onClick={handleSetupStart} className="w-full bg-blue-500 hover:bg-blue-600" disabled={loading}>
{loading ? "Starting..." : "Start Setup"}
</Button>
</div>
)}
{step === 2 && (
<div className="space-y-4">
<div className="space-y-2">
<h4 className="font-medium">1. Scan the QR code</h4>
<p className="text-sm text-muted-foreground">Open your authentication app and scan this QR code</p>
{qrCode && (
<div className="flex justify-center p-4 bg-white rounded-lg">
<img src={qrCode || "/placeholder.svg"} alt="QR Code" width={200} height={200} className="rounded" />
</div>
)}
</div>
<div className="space-y-2">
<h4 className="font-medium">Or enter the key manually:</h4>
<div className="flex gap-2">
<Input value={secret} readOnly className="font-mono text-sm" />
<Button
variant="outline"
size="icon"
onClick={() => copyToClipboard(secret, "secret")}
title="Copy key"
>
{copiedSecret ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
</Button>
</div>
</div>
<div className="space-y-2">
<h4 className="font-medium">2. Enter the verification code</h4>
<p className="text-sm text-muted-foreground">Enter the 6-digit code that appears in your app</p>
<Input
type="text"
placeholder="000000"
value={verificationCode}
onChange={(e) => setVerificationCode(e.target.value.replace(/\D/g, "").slice(0, 6))}
className="text-center text-lg tracking-widest font-mono text-base"
maxLength={6}
disabled={loading}
/>
</div>
<div className="flex gap-2">
<Button onClick={handleVerify} className="flex-1 bg-blue-500 hover:bg-blue-600" disabled={loading}>
{loading ? "Verifying..." : "Verify and Enable"}
</Button>
<Button onClick={handleClose} variant="outline" className="flex-1 bg-transparent" disabled={loading}>
Cancel
</Button>
</div>
</div>
)}
{step === 3 && (
<div className="space-y-4">
<div className="bg-green-500/10 border border-green-500/20 rounded-lg p-4 flex items-start gap-2">
<CheckCircle className="h-5 w-5 text-green-500 flex-shrink-0 mt-0.5" />
<div>
<p className="font-medium text-green-500">2FA Enabled Successfully</p>
<p className="text-sm text-green-500 mt-1">
Your account is now protected with two-factor authentication
</p>
</div>
</div>
<div className="space-y-2">
<h4 className="font-medium text-orange-500">Important: Save your backup codes</h4>
<p className="text-sm text-muted-foreground">
These codes will allow you to access your account if you lose access to your authentication app. Store
them in a safe place.
</p>
<div className="bg-muted/50 rounded-lg p-4 space-y-2">
<div className="flex justify-between items-center mb-2">
<span className="text-sm font-medium">Backup Codes</span>
<Button variant="outline" size="sm" onClick={() => copyToClipboard(backupCodes.join("\n"), "codes")}>
{copiedCodes ? (
<Check className="h-4 w-4 text-green-500 mr-2" />
) : (
<Copy className="h-4 w-4 mr-2" />
)}
Copy All
</Button>
</div>
<div className="grid grid-cols-2 gap-2">
{backupCodes.map((code, index) => (
<div key={index} className="bg-background rounded px-3 py-2 font-mono text-sm text-center">
{code}
</div>
))}
</div>
</div>
</div>
<Button onClick={handleFinish} className="w-full bg-blue-500 hover:bg-blue-600">
Finish
</Button>
</div>
)}
</DialogContent>
</Dialog>
)
}
+27
View File
@@ -0,0 +1,27 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { cn } from "@/lib/utils"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator className={cn("flex items-center justify-center text-current")}>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }
+1
View File
@@ -41,6 +41,7 @@ const DialogContent = React.forwardRef<
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] rounded-lg",
className,
)}
aria-describedby={props["aria-describedby"] || undefined}
{...props}
>
{children}
+1 -1
View File
@@ -9,7 +9,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type,
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
"flex h-10 w-full rounded-lg border border-input bg-background px-4 py-2 text-sm shadow-sm transition-all file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 hover:border-ring/50",
className,
)}
ref={ref}
+17
View File
@@ -0,0 +1,17 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "../../lib/utils"
const labelVariants = cva("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70")
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }
+131 -219
View File
@@ -25,6 +25,8 @@ import {
} from "lucide-react"
import useSWR from "swr"
import { MetricsView } from "./metrics-dialog"
import { formatStorage } from "@/lib/utils" // Import formatStorage utility
import { fetchApi } from "../lib/api-config"
interface VMData {
vmid: number
@@ -123,7 +125,6 @@ interface VMDetails extends VMData {
gpu_passthrough?: string[]
devices?: string[]
}
lxc_ip?: string
lxc_ip_info?: {
all_ips: string[]
real_ips: string[]
@@ -133,20 +134,7 @@ interface VMDetails extends VMData {
}
const fetcher = async (url: string) => {
const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
signal: AbortSignal.timeout(5000),
})
if (!response.ok) {
throw new Error(`Flask server responded with status: ${response.status}`)
}
const data = await response.json()
return data
return fetchApi(url)
}
const formatBytes = (bytes: number | undefined): string => {
@@ -194,18 +182,18 @@ const extractIPFromConfig = (config?: VMConfig, lxcIPInfo?: VMDetails["lxc_ip_in
return "DHCP"
}
const formatStorage = (sizeInGB: number): string => {
if (sizeInGB < 1) {
// Less than 1 GB, show in MB
return `${(sizeInGB * 1024).toFixed(1)} MB`
} else if (sizeInGB < 1024) {
// Less than 1024 GB, show in GB
return `${sizeInGB.toFixed(1)} GB`
} else {
// 1024 GB or more, show in TB
return `${(sizeInGB / 1024).toFixed(1)} TB`
}
}
// const formatStorage = (sizeInGB: number): string => {
// if (sizeInGB < 1) {
// // Less than 1 GB, show in MB
// return `${(sizeInGB * 1024).toFixed(1)} MB`
// } else if (sizeInGB < 1024) {
// // Less than 1024 GB, show in GB
// return `${sizeInGB.toFixed(1)} GB`
// } else {
// // 1024 GB or more, show in TB
// return `${(sizeInGB / 1024).toFixed(1)} TB`
// }
// }
const getUsageColor = (percent: number): string => {
if (percent >= 95) return "text-red-500"
@@ -263,9 +251,11 @@ export function VirtualMachines() {
isLoading,
mutate,
} = useSWR<VMData[]>("/api/vms", fetcher, {
refreshInterval: 30000,
refreshInterval: 23000,
revalidateOnFocus: false,
revalidateOnReconnect: true,
dedupingInterval: 10000,
errorRetryCount: 2,
})
const [selectedVM, setSelectedVM] = useState<VMData | null>(null)
@@ -280,37 +270,59 @@ export function VirtualMachines() {
const [editedNotes, setEditedNotes] = useState("")
const [savingNotes, setSavingNotes] = useState(false)
const [selectedMetric, setSelectedMetric] = useState<string | null>(null)
const [ipsLoaded, setIpsLoaded] = useState(false)
const [loadingIPs, setLoadingIPs] = useState(false)
useEffect(() => {
const fetchLXCIPs = async () => {
if (!vmData) return
// Only fetch if data exists, not already loaded, and not currently loading
if (!vmData || ipsLoaded || loadingIPs) return
const lxcs = vmData.filter((vm) => vm.type === "lxc")
if (lxcs.length === 0) {
setIpsLoaded(true)
return
}
setLoadingIPs(true)
const configs: Record<number, string> = {}
await Promise.all(
lxcs.map(async (lxc) => {
try {
const response = await fetch(`/api/vms/${lxc.vmid}`)
if (response.ok) {
const details = await response.json()
const batchSize = 5
for (let i = 0; i < lxcs.length; i += batchSize) {
const batch = lxcs.slice(i, i + batchSize)
await Promise.all(
batch.map(async (lxc) => {
try {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 10000)
const details = await fetchApi(`/api/vms/${lxc.vmid}`)
clearTimeout(timeoutId)
if (details.lxc_ip_info?.primary_ip) {
configs[lxc.vmid] = details.lxc_ip_info.primary_ip
} else if (details.config) {
configs[lxc.vmid] = extractIPFromConfig(details.config, details.lxc_ip_info)
}
} catch (error) {
console.log(`[v0] Could not fetch IP for LXC ${lxc.vmid}`)
configs[lxc.vmid] = "N/A"
}
} catch (error) {
console.error(`Error fetching config for LXC ${lxc.vmid}:`, error)
}
}),
)
}),
)
setVmConfigs(configs)
setVmConfigs((prev) => ({ ...prev, ...configs }))
}
setLoadingIPs(false)
setIpsLoaded(true)
}
fetchLXCIPs()
}, [vmData])
}, [vmData, ipsLoaded, loadingIPs])
const handleVMClick = async (vm: VMData) => {
setSelectedVM(vm)
@@ -321,11 +333,8 @@ export function VirtualMachines() {
setEditedNotes("")
setDetailsLoading(true)
try {
const response = await fetch(`/api/vms/${vm.vmid}`)
if (response.ok) {
const details = await response.json()
setVMDetails(details)
}
const details = await fetchApi(`/api/vms/${vm.vmid}`)
setVMDetails(details)
} catch (error) {
console.error("Error fetching VM details:", error)
} finally {
@@ -344,23 +353,16 @@ export function VirtualMachines() {
const handleVMControl = async (vmid: number, action: string) => {
setControlLoading(true)
try {
const response = await fetch(`/api/vms/${vmid}/control`, {
await fetchApi(`/api/vms/${vmid}/control`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ action }),
})
if (response.ok) {
mutate()
setSelectedVM(null)
setVMDetails(null)
} else {
console.error("Failed to control VM")
}
mutate()
setSelectedVM(null)
setVMDetails(null)
} catch (error) {
console.error("Error controlling VM:", error)
console.error("Failed to control VM")
} finally {
setControlLoading(false)
}
@@ -368,36 +370,33 @@ export function VirtualMachines() {
const handleDownloadLogs = async (vmid: number, vmName: string) => {
try {
const response = await fetch(`/api/vms/${vmid}/logs`)
if (response.ok) {
const data = await response.json()
const data = await fetchApi(`/api/vms/${vmid}/logs`)
// Format logs as plain text
let logText = `=== Logs for ${vmName} (VMID: ${vmid}) ===\n`
logText += `Node: ${data.node}\n`
logText += `Type: ${data.type}\n`
logText += `Total lines: ${data.log_lines}\n`
logText += `Generated: ${new Date().toISOString()}\n`
logText += `\n${"=".repeat(80)}\n\n`
// Format logs as plain text
let logText = `=== Logs for ${vmName} (VMID: ${vmid}) ===\n`
logText += `Node: ${data.node}\n`
logText += `Type: ${data.type}\n`
logText += `Total lines: ${data.log_lines}\n`
logText += `Generated: ${new Date().toISOString()}\n`
logText += `\n${"=".repeat(80)}\n\n`
if (data.logs && Array.isArray(data.logs)) {
data.logs.forEach((log: any) => {
if (typeof log === "object" && log.t) {
logText += `${log.t}\n`
} else if (typeof log === "string") {
logText += `${log}\n`
}
})
}
const blob = new Blob([logText], { type: "text/plain" })
const url = URL.createObjectURL(blob)
const a = document.createElement("a")
a.href = url
a.download = `${vmName}-${vmid}-logs.txt`
a.click()
URL.revokeObjectURL(url)
if (data.logs && Array.isArray(data.logs)) {
data.logs.forEach((log: any) => {
if (typeof log === "object" && log.t) {
logText += `${log.t}\n`
} else if (typeof log === "string") {
logText += `${log}\n`
}
})
}
const blob = new Blob([logText], { type: "text/plain" })
const url = URL.createObjectURL(blob)
const a = document.createElement("a")
a.href = url
a.download = `${vmName}-${vmid}-logs.txt`
a.click()
URL.revokeObjectURL(url)
} catch (error) {
console.error("Error downloading logs:", error)
}
@@ -450,7 +449,7 @@ export function VirtualMachines() {
"/api/system",
fetcher,
{
refreshInterval: 30000,
refreshInterval: 37000,
revalidateOnFocus: false,
},
)
@@ -592,29 +591,21 @@ export function VirtualMachines() {
setSavingNotes(true)
try {
const response = await fetch(`/api/vms/${selectedVM.vmid}/config`, {
await fetchApi(`/api/vms/${selectedVM.vmid}/config`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
description: editedNotes, // Send as-is, pvesh will handle encoding
}),
})
if (response.ok) {
setVMDetails({
...vmDetails,
config: {
...vmDetails.config,
description: editedNotes, // Store unencoded
},
})
setIsEditingNotes(false)
} else {
console.error("Failed to save notes")
alert("Failed to save notes. Please try again.")
}
setVMDetails({
...vmDetails,
config: {
...vmDetails.config,
description: editedNotes, // Store unencoded
},
})
setIsEditingNotes(false)
} catch (error) {
console.error("Error saving notes:", error)
alert("Error saving notes. Please try again.")
@@ -1041,11 +1032,15 @@ export function VirtualMachines() {
setEditedNotes("")
}}
>
<DialogContent className="max-w-4xl h-[95vh] sm:h-[90vh] flex flex-col p-0 overflow-hidden">
<DialogContent
className="max-w-4xl h-[95vh] sm:h-[90vh] flex flex-col p-0 overflow-hidden"
key={selectedVM?.vmid || "no-vm"}
>
{currentView === "main" ? (
<>
<DialogHeader className="pb-4 border-b border-border px-6 pt-6">
<DialogTitle className="flex flex-col gap-3">
{/* Desktop layout: Uptime now appears after status badge */}
<div className="hidden sm:flex items-center gap-3 flex-wrap">
<div className="flex items-center gap-2">
<Server className="h-5 w-5 flex-shrink-0" />
@@ -1062,15 +1057,16 @@ export function VirtualMachines() {
<Badge variant="outline" className={`${getStatusColor(selectedVM.status)} flex-shrink-0`}>
{selectedVM.status.toUpperCase()}
</Badge>
{selectedVM.status === "running" && (
<span className="text-sm text-muted-foreground">
Uptime: {formatUptime(selectedVM.uptime)}
</span>
)}
</div>
{selectedVM.status === "running" && (
<span className="text-sm text-muted-foreground ml-auto">
Uptime: {formatUptime(selectedVM.uptime)}
</span>
)}
</>
)}
</div>
{/* Mobile layout unchanged */}
<div className="sm:hidden flex flex-col gap-2">
<div className="flex items-center gap-2">
<Server className="h-5 w-5 flex-shrink-0" />
@@ -1101,7 +1097,7 @@ export function VirtualMachines() {
<div className="space-y-6">
{selectedVM && (
<>
<div>
<div key={`metrics-${selectedVM.vmid}`}>
<Card
className="cursor-pointer rounded-lg border border-black/10 dark:border-white/10 sm:border-border max-sm:bg-black/5 max-sm:dark:bg-white/5 sm:bg-card sm:hover:bg-black/5 sm:dark:hover:bg-white/5 transition-colors group"
onClick={handleMetricsClick}
@@ -1192,7 +1188,7 @@ export function VirtualMachines() {
<div className="text-center py-8 text-muted-foreground">Loading configuration...</div>
) : vmDetails?.config ? (
<>
<Card className="border border-border bg-card/50">
<Card className="border border-border bg-card/50" key={`config-${selectedVM.vmid}`}>
<CardContent className="p-4">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">
@@ -1258,26 +1254,25 @@ export function VirtualMachines() {
)}
</div>
{/* IP Addresses with proper keys */}
{selectedVM?.type === "lxc" && vmDetails?.lxc_ip_info && (
<div className="mt-4 lg:mt-6 pt-4 lg:pt-6 border-t border-border">
<h4 className="text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide">
IP Addresses
</h4>
<div className="flex flex-wrap gap-2">
{/* Real IPs (green, without "Real" label) */}
{vmDetails.lxc_ip_info.real_ips.map((ip, index) => (
<Badge
key={`real-${index}`}
key={`real-ip-${selectedVM.vmid}-${ip.replace(/[.:/]/g, "-")}-${index}`}
variant="outline"
className="bg-green-500/10 text-green-500 border-green-500/20"
>
{ip}
</Badge>
))}
{/* Docker bridge IPs (yellow, with "Bridge" label) */}
{vmDetails.lxc_ip_info.docker_ips.map((ip, index) => (
<Badge
key={`docker-${index}`}
key={`docker-ip-${selectedVM.vmid}-${ip.replace(/[.:/]/g, "-")}-${index}`}
variant="outline"
className="bg-yellow-500/10 text-yellow-500 border-yellow-500/20"
>
@@ -1387,7 +1382,7 @@ export function VirtualMachines() {
</div>
)}
{/* GPU Passthrough */}
{/* GPU Passthrough with proper keys */}
{vmDetails.hardware_info.gpu_passthrough &&
vmDetails.hardware_info.gpu_passthrough.length > 0 && (
<div>
@@ -1395,7 +1390,7 @@ export function VirtualMachines() {
<div className="flex flex-wrap gap-2">
{vmDetails.hardware_info.gpu_passthrough.map((gpu, index) => (
<Badge
key={index}
key={`gpu-${selectedVM.vmid}-${index}-${gpu.replace(/[^a-zA-Z0-9]/g, "-").substring(0, 30)}`}
variant="outline"
className={
gpu.includes("NVIDIA")
@@ -1410,7 +1405,7 @@ export function VirtualMachines() {
</div>
)}
{/* Other Hardware Devices */}
{/* Hardware Devices with proper keys */}
{vmDetails.hardware_info.devices &&
vmDetails.hardware_info.devices.length > 0 && (
<div>
@@ -1418,7 +1413,7 @@ export function VirtualMachines() {
<div className="flex flex-wrap gap-2">
{vmDetails.hardware_info.devices.map((device, index) => (
<Badge
key={index}
key={`device-${selectedVM.vmid}-${index}-${device.replace(/[^a-zA-Z0-9]/g, "-").substring(0, 30)}`}
variant="outline"
className="bg-blue-500/10 text-blue-500 border-blue-500/20"
>
@@ -1540,7 +1535,7 @@ export function VirtualMachines() {
</h4>
<div className="space-y-3">
{vmDetails.config.rootfs && (
<div>
<div key="rootfs">
<div className="text-xs text-muted-foreground mb-1">Root Filesystem</div>
<div className="font-medium text-foreground text-sm break-all font-mono bg-muted/50 p-2 rounded">
{vmDetails.config.rootfs}
@@ -1548,15 +1543,16 @@ export function VirtualMachines() {
</div>
)}
{vmDetails.config.scsihw && (
<div>
<div key="scsihw">
<div className="text-xs text-muted-foreground mb-1">SCSI Controller</div>
<div className="font-medium text-foreground">{vmDetails.config.scsihw}</div>
</div>
)}
{/* Disk Storage with proper keys */}
{Object.keys(vmDetails.config)
.filter((key) => key.match(/^(scsi|sata|ide|virtio)\d+$/))
.map((diskKey) => (
<div key={diskKey}>
<div key={`disk-${selectedVM.vmid}-${diskKey}`}>
<div className="text-xs text-muted-foreground mb-1">
{diskKey.toUpperCase().replace(/(\d+)/, " $1")}
</div>
@@ -1566,7 +1562,7 @@ export function VirtualMachines() {
</div>
))}
{vmDetails.config.efidisk0 && (
<div>
<div key="efidisk0">
<div className="text-xs text-muted-foreground mb-1">EFI Disk</div>
<div className="font-medium text-foreground text-sm break-all font-mono bg-muted/50 p-2 rounded">
{vmDetails.config.efidisk0}
@@ -1574,18 +1570,18 @@ export function VirtualMachines() {
</div>
)}
{vmDetails.config.tpmstate0 && (
<div>
<div key="tpmstate0">
<div className="text-xs text-muted-foreground mb-1">TPM State</div>
<div className="font-medium text-foreground text-sm break-all font-mono bg-muted/50 p-2 rounded">
{vmDetails.config.tpmstate0}
</div>
</div>
)}
{/* Mount points for LXC */}
{/* Mount Points with proper keys */}
{Object.keys(vmDetails.config)
.filter((key) => key.match(/^mp\d+$/))
.map((mpKey) => (
<div key={mpKey}>
<div key={`mp-${selectedVM.vmid}-${mpKey}`}>
<div className="text-xs text-muted-foreground mb-1">
Mount Point {mpKey.replace("mp", "")}
</div>
@@ -1603,10 +1599,11 @@ export function VirtualMachines() {
Network
</h4>
<div className="space-y-3">
{/* Network Interfaces with proper keys */}
{Object.keys(vmDetails.config)
.filter((key) => key.match(/^net\d+$/))
.map((netKey) => (
<div key={netKey}>
<div key={`net-${selectedVM.vmid}-${netKey}`}>
<div className="text-xs text-muted-foreground mb-1">
Network Interface {netKey.replace("net", "")}
</div>
@@ -1644,7 +1641,7 @@ export function VirtualMachines() {
</div>
</div>
{/* PCI Devices Section */}
{/* PCI Devices with proper keys */}
{Object.keys(vmDetails.config).some((key) => key.match(/^hostpci\d+$/)) && (
<div>
<h4 className="text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide">
@@ -1654,7 +1651,7 @@ export function VirtualMachines() {
{Object.keys(vmDetails.config)
.filter((key) => key.match(/^hostpci\d+$/))
.map((pciKey) => (
<div key={pciKey}>
<div key={`pci-${selectedVM.vmid}-${pciKey}`}>
<div className="text-xs text-muted-foreground mb-1">
{pciKey.toUpperCase().replace(/(\d+)/, " $1")}
</div>
@@ -1667,7 +1664,7 @@ export function VirtualMachines() {
</div>
)}
{/* USB Devices Section */}
{/* USB Devices with proper keys */}
{Object.keys(vmDetails.config).some((key) => key.match(/^usb\d+$/)) && (
<div>
<h4 className="text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide">
@@ -1677,7 +1674,7 @@ export function VirtualMachines() {
{Object.keys(vmDetails.config)
.filter((key) => key.match(/^usb\d+$/))
.map((usbKey) => (
<div key={usbKey}>
<div key={`usb-${selectedVM.vmid}-${usbKey}`}>
<div className="text-xs text-muted-foreground mb-1">
{usbKey.toUpperCase().replace(/(\d+)/, " $1")}
</div>
@@ -1690,7 +1687,7 @@ export function VirtualMachines() {
</div>
)}
{/* Serial Devices Section */}
{/* Serial Ports with proper keys */}
{Object.keys(vmDetails.config).some((key) => key.match(/^serial\d+$/)) && (
<div>
<h4 className="text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide">
@@ -1700,7 +1697,7 @@ export function VirtualMachines() {
{Object.keys(vmDetails.config)
.filter((key) => key.match(/^serial\d+$/))
.map((serialKey) => (
<div key={serialKey}>
<div key={`serial-${selectedVM.vmid}-${serialKey}`}>
<div className="text-xs text-muted-foreground mb-1">
{serialKey.toUpperCase().replace(/(\d+)/, " $1")}
</div>
@@ -1712,91 +1709,6 @@ export function VirtualMachines() {
</div>
</div>
)}
{/* Options Section */}
<div>
<h4 className="text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide">
Options
</h4>
<div className="grid grid-cols-2 lg:grid-cols-3 gap-4">
{vmDetails.config.onboot !== undefined && (
<div>
<div className="text-xs text-muted-foreground mb-1">Start on Boot</div>
<Badge
variant="outline"
className={
vmDetails.config.onboot
? "bg-green-500/10 text-green-500 border-green-500/20"
: "bg-red-500/10 text-red-500 border-red-500/20"
}
>
{vmDetails.config.onboot ? "Yes" : "No"}
</Badge>
</div>
)}
{vmDetails.config.ostype && (
<div>
<div className="text-xs text-muted-foreground mb-1">OS Type</div>
<div className="font-medium text-foreground">{vmDetails.config.ostype}</div>
</div>
)}
{vmDetails.config.arch && (
<div>
<div className="text-xs text-muted-foreground mb-1">Architecture</div>
<div className="font-medium text-foreground">{vmDetails.config.arch}</div>
</div>
)}
{vmDetails.config.boot && (
<div>
<div className="text-xs text-muted-foreground mb-1">Boot Order</div>
<div className="font-medium text-foreground">{vmDetails.config.boot}</div>
</div>
)}
{vmDetails.config.features && (
<div className="col-span-2 lg:grid-cols-3">
<div className="text-xs text-muted-foreground mb-1">Features</div>
<div className="font-medium text-foreground text-sm">
{vmDetails.config.features}
</div>
</div>
)}
</div>
</div>
{/* Advanced Section */}
{(vmDetails.config.vmgenid || vmDetails.config.smbios1 || vmDetails.config.meta) && (
<div>
<h4 className="text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide">
Advanced
</h4>
<div className="space-y-3">
{vmDetails.config.vmgenid && (
<div>
<div className="text-xs text-muted-foreground mb-1">VM Generation ID</div>
<div className="font-medium text-muted-foreground text-sm font-mono">
{vmDetails.config.vmgenid}
</div>
</div>
)}
{vmDetails.config.smbios1 && (
<div>
<div className="text-xs text-muted-foreground mb-1">SMBIOS</div>
<div className="font-medium text-muted-foreground text-sm font-mono break-all">
{vmDetails.config.smbios1}
</div>
</div>
)}
{vmDetails.config.meta && (
<div>
<div className="text-xs text-muted-foreground mb-1">Metadata</div>
<div className="font-medium text-muted-foreground text-sm font-mono">
{vmDetails.config.meta}
</div>
</div>
)}
</div>
</div>
)}
</div>
)}
</CardContent>
+23
View File
@@ -0,0 +1,23 @@
"use client"
import { useEffect, useState } from "react"
export function useIsMobile() {
const [isMobile, setIsMobile] = useState(false)
useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth < 768)
}
// Check on mount
checkMobile()
// Listen for resize
window.addEventListener("resize", checkMobile)
return () => window.removeEventListener("resize", checkMobile)
}, [])
return isMobile
}
+126
View File
@@ -0,0 +1,126 @@
/**
* API Configuration for ProxMenux Monitor
* Handles API URL generation with automatic proxy detection
*/
/**
* API Server Port Configuration
* Default: 8008 (production)
* Can be changed to 8009 for beta testing
* This can also be set via NEXT_PUBLIC_API_PORT environment variable
*/
export const API_PORT = process.env.NEXT_PUBLIC_API_PORT || "8008"
/**
* Gets the base URL for API calls
* Automatically detects if running behind a proxy by checking if we're on a standard port
*
* @returns Base URL for API endpoints
*/
export function getApiBaseUrl(): string {
if (typeof window === "undefined") {
console.log("[v0] getApiBaseUrl: Running on server (SSR)")
return ""
}
const { protocol, hostname, port } = window.location
console.log("[v0] getApiBaseUrl - protocol:", protocol, "hostname:", hostname, "port:", port)
// If accessing via standard ports (80/443) or no port, assume we're behind a proxy
// In this case, use relative URLs so the proxy handles routing
const isStandardPort = port === "" || port === "80" || port === "443"
console.log("[v0] getApiBaseUrl - isStandardPort:", isStandardPort)
if (isStandardPort) {
// Behind a proxy - use relative URL
console.log("[v0] getApiBaseUrl: Detected proxy access, using relative URLs")
return ""
} else {
// Direct access - use explicit API port
const baseUrl = `${protocol}//${hostname}:${API_PORT}`
console.log("[v0] getApiBaseUrl: Direct access detected, using:", baseUrl)
return baseUrl
}
}
/**
* Constructs a full API URL
*
* @param endpoint - API endpoint path (e.g., '/api/system')
* @returns Full API URL
*/
export function getApiUrl(endpoint: string): string {
const baseUrl = getApiBaseUrl()
// Ensure endpoint starts with /
const normalizedEndpoint = endpoint.startsWith("/") ? endpoint : `/${endpoint}`
return `${baseUrl}${normalizedEndpoint}`
}
/**
* Gets the JWT token from localStorage
*
* @returns JWT token or null if not authenticated
*/
export function getAuthToken(): string | null {
if (typeof window === "undefined") {
return null
}
const token = localStorage.getItem("proxmenux-auth-token")
console.log(
"[v0] getAuthToken called:",
token ? `Token found (length: ${token.length})` : "No token found in localStorage",
)
return token
}
/**
* Fetches data from an API endpoint with error handling
*
* @param endpoint - API endpoint path
* @param options - Fetch options
* @returns Promise with the response data
*/
export async function fetchApi<T>(endpoint: string, options?: RequestInit): Promise<T> {
const url = getApiUrl(endpoint)
const token = getAuthToken()
const headers: Record<string, string> = {
"Content-Type": "application/json",
...(options?.headers as Record<string, string>),
}
if (token) {
headers["Authorization"] = `Bearer ${token}`
console.log("[v0] fetchApi:", endpoint, "- Authorization header ADDED")
} else {
console.log("[v0] fetchApi:", endpoint, "- NO TOKEN - Request will fail if endpoint is protected")
}
try {
const response = await fetch(url, {
...options,
headers,
cache: "no-store",
})
console.log("[v0] fetchApi:", endpoint, "- Response status:", response.status)
if (!response.ok) {
if (response.status === 401) {
console.error("[v0] fetchApi: 401 UNAUTHORIZED -", endpoint, "- Token present:", !!token)
throw new Error(`Unauthorized: ${endpoint}`)
}
throw new Error(`API request failed: ${response.status} ${response.statusText}`)
}
return response.json()
} catch (error) {
console.error("[v0] fetchApi error for", endpoint, ":", error)
throw error
}
}
+15
View File
@@ -4,3 +4,18 @@ import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
export function formatStorage(sizeInGB: number): string {
if (sizeInGB < 1) {
// Less than 1 GB, show in MB
const mb = sizeInGB * 1024
return `${mb % 1 === 0 ? mb.toFixed(0) : mb.toFixed(1)} MB`
} else if (sizeInGB < 1024) {
// Less than 1024 GB, show in GB
return `${sizeInGB % 1 === 0 ? sizeInGB.toFixed(0) : sizeInGB.toFixed(1)} GB`
} else {
// 1024 GB or more, show in TB
const tb = sizeInGB / 1024
return `${tb % 1 === 0 ? tb.toFixed(0) : tb.toFixed(1)} TB`
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "proxmenux-monitor",
"version": "1.0.0",
"name": "ProxMenux-Monitor",
"version": "1.0.1",
"description": "Proxmox System Monitoring Dashboard",
"private": true,
"scripts": {
+504
View File
@@ -0,0 +1,504 @@
"""
Authentication Manager Module
Handles all authentication-related operations including:
- Loading/saving auth configuration
- Password hashing and verification
- JWT token generation and validation
- Auth status checking
- Two-Factor Authentication (2FA/TOTP)
"""
import os
import json
import hashlib
import secrets
from datetime import datetime, timedelta
from pathlib import Path
try:
import jwt
JWT_AVAILABLE = True
except ImportError:
JWT_AVAILABLE = False
print("Warning: PyJWT not available. Authentication features will be limited.")
try:
import pyotp
import segno
import io
import base64
TOTP_AVAILABLE = True
except ImportError:
TOTP_AVAILABLE = False
print("Warning: pyotp/segno not available. 2FA features will be disabled.")
# Configuration
CONFIG_DIR = Path.home() / ".config" / "proxmenux-monitor"
AUTH_CONFIG_FILE = CONFIG_DIR / "auth.json"
JWT_SECRET = "proxmenux-monitor-secret-key-change-in-production"
JWT_ALGORITHM = "HS256"
TOKEN_EXPIRATION_HOURS = 24
def ensure_config_dir():
"""Ensure the configuration directory exists"""
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
def load_auth_config():
"""
Load authentication configuration from file
Returns dict with structure:
{
"enabled": bool,
"username": str,
"password_hash": str,
"declined": bool,
"configured": bool,
"totp_enabled": bool, # 2FA enabled flag
"totp_secret": str, # TOTP secret key
"backup_codes": list # List of backup codes
}
"""
if not AUTH_CONFIG_FILE.exists():
return {
"enabled": False,
"username": None,
"password_hash": None,
"declined": False,
"configured": False,
"totp_enabled": False,
"totp_secret": None,
"backup_codes": []
}
try:
with open(AUTH_CONFIG_FILE, 'r') as f:
config = json.load(f)
# Ensure all required fields exist
config.setdefault("declined", False)
config.setdefault("configured", config.get("enabled", False) or config.get("declined", False))
config.setdefault("totp_enabled", False)
config.setdefault("totp_secret", None)
config.setdefault("backup_codes", [])
return config
except Exception as e:
print(f"Error loading auth config: {e}")
return {
"enabled": False,
"username": None,
"password_hash": None,
"declined": False,
"configured": False,
"totp_enabled": False,
"totp_secret": None,
"backup_codes": []
}
def save_auth_config(config):
"""Save authentication configuration to file"""
ensure_config_dir()
try:
with open(AUTH_CONFIG_FILE, 'w') as f:
json.dump(config, f, indent=2)
return True
except Exception as e:
print(f"Error saving auth config: {e}")
return False
def hash_password(password):
"""Hash a password using SHA-256"""
return hashlib.sha256(password.encode()).hexdigest()
def verify_password(password, password_hash):
"""Verify a password against its hash"""
return hash_password(password) == password_hash
def generate_token(username):
"""Generate a JWT token for the given username"""
if not JWT_AVAILABLE:
return None
payload = {
'username': username,
'exp': datetime.utcnow() + timedelta(hours=TOKEN_EXPIRATION_HOURS),
'iat': datetime.utcnow()
}
try:
token = jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)
return token
except Exception as e:
print(f"Error generating token: {e}")
return None
def verify_token(token):
"""
Verify a JWT token
Returns username if valid, None otherwise
"""
if not JWT_AVAILABLE or not token:
return None
try:
payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
return payload.get('username')
except jwt.ExpiredSignatureError:
print("Token has expired")
return None
except jwt.InvalidTokenError as e:
print(f"Invalid token: {e}")
return None
def get_auth_status():
"""
Get current authentication status
Returns dict with:
{
"auth_enabled": bool,
"auth_configured": bool,
"declined": bool,
"username": str or None,
"authenticated": bool,
"totp_enabled": bool # 2FA status
}
"""
config = load_auth_config()
return {
"auth_enabled": config.get("enabled", False),
"auth_configured": config.get("configured", False),
"declined": config.get("declined", False),
"username": config.get("username") if config.get("enabled") else None,
"authenticated": False,
"totp_enabled": config.get("totp_enabled", False) # Include 2FA status
}
def setup_auth(username, password):
"""
Set up authentication with username and password
Returns (success: bool, message: str)
"""
if not username or not password:
return False, "Username and password are required"
if len(password) < 6:
return False, "Password must be at least 6 characters"
config = {
"enabled": True,
"username": username,
"password_hash": hash_password(password),
"declined": False,
"configured": True,
"totp_enabled": False,
"totp_secret": None,
"backup_codes": []
}
if save_auth_config(config):
return True, "Authentication configured successfully"
else:
return False, "Failed to save authentication configuration"
def decline_auth():
"""
Mark authentication as declined by user
Returns (success: bool, message: str)
"""
config = load_auth_config()
config["enabled"] = False
config["declined"] = True
config["configured"] = True
config["username"] = None
config["password_hash"] = None
config["totp_enabled"] = False
config["totp_secret"] = None
config["backup_codes"] = []
if save_auth_config(config):
return True, "Authentication declined"
else:
return False, "Failed to save configuration"
def disable_auth():
"""
Disable authentication (different from decline - can be re-enabled)
Returns (success: bool, message: str)
"""
config = load_auth_config()
config["enabled"] = False
config["username"] = None
config["password_hash"] = None
config["declined"] = False
config["configured"] = False
config["totp_enabled"] = False
config["totp_secret"] = None
config["backup_codes"] = []
if save_auth_config(config):
return True, "Authentication disabled"
else:
return False, "Failed to save configuration"
def enable_auth():
"""
Enable authentication (must already be configured)
Returns (success: bool, message: str)
"""
config = load_auth_config()
if not config.get("username") or not config.get("password_hash"):
return False, "Authentication not configured. Please set up username and password first."
config["enabled"] = True
config["declined"] = False
if save_auth_config(config):
return True, "Authentication enabled"
else:
return False, "Failed to save configuration"
def change_password(old_password, new_password):
"""
Change the authentication password
Returns (success: bool, message: str)
"""
config = load_auth_config()
if not config.get("enabled"):
return False, "Authentication is not enabled"
if not verify_password(old_password, config.get("password_hash", "")):
return False, "Current password is incorrect"
if len(new_password) < 6:
return False, "New password must be at least 6 characters"
config["password_hash"] = hash_password(new_password)
if save_auth_config(config):
return True, "Password changed successfully"
else:
return False, "Failed to save new password"
def generate_totp_secret():
"""Generate a new TOTP secret key"""
if not TOTP_AVAILABLE:
return None
return pyotp.random_base32()
def generate_totp_qr(username, secret):
"""
Generate a QR code for TOTP setup
Returns base64 encoded SVG image
"""
if not TOTP_AVAILABLE:
return None
try:
# Create TOTP URI
totp = pyotp.TOTP(secret)
uri = totp.provisioning_uri(
name=username,
issuer_name="ProxMenux Monitor"
)
qr = segno.make(uri)
# Convert to SVG string
buffer = io.BytesIO()
qr.save(buffer, kind='svg', scale=4, border=2)
svg_bytes = buffer.getvalue()
svg_content = svg_bytes.decode('utf-8')
# Return as data URL
svg_base64 = base64.b64encode(svg_content.encode()).decode('utf-8')
return f"data:image/svg+xml;base64,{svg_base64}"
except Exception as e:
print(f"Error generating QR code: {e}")
return None
def generate_backup_codes(count=8):
"""Generate backup codes for 2FA recovery"""
codes = []
for _ in range(count):
# Generate 8-character alphanumeric code
code = ''.join(secrets.choice('ABCDEFGHJKLMNPQRSTUVWXYZ23456789') for _ in range(8))
# Format as XXXX-XXXX for readability
formatted = f"{code[:4]}-{code[4:]}"
codes.append({
"code": hashlib.sha256(formatted.encode()).hexdigest(),
"used": False
})
return codes
def setup_totp(username):
"""
Set up TOTP for a user
Returns (success: bool, secret: str, qr_code: str, backup_codes: list, message: str)
"""
if not TOTP_AVAILABLE:
return False, None, None, None, "2FA is not available (pyotp/segno not installed)"
config = load_auth_config()
if not config.get("enabled"):
return False, None, None, None, "Authentication must be enabled first"
if config.get("username") != username:
return False, None, None, None, "Invalid username"
# Generate new secret and backup codes
secret = generate_totp_secret()
qr_code = generate_totp_qr(username, secret)
backup_codes_plain = []
backup_codes_hashed = generate_backup_codes()
# Generate plain text backup codes for display (only returned once)
for i in range(8):
code = ''.join(secrets.choice('ABCDEFGHJKLMNPQRSTUVWXYZ23456789') for _ in range(8))
formatted = f"{code[:4]}-{code[4:]}"
backup_codes_plain.append(formatted)
backup_codes_hashed[i]["code"] = hashlib.sha256(formatted.encode()).hexdigest()
# Store secret and hashed backup codes (not enabled yet until verified)
config["totp_secret"] = secret
config["backup_codes"] = backup_codes_hashed
if save_auth_config(config):
return True, secret, qr_code, backup_codes_plain, "2FA setup initiated"
else:
return False, None, None, None, "Failed to save 2FA configuration"
def verify_totp(username, token, use_backup=False):
"""
Verify a TOTP token or backup code
Returns (success: bool, message: str)
"""
if not TOTP_AVAILABLE and not use_backup:
return False, "2FA is not available"
config = load_auth_config()
if not config.get("totp_enabled"):
return False, "2FA is not enabled"
if config.get("username") != username:
return False, "Invalid username"
# Check backup code
if use_backup:
token_hash = hashlib.sha256(token.encode()).hexdigest()
for backup_code in config.get("backup_codes", []):
if backup_code["code"] == token_hash and not backup_code["used"]:
backup_code["used"] = True
save_auth_config(config)
return True, "Backup code accepted"
return False, "Invalid or already used backup code"
# Check TOTP token
totp = pyotp.TOTP(config.get("totp_secret"))
if totp.verify(token, valid_window=1): # Allow 1 time step tolerance
return True, "2FA verification successful"
else:
return False, "Invalid 2FA code"
def enable_totp(username, verification_token):
"""
Enable TOTP after successful verification
Returns (success: bool, message: str)
"""
if not TOTP_AVAILABLE:
return False, "2FA is not available"
config = load_auth_config()
if not config.get("totp_secret"):
return False, "2FA has not been set up. Please set up 2FA first."
if config.get("username") != username:
return False, "Invalid username"
# Verify the token before enabling
totp = pyotp.TOTP(config.get("totp_secret"))
if not totp.verify(verification_token, valid_window=1):
return False, "Invalid verification code. Please try again."
config["totp_enabled"] = True
if save_auth_config(config):
return True, "2FA enabled successfully"
else:
return False, "Failed to enable 2FA"
def disable_totp(username, password):
"""
Disable TOTP (requires password confirmation)
Returns (success: bool, message: str)
"""
config = load_auth_config()
if config.get("username") != username:
return False, "Invalid username"
if not verify_password(password, config.get("password_hash", "")):
return False, "Invalid password"
config["totp_enabled"] = False
config["totp_secret"] = None
config["backup_codes"] = []
if save_auth_config(config):
return True, "2FA disabled successfully"
else:
return False, "Failed to disable 2FA"
def authenticate(username, password, totp_token=None):
"""
Authenticate a user with username, password, and optional TOTP
Returns (success: bool, token: str or None, requires_totp: bool, message: str)
"""
config = load_auth_config()
if not config.get("enabled"):
return False, None, False, "Authentication is not enabled"
if username != config.get("username"):
return False, None, False, "Invalid username or password"
if not verify_password(password, config.get("password_hash", "")):
return False, None, False, "Invalid username or password"
if config.get("totp_enabled"):
if not totp_token:
return False, None, True, "2FA code required"
# Verify TOTP token or backup code
success, message = verify_totp(username, totp_token, use_backup=len(totp_token) == 9) # Backup codes are formatted XXXX-XXXX
if not success:
return False, None, True, message
token = generate_token(username)
if token:
return True, token, False, "Authentication successful"
else:
return False, None, False, "Failed to generate authentication token"
+10 -24
View File
@@ -78,6 +78,13 @@ cd "$SCRIPT_DIR"
# Copy Flask server
echo "📋 Copying Flask server..."
cp "$SCRIPT_DIR/flask_server.py" "$APP_DIR/usr/bin/"
cp "$SCRIPT_DIR/flask_auth_routes.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ flask_auth_routes.py not found"
cp "$SCRIPT_DIR/auth_manager.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ auth_manager.py not found"
cp "$SCRIPT_DIR/jwt_middleware.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ jwt_middleware.py not found"
cp "$SCRIPT_DIR/health_monitor.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ health_monitor.py not found"
cp "$SCRIPT_DIR/health_persistence.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ health_persistence.py not found"
cp "$SCRIPT_DIR/flask_health_routes.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ flask_health_routes.py not found"
cp "$SCRIPT_DIR/flask_proxmenux_routes.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ flask_proxmenux_routes.py not found"
echo "📋 Adding translation support..."
cat > "$APP_DIR/usr/bin/translate_cli.py" << 'PYEOF'
@@ -279,6 +286,9 @@ pip3 install --target "$APP_DIR/usr/lib/python3/dist-packages" \
flask-cors \
psutil \
requests \
PyJWT \
pyotp \
segno \
googletrans==4.0.0-rc1 \
httpx==0.13.3 \
httpcore==0.9.1 \
@@ -321,10 +331,6 @@ echo "🔧 Installing hardware monitoring tools..."
mkdir -p "$WORK_DIR/debs"
cd "$WORK_DIR/debs"
# ==============================================================
echo "📥 Downloading hardware monitoring tools (dynamic via APT)..."
dl_pkg() {
@@ -361,21 +367,12 @@ dl_pkg() {
return 1
}
mkdir -p "$WORK_DIR/debs"
cd "$WORK_DIR/debs"
dl_pkg "ipmitool.deb" "ipmitool" || true
dl_pkg "libfreeipmi17.deb" "libfreeipmi17" || true
dl_pkg "lm-sensors.deb" "lm-sensors" || true
dl_pkg "nut-client.deb" "nut-client" || true
dl_pkg "libupsclient.deb" "libupsclient6" "libupsclient5" "libupsclient4" || true
# dl_pkg "nvidia-smi.deb" "nvidia-smi" "nvidia-utils" "nvidia-utils-535" "nvidia-utils-550" || true
# dl_pkg "intel-gpu-tools.deb" "intel-gpu-tools" || true
# dl_pkg "radeontop.deb" "radeontop" || true
echo "📦 Extracting .deb packages into AppDir..."
extracted_count=0
shopt -s nullglob
@@ -395,7 +392,6 @@ else
echo "✅ Extracted $extracted_count package(s)"
fi
if [ -d "$APP_DIR/bin" ]; then
echo "📋 Normalizing /bin -> /usr/bin"
mkdir -p "$APP_DIR/usr/bin"
@@ -403,24 +399,20 @@ if [ -d "$APP_DIR/bin" ]; then
rm -rf "$APP_DIR/bin"
fi
echo "🔍 Sanity check (ldd + presence of libfreeipmi)"
export LD_LIBRARY_PATH="$APP_DIR/lib:$APP_DIR/lib/x86_64-linux-gnu:$APP_DIR/usr/lib:$APP_DIR/usr/lib/x86_64-linux-gnu"
if ! find "$APP_DIR/usr/lib" "$APP_DIR/lib" -maxdepth 3 -name 'libfreeipmi.so.17*' | grep -q .; then
echo "❌ libfreeipmi.so.17 not found inside AppDir (ipmitool will fail)"
exit 1
fi
if [ -x "$APP_DIR/usr/bin/ipmitool" ] && ldd "$APP_DIR/usr/bin/ipmitool" | grep -q 'not found'; then
echo "❌ ipmitool has unresolved libs:"
ldd "$APP_DIR/usr/bin/ipmitool" | grep 'not found' || true
exit 1
fi
if [ -x "$APP_DIR/usr/bin/upsc" ] && ldd "$APP_DIR/usr/bin/upsc" | grep -q 'not found'; then
echo "⚠️ upsc has unresolved libs, trying to auto-fix..."
missing="$(ldd "$APP_DIR/usr/bin/upsc" | awk '/not found/{print $1}' | tr -d ' ')"
@@ -463,12 +455,6 @@ echo "✅ Sanity check OK (ipmitool/upsc ready; libfreeipmi present)"
[ -x "$APP_DIR/usr/bin/intel_gpu_top" ] && echo " • intel-gpu-tools: OK" || echo " • intel-gpu-tools: missing"
[ -x "$APP_DIR/usr/bin/radeontop" ] && echo " • radeontop: OK" || echo " • radeontop: missing"
# ==============================================================
# Build AppImage
echo "🔨 Building unified AppImage v${VERSION}..."
cd "$WORK_DIR"
+278
View File
@@ -0,0 +1,278 @@
"""
Flask Authentication Routes
Provides REST API endpoints for authentication management
"""
from flask import Blueprint, jsonify, request
import auth_manager
import jwt
import datetime
auth_bp = Blueprint('auth', __name__)
@auth_bp.route('/api/auth/status', methods=['GET'])
def auth_status():
"""Get current authentication status"""
try:
status = auth_manager.get_auth_status()
token = request.headers.get('Authorization', '').replace('Bearer ', '')
if token:
username = auth_manager.verify_token(token)
if username:
status['authenticated'] = True
return jsonify(status)
except Exception as e:
return jsonify({"error": str(e)}), 500
@auth_bp.route('/api/auth/setup', methods=['POST'])
def auth_setup():
"""Set up authentication with username and password"""
try:
data = request.json
username = data.get('username')
password = data.get('password')
success, message = auth_manager.setup_auth(username, password)
if success:
return jsonify({"success": True, "message": message})
else:
return jsonify({"success": False, "message": message}), 400
except Exception as e:
return jsonify({"success": False, "message": str(e)}), 500
@auth_bp.route('/api/auth/decline', methods=['POST'])
def auth_decline():
"""Decline authentication setup"""
try:
success, message = auth_manager.decline_auth()
if success:
return jsonify({"success": True, "message": message})
else:
return jsonify({"success": False, "message": message}), 400
except Exception as e:
return jsonify({"success": False, "message": str(e)}), 500
@auth_bp.route('/api/auth/login', methods=['POST'])
def auth_login():
"""Authenticate user and return JWT token"""
try:
data = request.json
username = data.get('username')
password = data.get('password')
totp_token = data.get('totp_token') # Optional 2FA token
success, token, requires_totp, message = auth_manager.authenticate(username, password, totp_token)
if success:
return jsonify({"success": True, "token": token, "message": message})
elif requires_totp:
return jsonify({"success": False, "requires_totp": True, "message": message}), 200
else:
return jsonify({"success": False, "message": message}), 401
except Exception as e:
return jsonify({"success": False, "message": str(e)}), 500
@auth_bp.route('/api/auth/enable', methods=['POST'])
def auth_enable():
"""Enable authentication"""
try:
success, message = auth_manager.enable_auth()
if success:
return jsonify({"success": True, "message": message})
else:
return jsonify({"success": False, "message": message}), 400
except Exception as e:
return jsonify({"success": False, "message": str(e)}), 500
@auth_bp.route('/api/auth/disable', methods=['POST'])
def auth_disable():
"""Disable authentication"""
try:
token = request.headers.get('Authorization', '').replace('Bearer ', '')
if not token or not auth_manager.verify_token(token):
return jsonify({"success": False, "message": "Unauthorized"}), 401
success, message = auth_manager.disable_auth()
if success:
return jsonify({"success": True, "message": message})
else:
return jsonify({"success": False, "message": message}), 400
except Exception as e:
return jsonify({"success": False, "message": str(e)}), 500
@auth_bp.route('/api/auth/change-password', methods=['POST'])
def auth_change_password():
"""Change authentication password"""
try:
data = request.json
old_password = data.get('old_password')
new_password = data.get('new_password')
success, message = auth_manager.change_password(old_password, new_password)
if success:
return jsonify({"success": True, "message": message})
else:
return jsonify({"success": False, "message": message}), 400
except Exception as e:
return jsonify({"success": False, "message": str(e)}), 500
@auth_bp.route('/api/auth/skip', methods=['POST'])
def auth_skip():
"""Skip authentication setup (same as decline)"""
try:
success, message = auth_manager.decline_auth()
if success:
# Return success with clear indication that APIs should be accessible
return jsonify({
"success": True,
"message": message,
"auth_declined": True # Add explicit flag for frontend
})
else:
return jsonify({"success": False, "message": message}), 400
except Exception as e:
return jsonify({"success": False, "message": str(e)}), 500
@auth_bp.route('/api/auth/totp/setup', methods=['POST'])
def totp_setup():
"""Initialize TOTP setup for a user"""
try:
token = request.headers.get('Authorization', '').replace('Bearer ', '')
username = auth_manager.verify_token(token)
if not username:
return jsonify({"success": False, "message": "Unauthorized"}), 401
success, secret, qr_code, backup_codes, message = auth_manager.setup_totp(username)
if success:
return jsonify({
"success": True,
"secret": secret,
"qr_code": qr_code,
"backup_codes": backup_codes,
"message": message
})
else:
return jsonify({"success": False, "message": message}), 400
except Exception as e:
return jsonify({"success": False, "message": str(e)}), 500
@auth_bp.route('/api/auth/totp/enable', methods=['POST'])
def totp_enable():
"""Enable TOTP after verification"""
try:
token = request.headers.get('Authorization', '').replace('Bearer ', '')
username = auth_manager.verify_token(token)
if not username:
return jsonify({"success": False, "message": "Unauthorized"}), 401
data = request.json
verification_token = data.get('token')
if not verification_token:
return jsonify({"success": False, "message": "Verification token required"}), 400
success, message = auth_manager.enable_totp(username, verification_token)
if success:
return jsonify({"success": True, "message": message})
else:
return jsonify({"success": False, "message": message}), 400
except Exception as e:
return jsonify({"success": False, "message": str(e)}), 500
@auth_bp.route('/api/auth/totp/disable', methods=['POST'])
def totp_disable():
"""Disable TOTP (requires password confirmation)"""
try:
token = request.headers.get('Authorization', '').replace('Bearer ', '')
username = auth_manager.verify_token(token)
if not username:
return jsonify({"success": False, "message": "Unauthorized"}), 401
data = request.json
password = data.get('password')
if not password:
return jsonify({"success": False, "message": "Password required"}), 400
success, message = auth_manager.disable_totp(username, password)
if success:
return jsonify({"success": True, "message": message})
else:
return jsonify({"success": False, "message": message}), 400
except Exception as e:
return jsonify({"success": False, "message": str(e)}), 500
@auth_bp.route('/api/auth/generate-api-token', methods=['POST'])
def generate_api_token():
"""Generate a long-lived API token for external integrations (Homepage, Home Assistant, etc.)"""
try:
auth_header = request.headers.get('Authorization', '')
token = auth_header.replace('Bearer ', '')
if not token:
return jsonify({"success": False, "message": "Unauthorized. Please log in first."}), 401
username = auth_manager.verify_token(token)
if not username:
return jsonify({"success": False, "message": "Invalid or expired session. Please log in again."}), 401
data = request.json
password = data.get('password')
totp_token = data.get('totp_token') # Optional 2FA token
token_name = data.get('token_name', 'API Token') # Optional token description
if not password:
return jsonify({"success": False, "message": "Password is required"}), 400
# Authenticate user with password and optional 2FA
success, _, requires_totp, message = auth_manager.authenticate(username, password, totp_token)
if success:
# Generate a long-lived token (1 year expiration)
api_token = jwt.encode({
'username': username,
'token_name': token_name,
'exp': datetime.datetime.utcnow() + datetime.timedelta(days=365),
'iat': datetime.datetime.utcnow()
}, auth_manager.JWT_SECRET, algorithm='HS256')
return jsonify({
"success": True,
"token": api_token,
"token_name": token_name,
"expires_in": "365 days",
"message": "API token generated successfully. Store this token securely, it will not be shown again."
})
elif requires_totp:
return jsonify({"success": False, "requires_totp": True, "message": message}), 200
else:
return jsonify({"success": False, "message": message}), 401
except Exception as e:
print(f"[ERROR] generate_api_token: {str(e)}") # Log error for debugging
return jsonify({"success": False, "message": f"Internal error: {str(e)}"}), 500
+74
View File
@@ -0,0 +1,74 @@
"""
Flask routes for health monitoring with persistence support
"""
from flask import Blueprint, jsonify, request
from health_monitor import health_monitor
from health_persistence import health_persistence
health_bp = Blueprint('health', __name__)
@health_bp.route('/api/health/status', methods=['GET'])
def get_health_status():
"""Get overall health status summary"""
try:
status = health_monitor.get_overall_status()
return jsonify(status)
except Exception as e:
return jsonify({'error': str(e)}), 500
@health_bp.route('/api/health/details', methods=['GET'])
def get_health_details():
"""Get detailed health status with all checks"""
try:
details = health_monitor.get_detailed_status()
return jsonify(details)
except Exception as e:
return jsonify({'error': str(e)}), 500
@health_bp.route('/api/system-info', methods=['GET'])
def get_system_info():
"""
Get lightweight system info for header display.
Returns: hostname, uptime, and health status with proper structure.
"""
try:
info = health_monitor.get_system_info()
if 'health' in info:
status_map = {
'OK': 'healthy',
'WARNING': 'warning',
'CRITICAL': 'critical',
'UNKNOWN': 'warning'
}
current_status = info['health'].get('status', 'OK').upper()
info['health']['status'] = status_map.get(current_status, 'healthy')
return jsonify(info)
except Exception as e:
return jsonify({'error': str(e)}), 500
@health_bp.route('/api/health/acknowledge', methods=['POST'])
def acknowledge_error():
"""Acknowledge an error manually (user dismissed it)"""
try:
data = request.get_json()
if not data or 'error_key' not in data:
return jsonify({'error': 'error_key is required'}), 400
error_key = data['error_key']
health_persistence.acknowledge_error(error_key)
return jsonify({'success': True, 'message': 'Error acknowledged'})
except Exception as e:
return jsonify({'error': str(e)}), 500
@health_bp.route('/api/health/active-errors', methods=['GET'])
def get_active_errors():
"""Get all active persistent errors"""
try:
category = request.args.get('category')
errors = health_persistence.get_active_errors(category)
return jsonify({'errors': errors})
except Exception as e:
return jsonify({'error': str(e)}), 500
@@ -0,0 +1,75 @@
from flask import Blueprint, jsonify
import json
import os
proxmenux_bp = Blueprint('proxmenux', __name__)
# Tool descriptions mapping
TOOL_DESCRIPTIONS = {
'lvm_repair': 'LVM PV Headers Repair',
'repo_cleanup': 'Repository Cleanup',
'subscription_banner': 'Subscription Banner Removal',
'time_sync': 'Time Synchronization',
'apt_languages': 'APT Language Skip',
'journald': 'Journald Optimization',
'logrotate': 'Logrotate Optimization',
'system_limits': 'System Limits Increase',
'entropy': 'Entropy Generation (haveged)',
'memory_settings': 'Memory Settings Optimization',
'kernel_panic': 'Kernel Panic Configuration',
'apt_ipv4': 'APT IPv4 Force',
'kexec': 'kexec for quick reboots',
'network_optimization': 'Network Optimizations',
'bashrc_custom': 'Bashrc Customization',
'figurine': 'Figurine',
'fastfetch': 'Fastfetch',
'log2ram': 'Log2ram (SSD Protection)',
'amd_fixes': 'AMD CPU (Ryzen/EPYC) fixes',
'persistent_network': 'Setting persistent network interfaces'
}
@proxmenux_bp.route('/api/proxmenux/installed-tools', methods=['GET'])
def get_installed_tools():
"""Get list of installed ProxMenux tools/optimizations"""
installed_tools_path = '/usr/local/share/proxmenux/installed_tools.json'
try:
if not os.path.exists(installed_tools_path):
return jsonify({
'success': True,
'installed_tools': [],
'message': 'No ProxMenux optimizations installed yet'
})
with open(installed_tools_path, 'r') as f:
data = json.load(f)
# Convert to list format with descriptions
tools = []
for tool_key, enabled in data.items():
if enabled: # Only include enabled tools
tools.append({
'key': tool_key,
'name': TOOL_DESCRIPTIONS.get(tool_key, tool_key.replace('_', ' ').title()),
'enabled': enabled
})
# Sort alphabetically by name
tools.sort(key=lambda x: x['name'])
return jsonify({
'success': True,
'installed_tools': tools,
'total_count': len(tools)
})
except json.JSONDecodeError:
return jsonify({
'success': False,
'error': 'Invalid JSON format in installed_tools.json'
}), 500
except Exception as e:
return jsonify({
'success': False,
'error': str(e)
}), 500
+319 -91
View File
@@ -12,6 +12,7 @@ import psutil
import subprocess
import json
import os
import sys
import time
import socket
from datetime import datetime, timedelta
@@ -22,10 +23,29 @@ import xml.etree.ElementTree as ET # Added for XML parsing
import math # Imported math for format_bytes function
import urllib.parse # Added for URL encoding
import platform # Added for platform.release()
import hashlib
import secrets
import jwt
from functools import wraps
from pathlib import Path
from flask_health_routes import health_bp
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from flask_auth_routes import auth_bp
from flask_proxmenux_routes import proxmenux_bp
from jwt_middleware import require_auth
app = Flask(__name__)
CORS(app) # Enable CORS for Next.js frontend
app.register_blueprint(auth_bp)
app.register_blueprint(health_bp)
app.register_blueprint(proxmenux_bp)
def identify_gpu_type(name, vendor=None, bus=None, driver=None):
"""
Returns: 'Integrated' or 'PCI' (discrete)
@@ -395,18 +415,18 @@ def get_intel_gpu_processes_from_text():
processes.append(process_info)
except (ValueError, IndexError) as e:
# print(f"[v0] Error parsing process line: {e}", flush=True)
# print(f"[v0] Error parsing process line: {e}")
pass
continue
break
if not header_found:
# print(f"[v0] No process table found in intel_gpu_top output", flush=True)
# print(f"[v0] No process table found in intel_gpu_top output")
pass
return processes
except Exception as e:
# print(f"[v0] Error getting processes from intel_gpu_top text: {e}", flush=True)
# print(f"[v0] Error getting processes from intel_gpu_top text: {e}")
pass
import traceback
traceback.print_exc()
@@ -917,6 +937,210 @@ def get_disk_hardware_info(disk_name):
"""Placeholder for disk hardware info - to be populated by lsblk later."""
return {}
def get_pcie_link_speed(disk_name):
"""Get PCIe link speed information for NVMe drives"""
pcie_info = {
'pcie_gen': None,
'pcie_width': None,
'pcie_max_gen': None,
'pcie_max_width': None
}
try:
# For NVMe drives, get PCIe information from sysfs
if disk_name.startswith('nvme'):
# Extract controller name properly using regex
import re
match = re.match(r'(nvme\d+)n\d+', disk_name)
if not match:
# print(f"[v0] Could not extract controller from {disk_name}")
pass
return pcie_info
controller = match.group(1) # nvme0n1 -> nvme0
# print(f"[v0] Getting PCIe info for {disk_name}, controller: {controller}")
pass
# Path to PCIe device in sysfs
sys_path = f'/sys/class/nvme/{controller}/device'
# print(f"[v0] Checking sys_path: {sys_path}, exists: {os.path.exists(sys_path)}")
pass
if os.path.exists(sys_path):
try:
pci_address = os.path.basename(os.readlink(sys_path))
# print(f"[v0] PCI address for {disk_name}: {pci_address}")
pass
# Use lspci to get detailed PCIe information
result = subprocess.run(['lspci', '-vvv', '-s', pci_address],
capture_output=True, text=True, timeout=5)
if result.returncode == 0:
# print(f"[v0] lspci output for {pci_address}:")
pass
for line in result.stdout.split('\n'):
# Look for "LnkSta:" line which shows current link status
if 'LnkSta:' in line:
# print(f"[v0] Found LnkSta: {line}")
pass
# Example: "LnkSta: Speed 8GT/s, Width x4"
if 'Speed' in line:
speed_match = re.search(r'Speed\s+([\d.]+)GT/s', line)
if speed_match:
gt_s = float(speed_match.group(1))
if gt_s <= 2.5:
pcie_info['pcie_gen'] = '1.0'
elif gt_s <= 5.0:
pcie_info['pcie_gen'] = '2.0'
elif gt_s <= 8.0:
pcie_info['pcie_gen'] = '3.0'
elif gt_s <= 16.0:
pcie_info['pcie_gen'] = '4.0'
else:
pcie_info['pcie_gen'] = '5.0'
# print(f"[v0] Current PCIe gen: {pcie_info['pcie_gen']}")
pass
if 'Width' in line:
width_match = re.search(r'Width\s+x(\d+)', line)
if width_match:
pcie_info['pcie_width'] = f'x{width_match.group(1)}'
# print(f"[v0] Current PCIe width: {pcie_info['pcie_width']}")
pass
# Look for "LnkCap:" line which shows maximum capabilities
elif 'LnkCap:' in line:
# print(f"[v0] Found LnkCap: {line}")
pass
if 'Speed' in line:
speed_match = re.search(r'Speed\s+([\d.]+)GT/s', line)
if speed_match:
gt_s = float(speed_match.group(1))
if gt_s <= 2.5:
pcie_info['pcie_max_gen'] = '1.0'
elif gt_s <= 5.0:
pcie_info['pcie_max_gen'] = '2.0'
elif gt_s <= 8.0:
pcie_info['pcie_max_gen'] = '3.0'
elif gt_s <= 16.0:
pcie_info['pcie_max_gen'] = '4.0'
else:
pcie_info['pcie_max_gen'] = '5.0'
# print(f"[v0] Max PCIe gen: {pcie_info['pcie_max_gen']}")
pass
if 'Width' in line:
width_match = re.search(r'Width\s+x(\d+)', line)
if width_match:
pcie_info['pcie_max_width'] = f'x{width_match.group(1)}'
# print(f"[v0] Max PCIe width: {pcie_info['pcie_max_width']}")
pass
else:
# print(f"[v0] lspci failed with return code: {result.returncode}")
pass
except Exception as e:
# print(f"[v0] Error getting PCIe info via lspci: {e}")
pass
import traceback
traceback.print_exc()
else:
# print(f"[v0] sys_path does not exist: {sys_path}")
pass
alt_sys_path = f'/sys/block/{disk_name}/device/device'
# print(f"[v0] Trying alternative path: {alt_sys_path}, exists: {os.path.exists(alt_sys_path)}")
pass
if os.path.exists(alt_sys_path):
try:
# Get PCI address from the alternative path
pci_address = os.path.basename(os.readlink(alt_sys_path))
# print(f"[v0] PCI address from alt path for {disk_name}: {pci_address}")
pass
# Use lspci to get detailed PCIe information
result = subprocess.run(['lspci', '-vvv', '-s', pci_address],
capture_output=True, text=True, timeout=5)
if result.returncode == 0:
# print(f"[v0] lspci output for {pci_address} (from alt path):")
pass
for line in result.stdout.split('\n'):
# Look for "LnkSta:" line which shows current link status
if 'LnkSta:' in line:
# print(f"[v0] Found LnkSta: {line}")
pass
if 'Speed' in line:
speed_match = re.search(r'Speed\s+([\d.]+)GT/s', line)
if speed_match:
gt_s = float(speed_match.group(1))
if gt_s <= 2.5:
pcie_info['pcie_gen'] = '1.0'
elif gt_s <= 5.0:
pcie_info['pcie_gen'] = '2.0'
elif gt_s <= 8.0:
pcie_info['pcie_gen'] = '3.0'
elif gt_s <= 16.0:
pcie_info['pcie_gen'] = '4.0'
else:
pcie_info['pcie_gen'] = '5.0'
# print(f"[v0] Current PCIe gen: {pcie_info['pcie_gen']}")
pass
if 'Width' in line:
width_match = re.search(r'Width\s+x(\d+)', line)
if width_match:
pcie_info['pcie_width'] = f'x{width_match.group(1)}'
# print(f"[v0] Current PCIe width: {pcie_info['pcie_width']}")
pass
# Look for "LnkCap:" line which shows maximum capabilities
elif 'LnkCap:' in line:
# print(f"[v0] Found LnkCap: {line}")
pass
if 'Speed' in line:
speed_match = re.search(r'Speed\s+([\d.]+)GT/s', line)
if speed_match:
gt_s = float(speed_match.group(1))
if gt_s <= 2.5:
pcie_info['pcie_max_gen'] = '1.0'
elif gt_s <= 5.0:
pcie_info['pcie_max_gen'] = '2.0'
elif gt_s <= 8.0:
pcie_info['pcie_max_gen'] = '3.0'
elif gt_s <= 16.0:
pcie_info['pcie_max_gen'] = '4.0'
else:
pcie_info['pcie_max_gen'] = '5.0'
# print(f"[v0] Max PCIe gen: {pcie_info['pcie_max_gen']}")
pass
if 'Width' in line:
width_match = re.search(r'Width\s+x(\d+)', line)
if width_match:
pcie_info['pcie_max_width'] = f'x{width_match.group(1)}'
# print(f"[v0] Max PCIe width: {pcie_info['pcie_max_width']}")
pass
else:
# print(f"[v0] lspci failed with return code: {result.returncode}")
pass
except Exception as e:
# print(f"[v0] Error getting PCIe info from alt path: {e}")
pass
import traceback
traceback.print_exc()
except Exception as e:
# print(f"[v0] Error in get_pcie_link_speed for {disk_name}: {e}")
pass
import traceback
traceback.print_exc()
# print(f"[v0] Final PCIe info for {disk_name}: {pcie_info}")
pass
return pcie_info
# get_pcie_link_speed function definition ends here
def get_smart_data(disk_name):
"""Get SMART data for a specific disk - Enhanced with multiple device type attempts"""
smart_data = {
@@ -947,8 +1171,8 @@ def get_smart_data(disk_name):
try:
commands_to_try = [
['smartctl', '-a', '-j', f'/dev/{disk_name}'], # JSON output (preferred)
['smartctl', '-a', '-j', '-d', 'ata', f'/dev/{disk_name}'], # JSON with ATA device type
['smartctl', '-a', '-j', '-d', 'sat', f'/dev/{disk_name}'], # JSON with SAT device type
['smartctl', '-a', '-d', 'ata', f'/dev/{disk_name}'], # JSON with ATA device type
['smartctl', '-a', '-d', 'sat', f'/dev/{disk_name}'], # JSON with SAT device type
['smartctl', '-a', f'/dev/{disk_name}'], # Text output (fallback)
['smartctl', '-a', '-d', 'ata', f'/dev/{disk_name}'], # Text with ATA device type
['smartctl', '-a', '-d', 'sat', f'/dev/{disk_name}'], # Text with SAT device type
@@ -1387,6 +1611,23 @@ def get_smart_data(disk_name):
smart_data['health'] = 'warning'
# print(f"[v0] Health: WARNING (temperature {smart_data['temperature']}°C)")
pass
# CHANGE: Use -1 to indicate HDD with unknown RPM instead of inventing 7200 RPM
# Fallback: Check kernel's rotational flag if smartctl didn't provide rotation_rate
# This fixes detection for older disks that don't report RPM via smartctl
if smart_data['rotation_rate'] == 0:
try:
rotational_path = f"/sys/block/{disk_name}/queue/rotational"
if os.path.exists(rotational_path):
with open(rotational_path, 'r') as f:
rotational = int(f.read().strip())
if rotational == 1:
# Disk is rotational (HDD), use -1 to indicate "HDD but RPM unknown"
smart_data['rotation_rate'] = -1
# If rotational == 0, it's an SSD, keep rotation_rate as 0
except Exception as e:
pass # If we can't read the file, leave rotation_rate as is
except FileNotFoundError:
# print(f"[v0] ERROR: smartctl not found - install smartmontools for disk monitoring.")
@@ -1500,6 +1741,7 @@ def get_proxmox_storage():
# END OF CHANGES FOR get_proxmox_storage
@app.route('/api/storage/summary', methods=['GET'])
@require_auth
def api_storage_summary():
"""Get storage summary without SMART data (optimized for Overview page)"""
try:
@@ -1598,8 +1840,15 @@ def get_interface_type(interface_name):
if '.' in interface_name:
return 'vlan'
# Check if it's a physical interface
if interface_name.startswith(('enp', 'eth', 'eno', 'ens', 'wlan', 'wlp')):
# Check if interface has a real device symlink in /sys/class/net
# This catches all physical interfaces including USB, regardless of naming
sys_path = f'/sys/class/net/{interface_name}/device'
if os.path.exists(sys_path):
# It's a physical interface (PCI, USB, etc.)
return 'physical'
# This handles cases where /sys might not be available
if interface_name.startswith(('enp', 'eth', 'eno', 'ens', 'enx', 'wlan', 'wlp', 'wlo', 'usb')):
return 'physical'
# Default to skip for unknown types
@@ -1996,7 +2245,7 @@ def get_proxmox_vms():
# print(f"[v0] Error getting VM/LXC info: {e}")
pass
return {
'error': f'Unable to access VM information: {str(e)}',
'error': 'Unable to access VM information: {str(e)}',
'vms': []
}
except Exception as e:
@@ -2531,7 +2780,7 @@ def get_detailed_gpu_info(gpu):
if 'clients' in json_data:
client_count = len(json_data['clients'])
for client_id, client_data in json_data['clients'].items():
for client_id, client_data in json_data['clients']:
client_name = client_data.get('name', 'Unknown')
client_pid = client_data.get('pid', 'Unknown')
@@ -3064,6 +3313,9 @@ def get_detailed_gpu_info(gpu):
data_retrieved = False
# CHANGE: Initialize sensors variable to None to avoid UnboundLocalError
sensors = None
# Parse temperature (Edge Temperature from sensors)
if 'sensors' in device:
sensors = device['sensors']
@@ -3074,22 +3326,23 @@ def get_detailed_gpu_info(gpu):
# print(f"[v0] Temperature: {detailed_info['temperature']}°C", flush=True)
pass
data_retrieved = True
# Parse power draw (GFX Power or average_socket_power)
if 'GFX Power' in sensors:
gfx_power = sensors['GFX Power']
if 'value' in gfx_power:
detailed_info['power_draw'] = f"{gfx_power['value']:.2f} W"
# print(f"[v0] Power Draw: {detailed_info['power_draw']}", flush=True)
pass
data_retrieved = True
elif 'average_socket_power' in sensors:
socket_power = sensors['average_socket_power']
if 'value' in socket_power:
detailed_info['power_draw'] = f"{socket_power['value']:.2f} W"
# print(f"[v0] Power Draw: {detailed_info['power_draw']}", flush=True)
pass
data_retrieved = True
# CHANGE: Added check to ensure sensors is not None before accessing
# Parse power draw (GFX Power or average_socket_power)
if sensors and 'GFX Power' in sensors:
gfx_power = sensors['GFX Power']
if 'value' in gfx_power:
detailed_info['power_draw'] = f"{gfx_power['value']:.2f} W"
# print(f"[v0] Power Draw: {detailed_info['power_draw']}", flush=True)
pass
data_retrieved = True
elif sensors and 'average_socket_power' in sensors:
socket_power = sensors['average_socket_power']
if 'value' in socket_power:
detailed_info['power_draw'] = f"{socket_power['value']:.2f} W"
# print(f"[v0] Power Draw: {detailed_info['power_draw']}", flush=True)
pass
data_retrieved = True
# Parse clocks (GFX_SCLK for graphics, GFX_MCLK for memory)
if 'Clocks' in device:
@@ -3098,7 +3351,7 @@ def get_detailed_gpu_info(gpu):
gfx_clock = clocks['GFX_SCLK']
if 'value' in gfx_clock:
detailed_info['clock_graphics'] = f"{gfx_clock['value']} MHz"
# print(f"[v0] Graphics Clock: {detailed_info['clock_graphics']}", flush=True)
# print(f"[v0] Graphics Clock: {detailed_info['clock_graphics']} MHz", flush=True)
pass
data_retrieved = True
@@ -3106,7 +3359,7 @@ def get_detailed_gpu_info(gpu):
mem_clock = clocks['GFX_MCLK']
if 'value' in mem_clock:
detailed_info['clock_memory'] = f"{mem_clock['value']} MHz"
# print(f"[v0] Memory Clock: {detailed_info['clock_memory']}", flush=True)
# print(f"[v0] Memory Clock: {detailed_info['clock_memory']} MHz", flush=True)
pass
data_retrieved = True
@@ -3223,7 +3476,7 @@ def get_detailed_gpu_info(gpu):
'shared': 0,
'resident': int(vram_mb * 1024 * 1024)
}
# print(f"[v0] VRAM: {vram_mb} MB", flush=True)
# print(f"[v0] VRAM: {vram_mb} MB", flush=True)
pass
# Parse GTT (Graphics Translation Table) usage (está dentro de usage.usage)
@@ -3237,7 +3490,7 @@ def get_detailed_gpu_info(gpu):
else:
# Add GTT to existing VRAM
process_info['memory']['total'] += int(gtt_mb * 1024 * 1024)
# print(f"[v0] GTT: {gtt_mb} MB", flush=True)
# print(f"[v0] GTT: {gtt_mb} MB", flush=True)
pass
# Parse engine utilization for this process (están dentro de usage.usage)
@@ -3343,7 +3596,6 @@ def get_detailed_gpu_info(gpu):
else:
# print(f"[v0] No fdinfo section found in device data", flush=True)
pass
detailed_info['processes'] = []
if data_retrieved:
detailed_info['has_monitoring_tool'] = True
@@ -3869,6 +4121,10 @@ def get_hardware_info():
except:
pass
pcie_info = {}
if disk_name.startswith('nvme'):
pcie_info = get_pcie_link_speed(disk_name)
# Build storage device with all available information
storage_device = {
'name': disk_name,
@@ -3884,6 +4140,9 @@ def get_hardware_info():
'sata_version': sata_version,
}
if pcie_info:
storage_device.update(pcie_info)
# Add family if available (from smartctl)
try:
result_smart = subprocess.run(['smartctl', '-i', f'/dev/{disk_name}'],
@@ -3905,7 +4164,7 @@ def get_hardware_info():
# print(f"[v0] Error getting storage info: {e}")
pass
# Graphics Cards (from lspci - will be duplicated by new PCI device listing, but kept for now)
# Graphics Cards
try:
# Try nvidia-smi first
result = subprocess.run(['nvidia-smi', '--query-gpu=name,memory.total,memory.used,temperature.gpu,power.draw,utilization.gpu,utilization.memory,clocks.graphics,clocks.memory', '--format=csv,noheader,nounits'],
@@ -4262,6 +4521,7 @@ def get_hardware_info():
@app.route('/api/system', methods=['GET'])
@require_auth
def api_system():
"""Get system information including CPU, memory, and temperature"""
try:
@@ -4318,21 +4578,25 @@ def api_system():
return jsonify({'error': str(e)}), 500
@app.route('/api/storage', methods=['GET'])
@require_auth
def api_storage():
"""Get storage information"""
return jsonify(get_storage_info())
@app.route('/api/proxmox-storage', methods=['GET'])
@require_auth
def api_proxmox_storage():
"""Get Proxmox storage information"""
return jsonify(get_proxmox_storage())
@app.route('/api/network', methods=['GET'])
@require_auth
def api_network():
"""Get network information"""
return jsonify(get_network_info())
@app.route('/api/network/summary', methods=['GET'])
@require_auth
def api_network_summary():
"""Optimized network summary endpoint - returns basic network info without detailed analysis"""
try:
@@ -4411,6 +4675,7 @@ def api_network_summary():
return jsonify({'error': str(e)}), 500
@app.route('/api/network/<interface_name>/metrics', methods=['GET'])
@require_auth
def api_network_interface_metrics(interface_name):
"""Get historical metrics (RRD data) for a specific network interface"""
try:
@@ -4450,7 +4715,7 @@ def api_network_interface_metrics(interface_name):
for point in all_data:
filtered_point = {'time': point.get('time')}
# Add network fields if they exist
for key in ['netin', 'netout', 'diskread', 'diskwrite']:
for key in ['netin', 'netout']:
if key in point:
filtered_point[key] = point[key]
rrd_data.append(filtered_point)
@@ -4493,12 +4758,13 @@ def api_network_interface_metrics(interface_name):
return jsonify({'error': str(e)}), 500
@app.route('/api/vms', methods=['GET'])
@require_auth
def api_vms():
"""Get virtual machine information"""
return jsonify(get_proxmox_vms())
# Add the new api_vm_metrics endpoint here
@app.route('/api/vms/<int:vmid>/metrics', methods=['GET'])
@require_auth
def api_vm_metrics(vmid):
"""Get historical metrics (RRD data) for a specific VM/LXC"""
try:
@@ -4565,6 +4831,7 @@ def api_vm_metrics(vmid):
return jsonify({'error': str(e)}), 500
@app.route('/api/node/metrics', methods=['GET'])
@require_auth
def api_node_metrics():
"""Get historical metrics (RRD data) for the node"""
try:
@@ -4608,6 +4875,7 @@ def api_node_metrics():
return jsonify({'error': str(e)}), 500
@app.route('/api/logs', methods=['GET'])
@require_auth
def api_logs():
"""Get system logs"""
try:
@@ -4666,7 +4934,7 @@ def api_logs():
'pid': log_entry.get('_PID', ''),
'hostname': log_entry.get('_HOSTNAME', '')
})
except (json.JSONDecodeError, ValueError) as e:
except (json.JSONDecodeError, ValueError):
continue
return jsonify({'logs': logs, 'total': len(logs)})
else:
@@ -4685,6 +4953,7 @@ def api_logs():
})
@app.route('/api/logs/download', methods=['GET'])
@require_auth
def api_logs_download():
"""Download system logs as a text file"""
try:
@@ -4743,6 +5012,7 @@ def api_logs_download():
return jsonify({'error': str(e)}), 500
@app.route('/api/notifications', methods=['GET'])
@require_auth
def api_notifications():
"""Get Proxmox notification history"""
try:
@@ -4859,6 +5129,7 @@ def api_notifications():
})
@app.route('/api/notifications/download', methods=['GET'])
@require_auth
def api_notifications_download():
"""Download complete log for a specific notification"""
try:
@@ -4914,6 +5185,7 @@ def api_notifications_download():
return jsonify({'error': str(e)}), 500
@app.route('/api/backups', methods=['GET'])
@require_auth
def api_backups():
"""Get list of all backup files from Proxmox storage"""
try:
@@ -5002,6 +5274,7 @@ def api_backups():
})
@app.route('/api/events', methods=['GET'])
@require_auth
def api_events():
"""Get recent Proxmox events and tasks"""
try:
@@ -5078,6 +5351,7 @@ def api_events():
})
@app.route('/api/task-log/<path:upid>')
@require_auth
def get_task_log(upid):
"""Get complete task log from Proxmox using UPID"""
try:
@@ -5175,15 +5449,17 @@ def get_task_log(upid):
return jsonify({'error': str(e)}), 500
@app.route('/api/health', methods=['GET'])
@require_auth
def api_health():
"""Health check endpoint"""
return jsonify({
'status': 'healthy',
'timestamp': datetime.now().isoformat(),
'version': '1.0.0'
'version': '1.0.1'
})
@app.route('/api/prometheus', methods=['GET'])
@require_auth
def api_prometheus():
"""Export metrics in Prometheus format"""
try:
@@ -5362,10 +5638,6 @@ def api_prometheus():
mem_used_bytes = mem_used * 1024 * 1024 # Convert MiB to bytes
mem_total_bytes = mem_total * 1024 * 1024
metrics.append(f'# HELP proxmox_gpu_memory_used_bytes GPU memory used in bytes')
metrics.append(f'# TYPE proxmox_gpu_memory_used_bytes gauge')
metrics.append(f'proxmox_gpu_memory_used_bytes{{node="{node}",gpu="{gpu_name}",vendor="{gpu_vendor}",slot="{gpu_slot}"}} {mem_used_bytes} {timestamp}')
metrics.append(f'# HELP proxmox_gpu_memory_total_bytes GPU memory total in bytes')
metrics.append(f'# TYPE proxmox_gpu_memory_total_bytes gauge')
metrics.append(f'proxmox_gpu_memory_total_bytes{{node="{node}",gpu="{gpu_name}",vendor="{gpu_vendor}",slot="{gpu_slot}"}} {mem_total_bytes} {timestamp}')
@@ -5442,64 +5714,14 @@ def api_prometheus():
traceback.print_exc()
return f'# Error generating metrics: {str(e)}\n', 500, {'Content-Type': 'text/plain; charset=utf-8'}
@app.route('/api/system-info', methods=['GET'])
def api_system_info():
"""Get system and node information for dashboard header"""
try:
hostname = socket.gethostname()
node_id = f"pve-{hostname}"
pve_version = None
# Try to get Proxmox version
try:
result = subprocess.run(['pveversion'], capture_output=True, text=True, timeout=5)
if result.returncode == 0:
pve_version = result.stdout.strip().split('\n')[0]
except:
pass
# Try to get node info from Proxmox API
try:
result = subprocess.run(['pvesh', 'get', '/nodes', '--output-format', 'json'],
capture_output=True, text=True, timeout=5)
if result.returncode == 0:
nodes = json.loads(result.stdout)
if nodes and len(nodes) > 0:
node_info = nodes[0]
node_id = node_info.get('node', node_id)
hostname = node_info.get('node', hostname)
except:
pass
response = {
'hostname': hostname,
'node_id': node_id,
'status': 'online',
'timestamp': datetime.now().isoformat()
}
if pve_version:
response['pve_version'] = pve_version
else:
response['error'] = 'Proxmox version not available - pveversion command not found'
return jsonify(response)
except Exception as e:
# print(f"Error getting system info: {e}")
pass
return jsonify({
'error': f'Unable to access system information: {str(e)}',
'hostname': socket.gethostname(),
'status': 'error',
'timestamp': datetime.now().isoformat()
})
@app.route('/api/info', methods=['GET'])
@require_auth
def api_info():
"""Root endpoint with API information"""
return jsonify({
'name': 'ProxMenux Monitor API',
'version': '1.0.0',
'version': '1.0.1',
'endpoints': [
'/api/system',
'/api/system-info',
@@ -5523,6 +5745,7 @@ def api_info():
})
@app.route('/api/hardware', methods=['GET'])
@require_auth
def api_hardware():
"""Get hardware information"""
try:
@@ -5559,6 +5782,7 @@ def api_hardware():
return jsonify({'error': str(e)}), 500
@app.route('/api/gpu/<slot>/realtime', methods=['GET'])
@require_auth
def api_gpu_realtime(slot):
"""Get real-time GPU monitoring data for a specific GPU"""
try:
@@ -5621,6 +5845,7 @@ def api_gpu_realtime(slot):
# CHANGE: Modificar el endpoint para incluir la información completa de IPs
@app.route('/api/vms/<int:vmid>', methods=['GET'])
@require_auth
def get_vm_config(vmid):
"""Get detailed configuration for a specific VM/LXC"""
try:
@@ -5717,6 +5942,7 @@ def get_vm_config(vmid):
return jsonify({'error': str(e)}), 500
@app.route('/api/vms/<int:vmid>/logs', methods=['GET'])
@require_auth
def api_vm_logs(vmid):
"""Download real logs for a specific VM/LXC (not task history)"""
try:
@@ -5766,6 +5992,7 @@ def api_vm_logs(vmid):
return jsonify({'error': str(e)}), 500
@app.route('/api/vms/<int:vmid>/control', methods=['POST'])
@require_auth
def api_vm_control(vmid):
"""Control VM/LXC (start, stop, shutdown, reboot)"""
try:
@@ -5818,6 +6045,7 @@ def api_vm_control(vmid):
return jsonify({'error': str(e)}), 500
@app.route('/api/vms/<int:vmid>/config', methods=['PUT'])
@require_auth
def api_vm_config_update(vmid):
"""Update VM/LXC configuration (description/notes)"""
try:
File diff suppressed because it is too large Load Diff
+367
View File
@@ -0,0 +1,367 @@
"""
Health Monitor Persistence Module
Manages persistent error tracking across AppImage updates using SQLite.
Stores errors in /root/.config/proxmenux-monitor/health_monitor.db
Features:
- Persistent error storage (survives AppImage updates)
- Smart error resolution (auto-clear when VM starts, or after 48h)
- Event system for future Telegram notifications
- Manual acknowledgment support
Author: MacRimi
Version: 1.0
"""
import sqlite3
import json
import os
from datetime import datetime, timedelta
from typing import Dict, List, Any, Optional
from pathlib import Path
class HealthPersistence:
"""Manages persistent health error tracking"""
# Error retention periods (seconds)
VM_ERROR_RETENTION = 48 * 3600 # 48 hours
LOG_ERROR_RETENTION = 24 * 3600 # 24 hours
DISK_ERROR_RETENTION = 48 * 3600 # 48 hours
UPDATES_SUPPRESSION = 180 * 24 * 3600 # 180 days (6 months)
def __init__(self):
"""Initialize persistence with database in config directory"""
self.data_dir = Path('/root/.config/proxmenux-monitor')
self.data_dir.mkdir(parents=True, exist_ok=True)
self.db_path = self.data_dir / 'health_monitor.db'
self._init_database()
def _init_database(self):
"""Initialize SQLite database with required tables"""
conn = sqlite3.connect(str(self.db_path))
cursor = conn.cursor()
# Errors table
cursor.execute('''
CREATE TABLE IF NOT EXISTS errors (
id INTEGER PRIMARY KEY AUTOINCREMENT,
error_key TEXT UNIQUE NOT NULL,
category TEXT NOT NULL,
severity TEXT NOT NULL,
reason TEXT NOT NULL,
details TEXT,
first_seen TEXT NOT NULL,
last_seen TEXT NOT NULL,
resolved_at TEXT,
acknowledged INTEGER DEFAULT 0,
notification_sent INTEGER DEFAULT 0
)
''')
# Events table (for future Telegram notifications)
cursor.execute('''
CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
event_type TEXT NOT NULL,
error_key TEXT NOT NULL,
timestamp TEXT NOT NULL,
data TEXT
)
''')
# Indexes for performance
cursor.execute('CREATE INDEX IF NOT EXISTS idx_error_key ON errors(error_key)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_category ON errors(category)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_resolved ON errors(resolved_at)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_events_error ON events(error_key)')
conn.commit()
conn.close()
def record_error(self, error_key: str, category: str, severity: str,
reason: str, details: Optional[Dict] = None) -> Dict[str, Any]:
"""
Record or update an error.
Returns event info (new_error, updated, etc.)
"""
conn = sqlite3.connect(str(self.db_path))
cursor = conn.cursor()
now = datetime.now().isoformat()
details_json = json.dumps(details) if details else None
cursor.execute('''
SELECT acknowledged, resolved_at
FROM errors
WHERE error_key = ? AND acknowledged = 1
''', (error_key,))
ack_check = cursor.fetchone()
if ack_check and ack_check[1]: # Has resolved_at timestamp
try:
resolved_dt = datetime.fromisoformat(ack_check[1])
hours_since_ack = (datetime.now() - resolved_dt).total_seconds() / 3600
if category == 'updates':
# Updates: suppress for 180 days (6 months)
suppression_hours = self.UPDATES_SUPPRESSION / 3600
else:
# Other errors: suppress for 24 hours
suppression_hours = 24
if hours_since_ack < suppression_hours:
# Skip re-adding recently acknowledged errors
conn.close()
return {'type': 'skipped_acknowledged', 'needs_notification': False}
except Exception:
pass
cursor.execute('''
SELECT id, first_seen, notification_sent, acknowledged, resolved_at
FROM errors WHERE error_key = ?
''', (error_key,))
existing = cursor.fetchone()
event_info = {'type': 'updated', 'needs_notification': False}
if existing:
error_id, first_seen, notif_sent, acknowledged, resolved_at = existing
if acknowledged == 1:
conn.close()
return {'type': 'skipped_acknowledged', 'needs_notification': False}
# Update existing error (only if NOT acknowledged)
cursor.execute('''
UPDATE errors
SET last_seen = ?, severity = ?, reason = ?, details = ?
WHERE error_key = ? AND acknowledged = 0
''', (now, severity, reason, details_json, error_key))
# Check if severity escalated
cursor.execute('SELECT severity FROM errors WHERE error_key = ?', (error_key,))
old_severity_row = cursor.fetchone()
if old_severity_row:
old_severity = old_severity_row[0]
if old_severity == 'WARNING' and severity == 'CRITICAL':
event_info['type'] = 'escalated'
event_info['needs_notification'] = True
else:
# Insert new error
cursor.execute('''
INSERT INTO errors
(error_key, category, severity, reason, details, first_seen, last_seen)
VALUES (?, ?, ?, ?, ?, ?, ?)
''', (error_key, category, severity, reason, details_json, now, now))
event_info['type'] = 'new'
event_info['needs_notification'] = True
# Record event
self._record_event(cursor, event_info['type'], error_key,
{'severity': severity, 'reason': reason})
conn.commit()
conn.close()
return event_info
def resolve_error(self, error_key: str, reason: str = 'auto-resolved'):
"""Mark an error as resolved"""
conn = sqlite3.connect(str(self.db_path))
cursor = conn.cursor()
now = datetime.now().isoformat()
cursor.execute('''
UPDATE errors
SET resolved_at = ?
WHERE error_key = ? AND resolved_at IS NULL
''', (now, error_key))
if cursor.rowcount > 0:
self._record_event(cursor, 'resolved', error_key, {'reason': reason})
conn.commit()
conn.close()
def acknowledge_error(self, error_key: str):
"""
Manually acknowledge an error (won't notify again or re-appear for 24h).
Also marks as resolved so it disappears from active errors.
"""
conn = sqlite3.connect(str(self.db_path))
cursor = conn.cursor()
now = datetime.now().isoformat()
cursor.execute('''
UPDATE errors
SET acknowledged = 1, resolved_at = ?
WHERE error_key = ?
''', (now, error_key))
self._record_event(cursor, 'acknowledged', error_key, {})
conn.commit()
conn.close()
def get_active_errors(self, category: Optional[str] = None) -> List[Dict[str, Any]]:
"""Get all active (unresolved) errors, optionally filtered by category"""
conn = sqlite3.connect(str(self.db_path))
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
if category:
cursor.execute('''
SELECT * FROM errors
WHERE resolved_at IS NULL AND category = ?
ORDER BY severity DESC, last_seen DESC
''', (category,))
else:
cursor.execute('''
SELECT * FROM errors
WHERE resolved_at IS NULL
ORDER BY severity DESC, last_seen DESC
''')
rows = cursor.fetchall()
conn.close()
errors = []
for row in rows:
error_dict = dict(row)
if error_dict.get('details'):
error_dict['details'] = json.loads(error_dict['details'])
errors.append(error_dict)
return errors
def cleanup_old_errors(self):
"""Clean up old resolved errors and auto-resolve stale errors"""
conn = sqlite3.connect(str(self.db_path))
cursor = conn.cursor()
now = datetime.now()
# Delete resolved errors older than 7 days
cutoff_resolved = (now - timedelta(days=7)).isoformat()
cursor.execute('DELETE FROM errors WHERE resolved_at < ?', (cutoff_resolved,))
# Auto-resolve VM/CT errors older than 48h
cutoff_vm = (now - timedelta(seconds=self.VM_ERROR_RETENTION)).isoformat()
cursor.execute('''
UPDATE errors
SET resolved_at = ?
WHERE category = 'vms'
AND resolved_at IS NULL
AND first_seen < ?
AND acknowledged = 0
''', (now.isoformat(), cutoff_vm))
# Auto-resolve log errors older than 24h
cutoff_logs = (now - timedelta(seconds=self.LOG_ERROR_RETENTION)).isoformat()
cursor.execute('''
UPDATE errors
SET resolved_at = ?
WHERE category = 'logs'
AND resolved_at IS NULL
AND first_seen < ?
AND acknowledged = 0
''', (now.isoformat(), cutoff_logs))
# Delete old events (>30 days)
cutoff_events = (now - timedelta(days=30)).isoformat()
cursor.execute('DELETE FROM events WHERE timestamp < ?', (cutoff_events,))
conn.commit()
conn.close()
def check_vm_running(self, vm_id: str) -> bool:
"""
Check if a VM/CT is running and resolve error if so.
Returns True if running and error was resolved.
"""
import subprocess
try:
# Check qm status for VMs
result = subprocess.run(
['qm', 'status', vm_id],
capture_output=True,
text=True,
timeout=2
)
if result.returncode == 0 and 'running' in result.stdout.lower():
self.resolve_error(f'vm_{vm_id}', 'VM started')
return True
# Check pct status for containers
result = subprocess.run(
['pct', 'status', vm_id],
capture_output=True,
text=True,
timeout=2
)
if result.returncode == 0 and 'running' in result.stdout.lower():
self.resolve_error(f'ct_{vm_id}', 'Container started')
return True
return False
except Exception:
return False
def _record_event(self, cursor, event_type: str, error_key: str, data: Dict):
"""Internal: Record an event"""
cursor.execute('''
INSERT INTO events (event_type, error_key, timestamp, data)
VALUES (?, ?, ?, ?)
''', (event_type, error_key, datetime.now().isoformat(), json.dumps(data)))
def get_unnotified_errors(self) -> List[Dict[str, Any]]:
"""Get errors that need Telegram notification"""
conn = sqlite3.connect(str(self.db_path))
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute('''
SELECT * FROM errors
WHERE notification_sent = 0
AND resolved_at IS NULL
AND acknowledged = 0
ORDER BY severity DESC, first_seen ASC
''')
rows = cursor.fetchall()
conn.close()
errors = []
for row in rows:
error_dict = dict(row)
if error_dict.get('details'):
error_dict['details'] = json.loads(error_dict['details'])
errors.append(error_dict)
return errors
def mark_notified(self, error_key: str):
"""Mark error as notified"""
conn = sqlite3.connect(str(self.db_path))
cursor = conn.cursor()
cursor.execute('''
UPDATE errors
SET notification_sent = 1
WHERE error_key = ?
''', (error_key,))
conn.commit()
conn.close()
# Global instance
health_persistence = HealthPersistence()
+98
View File
@@ -0,0 +1,98 @@
"""
JWT Middleware Module
Provides decorator to protect Flask routes with JWT authentication
Automatically checks auth status and validates tokens
"""
from flask import request, jsonify
from functools import wraps
from auth_manager import load_auth_config, verify_token
def require_auth(f):
"""
Decorator to protect Flask routes with JWT authentication
Behavior:
- If auth is disabled or declined: Allow access (no token required)
- If auth is enabled: Require valid JWT token in Authorization header
- Returns 401 if auth required but token missing/invalid
Usage:
@app.route('/api/protected')
@require_auth
def protected_route():
return jsonify({"data": "secret"})
"""
@wraps(f)
def decorated_function(*args, **kwargs):
# Check if authentication is enabled
config = load_auth_config()
# If auth is disabled or declined, allow access
if not config.get("enabled", False) or config.get("declined", False):
return f(*args, **kwargs)
# Auth is enabled, require token
auth_header = request.headers.get('Authorization')
if not auth_header:
return jsonify({
"error": "Authentication required",
"message": "No authorization header provided"
}), 401
# Extract token from "Bearer <token>" format
parts = auth_header.split()
if len(parts) != 2 or parts[0].lower() != 'bearer':
return jsonify({
"error": "Invalid authorization header",
"message": "Authorization header must be in format: Bearer <token>"
}), 401
token = parts[1]
# Verify token
username = verify_token(token)
if not username:
return jsonify({
"error": "Invalid or expired token",
"message": "Please log in again"
}), 401
# Token is valid, allow access
return f(*args, **kwargs)
return decorated_function
def optional_auth(f):
"""
Decorator for routes that can optionally use auth
Passes username if authenticated, None otherwise
Usage:
@app.route('/api/optional')
@optional_auth
def optional_route(username=None):
if username:
return jsonify({"message": f"Hello {username}"})
return jsonify({"message": "Hello guest"})
"""
@wraps(f)
def decorated_function(*args, **kwargs):
config = load_auth_config()
username = None
if config.get("enabled", False):
auth_header = request.headers.get('Authorization')
if auth_header:
parts = auth_header.split()
if len(parts) == 2 and parts[0].lower() == 'bearer':
username = verify_token(parts[1])
# Inject username into kwargs
kwargs['username'] = username
return f(*args, **kwargs)
return decorated_function
+14 -1
View File
@@ -1,3 +1,5 @@
import { fetchApi } from "@/lib/api-config"
export interface Temperature {
name: string
original_name?: string
@@ -33,6 +35,13 @@ export interface StorageDevice {
rotation_rate?: number | string
form_factor?: string
sata_version?: string
pcie_gen?: string // e.g., "PCIe 4.0"
pcie_width?: string // e.g., "x4"
pcie_max_gen?: string // Maximum supported PCIe generation
pcie_max_width?: string // Maximum supported PCIe lanes
sas_version?: string // e.g., "SAS-3"
sas_speed?: string // e.g., "12Gb/s"
link_speed?: string // Generic link speed info
}
export interface PCIDevice {
@@ -201,4 +210,8 @@ export interface HardwareData {
ups?: UPS | UPS[]
}
export const fetcher = (url: string) => fetch(url).then((res) => res.json())
export const fetcher = async (url: string) => {
// Extract just the endpoint from the URL if it's a full URL
const endpoint = url.startsWith("http") ? new URL(url).pathname : url
return fetchApi(endpoint)
}
+2 -1
View File
@@ -9,8 +9,9 @@
ProxMenux Monitor is designed to support future updates where **actions can be triggered without using the terminal**, and managed through a **user-friendly interface** accessible across multiple formats and devices.
![ProxMenux Monitor](https://macrimi.github.io/ProxMenux/monitor/welcome.png)
Access it at: **http://your-server-ip:8008**
![ProxMenux Monitor](https://macrimi.github.io/ProxMenux/monitor/welcome.png)
- **New Banner Removal Method**
A new function to disable the Proxmox subscription message with improved safety:
- Creates a full backup before modifying any files
+30 -17
View File
@@ -1,21 +1,34 @@
MIT License
ProxMenux An Interactive Menu for Proxmox VE Management
Copyright (c) 2025 MacRimi
Copyright (c) 2024 MacRimi
This project is licensed under the Creative Commons Attribution-NonCommercial 4.0 International License.
See the full license terms below.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
======================================================================
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
Attribution-NonCommercial 4.0 International (CC BY-NC 4.0)
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
This is a human-readable summary of (and not a substitute for) the license.
You may obtain a copy of the full license at:
https://creativecommons.org/licenses/by-nc/4.0/
You are free to:
- Share — copy and redistribute the material in any medium or format.
- Adapt — remix, transform, and build upon the material.
Under the following terms:
- Attribution — You must give appropriate credit, provide a link to the license,
and indicate if changes were made.
- NonCommercial — You may not use the material for commercial purposes.
No additional restrictions — You may not apply legal terms or technological
measures that legally restrict others from doing anything the license permits.
Disclaimer:
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE, AND NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE.
+512
View File
@@ -0,0 +1,512 @@
---
# **Análisis Completo del proyecto ProxMenux**
## **1. Estructura General del Proyecto**
### **Archivos Principales**
- **[install_proxmenux.sh](cci:7://file:///home/debian/src/ProxMenuxOffline/install_proxmenux.sh:0:0-0:0)**: Script de instalación principal (723 líneas)
- **[menu](cci:7://file:///home/debian/src/ProxMenuxOffline/menu:0:0-0:0)**: Script principal que se instala como comando del sistema (93 líneas)
- **[version.txt](cci:7://file:///home/debian/src/ProxMenuxOffline/version.txt:0:0-0:0)**: Control de versiones (actual: 1.1.7)
### **Directorios Principales**
```
ProxMenuxOffline/
├── scripts/ # 122 archivos de scripts bash
│ ├── menus/ # 13 scripts de menús
│ ├── lxc/ # 6 scripts para contenedores LXC
│ ├── vm/ # 13 scripts para máquinas virtuales
│ ├── storage/ # 9 scripts de almacenamiento
│ ├── share/ # 12 scripts para compartir recursos
│ ├── utilities/ # 6 utilidades del sistema
│ ├── global/ # 10 funciones comunes
│ ├── backup_restore/ # 6 scripts de respaldo
│ ├── post_install/ # 3 scripts post-instalación
│ └── gpu_tpu/ # Scripts para hardware gráfico
├── web/ # 136 archivos - Dashboard Next.js
├── AppImage/ # 54 archivos - ProxMenux Monitor
├── json/ # Archivos de caché de traducciones
├── lang/ # Archivos de idioma
├── guides/ # 5 guías de usuario
└── images/ # 7 imágenes del proyecto
```
---
## **2. Flujo de Instalación**
### **Script: [install_proxmenux.sh](cci:7://file:///home/debian/src/ProxMenuxOffline/install_proxmenux.sh:0:0-0:0)**
**Fase 1: Inicialización**
- Verifica permisos root (línea 716-719)
- Carga [utils.sh](cci:7://file:///home/debian/src/ProxMenuxOffline/scripts/utils.sh:0:0-0:0) desde GitHub (línea 54-57)
- Limpia archivos corruptos de configuración (línea 59-68)
**Fase 2: Detección de Instalación Existente**
- Función [check_existing_installation()](cci:1://file:///home/debian/src/ProxMenuxOffline/install_proxmenux.sh:70:0-106:1) (línea 71-107)
- Detecta 4 tipos: `none`, `normal`, `translation`, `unknown`
- Verifica entorno virtual Python en `/opt/googletrans-env`
- Verifica configuración de idioma en `/usr/local/share/proxmenux/config.json`
**Fase 3: Selección de Versión**
- **Versión Normal** (opción 1):
- Dependencias: `dialog`, `curl`, `jq`
- Solo inglés
- Más ligera y rápida
- **Versión con Traducción** (opción 2):
- Dependencias adicionales: `python3`, `python3-venv`, `python3-pip`
- Instala `googletrans==4.0.0-rc1` en entorno virtual
- Soporte multiidioma: en, es, fr, de, it, pt
- **Nota**: No compatible con Proxmox VE 9+ (línea 639-658)
**Fase 4: Instalación Normal** ([install_normal_version()](cci:1://file:///home/debian/src/ProxMenuxOffline/install_proxmenux.sh:402:0-484:1))
1. Instala dependencias básicas
2. Crea directorios:
- `/usr/local/bin` (ejecutables)
- `/usr/local/share/proxmenux` (archivos del sistema)
3. Descarga desde GitHub:
- `utils.sh``/usr/local/share/proxmenux/utils.sh`
- `menu``/usr/local/bin/menu`
- `version.txt``/usr/local/share/proxmenux/version.txt`
4. Instala ProxMenux Monitor (AppImage)
**Fase 5: Instalación con Traducción** (`install_translation_version()`)
- Pasos adicionales:
- Selector de idioma interactivo (línea 234-273)
- Crea entorno virtual Python en `/opt/googletrans-env`
- Instala googletrans con pip
- Descarga `cache.json` con traducciones precargadas
- Sistema de caché para reducir llamadas a la API de traducción
**Fase 6: ProxMenux Monitor**
- Descarga AppImage desde GitHub (línea 317-360)
- Verifica checksum SHA256 (línea 333-351)
- Crea servicio systemd `/etc/systemd/system/proxmenux-monitor.service`
- Puerto por defecto: 8008
- Se ejecuta como usuario root
- Auto-inicio en boot
---
## **3. Funcionamiento del Comando `menu`**
### **Script Principal: `/usr/local/bin/menu`**
**Flujo de Ejecución:**
1. **Carga de Configuración** (línea 33-44):
```bash
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
BASE_DIR="/usr/local/share/proxmenux"
source "$UTILS_FILE"
```
2. **Sistema de Traducción** (línea 89-92):
- Carga idioma desde `config.json`
- Inicializa caché de traducciones
- Función `translate()` en `utils.sh`
3. **Verificación de Actualizaciones** (línea 48-80):
- Compara versión local vs remota
- Prompt interactivo para actualizar
- Descarga y ejecuta nuevo `install_proxmenux.sh` si hay actualización
4. **Ejecución del Menú Principal** (línea 84-86):
```bash
exec bash <(curl -fsSL "$REPO_URL/scripts/menus/main_menu.sh")
```
**Importante**: El comando `menu` **NO ejecuta scripts locales**, siempre descarga desde GitHub.
---
## **4. Sistema de Menús**
### **Menú Principal: `scripts/menus/main_menu.sh`**
**Compatibilidad PVE 9** (línea 26-64):
- Detecta versión de Proxmox
- Si PVE 9+ y tiene traducciones instaladas → fuerza reinstalación en versión normal
- Previene errores de compatibilidad
**Opciones del Menú** (línea 97-111):
```
1. Settings post-install Proxmox → menu_post_install.sh
2. Hardware: GPUs and Coral-TPU → hw_grafics_menu.sh
3. Create VM from template → create_vm_menu.sh
4. Disk and Storage Manager → storage_menu.sh
5. Mount and Share Manager → share_menu.sh
6. Proxmox VE Helper Scripts → menu_Helper_Scripts.sh
7. Network Management → network_menu.sh
8. Utilities and Tools → utilities_menu.sh
h. Help and Info Commands → help_info_menu.sh
s. Settings → config_menu.sh
0. Exit
```
**Patrón de Ejecución**:
```bash
exec bash <(curl -s "$REPO_URL/scripts/menus/submenu.sh")
```
Todos los menús descargan y ejecutan scripts desde GitHub en tiempo real.
---
## **5. Scripts Locales vs Remotos**
### **Estado Actual**
- **Scripts locales**: Están presentes en el repositorio (122 archivos)
- **Ejecución**: Siempre desde GitHub mediante `curl`
- **Ventaja actual**: Actualizaciones automáticas sin reinstalar
- **Desventaja**: Requiere conexión a internet constante
### **Scripts Principales Disponibles Localmente**
**Gestión de VMs** (`scripts/vm/`):
- `create_vm.sh` - Crear VMs
- `synology.sh` (39KB) - Instalación Synology DSM
- `zimaos.sh` (40KB) - Instalación ZimaOS
- `uupdump_creator.sh` - Creador de ISOs Windows
- `select_windows_iso.sh`, `select_linux_iso.sh`, `select_nas_iso.sh`
**Gestión de LXC** (`scripts/lxc/`):
- `lxc-manual-guide.sh` - Guía manual
- `lxc-privileged-to-unprivileged.sh`
- `lxc-unprivileged-to-privileged.sh`
**Almacenamiento** (`scripts/storage/`):
- `disk-passthrough.sh` - Passthrough disco a VM
- `disk-passthrough_ct.sh` - Passthrough disco a LXC (22KB)
- `import-disk-image.sh` - Importar imágenes
- `format-disk.sh`, `mount-disk-on-host.sh`
**Compartir Recursos** (`scripts/share/`):
- `lxc-mount-manager_minimal.sh` (35KB) - Gestión mount points
- `nfs_host.sh` (35KB) - Servidor NFS en host
- `samba_host.sh` (52KB) - Servidor Samba en host
- `nfs_client.sh`, `samba_client.sh` - Clientes en LXC
- `local-shared-manager.sh` - Directorios compartidos locales
**Post-Instalación** (`scripts/post_install/`):
- `auto_post_install.sh` (29KB) - Automatizado sin interacción
- `customizable_post_install.sh` (148KB) - Personalizable
- `uninstall-tools.sh` (34KB) - Desinstalador
**Utilidades** (`scripts/utilities/`):
- `upgrade_pve8_to_pve9.sh` (35KB) - Upgrade PVE 8→9
- `system_utils.sh` (20KB) - Instalador de utilidades
- `proxmox_update.sh` - Actualización de Proxmox
**Red** (`scripts/menus/network_menu.sh`):
- 43KB de funcionalidades de red
- Optimizaciones para LXC+NFS
**Global** (`scripts/global/`):
- `update-pve.sh`, `update-pve8.sh`, `update-pve9_2.sh`
- `remove-banner-pve8.sh`, `remove-banner-pve9.sh`
- `share-common.func` (30KB) - Funciones compartidas
---
## **6. Sistema de Utilidades: `utils.sh`**
### **Funciones Principales**
**Interfaz Visual** (línea 50-71):
- Definición de colores ANSI
- Códigos de estilo para terminal
- Spinner animado (línea 75-88)
**Mensajes Estandarizados**:
- `msg_info()` - Info con spinner
- `msg_ok()` - Éxito (checkmark verde)
- `msg_error()` - Error (rojo)
- `msg_warn()` - Advertencia (amarillo)
- `msg_title()` - Títulos
- `type_text()` - Efecto máquina de escribir
**Sistema de Traducción** (línea 232-305):
```bash
translate() {
# Si idioma es "en" → retorna texto original
# Busca en caché local (cache.json)
# Si no existe → llama a googletrans vía Python
# Guarda en caché para futuras traducciones
# Limpia prefijos de contexto
}
```
**Contexto de Traducción** (línea 48):
```bash
TRANSLATION_CONTEXT="Context: Technical message for Proxmox and IT. Translate:"
```
**Logo ASCII** (línea 314-400):
- Dos versiones: terminal noVNC y SSH
- Detección automática del entorno
- Diseño en ASCII art con colores
---
## **7. ProxMenux Monitor**
### **Componente Web (AppImage)**
**Tecnología**:
- **Frontend**: Next.js 14, React 18, TypeScript
- **UI**: Radix UI + shadcn/ui + Tailwind CSS
- **Gráficos**: Recharts
- **Backend**: Flask (Python) para recolección de datos del sistema
- **Empaquetado**: AppImage (10.3 MB)
**Características**:
- Dashboard en tiempo real
- Monitoreo de CPU, RAM, temperatura
- Estado de VMs y LXC containers
- Gestión de almacenamiento visual
- Estadísticas de red
- Logs del sistema
- Tema oscuro/claro
- Responsive design
- Puerto: 8008
**Servicio Systemd**:
```ini
[Unit]
Description=ProxMenux Monitor - Web Dashboard
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/usr/local/share/proxmenux
ExecStart=/usr/local/share/proxmenux/ProxMenux-Monitor.AppImage
Restart=on-failure
RestartSec=10
Environment="PORT=8008"
```
**Estado**: Se instala automáticamente en ambas versiones (normal y traducción)
---
## **8. Sistema de Configuración**
### **Archivos de Configuración**
**`/usr/local/share/proxmenux/config.json`**:
- Estado de instalación de componentes
- Idioma seleccionado
- Timestamps de instalación
- Estados: `installed`, `already_installed`, `failed`
**Componentes Rastreados** (línea 201):
```json
{
"dialog": {"status": "installed", "timestamp": "..."},
"curl": {"status": "already_installed", "timestamp": "..."},
"jq": {"status": "installed", "timestamp": "..."},
"python3": {"status": "installed", "timestamp": "..."},
"virtual_environment": {"status": "created", "timestamp": "..."},
"googletrans": {"status": "installed", "timestamp": "..."},
"proxmenux_monitor": {"status": "installed", "timestamp": "..."},
"language": "es"
}
```
**`/usr/local/share/proxmenux/cache.json`**:
- Traducciones cacheadas (100 KB)
- Formato: `{"texto_original": {"es": "traducción", "fr": "traduction"}}`
- Reduce llamadas a Google Translate API
**`/usr/local/share/proxmenux/installed_tools.json`**:
- Registro de herramientas post-instalación
- Usado por el desinstalador
---
## **9. Funcionalidades Destacadas**
### **Post-Instalación Automatizada**
- **Optimizaciones de repositorios**: Limpia duplicados, configura repos gratuitos
- **Eliminación de banner de suscripción**: Con respaldo y reversión
- **Optimización de memoria y kernel**: Ajustes según RAM disponible
- **Log2RAM**: Instalación automática en SSD/NVMe
- **Network tuning**: Optimización de stack de red
- **Límites del sistema**: Aumenta límites de archivos y procesos
- **Configuración de journald**: Ajustada para Log2RAM
- **Entropía**: Mejora generación de números aleatorios
- **Aliases bash**: Personalización del entorno
### **Gestión de Compartición de Recursos**
**Enfoque**: Mount Points LXC (Host ↔ Container)
- Detección automática de tipo de filesystem
- Mapeo UID/GID para contenedores unprivileged
- Visualización de mount points existentes
- Eliminación segura con verificación
**Configuraciones disponibles**:
- NFS: Host, Client LXC, Server LXC
- Samba: Host, Client LXC, Server LXC
- Directorios locales compartidos
### **Hardware Especializado**
- **Coral TPU**: Instalación de drivers compatible con PVE 8 y 9
- **GPUs**: Passthrough y configuración para VMs y LXC
- **iGPU**: Configuración para contenedores LXC
### **Upgrade PVE 8 → 9**
- Script de 35 KB con verificaciones exhaustivas
- Guía manual interactiva
- Checker de compatibilidad
---
## **10. Arquitectura de Ejecución**
### **Patrón de Descarga Dinámica**
**Todos los scripts siguen este patrón**:
```bash
exec bash <(curl -s "$REPO_URL/scripts/path/to/script.sh")
```
**Ventajas**:
- ✅ Usuarios siempre tienen la última versión
- ✅ No requiere reinstalación para actualizaciones
- ✅ Hotfixes inmediatos
- ✅ Control centralizado de versiones
**Consideraciones**:
- ⚠️ Requiere internet en cada ejecución
- ⚠️ Dependencia de disponibilidad de GitHub
- ⚠️ No funciona offline
- ⚠️ Los scripts locales del repo no se usan directamente
### **Sistema de Versionado**
- `version.txt` en repo: versión remota
- `/usr/local/share/proxmenux/version.txt`: versión local instalada
- Check en cada ejecución del comando `menu`
- Prompt para actualizar si hay nueva versión
---
## **11. Flujo de Navegación**
```
Comando: menu
Verifica actualizaciones
Carga utils.sh y traducciones
Descarga main_menu.sh desde GitHub
Usuario selecciona opción
Descarga submenu correspondiente desde GitHub
Usuario selecciona acción
Descarga y ejecuta script específico desde GitHub
Retorna al menú anterior
```
**Ejemplo de navegación**:
```
menu → main_menu.sh
→ opción 5: share_menu.sh
→ opción 4: lxc-mount-manager_minimal.sh (35KB)
→ Ejecuta acciones
→ Retorna a share_menu.sh
→ opción 0: Retorna a main_menu.sh
→ opción 0: Exit
```
---
## **12. Integración con Comunidad**
### **Scripts de la Comunidad Integrados**
**Proxmox VE Helper-Scripts**:
- Post-install script oficial
- Ejecutado desde: `https://github.com/community-scripts/ProxmoxVE`
**Xshok-proxmox** (fork):
- Post-install alternativo
- Descarga desde fork de MacRimi
**Elementos compartidos**:
- Funciones de `utils.sh` basadas en Helper-Scripts
- Misma filosofía de mensajes estandarizados
- Licencia MIT compatible
---
## **13. Sistema de Desinstalación**
### **Función: `uninstall_proxmenux()`** (línea 109-161)
**Proceso**:
1. Confirmación interactiva (whiptail)
2. Desinstala googletrans y entorno virtual Python
3. Selector de dependencias a eliminar (python3, python3-venv, pip)
4. Elimina `/usr/local/bin/menu`
5. Elimina `/usr/local/share/proxmenux/`
6. Restaura `.bashrc` desde backup
7. Restaura `/etc/motd` desde backup
**Tool-specific uninstaller**: `scripts/post_install/uninstall-tools.sh`
- Lee `installed_tools.json`
- Permite desinstalar herramientas individualmente
- Restaura configuraciones originales
---
## **14. Estructura de Archivos JSON**
### **`json/cache.json`** (100 KB)
Traducciones precargadas para acelerar el sistema
### **`json/helpers_cache.json`** (273 KB)
Caché extendido, probablemente para Helper Scripts
### **`lang/cache.json`** (5.5 KB)
Caché de idiomas específico
### **`lang/en.lang`** y **`lang/es.lang`**
Archivos de idioma estáticos (4-5 KB cada uno)
---
## **15. Resumen de Componentes**
| Componente | Ubicación | Función |
|------------|-----------|---------|
| **Instalador** | `install_proxmenux.sh` | Instalación inicial y actualizaciones |
| **Comando principal** | `/usr/local/bin/menu` | Punto de entrada del usuario |
| **Utilidades** | `/usr/local/share/proxmenux/utils.sh` | Funciones compartidas |
| **Configuración** | `/usr/local/share/proxmenux/config.json` | Estado del sistema |
| **Caché traducciones** | `/usr/local/share/proxmenux/cache.json` | Traducciones cacheadas |
| **Entorno Python** | `/opt/googletrans-env/` | Traducción (solo versión translation) |
| **Monitor** | `/usr/local/share/proxmenux/ProxMenux-Monitor.AppImage` | Dashboard web |
| **Servicio Monitor** | `/etc/systemd/system/proxmenux-monitor.service` | Servicio systemd |
| **Scripts** | GitHub (descarga dinámica) | Todos los scripts funcionales |
---
## **Conclusión**
ProxMenuxOffline es un **sistema modular de gestión de Proxmox VE** que utiliza una arquitectura híbrida:
- **Núcleo local**: Comando `menu`, utilidades, sistema de configuración
- **Scripts remotos**: Toda la funcionalidad se descarga dinámicamente desde GitHub
- **Dashboard web**: AppImage independiente con Next.js + Flask
- **Sistema de traducción**: Opcional, basado en Python + googletrans + caché
El proyecto tiene **122 scripts bash** en el repositorio local que **podrían ejecutarse localmente**, pero actualmente **todos se descargan desde GitHub en tiempo de ejecución**. Esta arquitectura prioriza mantener a los usuarios actualizados sobre la ejecución offline.
+699
View File
@@ -0,0 +1,699 @@
# Scripts a Modificar para Ejecución 100% Local
**Fecha**: 2025-11-01
**Objetivo**: Eliminar dependencias de GitHub y permitir ejecución completamente local
**Repositorio**: ProxMenuxDotDeb
---
## Resumen Ejecutivo
Para que ProxMenux funcione 100% localmente sin depender de GitHub, se deben modificar **47 archivos** en total:
- **2 archivos principales** (instalador y comando menu)
- **13 scripts de menús** (sistema de navegación)
- **32 scripts funcionales** (operaciones específicas)
**Cambios principales**:
1. Cambiar `REPO_URL` de GitHub a rutas locales del sistema
2. Reemplazar descargas `curl` por ejecución de scripts locales
3. Copiar todos los scripts a `/usr/local/share/proxmenux/scripts/` durante instalación
---
## 1. Archivos Principales (CRÍTICOS) ⚠️
### 1.1. `install_proxmenux.sh` (Raíz del repositorio)
**Líneas a modificar**:
- **Línea 37**: `REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"`
- **Línea 38**: `UTILS_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main/scripts/utils.sh"`
- **Línea 54-57**: Carga de `utils.sh` con curl
- **Línea 459-476**: Descarga de archivos con wget (versión normal)
- **Línea 583-603**: Descarga de archivos con wget (versión traducción)
**Cambios necesarios**:
```bash
# Cambiar URLs a rutas locales
REPO_URL="/usr/local/share/proxmenux"
UTILS_URL="./scripts/utils.sh"
# Reemplazar wget por cp
# En lugar de descargar, copiar archivos locales del repositorio
```
**Impacto**: 🔴 CRÍTICO - Sin esto, la instalación falla completamente
---
### 1.2. `menu` (Raíz del repositorio)
**Líneas a modificar**:
- **Línea 34**: `REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"`
- **Línea 52**: Verificación de actualizaciones (curl remoto)
- **Línea 72**: Descarga de instalador actualizado
- **Línea 85**: `exec bash <(curl -fsSL "$REPO_URL/scripts/menus/main_menu.sh")`
**Cambios necesarios**:
```bash
# Cambiar a ruta local
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
# Ejecutar localmente
exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"
```
**Impacto**: 🔴 CRÍTICO - Es el punto de entrada del usuario
---
## 2. Scripts de Menús (13 archivos)
### 2.1. `scripts/menus/main_menu.sh` ⭐
**Modificaciones**:
- **Línea 14**: `REPO_URL`
- **Línea 57**: curl para reinstalación PVE9
- **Líneas 125-135**: Todas las opciones del menú (12 líneas)
**Comandos a reemplazar**:
```bash
# Todas estas líneas:
exec bash <(curl -s "$REPO_URL/scripts/menus/menu_post_install.sh")
exec bash <(curl -s "$REPO_URL/scripts/menus/hw_grafics_menu.sh")
exec bash <(curl -s "$REPO_URL/scripts/menus/create_vm_menu.sh")
exec bash <(curl -s "$REPO_URL/scripts/menus/storage_menu.sh")
exec bash <(curl -s "$REPO_URL/scripts/menus/share_menu.sh")
exec bash <(curl -s "$REPO_URL/scripts/menus/menu_Helper_Scripts.sh")
exec bash <(curl -s "$REPO_URL/scripts/menus/network_menu.sh")
exec bash <(curl -s "$REPO_URL/scripts/menus/utilities_menu.sh")
bash <(curl -s "$REPO_URL/scripts/help_info_menu.sh")
exec bash <(curl -s "$REPO_URL/scripts/menus/config_menu.sh")
```
---
### 2.2. `scripts/menus/menu_post_install.sh`
**Modificaciones**:
- **Línea 12**: `REPO_URL`
- **Línea 73**: `bash <(curl -s $REPO_URL/scripts/post_install/auto_post_install.sh)`
- **Línea 171**: `exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")`
**Nota**: Mantener URLs remotas para scripts de comunidad externa (líneas 90-91)
---
### 2.3. `scripts/menus/config_menu.sh`
**Modificaciones**:
- **Línea 13**: `REPO_URL`
- No tiene llamadas curl ✅
---
### 2.4. `scripts/menus/create_vm_menu.sh`
**Modificaciones**:
- **Línea 13**: `REPO_URL`
- Múltiples `exec bash <(curl -s ...)` en opciones del menú
---
### 2.5. `scripts/menus/hw_grafics_menu.sh`
**Modificaciones**:
- **Línea 13**: `REPO_URL`
- **Líneas 38, 44, 50, 55, 56**: Llamadas curl
**Comandos a reemplazar**:
```bash
bash <(curl -s "$REPO_URL/scripts/configure_igpu_lxc.sh")
bash <(curl -s "$REPO_URL/scripts/install_coral_lxc.sh")
bash <(curl -s "$REPO_URL/scripts/gpu_tpu/install_coral_pve9.sh")
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")
```
---
### 2.6. `scripts/menus/lxc_menu.sh`
**Modificaciones**:
- **Línea 13**: `REPO_URL`
- Todos los `exec bash <(curl ...)`
---
### 2.7. `scripts/menus/menu_Helper_Scripts.sh`
**Modificaciones**:
- **Línea 13**: `REPO_URL`
- **Línea 296**: `exec bash <(curl -s ...)`
**Nota**: Mantener URLs de Helper-Scripts externos (comunidad)
---
### 2.8. `scripts/menus/network_menu.sh`
**Modificaciones**:
- **Línea 13**: `REPO_URL`
- **Línea 1085**: `exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")`
---
### 2.9. `scripts/menus/share_menu.sh`
**Modificaciones**:
- **Línea 13**: `REPO_URL`
- **Líneas 46, 55-82, 85**: 11 llamadas curl
**Comandos a reemplazar**:
```bash
bash <(curl -s "$REPO_URL/scripts/share/nfs_host.sh")
bash <(curl -s "$REPO_URL/scripts/share/samba_host.sh")
bash <(curl -s "$REPO_URL/scripts/share/local-shared-manager.sh")
bash <(curl -s "$REPO_URL/scripts/share/lxc-mount-manager_minimal.sh")
bash <(curl -s "$REPO_URL/scripts/share/nfs_client.sh")
bash <(curl -s "$REPO_URL/scripts/share/samba_client.sh")
bash <(curl -s "$REPO_URL/scripts/share/nfs_lxc_server.sh")
bash <(curl -s "$REPO_URL/scripts/share/samba_lxc_server.sh")
bash <(curl -s "$REPO_URL/scripts/share/commands_share.sh")
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh") # 2 veces
```
---
### 2.10. `scripts/menus/storage_menu.sh`
**Modificaciones**:
- **Línea 15**: `REPO_URL`
- **Líneas 39, 42, 45, 48, 51**: 5 llamadas curl
**Comandos a reemplazar**:
```bash
bash <(curl -s "$REPO_URL/scripts/storage/disk-passthrough.sh")
bash <(curl -s "$REPO_URL/scripts/storage/disk-passthrough_ct.sh")
bash <(curl -s "$REPO_URL/scripts/storage/import-disk-image.sh")
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh") # 2 veces
```
---
### 2.11. `scripts/menus/utilities_menu.sh`
**Modificaciones**:
- **Línea 15**: `REPO_URL`
- **Líneas 39, 45, 67, 74, 79, 80**: 6 llamadas curl
**Comandos a reemplazar**:
```bash
bash <(curl -s "$REPO_URL/scripts/utilities/uup_dump_iso_creator.sh")
bash <(curl -s "$REPO_URL/scripts/utilities/system_utils.sh")
bash <(curl -s "$REPO_URL/scripts/utilities/proxmox_update.sh")
bash <(curl -s "$REPO_URL/scripts/utilities/upgrade_pve8_to_pve9.sh")
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh") # 2 veces
```
---
### 2.12. `scripts/menus/main_menu_.sh`
**Modificaciones**: Igual que `main_menu.sh` (archivo alternativo/backup)
---
### 2.13. `scripts/menus/sm.sh`
**Modificaciones**: Igual que `share_menu.sh` (archivo alternativo)
---
## 3. Scripts Post-Instalación (3 archivos)
### 3.1. `scripts/post_install/auto_post_install.sh`
**Modificaciones**:
- **Línea 39**: `REPO_URL`
- **Línea 110**: `bash <(curl -fsSL "$REPO_URL/scripts/global/update-pve9_2.sh")`
- **Línea 113**: `bash <(curl -fsSL "$REPO_URL/scripts/global/update-pve8.sh")`
- **Línea 150**: `bash <(curl -fsSL "$REPO_URL/scripts/global/remove-banner-pve-v3.sh")`
- **Línea 157**: `bash <(curl -fsSL "$REPO_URL/scripts/global/remove-banner-pve8.sh")`
---
### 3.2. `scripts/post_install/customizable_post_install.sh`
**Modificaciones**:
- **Línea 39**: `REPO_URL`
- **Línea 197**: `bash <(curl -fsSL "$REPO_URL/scripts/global/update-pve9_2.sh")`
- **Línea 200**: `bash <(curl -fsSL "$REPO_URL/scripts/global/update-pve8.sh")`
- **Línea 2905**: `bash <(curl -fsSL "$REPO_URL/scripts/global/remove-banner-pve-v3.sh")`
- **Línea 2908**: `bash <(curl -fsSL "$REPO_URL/scripts/global/remove-banner-pve8.sh")`
---
### 3.3. `scripts/post_install/uninstall-tools.sh`
**Modificaciones**: Solo lectura de configs locales ✅
---
## 4. Scripts de VMs (8 archivos)
### 4.1. `scripts/vm/create_vm.sh`
**Modificaciones**:
- **Línea 29**: `REPO_URL`
- **Líneas 30-32**: `VM_REPO`, `ISO_REPO`, `MENU_REPO`
---
### 4.2. `scripts/vm/select_linux_iso.sh`
**Modificaciones**:
- **Línea 28**: `REPO_URL`
- **Línea 222**: `exec bash <(curl -s "$REPO_URL/scripts/vm/create_vm.sh")`
---
### 4.3. `scripts/vm/select_windows_iso.sh`
**Modificaciones**:
- **Línea 27**: `REPO_URL`
- **Línea 28**: `UUP_REPO`
---
### 4.4. `scripts/vm/select_nas_iso.sh`
**Modificaciones**:
- **Línea 31**: `REPO_URL`
- **Línea 65**: `bash <(curl -s "$REPO_URL/scripts/vm/synology.sh")`
- **Línea 106**: `bash <(curl -s "$REPO_URL/scripts/vm/zimaos.sh")`
---
### 4.5. `scripts/vm/synology.sh`
**Modificaciones**:
- **Línea 32**: `REPO_URL`
---
### 4.6. `scripts/vm/synology_.sh`
**Modificaciones**:
- **Línea 32**: `REPO_URL`
---
### 4.7. `scripts/vm/zimaos.sh`
**Modificaciones**:
- Verificar si tiene `REPO_URL`
---
### 4.8. `scripts/vm/vm_creator.sh`
**Modificaciones**:
- **Línea 497**: `bash <(curl -fsSL "$REPO_URL/scripts/menus/create_vm_menu.sh")`
---
## 5. Scripts de LXC (4 archivos)
### 5.1. `scripts/lxc/lxc-manual-guide.sh`
**Modificaciones**:
- **Línea 14**: `REPO_URL`
---
### 5.2. `scripts/lxc/lxc-privileged-to-unprivileged.sh`
**Modificaciones**:
- **Línea 18**: `REPO_URL`
---
### 5.3. `scripts/lxc/lxc-unprivileged-to-privileged.sh`
**Modificaciones**:
- **Línea 19**: `REPO_URL`
---
### 5.4. `scripts/lxc/lxc-mount-manager_minimal.sh`
**Modificaciones**:
- Verificar si tiene `REPO_URL`
---
## 6. Scripts de Compartir Recursos (9 archivos)
### 6.1. `scripts/share/nfs_host.sh`
**Modificaciones**:
- **Línea 16**: `REPO_URL`
---
### 6.2. `scripts/share/nfs_client.sh`
**Modificaciones**:
- **Línea 16**: `REPO_URL`
---
### 6.3. `scripts/share/nfs_lxc_server.sh`
**Modificaciones**:
- **Línea 16**: `REPO_URL`
---
### 6.4. `scripts/share/samba_host.sh`
**Modificaciones**:
- **Línea 16**: `REPO_URL`
---
### 6.5. `scripts/share/samba_client.sh`
**Modificaciones**:
- **Línea 18**: `REPO_URL`
---
### 6.6. `scripts/share/samba_lxc_server.sh`
**Modificaciones**:
- **Línea 16**: `REPO_URL`
---
### 6.7. `scripts/share/local-shared-manager.sh`
**Modificaciones**:
- **Línea 13**: `REPO_URL`
---
### 6.8. `scripts/share/lxc-mount-manager_minimal.sh`
**Modificaciones**:
- Verificar `REPO_URL`
---
### 6.9. `scripts/share/commands_share.sh`
**Modificaciones**:
- **Línea 14**: `REPO_URL`
---
## 7. Scripts de Almacenamiento (3 archivos)
### 7.1. `scripts/storage/disk-passthrough.sh`
**Modificaciones**:
- Verificar `REPO_URL`
---
### 7.2. `scripts/storage/disk-passthrough_ct.sh`
**Modificaciones**:
- Verificar `REPO_URL`
---
### 7.3. `scripts/storage/import-disk-image.sh`
**Modificaciones**:
- **Línea 30**: `REPO_URL`
---
## 8. Scripts de Utilidades (4 archivos)
### 8.1. `scripts/utilities/upgrade_pve8_to_pve9.sh`
**Modificaciones**:
- Verificar `REPO_URL`
---
### 8.2. `scripts/utilities/system_utils.sh`
**Modificaciones**:
- Verificar `REPO_URL`
---
### 8.3. `scripts/utilities/proxmox_update.sh`
**Modificaciones**:
- Verificar `REPO_URL`
---
### 8.4. `scripts/utilities/uup_dump_iso_creator.sh`
**Modificaciones**:
- Verificar `REPO_URL`
---
## 9. Scripts Globales (3 archivos)
### 9.1. `scripts/global/update-pve.sh`
**Modificaciones**:
- **Línea 32**: `source <(curl -s "$REPO_URL/scripts/global/common-functions.sh")`
**Cambiar a**:
```bash
source "$LOCAL_SCRIPTS/global/common-functions.sh"
```
---
### 9.2. `scripts/global/update-pve8.sh`
**Modificaciones**:
- **Línea 32**: `source <(curl -s "$REPO_URL/scripts/global/common-functions.sh")`
---
### 9.3. `scripts/global/update-pve9_2.sh`
**Modificaciones**:
- **Línea 32**: `source <(curl -s "$REPO_URL/scripts/global/common-functions.sh")`
---
## 10. Scripts de Hardware (2 archivos)
### 10.1. `scripts/configure_igpu_lxc.sh`
**Modificaciones**:
- **Línea 19**: `REPO_URL`
---
### 10.2. `scripts/install_coral_lxc.sh`
**Modificaciones**:
- **Línea 25**: `REPO_URL`
---
## 11. Scripts de Red (2 archivos)
### 11.1. `scripts/repair_network.sh`
**Modificaciones**:
- **Línea 204**: `exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")`
- **Línea 205**: `exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")`
---
### 11.2. `scripts/telegram-notifier.sh`
**Modificaciones**:
- **Línea 5**: `REPO_URL`
---
## 12. Scripts Duplicados/Alternos (en `scripts/auto_post_install.sh`)
**Modificaciones**: Igual que `scripts/post_install/auto_post_install.sh`
---
## Tabla Resumen
| Categoría | Archivos | Modificaciones Principales |
|-----------|----------|---------------------------|
| **Principales** | 2 | REPO_URL + curl → rutas locales |
| **Menús** | 13 | REPO_URL + exec bash curl |
| **Post-Install** | 3 | bash curl a scripts global |
| **VMs** | 8 | REPO_URL + llamadas remotas |
| **LXC** | 4 | REPO_URL |
| **Share** | 9 | REPO_URL |
| **Storage** | 3 | REPO_URL |
| **Utilities** | 4 | REPO_URL |
| **Global** | 3 | source curl |
| **Hardware** | 2 | REPO_URL |
| **Red** | 2 | exec bash curl |
| **TOTAL** | **47** | **~150-200 líneas** |
---
## Plan de Implementación Recomendado
### Paso 1: Preparación
```bash
# Crear backup
cp -r . ../ProxMenuxDotDeb_backup
# --------------------------------------------------------------------
# Documentar información relevante del proyecto en directorio "docs"
# --------------------------------------------------------------------
```
### Paso 2: Modificación Automática Global
```bash
# Script de conversión masiva
find . -name "*.sh" -o -name "menu" | xargs sed -i \
's|REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"|LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"|g'
# Cambiar referencias
find . -name "*.sh" -o -name "menu" | xargs sed -i \
's|\$REPO_URL/scripts|\$LOCAL_SCRIPTS|g'
# Cambiar bash curl
find . -name "*.sh" -o -name "menu" | xargs sed -i -E \
's|bash <\(curl -[sfSL]+ "\$REPO_URL/([^"]+)"|bash "\$LOCAL_SCRIPTS/\1"|g'
# Cambiar exec bash curl
find . -name "*.sh" -o -name "menu" | xargs sed -i -E \
's|exec bash <\(curl -[sfSL]+ "\$REPO_URL/([^"]+)"|exec bash "\$LOCAL_SCRIPTS/\1"|g'
# Cambiar source curl
find . -name "*.sh" | xargs sed -i -E \
's|source <\(curl -[sfSL]+ "\$REPO_URL/([^"]+)"|source "\$LOCAL_SCRIPTS/\1"|g'
```
### Paso 3: Modificar install_proxmenux.sh manualmente
Cambiar secciones de descarga wget por copias locales:
```bash
# En lugar de:
wget -qO "$dest" "$url"
# Usar:
cp "./scripts/utils.sh" "$UTILS_FILE"
cp "./menu" "$INSTALL_DIR/$MENU_SCRIPT"
cp "./version.txt" "$LOCAL_VERSION_FILE"
```
Agregar copia de todos los scripts:
```bash
msg_info "Copying local scripts..."
mkdir -p "$BASE_DIR/scripts"
cp -r "./scripts/"* "$BASE_DIR/scripts/"
chmod -R +x "$BASE_DIR/scripts/"
```
### Paso 4: Modificar comando menu
Comentar o modificar verificación de actualizaciones remotas.
### Paso 5: Validación
```bash
# Verificar que no queden referencias remotas
grep -r "githubusercontent.com" . --include="*.sh" --include="menu"
# Verificar llamadas curl
grep -r "curl.*REPO_URL" . --include="*.sh" --include="menu"
# Contar archivos modificados
grep -r "LOCAL_SCRIPTS=" . --include="*.sh" --include="menu" | wc -l
```
---
## Estructura Post-Modificación
```
/usr/local/share/proxmenux/
├── utils.sh
├── config.json
├── cache.json
├── version.txt
├── ProxMenux-Monitor.AppImage
└── scripts/ # ⭐ NUEVO
├── menus/
│ ├── main_menu.sh
│ ├── menu_post_install.sh
│ └── ...
├── post_install/
├── vm/
├── lxc/
├── storage/
├── share/
├── utilities/
├── global/
└── gpu_tpu/
/usr/local/bin/
└── menu
```
---
## Consideraciones Especiales
### Scripts Externos de la Comunidad
Mantener URLs remotas para:
- Proxmox VE Helper-Scripts (community-scripts)
- xshok-proxmox scripts
### ProxMenux Monitor
El AppImage se mantiene descargable desde GitHub durante la instalación inicial (10 MB).
### Sistema de Actualizaciones
Opciones:
1. Deshabilitar completamente
2. Mostrar mensaje para ejecutar `install_proxmenux.sh` manualmente
3. Sistema híbrido (check opcional remoto)
---
## Checklist de Validación
- [ok] Backup completo del repositorio
- [ok] Conversión automática ejecutada
- [ok] `install_proxmenux.sh` modificado
- [ok] `menu` modificado
- [ip] Scripts de menús verificados
- [ ] Sin referencias a githubusercontent.com
- [ ] Sin llamadas curl a REPO_URL
- [ ] Instalación local funcional
- [ ] Navegación por todos los menús OK
- [ ] Ejecución offline confirmada
---
**Total de archivos a modificar**: 47
**Líneas estimadas**: ~150-200
**Tiempo estimado**: 2-4 horas
**Riesgo**: Medio (requiere testing)
**Beneficio**: Sistema completamente offline
Binary file not shown.

After

Width:  |  Height:  |  Size: 323 KiB

Regular → Executable
+476 -150
View File
@@ -1,41 +1,46 @@
#!/bin/bash
# ==========================================================
# ProxMenux - A menu-driven script for Proxmox VE management
# ProxMenux - A menu-driven toolkit for Proxmox VE management
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# Version : 1.3
# Last Updated: 04/07/2025
# Author : MacRimi
# Contributors : cod378
# Subproject : ProxMenux Monitor (System Health & Web Dashboard)
# Copyright : (c) 2024-2025 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.4
# Last Updated : 12/11/2025
# ==========================================================
# Description:
# This script installs and configures ProxMenux, a menu-driven
# tool for managing Proxmox VE.
# toolkit for managing and optimizing Proxmox VE servers.
#
# - Ensures the script is run with root privileges.
# - Displays an installation confirmation prompt.
# - Installs required dependencies:
# - whiptail (for interactive terminal menus)
# - curl (for downloading remote files)
# - jq (for handling JSON data)
# - Python 3 and virtual environment (for translations)
# - Configures the Python virtual environment and installs googletrans.
# - Creates necessary directories for storing ProxMenux data.
# - Downloads required files from GitHub, including:
# - Cache file (`cache.json`) for translation caching.
# - Utility script (`utils.sh`) for core functions.
# - Main script (`menu.sh`) to launch ProxMenux.
# - Sets correct permissions for execution.
# - Displays final instructions on how to start ProxMenux.
# whiptail (interactive terminal menus)
# curl (downloads and connectivity checks)
# • jq (JSON parsing)
# Python 3 + venv (for translation support)
# - Creates the ProxMenux base directories and configuration files:
# • $BASE_DIR/config.json
# • $BASE_DIR/cache.json
# - Copies local project files into the target paths (offline mode by default):
# • scripts/* → $BASE_DIR/scripts/
# • utils.sh → $BASE_DIR/scripts/utils.sh
# • menu → $INSTALL_DIR/menu (main launcher)
# • install_proxmenux.sh → $BASE_DIR/install_proxmenux.sh
# - Sets correct permissions for all executables.
# - Displays the final instruction on how to start ProxMenux ("menu").
#
# This installer ensures a smooth setup process and prepares
# the system for running ProxMenux efficiently.
# Notes:
# - This installer supports both offline and online setups.
# - ProxMenux Monitor can be installed later as an optional module
# to provide real-time system monitoring and a web dashboard.
# ==========================================================
# Configuration ============================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
UTILS_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main/scripts/utils.sh"
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
INSTALL_DIR="/usr/local/bin"
BASE_DIR="/usr/local/share/proxmenux"
CONFIG_FILE="$BASE_DIR/config.json"
@@ -45,17 +50,222 @@ LOCAL_VERSION_FILE="$BASE_DIR/version.txt"
MENU_SCRIPT="menu"
VENV_PATH="/opt/googletrans-env"
MONITOR_APPIMAGE_URL="https://github.com/MacRimi/ProxMenux/raw/refs/heads/main/AppImage/ProxMenux-1.0.0.AppImage"
MONITOR_SHA256_URL="https://github.com/MacRimi/ProxMenux/raw/refs/heads/main/AppImage/ProxMenux-Monitor.AppImage.sha256"
MONITOR_INSTALL_PATH="$BASE_DIR/ProxMenux-Monitor.AppImage"
MONITOR_INSTALL_DIR="$BASE_DIR"
MONITOR_SERVICE_FILE="/etc/systemd/system/proxmenux-monitor.service"
MONITOR_PORT=8008
if ! source <(curl -sSf "$UTILS_URL"); then
echo "Error: Could not load utils.sh from $UTILS_URL"
exit 1
# Offline installer envs
REPO_URL="https://github.com/MacRimi/ProxMenux.git"
TEMP_DIR="/tmp/proxmenux-install-$$"
# Load utility functions
NEON_PURPLE_BLUE="\033[38;5;99m"
WHITE="\033[38;5;15m"
RESET="\033[0m"
DARK_GRAY="\033[38;5;244m"
ORANGE="\033[38;5;208m"
YW="\033[33m"
YWB="\033[1;33m"
GN="\033[1;92m"
RD="\033[01;31m"
CL="\033[m"
BL="\033[36m"
DGN="\e[32m"
BGN="\e[1;32m"
DEF="\e[1;36m"
CUS="\e[38;5;214m"
BOLD="\033[1m"
BFR="\\r\\033[K"
HOLD="-"
BOR=" | "
CM="${GN}${CL}"
TAB=" "
# Create and display spinner
spinner() {
local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
local spin_i=0
local interval=0.1
printf "\e[?25l"
local color="${YW}"
while true; do
printf "\r ${color}%s${CL}" "${frames[spin_i]}"
spin_i=$(( (spin_i + 1) % ${#frames[@]} ))
sleep "$interval"
done
}
# Function to simulate typing effect
type_text() {
local text="$1"
local delay=0.05
for ((i=0; i<${#text}; i++)); do
echo -n "${text:$i:1}"
sleep $delay
done
echo
}
# Display info message with spinner
msg_info() {
local msg="$1"
echo -ne "${TAB}${YW}${HOLD}${msg}"
spinner &
SPINNER_PID=$!
}
# Display info2 message
msg_info2() {
local msg="$1"
echo -e "${TAB}${BOLD}${YW}${HOLD}${msg}${CL}"
}
# Display title script
msg_title() {
local msg="$1"
echo -e "\n"
echo -e "${TAB}${BOLD}${HOLD}${BOR}${msg}${BOR}${HOLD}${CL}"
echo -e "\n"
}
# Display warning or highlighted information message
msg_warn() {
if [ -n "$SPINNER_PID" ] && ps -p $SPINNER_PID > /dev/null; then
kill $SPINNER_PID > /dev/null
fi
printf "\e[?25h"
local msg="$1"
echo -e "${BFR}${TAB}${CL} ${YWB}${msg}${CL}"
}
# Display success message
msg_ok() {
if [ -n "$SPINNER_PID" ] && ps -p $SPINNER_PID > /dev/null; then
kill $SPINNER_PID > /dev/null
fi
printf "\e[?25h"
local msg="$1"
echo -e "${BFR}${TAB}${CM}${GN}${msg}${CL}"
}
# Display error message
msg_error() {
if [ -n "$SPINNER_PID" ] && ps -p $SPINNER_PID > /dev/null; then
kill $SPINNER_PID > /dev/null
fi
printf "\e[?25h"
local msg="$1"
echo -e "${BFR}${TAB}${RD}[ERROR] ${msg}${CL}"
}
show_proxmenux_logo() {
clear
if [[ -z "$SSH_TTY" && -z "$(who am i | awk '{print $NF}' | grep -E '([0-9]{1,3}\.){3}[0-9]{1,3}')" ]]; then
# Logo for terminal noVNC
LOGO=$(cat << "EOF"
\e[0m\e[38;2;61;61;61m▆\e[38;2;60;60;60m▄\e[38;2;54;54;54m▂\e[0m \e[38;2;0;0;0m \e[0m \e[38;2;54;54;54m▂\e[38;2;60;60;60m▄\e[38;2;61;61;61m▆\e[0m
\e[38;2;59;59;59;48;2;62;62;62m▏ \e[38;2;61;61;61;48;2;37;37;37m▇\e[0m\e[38;2;60;60;60m▅\e[38;2;56;56;56m▃\e[38;2;37;37;37m▁ \e[38;2;36;36;36m▁\e[38;2;56;56;56m▃\e[38;2;60;60;60m▅\e[38;2;61;61;61;48;2;37;37;37m▇\e[48;2;62;62;62m \e[0m\e[7m\e[38;2;60;60;60m▁\e[0m
\e[38;2;59;59;59;48;2;62;62;62m▏ \e[0m\e[7m\e[38;2;61;61;61m▂\e[0m\e[38;2;62;62;62;48;2;61;61;61m┈\e[48;2;62;62;62m \e[48;2;61;61;61m┈\e[0m\e[38;2;60;60;60m▆\e[38;2;57;57;57m▄\e[38;2;48;48;48m▂\e[0m \e[38;2;47;47;47m▂\e[38;2;57;57;57m▄\e[38;2;60;60;60m▆\e[38;2;62;62;62;48;2;61;61;61m┈\e[48;2;62;62;62m \e[48;2;61;61;61m┈\e[0m\e[7m\e[38;2;60;60;60m▂\e[38;2;57;57;57m▄\e[38;2;47;47;47m▆\e[0m \e[0m
\e[38;2;59;59;59;48;2;62;62;62m▏ \e[0m\e[38;2;32;32;32m▏\e[7m\e[38;2;39;39;39m▇\e[38;2;57;57;57m▅\e[38;2;60;60;60m▃\e[0m\e[38;2;40;40;40;48;2;61;61;61m▁\e[48;2;62;62;62m \e[38;2;54;54;54;48;2;61;61;61m┊\e[48;2;62;62;62m \e[38;2;39;39;39;48;2;61;61;61m▁\e[0m\e[7m\e[38;2;60;60;60m▃\e[38;2;57;57;57m▅\e[38;2;38;38;38m▇\e[0m \e[38;2;193;60;2m▃\e[38;2;217;67;2m▅\e[38;2;225;70;2m▇\e[0m
\e[38;2;59;59;59;48;2;62;62;62m▏ \e[0m\e[38;2;32;32;32m▏\e[0m \e[38;2;203;63;2m▄\e[38;2;147;45;1m▂\e[0m \e[7m\e[38;2;55;55;55m▆\e[38;2;60;60;60m▄\e[38;2;61;61;61m▂\e[38;2;60;60;60m▄\e[38;2;55;55;55m▆\e[0m \e[38;2;144;44;1m▂\e[38;2;202;62;2m▄\e[38;2;219;68;2m▆\e[38;2;231;72;3;48;2;226;70;2m┈\e[48;2;231;72;3m \e[48;2;225;70;2m▉\e[0m
\e[38;2;59;59;59;48;2;62;62;62m▏ \e[0m\e[38;2;32;32;32m▏\e[7m\e[38;2;121;37;1m▉\e[0m\e[38;2;0;0;0;48;2;231;72;3m \e[0m\e[38;2;221;68;2m▇\e[38;2;208;64;2m▅\e[38;2;212;66;2m▂\e[38;2;123;37;0m▁\e[38;2;211;65;2m▂\e[38;2;207;64;2m▅\e[38;2;220;68;2m▇\e[48;2;231;72;3m \e[38;2;231;72;3;48;2;225;70;2m┈\e[0m\e[7m\e[38;2;221;68;2m▂\e[0m\e[38;2;44;13;0;48;2;231;72;3m \e[38;2;231;72;3;48;2;225;70;2m▉\e[0m
\e[38;2;59;59;59;48;2;62;62;62m▏ \e[0m\e[38;2;32;32;32m▏\e[0m \e[7m\e[38;2;190;59;2m▅\e[38;2;216;67;2m▃\e[38;2;225;70;2m▁\e[0m\e[38;2;95;29;0;48;2;231;72;3m \e[38;2;231;72;3;48;2;230;71;2m┈\e[48;2;231;72;3m \e[0m\e[7m\e[38;2;225;70;2m▁\e[38;2;216;67;2m▃\e[38;2;191;59;2m▅\e[0m \e[38;2;0;0;0;48;2;231;72;3m \e[38;2;231;72;3;48;2;225;70;2m▉\e[0m
\e[38;2;59;59;59;48;2;62;62;62m▏ \e[0m\e[38;2;32;32;32m▏ \e[0m \e[7m\e[38;2;172;53;1m▆\e[38;2;213;66;2m▄\e[38;2;219;68;2m▂\e[38;2;213;66;2m▄\e[38;2;174;54;2m▆\e[0m \e[38;2;0;0;0m \e[0m \e[38;2;0;0;0;48;2;231;72;3m \e[38;2;231;72;3;48;2;225;70;2m▉\e[0m
\e[38;2;59;59;59;48;2;62;62;62m▏ \e[0m\e[38;2;32;32;32m▏ \e[0m \e[38;2;0;0;0;48;2;231;72;3m \e[38;2;231;72;3;48;2;225;70;2m▉\e[0m
\e[7m\e[38;2;52;52;52m▆\e[38;2;59;59;59m▄\e[38;2;61;61;61m▂\e[0m\e[38;2;31;31;31m▏ \e[0m \e[7m\e[38;2;228;71;2m▂\e[38;2;221;69;2m▄\e[38;2;196;60;2m▆\e[0m
EOF
)
TEXT=(
""
""
"${BOLD}ProxMenux${RESET}"
""
"${BOLD}${NEON_PURPLE_BLUE}An Interactive Menu for${RESET}"
"${BOLD}${NEON_PURPLE_BLUE}Proxmox VE management${RESET}"
""
""
""
""
)
mapfile -t logo_lines <<< "$LOGO"
for i in {0..9}; do
echo -e "${TAB}${logo_lines[i]} ${WHITE}${RESET} ${TEXT[i]}"
done
echo -e
else
# Logo for terminal SSH
TEXT=(
""
""
""
""
"${BOLD}ProxMenux${RESET}"
""
"${BOLD}${NEON_PURPLE_BLUE}An Interactive Menu for${RESET}"
"${BOLD}${NEON_PURPLE_BLUE}Proxmox VE management${RESET}"
""
""
""
""
""
""
)
LOGO=(
"${DARK_GRAY}░░░░ ░░░░${RESET}"
"${DARK_GRAY}░░░░░░░ ░░░░░░ ${RESET}"
"${DARK_GRAY}░░░░░░░░░░░ ░░░░░░░ ${RESET}"
"${DARK_GRAY}░░░░ ░░░░░░ ░░░░░░ ${ORANGE}░░${RESET}"
"${DARK_GRAY}░░░░ ░░░░░░░ ${ORANGE}░░▒▒▒${RESET}"
"${DARK_GRAY}░░░░ ░░░ ${ORANGE}░▒▒▒▒▒▒▒${RESET}"
"${DARK_GRAY}░░░░ ${ORANGE}▒▒▒░ ░▒▒▒▒▒▒▒▒▒▒${RESET}"
"${DARK_GRAY}░░░░ ${ORANGE}░▒▒▒▒▒ ▒▒▒▒▒░░ ▒▒▒▒${RESET}"
"${DARK_GRAY}░░░░ ${ORANGE}░░▒▒▒▒▒▒▒░░ ▒▒▒▒${RESET}"
"${DARK_GRAY}░░░░ ${ORANGE}░░░ ▒▒▒▒${RESET}"
"${DARK_GRAY}░░░░ ${ORANGE}▒▒▒▒${RESET}"
"${DARK_GRAY}░░░░ ${ORANGE}▒▒▒░${RESET}"
"${DARK_GRAY} ░░ ${ORANGE}░░ ${RESET}"
)
for i in {0..12}; do
echo -e "${TAB}${LOGO[i]}${RESET} ${TEXT[i]}"
done
echo -e
fi
}
# ==========================================================
cleanup_corrupted_files() {
if [ -f "$CONFIG_FILE" ] && ! jq empty "$CONFIG_FILE" >/dev/null 2>&1; then
echo "Cleaning up corrupted configuration file..."
@@ -67,6 +277,17 @@ cleanup_corrupted_files() {
fi
}
# Cleanup function
cleanup() {
if [ -d "$TEMP_DIR" ]; then
rm -rf "$TEMP_DIR"
fi
}
# Set trap to ensure cleanup on exit
trap cleanup EXIT
# ==========================================================
check_existing_installation() {
local has_venv=false
@@ -118,6 +339,27 @@ uninstall_proxmenux() {
echo "Uninstalling ProxMenux..."
if systemctl is-active --quiet proxmenux-monitor.service; then
echo "Stopping ProxMenux Monitor service..."
systemctl stop proxmenux-monitor.service
fi
if systemctl is-enabled --quiet proxmenux-monitor.service 2>/dev/null; then
echo "Disabling ProxMenux Monitor service..."
systemctl disable proxmenux-monitor.service
fi
if [ -f "$MONITOR_SERVICE_FILE" ]; then
echo "Removing ProxMenux Monitor service file..."
rm -f "$MONITOR_SERVICE_FILE"
systemctl daemon-reload
fi
if [ -d "$MONITOR_INSTALL_DIR" ]; then
echo "Removing ProxMenux Monitor directory..."
rm -rf "$MONITOR_INSTALL_DIR"
fi
if [ -f "$VENV_PATH/bin/activate" ]; then
echo "Removing googletrans and virtual environment..."
source "$VENV_PATH/bin/activate"
@@ -314,55 +556,108 @@ get_server_ip() {
echo "$ip"
}
detect_latest_appimage() {
local appimage_dir="$TEMP_DIR/AppImage"
if [ ! -d "$appimage_dir" ]; then
return 1
fi
local latest_appimage=$(find "$appimage_dir" -name "ProxMenux-*.AppImage" -type f | sort -V | tail -1)
if [ -z "$latest_appimage" ]; then
return 1
fi
echo "$latest_appimage"
return 0
}
get_appimage_version() {
local appimage_path="$1"
local filename=$(basename "$appimage_path")
local version=$(echo "$filename" | grep -oP 'ProxMenux-\K[0-9]+\.[0-9]+\.[0-9]+')
echo "$version"
}
install_proxmenux_monitor() {
# Check if URL is accessible
if ! wget --spider -q "$MONITOR_APPIMAGE_URL" 2>/dev/null; then
msg_warn "ProxMenux Monitor AppImage not available at: $MONITOR_APPIMAGE_URL"
msg_info "The monitor will be available in future releases."
local appimage_source=$(detect_latest_appimage)
if [ -z "$appimage_source" ] || [ ! -f "$appimage_source" ]; then
msg_error "ProxMenux Monitor AppImage not found in $TEMP_DIR/AppImage/"
msg_warn "Please ensure the AppImage directory exists with ProxMenux-*.AppImage files."
update_config "proxmenux_monitor" "appimage_not_found"
return 1
fi
# Download AppImage silently
if ! wget -q -O "$MONITOR_INSTALL_PATH" "$MONITOR_APPIMAGE_URL" 2>&1; then
msg_warn "Failed to download ProxMenux Monitor from GitHub."
msg_info "You can install it manually later when available."
return 1
local appimage_version=$(get_appimage_version "$appimage_source")
if systemctl is-active --quiet proxmenux-monitor.service; then
systemctl stop proxmenux-monitor.service
fi
# Download SHA256 checksum silently
local sha256_file="/tmp/proxmenux-monitor.sha256"
if ! wget -q -O "$sha256_file" "$MONITOR_SHA256_URL" 2>/dev/null; then
msg_warn "SHA256 checksum file not available. Skipping verification."
msg_info "AppImage downloaded but integrity cannot be verified."
rm -f "$sha256_file"
else
# Verify SHA256 silently
local expected_hash=$(cat "$sha256_file" | awk '{print $1}')
local actual_hash=$(sha256sum "$MONITOR_INSTALL_PATH" | awk '{print $1}')
local service_exists=false
if [ -f "$MONITOR_SERVICE_FILE" ]; then
service_exists=true
fi
local sha256_file="$TEMP_DIR/AppImage/ProxMenux-Monitor.AppImage.sha256"
if [ -f "$sha256_file" ]; then
msg_info "Verifying AppImage integrity..."
local expected_hash=$(cat "$sha256_file" | grep -Eo '^[a-f0-9]+' | tr -d '\n')
local actual_hash=$(sha256sum "$appimage_source" | awk '{print $1}')
if [ "$expected_hash" != "$actual_hash" ]; then
msg_error "SHA256 verification failed! AppImage may be corrupted."
msg_info "Expected: $expected_hash"
msg_info "Got: $actual_hash"
rm -f "$MONITOR_INSTALL_PATH" "$sha256_file"
return 1
fi
rm -f "$sha256_file"
msg_ok "SHA256 verification passed."
else
msg_warn "SHA256 checksum not available. Skipping verification."
fi
# Make executable
chmod +x "$MONITOR_INSTALL_PATH"
msg_info "Installing ProxMenux Monitor..."
mkdir -p "$MONITOR_INSTALL_DIR"
# Show single success message at the end
msg_ok "ProxMenux Monitor installed and activated successfully."
local target_path="$MONITOR_INSTALL_DIR/ProxMenux-Monitor.AppImage"
cp "$appimage_source" "$target_path"
chmod +x "$target_path"
return 0
msg_ok "ProxMenux Monitor v$appimage_version installed."
if [ "$service_exists" = false ]; then
return 0 # New installation - service needs to be created
else
systemctl start proxmenux-monitor.service
sleep 2
if systemctl is-active --quiet proxmenux-monitor.service; then
update_config "proxmenux_monitor" "updated"
return 2 # Update successful
else
msg_warn "Service failed to restart. Check: journalctl -u proxmenux-monitor"
update_config "proxmenux_monitor" "failed"
return 1
fi
fi
}
create_monitor_service() {
msg_info "Creating ProxMenux Monitor service..."
cat > "$MONITOR_SERVICE_FILE" << EOF
local exec_path="$MONITOR_INSTALL_DIR/ProxMenux-Monitor.AppImage"
if [ -f "$TEMP_DIR/systemd/proxmenux-monitor.service" ]; then
sed "s|ExecStart=.*|ExecStart=$exec_path|g" \
"$TEMP_DIR/systemd/proxmenux-monitor.service" > "$MONITOR_SERVICE_FILE"
msg_ok "Using service file from repository."
else
cat > "$MONITOR_SERVICE_FILE" << EOF
[Unit]
Description=ProxMenux Monitor - Web Dashboard
After=network.target
@@ -370,8 +665,8 @@ After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=$BASE_DIR
ExecStart=$MONITOR_INSTALL_PATH
WorkingDirectory=$MONITOR_INSTALL_DIR
ExecStart=$exec_path
Restart=on-failure
RestartSec=10
Environment="PORT=$MONITOR_PORT"
@@ -379,55 +674,63 @@ Environment="PORT=$MONITOR_PORT"
[Install]
WantedBy=multi-user.target
EOF
msg_ok "Created default service file."
fi
# Reload systemd, enable and start service
systemctl daemon-reload
systemctl enable proxmenux-monitor.service > /dev/null 2>&1
systemctl start proxmenux-monitor.service > /dev/null 2>&1
# Wait a moment for service to start
sleep 2
sleep 3
# Check if service is running
if systemctl is-active --quiet proxmenux-monitor.service; then
msg_ok "ProxMenux Monitor service started successfully."
update_config "proxmenux_monitor" "installed"
return 0
else
msg_warn "ProxMenux Monitor service failed to start. Check logs with: journalctl -u proxmenux-monitor"
msg_warn "ProxMenux Monitor service failed to start."
msg_info "Check logs with: journalctl -u proxmenux-monitor -n 20"
msg_info "Check status with: systemctl status proxmenux-monitor"
update_config "proxmenux_monitor" "failed"
return 1
fi
}
####################################################
install_normal_version() {
local total_steps=4 # Increased from 3 to 4 for monitor installation
local total_steps=5
local current_step=1
show_progress $current_step $total_steps "Installing basic dependencies"
show_progress $current_step $total_steps "Installing basic dependencies."
if ! dpkg -l | grep -qw "jq"; then
msg_info "Installing jq..."
if ! command -v jq > /dev/null 2>&1; then
apt-get update > /dev/null 2>&1
if apt-get install -y jq > /dev/null 2>&1; then
msg_ok "jq installed successfully."
if apt-get install -y jq > /dev/null 2>&1 && command -v jq > /dev/null 2>&1; then
update_config "jq" "installed"
else
msg_error "Failed to install jq. Please install it manually."
update_config "jq" "failed"
return 1
local jq_url="https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-amd64"
if wget -q -O /usr/local/bin/jq "$jq_url" 2>/dev/null && chmod +x /usr/local/bin/jq; then
if command -v jq > /dev/null 2>&1; then
update_config "jq" "installed_from_github"
else
msg_error "Failed to install jq. Please install it manually."
update_config "jq" "failed"
return 1
fi
else
msg_error "Failed to install jq from both APT and GitHub. Please install it manually."
update_config "jq" "failed"
return 1
fi
fi
else
msg_ok "jq is already installed."
update_config "jq" "already_installed"
fi
BASIC_DEPS=("dialog" "curl")
BASIC_DEPS=("dialog" "curl" "git")
for pkg in "${BASIC_DEPS[@]}"; do
if ! dpkg -l | grep -qw "$pkg"; then
msg_info "Installing $pkg..."
if apt-get install -y "$pkg" > /dev/null 2>&1; then
msg_ok "$pkg installed successfully."
update_config "$pkg" "installed"
else
msg_error "Failed to install $pkg. Please install it manually."
@@ -435,11 +738,25 @@ install_normal_version() {
return 1
fi
else
msg_ok "$pkg is already installed."
update_config "$pkg" "already_installed"
fi
done
msg_ok "jq, dialog, curl and git installed successfully."
((current_step++))
show_progress $current_step $total_steps "Install ProxMenux repository"
msg_info "Cloning ProxMenux repositoryy."
if ! git clone --depth 1 "$REPO_URL" "$TEMP_DIR" 2>/dev/null; then
msg_error "Failed to clone repository from $REPO_URL"
exit 1
fi
msg_ok "Repository cloned successfully."
cd "$TEMP_DIR"
((current_step++))
show_progress $current_step $total_steps "Creating directories and configuration"
@@ -454,39 +771,36 @@ install_normal_version() {
msg_ok "Directories and configuration created."
((current_step++))
show_progress $current_step $total_steps "Downloading necessary files"
FILES=(
"$UTILS_FILE $REPO_URL/scripts/utils.sh"
"$INSTALL_DIR/$MENU_SCRIPT $REPO_URL/$MENU_SCRIPT"
"$LOCAL_VERSION_FILE $REPO_URL/version.txt"
)
for file in "${FILES[@]}"; do
IFS=" " read -r dest url <<< "$file"
msg_info "Downloading ${dest##*/}..."
sleep 2
if wget -qO "$dest" "$url"; then
msg_ok "${dest##*/} downloaded successfully."
else
msg_error "Failed to download ${dest##*/}. Check your Internet connection."
return 1
fi
done
show_progress $current_step $total_steps "Copying necessary files"
cp "./scripts/utils.sh" "$UTILS_FILE"
cp "./menu" "$INSTALL_DIR/$MENU_SCRIPT"
cp "./version.txt" "$LOCAL_VERSION_FILE"
cp "./install_proxmenux.sh" "$BASE_DIR/install_proxmenux.sh"
mkdir -p "$BASE_DIR/scripts"
cp -r "./scripts/"* "$BASE_DIR/scripts/"
chmod -R +x "$BASE_DIR/scripts/"
chmod +x "$BASE_DIR/install_proxmenux.sh"
msg_ok "Necessary files created."
chmod +x "$INSTALL_DIR/$MENU_SCRIPT"
((current_step++))
show_progress $current_step $total_steps "Installing ProxMenux Monitor"
if install_proxmenux_monitor; then
install_proxmenux_monitor
local monitor_status=$?
if [ $monitor_status -eq 0 ]; then
create_monitor_service
fi
msg_ok "ProxMenux Normal Version installation completed successfully."
}
####################################################
install_translation_version() {
local total_steps=5 # Increased from 4 to 5 for monitor installation
local total_steps=5
local current_step=1
show_progress $current_step $total_steps "Language selection"
@@ -495,28 +809,35 @@ install_translation_version() {
show_progress $current_step $total_steps "Installing system dependencies"
if ! dpkg -l | grep -qw "jq"; then
msg_info "Installing jq..."
if ! command -v jq > /dev/null 2>&1; then
apt-get update > /dev/null 2>&1
if apt-get install -y jq > /dev/null 2>&1; then
msg_ok "jq installed successfully."
if apt-get install -y jq > /dev/null 2>&1 && command -v jq > /dev/null 2>&1; then
update_config "jq" "installed"
else
msg_error "Failed to install jq. Please install it manually."
update_config "jq" "failed"
return 1
local jq_url="https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-amd64"
if wget -q -O /usr/local/bin/jq "$jq_url" 2>/dev/null && chmod +x /usr/local/bin/jq; then
if command -v jq > /dev/null 2>&1; then
update_config "jq" "installed_from_github"
else
msg_error "Failed to install jq. Please install it manually."
update_config "jq" "failed"
return 1
fi
else
msg_error "Failed to install jq from both APT and GitHub. Please install it manually."
update_config "jq" "failed"
return 1
fi
fi
else
msg_ok "jq is already installed."
update_config "jq" "already_installed"
fi
DEPS=("dialog" "curl" "python3" "python3-venv" "python3-pip")
DEPS=("dialog" "curl" "git" "python3" "python3-venv" "python3-pip")
for pkg in "${DEPS[@]}"; do
if ! dpkg -l | grep -qw "$pkg"; then
msg_info "Installing $pkg..."
if apt-get install -y "$pkg" > /dev/null 2>&1; then
msg_ok "$pkg installed successfully."
update_config "$pkg" "installed"
else
msg_error "Failed to install $pkg. Please install it manually."
@@ -524,36 +845,32 @@ install_translation_version() {
return 1
fi
else
msg_ok "$pkg is already installed."
update_config "$pkg" "already_installed"
fi
done
msg_ok "jq, dialog, curl, git, python3, python3-venv and python3-pip installed successfully."
((current_step++))
show_progress $current_step $total_steps "Setting up translation environment"
if [ ! -d "$VENV_PATH" ] || [ ! -f "$VENV_PATH/bin/activate" ]; then
msg_info "Creating the virtual environment..."
python3 -m venv --system-site-packages "$VENV_PATH" > /dev/null 2>&1
if [ ! -f "$VENV_PATH/bin/activate" ]; then
msg_error "Failed to create virtual environment. Please check your Python installation."
update_config "virtual_environment" "failed"
return 1
else
msg_ok "Virtual environment created successfully."
update_config "virtual_environment" "created"
fi
else
msg_ok "Virtual environment already exists."
update_config "virtual_environment" "already_exists"
fi
source "$VENV_PATH/bin/activate"
msg_info "Upgrading pip..."
if pip install --upgrade pip > /dev/null 2>&1; then
msg_ok "Pip upgraded successfully."
update_config "pip" "upgraded"
else
msg_error "Failed to upgrade pip."
@@ -561,9 +878,7 @@ install_translation_version() {
return 1
fi
msg_info "Installing googletrans..."
if pip install --break-system-packages --no-cache-dir googletrans==4.0.0-rc1 > /dev/null 2>&1; then
msg_ok "Googletrans installed successfully."
update_config "googletrans" "installed"
else
msg_error "Failed to install googletrans. Please check your internet connection."
@@ -573,46 +888,54 @@ install_translation_version() {
fi
deactivate
show_progress $current_step $total_steps "Cloning ProxMenux repository"
if ! git clone --depth 1 "$REPO_URL" "$TEMP_DIR" 2>/dev/null; then
msg_error "Failed to clone repository from $REPO_URL"
exit 1
fi
msg_ok "Repository cloned successfully."
cd "$TEMP_DIR"
((current_step++))
show_progress $current_step $total_steps "Downloading necessary files"
show_progress $current_step $total_steps "Copying necessary files"
mkdir -p "$BASE_DIR"
mkdir -p "$INSTALL_DIR"
FILES=(
"$CACHE_FILE $REPO_URL/json/cache.json"
"$UTILS_FILE $REPO_URL/scripts/utils.sh"
"$INSTALL_DIR/$MENU_SCRIPT $REPO_URL/$MENU_SCRIPT"
"$LOCAL_VERSION_FILE $REPO_URL/version.txt"
)
cp "./json/cache.json" "$CACHE_FILE"
msg_ok "Cache file copied with translations."
for file in "${FILES[@]}"; do
IFS=" " read -r dest url <<< "$file"
msg_info "Downloading ${dest##*/}..."
sleep 2
if wget -qO "$dest" "$url"; then
msg_ok "${dest##*/} downloaded successfully."
if [[ "$dest" == "$CACHE_FILE" ]]; then
msg_ok "Cache file updated with latest translations."
fi
else
msg_error "Failed to download ${dest##*/}. Check your Internet connection."
return 1
fi
done
cp "./scripts/utils.sh" "$UTILS_FILE"
cp "./menu" "$INSTALL_DIR/$MENU_SCRIPT"
cp "./version.txt" "$LOCAL_VERSION_FILE"
cp "./install_proxmenux.sh" "$BASE_DIR/install_proxmenux.sh"
mkdir -p "$BASE_DIR/scripts"
cp -r "./scripts/"* "$BASE_DIR/scripts/"
chmod -R +x "$BASE_DIR/scripts/"
chmod +x "$BASE_DIR/install_proxmenux.sh"
msg_ok "Necessary files created."
chmod +x "$INSTALL_DIR/$MENU_SCRIPT"
((current_step++))
show_progress $current_step $total_steps "Installing ProxMenux Monitor"
if install_proxmenux_monitor; then
install_proxmenux_monitor
local monitor_status=$?
if [ $monitor_status -eq 0 ]; then
create_monitor_service
elif [ $monitor_status -eq 2 ]; then
msg_ok "ProxMenux Monitor updated successfully."
fi
msg_ok "ProxMenux Translation Version installation completed successfully."
}
####################################################
show_installation_options() {
local current_install_type
current_install_type=$(check_existing_installation)
@@ -663,7 +986,6 @@ show_installation_options() {
exit 1
fi
# For new installations, show confirmation with details
if [ "$current_install_type" = "none" ]; then
if ! show_installation_confirmation "$INSTALL_TYPE"; then
show_proxmenux_logo
@@ -679,7 +1001,7 @@ show_installation_options() {
fi
}
install_proxmenu() {
install_proxmenux() {
show_installation_options
case "$INSTALL_TYPE" in
@@ -698,17 +1020,21 @@ install_proxmenu() {
exit 1
;;
esac
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
msg_title "$(translate "ProxMenux has been installed successfully")"
msg_title "ProxMenux has been installed successfully"
if systemctl is-active --quiet proxmenux-monitor.service; then
local server_ip=$(get_server_ip)
echo -e "${GN}🌐 $(translate "ProxMenux Monitor activated")${CL}: ${BL}http://${server_ip}:${MONITOR_PORT}${CL}"
echo -e "${GN}🌐 ProxMenux Monitor activated${CL}: ${BL}http://${server_ip}:${MONITOR_PORT}${CL}"
echo
fi
echo -ne "${GN}"
type_text "$(translate "To run ProxMenux, simply execute this command in the console or terminal:")"
type_text "To run ProxMenux, simply execute this command in the console or terminal:"
echo -e "${YWB} menu${CL}"
echo
}
@@ -719,4 +1045,4 @@ if [ "$(id -u)" -ne 0 ]; then
fi
cleanup_corrupted_files
install_proxmenu
install_proxmenux
+1517 -214
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+9 -6
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.1
# Last Updated: 04/07/2025
# ==========================================================
@@ -31,7 +31,7 @@
# Configuration ============================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
CONFIG_FILE="$BASE_DIR/config.json"
CACHE_FILE="$BASE_DIR/cache.json"
@@ -44,7 +44,10 @@ if [[ -f "$UTILS_FILE" ]]; then
fi
# =========================================================
# For now, update is not available in the local version.
# Take in mind that in future versions, updates must be
# a warning to update the .deb package
# =========================================================
check_updates() {
local INSTALL_SCRIPT="$BASE_DIR/install_proxmenux.sh"
@@ -80,13 +83,13 @@ check_updates() {
}
main_menu() {
exec bash <(curl -fsSL "$REPO_URL/scripts/menus/main_menu.sh")
exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"
}
load_language
initialize_cache
check_updates
# Check updates doesn't make sense in offline mode
# check_updates
main_menu
+15 -8
View File
@@ -4,7 +4,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 06/07/2025
# ==========================================================
@@ -31,12 +31,12 @@
# - Translation support: Multi-language compatible through ProxMenux framework
# - Rollback compatibility: All optimizations can be reversed using the uninstall script
#
# This script is based on the post-install script cutotomizable
# This script is based on the post-install script customizable
# ==========================================================
# Configuration
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
@@ -99,7 +99,7 @@ lvm_repair_check() {
done
msg_ok "$(translate "LVM PV headers check completed")"
register_tool "lvm_repair" true
}
# ==========================================================
@@ -257,7 +257,7 @@ apt_upgrade() {
if [ "$total_packages" -eq 0 ]; then
total_packages=1
fi
msg_ok "$(translate "Packages upgrade successfull")"
msg_ok "$(translate "Packages upgrade successful")"
tput civis
tput sc
@@ -748,8 +748,9 @@ install_log2ram_auto() {
return 1
fi
# Detect RAM
RAM_SIZE_GB=$(free -g | awk '/^Mem:/{print $2}')
# Detect RAM (in MB first for better accuracy)
RAM_SIZE_MB=$(free -m | awk '/^Mem:/{print $2}')
RAM_SIZE_GB=$((RAM_SIZE_MB / 1024))
[[ -z "$RAM_SIZE_GB" || "$RAM_SIZE_GB" -eq 0 ]] && RAM_SIZE_GB=4
if (( RAM_SIZE_GB <= 8 )); then
@@ -773,7 +774,13 @@ install_log2ram_auto() {
cat << 'EOF' > /usr/local/bin/log2ram-check.sh
#!/bin/bash
CONF_FILE="/etc/log2ram.conf"
LIMIT_KB=$(grep '^SIZE=' "$CONF_FILE" | cut -d'=' -f2 | tr -d 'M')000
SIZE_VALUE=$(grep '^SIZE=' "$CONF_FILE" | cut -d'=' -f2)
# Convert to KB: handle M (megabytes) and G (gigabytes)
if [[ "$SIZE_VALUE" == *"G" ]]; then
LIMIT_KB=$(($(echo "$SIZE_VALUE" | tr -d 'G') * 1024 * 1024))
else
LIMIT_KB=$(($(echo "$SIZE_VALUE" | tr -d 'M') * 1024))
fi
USED_KB=$(df /var/log --output=used | tail -1)
THRESHOLD=$(( LIMIT_KB * 90 / 100 ))
if (( USED_KB > THRESHOLD )); then
+3 -3
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# Configuration ============================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
@@ -16,7 +16,7 @@ initialize_cache
get_external_backup_mount_point() {
local BACKUP_MOUNT_FILE="/usr/local/share/proxmenux/last_backup_mount.txt"
local STORAGE_REPO="$REPO_URL/scripts/backup_restore"
local STORAGE_REPO="$LOCAL_SCRIPTS/backup_restore"
local MOUNT_POINT
if [[ -f "$BACKUP_MOUNT_FILE" ]]; then
@@ -36,7 +36,7 @@ get_external_backup_mount_point() {
echo "$MOUNT_POINT"
return 0
else
source <(curl -s "$STORAGE_REPO/mount_disk_host_bk.sh")
source "$STORAGE_REPO/mount_disk_host_bk.sh"
MOUNT_POINT=$(mount_disk_host_bk)
[[ -z "$MOUNT_POINT" ]] && msg_error "$(translate "No disk mounted.")" && return 1
echo "$MOUNT_POINT"
+4 -4
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# Configuration ============================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
@@ -16,7 +16,7 @@ initialize_cache
get_external_backup_mount_point() {
local BACKUP_MOUNT_FILE="/usr/local/share/proxmenux/last_backup_mount.txt"
local STORAGE_REPO="$REPO_URL/scripts/backup_restore"
local STORAGE_REPO="$LOCAL_SCRIPTS/backup_restore"
local MOUNT_POINT
if [[ -f "$BACKUP_MOUNT_FILE" ]]; then
@@ -36,7 +36,7 @@ get_external_backup_mount_point() {
echo "$MOUNT_POINT"
return 0
else
source <(curl -s "$STORAGE_REPO/mount_disk_host_bk.sh")
source "$STORAGE_REPO/mount_disk_host_bk.sh"
MOUNT_POINT=$(mount_disk_host_bk)
[[ -z "$MOUNT_POINT" ]] && msg_error "$(translate "No disk mounted.")" && return 1
echo "$MOUNT_POINT"
@@ -1058,4 +1058,4 @@ read -r
# ===============================
host_backup_menu
host_backup_menu
+4 -4
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# Configuration ============================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
@@ -16,7 +16,7 @@ initialize_cache
get_external_backup_mount_point() {
local BACKUP_MOUNT_FILE="/usr/local/share/proxmenux/last_backup_mount.txt"
local STORAGE_REPO="$REPO_URL/scripts/backup_restore"
local STORAGE_REPO="$LOCAL_SCRIPTS/backup_restore"
local MOUNT_POINT
if [[ -f "$BACKUP_MOUNT_FILE" ]]; then
@@ -36,7 +36,7 @@ get_external_backup_mount_point() {
echo "$MOUNT_POINT"
return 0
else
source <(curl -s "$STORAGE_REPO/mount_disk_host_bk.sh")
source "$STORAGE_REPO/mount_disk_host_bk.sh"
MOUNT_POINT=$(mount_disk_host_bk)
[[ -z "$MOUNT_POINT" ]] && msg_error "$(translate "No disk mounted.")" && return 1
echo "$MOUNT_POINT"
@@ -1291,4 +1291,4 @@ read -r
# ===============================
host_backup_menu
host_backup_menu
+3 -3
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# Configuration ============================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
@@ -16,7 +16,7 @@ initialize_cache
get_external_backup_mount_point() {
local BACKUP_MOUNT_FILE="/usr/local/share/proxmenux/last_backup_mount.txt"
local STORAGE_REPO="$REPO_URL/scripts/backup_restore"
local STORAGE_REPO="$LOCAL_SCRIPTS/backup_restore"
local MOUNT_POINT
if [[ -f "$BACKUP_MOUNT_FILE" ]]; then
@@ -36,7 +36,7 @@ get_external_backup_mount_point() {
echo "$MOUNT_POINT"
return 0
else
source <(curl -s "$STORAGE_REPO/mount_disk_host_bk.sh")
source "$STORAGE_REPO/mount_disk_host_bk.sh"
MOUNT_POINT=$(mount_disk_host_bk)
[[ -z "$MOUNT_POINT" ]] && msg_error "$(translate "No disk mounted.")" && return 1
echo "$MOUNT_POINT"
+1 -1
View File
@@ -10,7 +10,7 @@
# Last Updated: 13/12/2024
# ==========================================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
+2 -2
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.1
# Last Updated: 17/08/2025
# ==========================================================
@@ -16,7 +16,7 @@
# ==========================================================
# Configuration ============================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
+2 -2
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 28/01/2025
# ==========================================================
@@ -28,7 +28,7 @@
# Configuration ============================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
+2 -2
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 28/01/2025
# ==========================================================
@@ -21,7 +21,7 @@
# Configuration ============================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
+1 -1
View File
@@ -4,7 +4,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 2.0
# Last Updated: 07/01/2025
# ==========================================================
+1 -1
View File
@@ -4,7 +4,7 @@
# ==========================================================
# Configuration
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
+107 -22
View File
@@ -100,8 +100,16 @@ verify_js_integrity() {
patch_checked_command() {
[ -f "$JS_FILE" ] || return 0
# Check if already patched
grep -q "$MARK" "$JS_FILE" && return 0
# Check if already patched - look for our marker
if grep -q "$MARK" "$JS_FILE"; then
# Verify the patch is actually applied by checking if function is simplified
if grep -A 2 "checked_command: function" "$JS_FILE" | grep -q "orig_cmd();"; then
return 0
else
# Marker exists but patch not applied - remove marker and try again
sed -i "/$MARK/d" "$JS_FILE"
fi
fi
# Create backup
mkdir -p "$BACKUP_DIR"
@@ -111,27 +119,105 @@ patch_checked_command() {
# Set trap to restore on error
trap "cp -a '$backup' '$JS_FILE' 2>/dev/null || true" ERR
# Use Python to replace the entire checked_command function using brace counting
python3 <<'PYTHON_END'
import sys
js_file = "/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js"
try:
with open(js_file, 'r', encoding='utf-8') as f:
lines = f.readlines()
# Find the line with checked_command
start_line = -1
for i, line in enumerate(lines):
if 'checked_command: function' in line or 'checked_command:function' in line:
start_line = i
break
if start_line == -1:
print("checked_command function not found", file=sys.stderr)
sys.exit(1)
# Count braces to find the end of the function
brace_count = 0
end_line = -1
started_counting = False
for i in range(start_line, len(lines)):
line = lines[i]
# Count opening and closing braces
for char in line:
if char == '{':
brace_count += 1
started_counting = True
elif char == '}':
brace_count -= 1
# When we reach 0 and we've started counting, we found the end
if started_counting and brace_count == 0:
# Check if this line ends with "}," which is the function closure
if '},' in line or '},\n' in line:
end_line = i
break
if end_line == -1:
print("Could not find end of checked_command function", file=sys.stderr)
sys.exit(1)
# Get the indentation of the original function
indent = len(lines[start_line]) - len(lines[start_line].lstrip())
indent_str = ' ' * indent
# Create the replacement function (simple version that just calls orig_cmd)
replacement = [
f"{indent_str}checked_command: function (orig_cmd) {{\n",
f"{indent_str} orig_cmd();\n",
f"{indent_str}}},\n"
]
# Replace the function
new_lines = lines[:start_line] + replacement + lines[end_line+1:]
# Write the modified content
with open(js_file, 'w', encoding='utf-8') as f:
f.writelines(new_lines)
#print(f"Successfully replaced lines {start_line+1} to {end_line+1}")
sys.exit(0)
except Exception as e:
print(f"Python patch error: {e}", file=sys.stderr)
import traceback
traceback.print_exc(file=sys.stderr)
sys.exit(1)
PYTHON_END
local python_result=$?
if [ $python_result -ne 0 ]; then
# Python failed, restore backup
cp -a "$backup" "$JS_FILE"
trap - ERR
return 1
fi
# Verify the patch was applied
if ! grep -A 2 "checked_command: function" "$JS_FILE" | grep -q "orig_cmd();"; then
cp -a "$backup" "$JS_FILE"
trap - ERR
return 1
fi
# Add patch marker at the beginning
sed -i "1s|^|$MARK\n|" "$JS_FILE"
# Surgical patch: Change the condition in checked_command function
# This changes the if condition to 'if (false)' making the banner never show
if grep -q "res\.data\.status\.toLowerCase() !== 'active'" "$JS_FILE"; then
# Pattern for newer versions (8.4.5+)
sed -i "/checked_command: function/,/},$/s/res === null || res === undefined || !res || res\.data\.status\.toLowerCase() !== 'active'/false/g" "$JS_FILE"
elif grep -q "res\.data\.status !== 'Active'" "$JS_FILE"; then
# Pattern for older versions
sed -i "/checked_command: function/,/},$/s/res === null || res === undefined || !res || res\.data\.status !== 'Active'/false/g" "$JS_FILE"
fi
# Also handle the NoMoreNagging pattern if present
if grep -q "res\.data\.status\.toLowerCase() !== 'NoMoreNagging'" "$JS_FILE"; then
sed -i "/checked_command: function/,/},$/s/res === null || res === undefined || !res || res\.data\.status\.toLowerCase() !== 'NoMoreNagging'/false/g" "$JS_FILE"
fi
# Verify integrity after patch
if ! verify_js_integrity "$JS_FILE"; then
cp -a "$backup" "$JS_FILE"
trap - ERR
return 1
fi
@@ -217,6 +303,7 @@ EOFAPT
# Verify APT hook syntax
apt-config dump >/dev/null 2>&1 || {
msg_warn "APT hook syntax issue, removing..."
rm -f "$APT_HOOK"
}
}
@@ -226,7 +313,7 @@ remove_subscription_banner_v3() {
local pve_version
pve_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+\.[0-9]+' | head -1 || echo "unknown")
msg_info "$(translate "Detected Proxmox VE") ${pve_version} - $(translate "applying banner patch")"
msg_info "$(translate "Detected Proxmox VE") ${pve_version} - $(translate "applying minimal banner patch")"
@@ -239,16 +326,14 @@ remove_subscription_banner_v3() {
local backup_file
backup_file=$(create_backup "$JS_FILE")
if [ -n "$backup_file" ]; then
# msg_ok "$(translate "Desktop UI backup created"): $backup_file"
:
msg_ok "$(translate "Desktop UI backup created")"
fi
if [ -f "$MOBILE_UI_FILE" ]; then
local mobile_backup
mobile_backup=$(create_backup "$MOBILE_UI_FILE")
if [ -n "$mobile_backup" ]; then
# msg_ok "$(translate "Mobile UI backup created"): $mobile_backup"
:
msg_ok "$(translate "Mobile UI backup created")"
fi
fi
+277
View File
@@ -0,0 +1,277 @@
#!/bin/bash
# ==========================================================
# Remove Subscription Banner - Proxmox VE (v3 - Minimal Intrusive)
# ==========================================================
# This version makes a surgical change to the checked_command function
# by changing the condition to 'if (false)' and commenting out the banner logic.
# Also patches the mobile UI to remove the subscription dialog.
# ==========================================================
set -euo pipefail
# Source utilities if available
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
TOOLS_JSON="/usr/local/share/proxmenux/installed_tools.json"
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
load_language
initialize_cache
# File paths
JS_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js"
GZ_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js.gz"
MIN_JS_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.min.js"
MOBILE_UI_FILE="/usr/share/pve-yew-mobile-gui/index.html.tpl"
BACKUP_DIR="$BASE_DIR/backups"
APT_HOOK="/etc/apt/apt.conf.d/no-nag-script"
PATCH_BIN="/usr/local/bin/pve-remove-nag-v3.sh"
MARK="/* PROXMENUX_NAG_PATCH_V3 */"
MOBILE_MARK="<!-- PROXMENUX_MOBILE_NAG_PATCH -->"
# Ensure tools JSON exists
ensure_tools_json() {
[ -f "$TOOLS_JSON" ] || echo "{}" > "$TOOLS_JSON"
}
# Register tool in JSON
register_tool() {
command -v jq >/dev/null 2>&1 || return 0
local tool="$1" state="$2"
ensure_tools_json
jq --arg t "$tool" --argjson v "$state" '.[$t]=$v' "$TOOLS_JSON" \
> "$TOOLS_JSON.tmp" && mv "$TOOLS_JSON.tmp" "$TOOLS_JSON"
}
# Verify JS file integrity
verify_js_integrity() {
local file="$1"
[ -f "$file" ] || return 1
[ -s "$file" ] || return 1
grep -Eq 'Ext|function|var|const|let' "$file" || return 1
if LC_ALL=C grep -qP '\x00' "$file" 2>/dev/null; then
return 1
fi
return 0
}
# Create timestamped backup
create_backup() {
local file="$1"
local timestamp
timestamp=$(date +%Y%m%d_%H%M%S)
local backup_file="$BACKUP_DIR/$(basename "$file").backup.$timestamp"
mkdir -p "$BACKUP_DIR"
if [ -f "$file" ]; then
rm -f "$BACKUP_DIR"/"$(basename "$file")".backup.* 2>/dev/null || true
cp -a "$file" "$backup_file"
echo "$backup_file"
fi
}
# Create the patch script that will be called by APT hook
create_patch_script() {
cat > "$PATCH_BIN" <<'EOFPATCH'
#!/usr/bin/env bash
# ==========================================================
# Proxmox Subscription Banner Patch (v3 - Minimal)
# ==========================================================
set -euo pipefail
JS_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js"
GZ_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js.gz"
MIN_JS_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.min.js"
MOBILE_UI_FILE="/usr/share/pve-yew-mobile-gui/index.html.tpl"
BACKUP_DIR="/usr/local/share/proxmenux/backups"
MARK="/* PROXMENUX_NAG_PATCH_V3 */"
MOBILE_MARK="<!-- PROXMENUX_MOBILE_NAG_PATCH -->"
verify_js_integrity() {
local file="$1"
[ -f "$file" ] && [ -s "$file" ] && grep -Eq 'Ext|function' "$file" && ! LC_ALL=C grep -qP '\x00' "$file" 2>/dev/null
}
patch_checked_command() {
[ -f "$JS_FILE" ] || return 0
# Check if already patched
grep -q "$MARK" "$JS_FILE" && return 0
# Create backup
mkdir -p "$BACKUP_DIR"
local backup="$BACKUP_DIR/$(basename "$JS_FILE").backup.$(date +%Y%m%d_%H%M%S)"
cp -a "$JS_FILE" "$backup"
# Set trap to restore on error
trap "cp -a '$backup' '$JS_FILE' 2>/dev/null || true" ERR
# Add patch marker at the beginning
sed -i "1s|^|$MARK\n|" "$JS_FILE"
# Surgical patch: Change the condition in checked_command function
# This changes the if condition to 'if (false)' making the banner never show
if grep -q "res\.data\.status\.toLowerCase() !== 'active'" "$JS_FILE"; then
# Pattern for newer versions (8.4.5+)
sed -i "/checked_command: function/,/},$/s/res === null || res === undefined || !res || res\.data\.status\.toLowerCase() !== 'active'/false/g" "$JS_FILE"
elif grep -q "res\.data\.status !== 'Active'" "$JS_FILE"; then
# Pattern for older versions
sed -i "/checked_command: function/,/},$/s/res === null || res === undefined || !res || res\.data\.status !== 'Active'/false/g" "$JS_FILE"
fi
# Also handle the NoMoreNagging pattern if present
if grep -q "res\.data\.status\.toLowerCase() !== 'NoMoreNagging'" "$JS_FILE"; then
sed -i "/checked_command: function/,/},$/s/res === null || res === undefined || !res || res\.data\.status\.toLowerCase() !== 'NoMoreNagging'/false/g" "$JS_FILE"
fi
# Verify integrity after patch
if ! verify_js_integrity "$JS_FILE"; then
cp -a "$backup" "$JS_FILE"
return 1
fi
# Clean up generated files
rm -f "$MIN_JS_FILE" "$GZ_FILE" 2>/dev/null || true
find /var/cache/pve-manager/ -name "*.js*" -delete 2>/dev/null || true
find /var/lib/pve-manager/ -name "*.js*" -delete 2>/dev/null || true
find /var/cache/nginx/ -type f -delete 2>/dev/null || true
trap - ERR
return 0
}
patch_mobile_ui() {
[ -f "$MOBILE_UI_FILE" ] || return 0
# Check if already patched
grep -q "$MOBILE_MARK" "$MOBILE_UI_FILE" && return 0
# Create backup
mkdir -p "$BACKUP_DIR"
local backup="$BACKUP_DIR/$(basename "$MOBILE_UI_FILE").backup.$(date +%Y%m%d_%H%M%S)"
cp -a "$MOBILE_UI_FILE" "$backup"
# Set trap to restore on error
trap "cp -a '$backup' '$MOBILE_UI_FILE' 2>/dev/null || true" ERR
# Insert the script before </head> tag
sed -i "/<\/head>/i\\
$MOBILE_MARK\\
<!-- Script to remove subscription banner from mobile UI -->\\
<script>\\
function removeNoSubDialog() {\\
const observer = new MutationObserver(() => {\\
const diag = document.querySelector('dialog[aria-label=\"No valid subscription\"]');\\
if (diag) {\\
diag.remove();\\
}\\
});\\
observer.observe(document.body, { childList: true, subtree: true });\\
}\\
window.addEventListener('load', () => {\\
setTimeout(removeNoSubDialog, 200);\\
});\\
</script>" "$MOBILE_UI_FILE"
trap - ERR
return 0
}
reload_services() {
systemctl is-active --quiet pveproxy 2>/dev/null && {
systemctl reload pveproxy 2>/dev/null || systemctl restart pveproxy 2>/dev/null || true
}
systemctl is-active --quiet nginx 2>/dev/null && {
systemctl reload nginx 2>/dev/null || true
}
systemctl is-active --quiet pvedaemon 2>/dev/null && {
systemctl reload pvedaemon 2>/dev/null || true
}
}
main() {
patch_checked_command || return 1
patch_mobile_ui || true
reload_services
}
main
EOFPATCH
chmod 755 "$PATCH_BIN"
}
# Create APT hook to reapply patch after updates
create_apt_hook() {
cat > "$APT_HOOK" <<'EOFAPT'
/* ProxMenux: reapply minimal nag patch after upgrades */
DPkg::Post-Invoke { "/usr/local/bin/pve-remove-nag-v3.sh || true"; };
EOFAPT
chmod 644 "$APT_HOOK"
# Verify APT hook syntax
apt-config dump >/dev/null 2>&1 || {
rm -f "$APT_HOOK"
}
}
# Main function to remove subscription banner
remove_subscription_banner_v3() {
local pve_version
pve_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+\.[0-9]+' | head -1 || echo "unknown")
msg_info "$(translate "Detected Proxmox VE") ${pve_version} - $(translate "applying banner patch")"
# Remove old APT hooks
for f in /etc/apt/apt.conf.d/*nag*; do
[[ -e "$f" ]] && rm -f "$f"
done
# Create backup for desktop UI
local backup_file
backup_file=$(create_backup "$JS_FILE")
if [ -n "$backup_file" ]; then
# msg_ok "$(translate "Desktop UI backup created"): $backup_file"
:
fi
if [ -f "$MOBILE_UI_FILE" ]; then
local mobile_backup
mobile_backup=$(create_backup "$MOBILE_UI_FILE")
if [ -n "$mobile_backup" ]; then
# msg_ok "$(translate "Mobile UI backup created"): $mobile_backup"
:
fi
fi
# Create patch script and APT hook
create_patch_script
create_apt_hook
# Apply the patch
if ! "$PATCH_BIN"; then
msg_error "$(translate "Error applying patch. Backups preserved at"): $BACKUP_DIR"
return 1
fi
# Register tool as applied
register_tool "subscription_banner" true
msg_ok "$(translate "Subscription banner removed successfully")"
msg_ok "$(translate "Desktop and Mobile UI patched")"
msg_ok "$(translate "Refresh your browser (Ctrl+Shift+R) to see changes")"
}
# Run if executed directly
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
remove_subscription_banner_v3
fi
+1 -1
View File
@@ -2,7 +2,7 @@
# ==========================================================
# Remove Subscription Banner - Proxmox VE 8.4.9
# ==========================================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
+1 -1
View File
@@ -2,7 +2,7 @@
# ==========================================================
# Remove Subscription Banner - Proxmox VE 9.x
# ==========================================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
+1 -1
View File
@@ -2,7 +2,7 @@
# ==========================================================
# Remove Subscription Banner - Proxmox VE 9.x ONLY
# ==========================================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
+2 -2
View File
@@ -4,7 +4,7 @@
# ==========================================================
# Configuration
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
@@ -29,7 +29,7 @@ register_tool() {
}
download_common_functions() {
if ! source <(curl -s "$REPO_URL/scripts/global/common-functions.sh"); then
if ! source "$LOCAL_SCRIPTS/global/common-functions.sh"; then
return 1
fi
}
+2 -2
View File
@@ -4,7 +4,7 @@
# ==========================================================
# Configuration
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
@@ -29,7 +29,7 @@ register_tool() {
}
download_common_functions() {
if ! source <(curl -s "$REPO_URL/scripts/global/common-functions.sh"); then
if ! source "$LOCAL_SCRIPTS/global/common-functions.sh"; then
return 1
fi
}
+37 -11
View File
@@ -4,7 +4,7 @@
# ==========================================================
# Configuration
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
@@ -29,7 +29,7 @@ register_tool() {
}
download_common_functions() {
if ! source <(curl -s "$REPO_URL/scripts/global/common-functions.sh"); then
if ! source "$LOCAL_SCRIPTS/global/common-functions.sh"; then
return 1
fi
}
@@ -152,23 +152,48 @@ EOF
msg_ok "$(translate "Non-free firmware warnings disabled")"
fi
#update_output=$(apt-get update 2>&1)
update_output=$(apt-get -o Dpkg::Progress-Fancy=1 update 2>&1)
# UPDATE: no progress bar here (dpkg is not involved); capture output to parse errors
update_output=$(apt-get update 2>&1)
update_exit_code=$?
if [ $update_exit_code -eq 0 ]; then
msg_ok "$(translate "Package lists updated successfully")" | tee -a "$screen_capture"
else
if echo "$update_output" | grep -q "NO_PUBKEY\|GPG error"; then
msg_info "$(translate "Fixing GPG key issues...")"
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys $(echo "$update_output" | grep "NO_PUBKEY" | sed 's/.*NO_PUBKEY //' | head -1) 2>/dev/null
# Handle common apt errors
if echo "$update_output" | grep -Eq "NO_PUBKEY|GPG error"; then
# Extract first missing key (NO_PUBKEY ABCDEF... pattern)
key=$(echo "$update_output" | sed -n 's/.*NO_PUBKEY \([0-9A-F]\{8,40\}\).*/\1/p' | head -1)
if [ -n "$key" ]; then
mkdir -p /etc/apt/keyrings
if command -v gpg >/dev/null 2>&1; then
# Modern approach: receive -> export -> dearmor into /etc/apt/keyrings/<KEY>.gpg
if gpg --batch --keyserver keyserver.ubuntu.com --recv-keys "$key" \
&& gpg --batch --export "$key" | gpg --dearmor -o "/etc/apt/keyrings/${key}.gpg"; then
msg_ok "$(translate "Imported missing GPG key: $key")"
else
msg_warn "$(translate "Keyrings method failed; trying apt-key fallback")"
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys "$key" >/dev/null 2>&1 || true
fi
else
# Fallback for minimal systems without gpg installed
msg_warn "$(translate "gpg not found; trying apt-key fallback")"
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys "$key" >/dev/null 2>&1 || true
fi
fi
# Retry update after importing the key
if apt-get update > "$log_file" 2>&1; then
msg_ok "$(translate "Package lists updated after GPG fix")" | tee -a "$screen_capture"
else
msg_error "$(translate "Failed to update package lists. Check log: $log_file")"
return 1
fi
elif echo "$update_output" | grep -q "404\|Failed to fetch"; then
elif echo "$update_output" | grep -Eq "404|Failed to fetch"; then
msg_warn "$(translate "Some repositories are not available, continuing with available ones...")"
else
msg_error "$(translate "Failed to update package lists. Check log: $log_file")"
@@ -177,10 +202,11 @@ EOF
fi
fi
if apt policy 2>/dev/null | grep -q "${TARGET_CODENAME}.*pve-no-subscription"; then
msg_ok "$(translate "Proxmox VE 9.x repositories verified")" | tee -a "$screen_capture"
msg_ok "$(translate "Proxmox VE $pve_version repositories verified")" | tee -a "$screen_capture"
else
msg_warn "$(translate "Proxmox VE 9.x repositories verification inconclusive, continuing...")"
msg_warn "$(translate "Proxmox VE $pve_version repositories verification inconclusive, continuing...")"
fi
local current_pve_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+\.[0-9]+\.[0-9]+' | head -1)
@@ -294,7 +320,7 @@ EOF
echo -e "${TAB}${GN}📦 $(translate "Packages upgraded")${CL}: ${BL}$upgradable${CL}"
echo -e "${TAB}${GN}🖥️ $(translate "Proxmox VE")${CL}: ${BL}$available_pve_version (Debian $OS_CODENAME)${CL}"
msg_ok "$(translate "Proxmox VE 9.x configuration completed.")"
msg_ok "$(translate "Proxmox VE configuration completed.")"
rm -f "$screen_capture"
}
+304
View File
@@ -0,0 +1,304 @@
#!/bin/bash
# ==========================================================
# Proxmox VE Update Script - Improved Version
# ==========================================================
# Configuration
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
TOOLS_JSON="/usr/local/share/proxmenux/installed_tools.json"
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
load_language
initialize_cache
ensure_tools_json() {
[ -f "$TOOLS_JSON" ] || echo "{}" > "$TOOLS_JSON"
}
register_tool() {
local tool="$1"
local state="$2"
ensure_tools_json
jq --arg t "$tool" --argjson v "$state" '.[$t]=$v' "$TOOLS_JSON" > "$TOOLS_JSON.tmp" && mv "$TOOLS_JSON.tmp" "$TOOLS_JSON"
}
download_common_functions() {
if ! source <(curl -s "$REPO_URL/scripts/global/common-functions.sh"); then
return 1
fi
}
update_pve9() {
local pve_version=$(pveversion | awk -F'/' '{print $2}' | cut -d'-' -f1)
local start_time=$(date +%s)
local log_file="/var/log/proxmox-update-$(date +%Y%m%d-%H%M%S).log"
local changes_made=false
local OS_CODENAME="$(grep "VERSION_CODENAME=" /etc/os-release | cut -d"=" -f 2 | xargs)"
local TARGET_CODENAME="trixie"
local screen_capture="/tmp/proxmenux_screen_capture_$$.txt"
if [ -z "$OS_CODENAME" ]; then
OS_CODENAME=$(lsb_release -cs 2>/dev/null || echo "trixie")
fi
download_common_functions
{
msg_info2 "$(translate "Detected: Proxmox VE $pve_version (Current: $OS_CODENAME, Target: $TARGET_CODENAME)")"
} | tee -a "$screen_capture"
local available_space=$(df /var/cache/apt/archives | awk 'NR==2 {print int($4/1024)}')
if [ "$available_space" -lt 1024 ]; then
msg_error "$(translate "Insufficient disk space. Available: ${available_space}MB")"
echo -e
msg_success "$(translate "Press Enter to return to menu...")"
read -r
return 1
fi
if ! ping -c 1 download.proxmox.com >/dev/null 2>&1; then
msg_error "$(translate "Cannot reach Proxmox repositories")"
echo -e
msg_success "$(translate "Press Enter to return to menu...")"
read -r
return 1
fi
disable_sources_repo() {
local file="$1"
if [[ -f "$file" ]]; then
sed -i ':a;/^\n*$/{$d;N;ba}' "$file"
if grep -q "^Enabled:" "$file"; then
sed -i 's/^Enabled:.*$/Enabled: false/' "$file"
else
echo "Enabled: false" >> "$file"
fi
if ! grep -q "^Types: " "$file"; then
msg_warn "$(translate "Malformed .sources file detected, removing: $(basename "$file")")"
rm -f "$file"
fi
return 0
fi
return 1
}
if disable_sources_repo "/etc/apt/sources.list.d/pve-enterprise.sources"; then
msg_ok "$(translate "Enterprise Proxmox repository disabled")" | tee -a "$screen_capture"
changes_made=true
fi
if disable_sources_repo "/etc/apt/sources.list.d/ceph.sources"; then
msg_ok "$(translate "Enterprise Proxmox Ceph repository disabled")" | tee -a "$screen_capture"
changes_made=true
fi
for legacy_file in /etc/apt/sources.list.d/pve-public-repo.list \
/etc/apt/sources.list.d/pve-install-repo.list \
/etc/apt/sources.list.d/debian.list; do
if [[ -f "$legacy_file" ]]; then
rm -f "$legacy_file"
msg_ok "$(translate "Removed legacy repository: $(basename "$legacy_file")")" | tee -a "$screen_capture"
fi
done
if [[ -f /etc/apt/sources.list.d/debian.sources ]]; then
rm -f /etc/apt/sources.list.d/debian.sources
msg_ok "$(translate "Old debian.sources file removed to prevent duplication")" | tee -a "$screen_capture"
fi
msg_info "$(translate "Creating Proxmox VE 9.x no-subscription repository...")"
cat > /etc/apt/sources.list.d/proxmox.sources << EOF
Enabled: true
Types: deb
URIs: http://download.proxmox.com/debian/pve
Suites: ${TARGET_CODENAME}
Components: pve-no-subscription
Signed-By: /usr/share/keyrings/proxmox-archive-keyring.gpg
EOF
msg_ok "$(translate "Proxmox VE 9.x no-subscription repository created")" | tee -a "$screen_capture"
changes_made=true
msg_info "$(translate "Creating Debian ${TARGET_CODENAME} sources file...")"
cat > /etc/apt/sources.list.d/debian.sources << EOF
Types: deb
URIs: http://deb.debian.org/debian/
Suites: ${TARGET_CODENAME} ${TARGET_CODENAME}-updates
Components: main contrib non-free non-free-firmware
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
Types: deb
URIs: http://security.debian.org/debian-security/
Suites: ${TARGET_CODENAME}-security
Components: main contrib non-free non-free-firmware
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
EOF
msg_ok "$(translate "Debian repositories configured for $TARGET_CODENAME")"
local firmware_conf="/etc/apt/apt.conf.d/no-firmware-warnings.conf"
if [ ! -f "$firmware_conf" ]; then
msg_info "$(translate "Disabling non-free firmware warnings...")"
echo 'APT::Get::Update::SourceListWarnings::NonFreeFirmware "false";' > "$firmware_conf"
msg_ok "$(translate "Non-free firmware warnings disabled")"
fi
#update_output=$(apt-get update 2>&1)
update_output=$(apt-get -o Dpkg::Progress-Fancy=1 update 2>&1)
update_exit_code=$?
if [ $update_exit_code -eq 0 ]; then
msg_ok "$(translate "Package lists updated successfully")" | tee -a "$screen_capture"
else
if echo "$update_output" | grep -q "NO_PUBKEY\|GPG error"; then
msg_info "$(translate "Fixing GPG key issues...")"
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys $(echo "$update_output" | grep "NO_PUBKEY" | sed 's/.*NO_PUBKEY //' | head -1) 2>/dev/null
if apt-get update > "$log_file" 2>&1; then
msg_ok "$(translate "Package lists updated after GPG fix")" | tee -a "$screen_capture"
else
msg_error "$(translate "Failed to update package lists. Check log: $log_file")"
return 1
fi
elif echo "$update_output" | grep -q "404\|Failed to fetch"; then
msg_warn "$(translate "Some repositories are not available, continuing with available ones...")"
else
msg_error "$(translate "Failed to update package lists. Check log: $log_file")"
echo "Error details: $update_output"
return 1
fi
fi
if apt policy 2>/dev/null | grep -q "${TARGET_CODENAME}.*pve-no-subscription"; then
msg_ok "$(translate "Proxmox VE 9.x repositories verified")" | tee -a "$screen_capture"
else
msg_warn "$(translate "Proxmox VE 9.x repositories verification inconclusive, continuing...")"
fi
local current_pve_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+\.[0-9]+\.[0-9]+' | head -1)
local available_pve_version=$(apt-cache policy pve-manager 2>/dev/null | grep -oP 'Candidate: \K[0-9]+\.[0-9]+\.[0-9]+' | head -1)
local upgradable=$(apt list --upgradable 2>/dev/null | grep -c "upgradable")
local security_updates=$(apt list --upgradable 2>/dev/null | grep -c "security")
show_update_menu() {
local current_version="$1"
local target_version="$2"
local upgradable_count="$3"
local security_count="$4"
local menu_text="$(translate "System Update Information")\n\n"
menu_text+="$(translate "Current PVE Version"): $current_version\n"
if [ -n "$target_version" ] && [ "$target_version" != "$current_version" ]; then
menu_text+="$(translate "Available PVE Version"): $target_version\n"
fi
menu_text+="\n$(translate "Package Updates Available"): $upgradable_count\n"
menu_text+="$(translate "Security Updates"): $security_count\n\n"
if [ "$upgradable_count" -eq 0 ]; then
menu_text+="$(translate "System is already up to date")"
whiptail --title "$(translate "Update Status")" --msgbox "$menu_text" 15 70
return 2
else
menu_text+="$(translate "Do you want to proceed with the system update?")"
if whiptail --title "$(translate "Proxmox Update")" --yesno "$menu_text" 18 70; then
return 0
else
return 1
fi
fi
}
show_update_menu "$current_pve_version" "$available_pve_version" "$upgradable" "$security_updates"
MENU_RESULT=$?
clear
show_proxmenux_logo
msg_title "$(translate "$SCRIPT_TITLE")"
cat "$screen_capture"
if [[ $MENU_RESULT -eq 1 ]]; then
msg_info2 "$(translate "Update cancelled by user")"
apt-get -y autoremove > /dev/null 2>&1 || true
apt-get -y autoclean > /dev/null 2>&1 || true
rm -f "$screen_capture"
return 0
elif [[ $MENU_RESULT -eq 2 ]]; then
msg_ok "$(translate "System is already up to date. No update needed.")"
apt-get -y autoremove > /dev/null 2>&1 || true
apt-get -y autoclean > /dev/null 2>&1 || true
rm -f "$screen_capture"
return 0
fi
msg_info "$(translate "Cleaning up unused time synchronization services...")"
if /usr/bin/env DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Options::='--force-confdef' purge ntp openntpd systemd-timesyncd > /dev/null 2>&1; then
msg_ok "$(translate "Old time services removed successfully")"
else
msg_warn "$(translate "Some old time services could not be removed (not installed)")"
fi
echo -e
DEBIAN_FRONTEND=noninteractive apt-get -y \
-o Dpkg::Options::='--force-confdef' \
-o Dpkg::Options::='--force-confold' \
dist-upgrade 2>&1 | tee -a "$log_file"
upgrade_exit_code=${PIPESTATUS[0]}
echo -e
clear
show_proxmenux_logo
msg_title "$(translate "$SCRIPT_TITLE")"
cat "$screen_capture"
if [ $upgrade_exit_code -ne 0 ]; then
msg_error "$(translate "System upgrade failed. Check log: $log_file")"
rm -f "$screen_capture"
return 1
fi
msg_info "$(translate "Installing essential Proxmox packages...")"
local additional_packages="zfsutils-linux proxmox-backup-restore-image chrony"
if /usr/bin/env DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Options::='--force-confdef' install $additional_packages >> "$log_file" 2>&1; then
msg_ok "$(translate "Essential Proxmox packages installed")"
else
msg_warn "$(translate "Some essential Proxmox packages may not have been installed")"
fi
lvm_repair_check
cleanup_duplicate_repos
apt-get -y autoremove > /dev/null 2>&1 || true
apt-get -y autoclean > /dev/null 2>&1 || true
msg_ok "$(translate "Cleanup finished")"
local end_time=$(date +%s)
local duration=$((end_time - start_time))
local minutes=$((duration / 60))
local seconds=$((duration % 60))
echo -e "${TAB}${BGN}$(translate "====== PVE UPDATE COMPLETED ======")${CL}"
echo -e "${TAB}${GN}⏱️ $(translate "Duration")${CL}: ${BL}${minutes}m ${seconds}s${CL}"
echo -e "${TAB}${GN}📄 $(translate "Log file")${CL}: ${BL}$log_file${CL}"
echo -e "${TAB}${GN}📦 $(translate "Packages upgraded")${CL}: ${BL}$upgradable${CL}"
echo -e "${TAB}${GN}🖥️ $(translate "Proxmox VE")${CL}: ${BL}$available_pve_version (Debian $OS_CODENAME)${CL}"
msg_ok "$(translate "Proxmox VE 9.x configuration completed.")"
rm -f "$screen_capture"
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
update_pve9
fi
+1 -1
View File
@@ -7,7 +7,7 @@
# Last Updated: 25/09/2025
# =========================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
LOG_FILE="/tmp/coral_install.log"
+2 -2
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 28/01/2025
# ==========================================================
@@ -19,7 +19,7 @@
# ==========================================================
# Configuration ============================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
+2 -2
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.1
# Last Updated: 29/05/2025
# ==========================================================
@@ -27,7 +27,7 @@
# ==========================================================
# Configuration ============================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
+2 -2
View File
@@ -6,7 +6,7 @@
# Author : MacRimi
# Revision : @Blaspt (USB passthrough via udev rule with persistent /dev/coral)
# Copyright : (c) 2024 MacRimi
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.1
# Last Updated: 16/05/2025
# ==========================================================
@@ -22,7 +22,7 @@
# Includes USB passthrough enhancement using persistent udev alias (/dev/coral).
# ==========================================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
+2 -2
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 28/01/2025
# ==========================================================
@@ -17,7 +17,7 @@
# Configuration ============================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
+2 -2
View File
@@ -5,13 +5,13 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 19/08/2025
# ==========================================================
# Configuration ============================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.1
# Last Updated: 19/08/2025
# ==========================================================
@@ -15,7 +15,7 @@
# ==========================================================
# Configuration ============================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 2.0
# Last Updated: 19/08/2025
# ==========================================================
@@ -16,7 +16,7 @@
# ==========================================================
# Configuration ============================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
+55 -8
View File
@@ -4,13 +4,14 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# Contributors : cod378
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.1
# Last Updated: 04/07/2025
# ==========================================================
# Configuration ============================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
CONFIG_FILE="$BASE_DIR/config.json"
CACHE_FILE="$BASE_DIR/cache.json"
@@ -19,7 +20,10 @@ LOCAL_VERSION_FILE="$BASE_DIR/version.txt"
INSTALL_DIR="/usr/local/bin"
MENU_SCRIPT="menu"
VENV_PATH="/opt/googletrans-env"
MONITOR_SERVICE="proxmenux-monitor.service"
MONITOR_UNIT_FILE="/etc/systemd/system/${MONITOR_SERVICE}"
MONITOR_CONFIG_DIR="/root/.config/proxmenux-monitor"
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
@@ -30,6 +34,50 @@ initialize_cache
# ==========================================================
uninstall_proxmenux_monitor() {
# 1. Stop service if it is running
if systemctl is-active --quiet "${MONITOR_SERVICE}"; then
echo " - Stoping service..."
systemctl stop "${MONITOR_SERVICE}" > /dev/null 2>&1
else
echo " - Service is not running (ok)"
fi
# 2. Disable service if enabled
if systemctl is-enabled --quiet "${MONITOR_SERVICE}"; then
echo " - Disabling service..."
systemctl disable "${MONITOR_SERVICE}" > /dev/null 2>&1
else
echo " - Service is not enabled (ok)"
fi
# 3. Remove unit file
if [ -f "${MONITOR_UNIT_FILE}" ]; then
echo " - Removing unit file ${MONITOR_UNIT_FILE}..."
rm -f "${MONITOR_UNIT_FILE}"
else
echo " - Unit file ${MONITOR_UNIT_FILE} does not exist (ok)"
fi
# 4. Remove config directory (~/.config/proxmenux-monitor)
if [ -d "${MONITOR_CONFIG_DIR}" ]; then
echo " - Removing config dir ${MONITOR_CONFIG_DIR}..."
rm -rf "${MONITOR_CONFIG_DIR}"
else
echo " - Config dir ${MONITOR_CONFIG_DIR} does not exist (ok)"
fi
# 5. Reload systemd
echo " - Recargando systemd..."
systemctl daemon-reload > /dev/null 2>&1
systemctl reset-failed > /dev/null 2>&1 || true
echo "==> Service ${MONITOR_SERVICE} uninstalled."
}
detect_installation_type() {
local has_venv=false
local has_language=false
@@ -218,7 +266,7 @@ show_config_menu() {
uninstall_proxmenu
;;
"return_main"|"")
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")
exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"
;;
esac
done
@@ -257,11 +305,7 @@ change_language() {
--msgbox "\n\n$(translate "Language changed to") $new_language" 10 50
# Reload menu with new language
TMP_FILE=$(mktemp)
curl -s "$REPO_URL/scripts/menus/config_menu.sh" > "$TMP_FILE"
chmod +x "$TMP_FILE"
trap 'rm -f "$TMP_FILE"' EXIT
exec bash "$TMP_FILE"
exec bash "$LOCAL_SCRIPTS/menus/config_menu.sh"
}
# ==========================================================
@@ -404,6 +448,9 @@ uninstall_proxmenu() {
done
apt-get autoremove -y --purge >/dev/null 2>&1
fi
echo "80" ; echo "Removing ProxMenux Monitor..."
uninstall_proxmenux_monitor
echo "90" ; echo "Restoring system files..."
# Restore .bashrc and motd
+19 -42
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 07/05/2025
# ==========================================================
@@ -26,30 +26,30 @@
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
VM_REPO="$REPO_URL/scripts/vm"
ISO_REPO="$REPO_URL/scripts/vm"
MENU_REPO="$REPO_URL/scripts/menus"
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
VM_REPO="$LOCAL_SCRIPTS/vm"
ISO_REPO="$LOCAL_SCRIPTS/vm"
MENU_REPO="$LOCAL_SCRIPTS/menus"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
[[ -f "$UTILS_FILE" ]] && source "$UTILS_FILE"
source <(curl -s "$VM_REPO/vm_configurator.sh")
source <(curl -s "$VM_REPO/disk_selector.sh")
source <(curl -s "$VM_REPO/vm_creator.sh")
# Source utilities and required scripts
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
else
echo "Error: $UTILS_FILE not found"
exit 1
fi
load_language
initialize_cache
# Source VM management scripts
[[ -f "$VM_REPO/vm_configurator.sh" ]] && source "$VM_REPO/vm_configurator.sh" || { echo "Error: vm_configurator.sh not found"; exit 1; }
[[ -f "$VM_REPO/disk_selector.sh" ]] && source "$VM_REPO/disk_selector.sh" || { echo "Error: disk_selector.sh not found"; exit 1; }
[[ -f "$VM_REPO/vm_creator.sh" ]] && source "$VM_REPO/vm_creator.sh" || { echo "Error: vm_creator.sh not found"; exit 1; }
function header_info() {
@@ -108,17 +108,17 @@ while true; do
3>&1 1>&2 2>&3)
[[ $? -ne 0 || "$OS_TYPE" == "6" ]] && exec bash <(curl -s "$MENU_REPO/main_menu.sh")
[[ $? -ne 0 || "$OS_TYPE" == "6" ]] && exec bash "$MENU_REPO/main_menu.sh"
case "$OS_TYPE" in
1)
source <(curl -fsSL "$ISO_REPO/select_nas_iso.sh") && select_nas_iso || continue
source "$ISO_REPO/select_nas_iso.sh" && select_nas_iso || continue
;;
2)
source <(curl -fsSL "$ISO_REPO/select_windows_iso.sh") && select_windows_iso || continue
source "$ISO_REPO/select_windows_iso.sh" && select_windows_iso || continue
;;
3)
source <(curl -fsSL "$ISO_REPO/select_linux_iso.sh") && select_linux_iso || continue
source "$ISO_REPO/select_linux_iso.sh" && select_linux_iso || continue
;;
4)
whiptail --title "OSX-PROXMOX" --yesno "$(translate "This is an external script that creates a macOS VM in Proxmox VE in just a few steps, whether you are using AMD or Intel hardware.")\n\n$(translate "The script clones the osx-proxmox.com repository and once the setup is complete, the server will automatically reboot.")\n\n$(translate "Make sure there are no critical services running as they will be interrupted. Ensure your server can be safely rebooted.")\n\n$(translate "Visit https://osx-proxmox.com for more information.")\n\n$(translate "Do you want to run the script now?")" 24 70
@@ -128,7 +128,7 @@ while true; do
continue
;;
5)
source <(curl -fsSL "$ISO_REPO/select_linux_iso.sh") && select_linux_other_scripts || continue
source "$ISO_REPO/select_linux_iso.sh" && select_linux_other_scripts || continue
;;
esac
@@ -149,26 +149,3 @@ while true; do
create_vm
break
done
function start_vm_configuration() {
if (whiptail --title "ProxMenux" --yesno "$(translate "Use Default Settings?")" --no-button "$(translate "Advanced")" 10 60); then
header_info
load_default_vm_config "$OS_TYPE"
if [[ -z "$HN" ]]; then
HN=$(whiptail --inputbox "$(translate "Enter a name for the new virtual machine:")" 10 60 --title "VM Hostname" 3>&1 1>&2 2>&3)
[[ -z "$HN" ]] && HN="custom-vm"
fi
apply_default_vm_config
else
header_info
echo -e "${CUS}$(translate "Using advanced configuration")${CL}"
configure_vm_advanced "$OS_TYPE"
fi
}
+7 -7
View File
@@ -5,14 +5,14 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 28/01/2025
# ==========================================================
# Configuration ============================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
@@ -35,24 +35,24 @@ initialize_cache
case $OPTION in
1)
bash <(curl -s "$REPO_URL/scripts/configure_igpu_lxc.sh")
bash "$LOCAL_SCRIPTS/configure_igpu_lxc.sh"
if [ $? -ne 0 ]; then
return
fi
;;
2)
bash <(curl -s "$REPO_URL/scripts/install_coral_lxc.sh")
bash "$LOCAL_SCRIPTS/install_coral_lxc.sh"
if [ $? -ne 0 ]; then
return
fi
;;
3)
bash <(curl -s "$REPO_URL/scripts/gpu_tpu/install_coral_pve9.sh")
bash "$LOCAL_SCRIPTS/gpu_tpu/install_coral_pve9.sh"
if [ $? -ne 0 ]; then
return
fi
;;
4) exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh") ;;
*) exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh") ;;
4) exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh" ;;
*) exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh" ;;
esac
done
+9 -10
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 19/08/2025
# ==========================================================
@@ -15,7 +15,7 @@
# ==========================================================
# Configuration ============================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
@@ -40,22 +40,23 @@ show_main_menu() {
case $CHOICE in
1)
bash <(curl -s "$REPO_URL/scripts/lxc/lxc-privileged-to-unprivileged.sh")
bash "$LOCAL_SCRIPTS/lxc/lxc-privileged-to-unprivileged.sh"
;;
2)
bash <(curl -s "$REPO_URL/scripts/lxc/lxc-unprivileged-to-privileged.sh")
bash "$LOCAL_SCRIPTS/lxc/lxc-unprivileged-to-privileged.sh"
;;
3)
show_container_status
;;
4)
bash <(curl -s "$REPO_URL/scripts/lxc/lxc-conversion-manual-guide.sh")
bash "$LOCAL_SCRIPTS/lxc/lxc-conversion-manual-guide.sh"
;;
5)
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")
exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"
;;
*)
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")
exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"
;;
esac
}
@@ -99,6 +100,4 @@ show_container_status() {
show_main_menu
}
show_main_menu
show_main_menu
+14 -17
View File
@@ -5,13 +5,13 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 2.0
# Last Updated: 04/04/2025
# ==========================================================
# Configuration ============================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
@@ -54,10 +54,10 @@ check_pve9_translation_compatibility() {
--title "Translation Environment Incompatible with PVE $pve_version" \
--msgbox "NOTICE: You are running Proxmox VE $pve_version with translation components installed.\n\nTranslations are NOT supported in PVE 9+. This causes:\n• Menu loading errors\n• Translation failures\n• System instability\n\nREQUIRED ACTION:\nProxMenux will now automatically reinstall the Normal Version.\n\nThis process will:\n• Remove incompatible translation components\n• Install PVE 9+ compatible version\n• Preserve all your settings and preferences\n\nPress OK to continue with automatic reinstallation..." 20 75
bash <(curl -sSL "$REPO_URL/install_proxmenux.sh")
bash "$BASE_DIR/install_proxmenux.sh"
fi
exit
exit 0
fi
}
@@ -90,9 +90,6 @@ show_menu() {
while true; do
local menu_title="Main ProxMenux"
if [[ -n "$PROXMENUX_PVE9_WARNING_SHOWN" ]]; then
menu_title="Main ProxMenux"
fi
dialog --clear \
--backtitle "ProxMenux" \
@@ -122,16 +119,16 @@ show_menu() {
OPTION=$(<"$TEMP_FILE")
case $OPTION in
1) exec bash <(curl -s "$REPO_URL/scripts/menus/menu_post_install.sh") ;;
2) exec bash <(curl -s "$REPO_URL/scripts/menus/hw_grafics_menu.sh") ;;
3) exec bash <(curl -s "$REPO_URL/scripts/menus/create_vm_menu.sh") ;;
4) exec bash <(curl -s "$REPO_URL/scripts/menus/storage_menu.sh") ;;
5) exec bash <(curl -s "$REPO_URL/scripts/menus/share_menu.sh") ;;
6) exec bash <(curl -s "$REPO_URL/scripts/menus/menu_Helper_Scripts.sh") ;;
7) exec bash <(curl -s "$REPO_URL/scripts/menus/network_menu.sh") ;;
8) exec bash <(curl -s "$REPO_URL/scripts/menus/utilities_menu.sh") ;;
h) bash <(curl -s "$REPO_URL/scripts/help_info_menu.sh") ;;
s) exec bash <(curl -s "$REPO_URL/scripts/menus/config_menu.sh") ;;
1) exec bash "$LOCAL_SCRIPTS/menus/menu_post_install.sh" ;;
2) exec bash "$LOCAL_SCRIPTS/menus/hw_grafics_menu.sh" ;;
3) exec bash "$LOCAL_SCRIPTS/menus/create_vm_menu.sh" ;;
4) exec bash "$LOCAL_SCRIPTS/menus/storage_menu.sh" ;;
5) exec bash "$LOCAL_SCRIPTS/menus/share_menu.sh" ;;
6) exec bash "$LOCAL_SCRIPTS/menus/menu_Helper_Scripts.sh" ;;
7) exec bash "$LOCAL_SCRIPTS/menus/network_menu.sh" ;;
8) exec bash "$LOCAL_SCRIPTS/menus/utilities_menu.sh" ;;
h) bash "$LOCAL_SCRIPTS/help_info_menu.sh" ;;
s) exec bash "$LOCAL_SCRIPTS/menus/config_menu.sh" ;;
0) clear; msg_ok "$(translate "Thank you for using ProxMenux. Goodbye!")"; rm -f "$TEMP_FILE"; exit 0 ;;
*) msg_warn "$(translate "Invalid option")"; sleep 2 ;;
esac
+11 -13
View File
@@ -5,13 +5,13 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 2.0
# Last Updated: 04/04/2025
# ==========================================================
# Configuration ============================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
@@ -62,21 +62,19 @@ show_menu() {
OPTION=$(<"$TEMP_FILE")
case $OPTION in
1) exec bash <(curl -s "$REPO_URL/scripts/menus/menu_post_install.sh") ;;
2) bash <(curl -s "$REPO_URL/scripts/help_info_menu.sh") ;;
3) exec bash <(curl -s "$REPO_URL/scripts/menus/hw_grafics_menu.sh") ;;
4) exec bash <(curl -s "$REPO_URL/scripts/menus/create_vm_menu.sh") ;;
5) exec bash <(curl -s "$REPO_URL/scripts/menus/storage_menu.sh") ;;
6) exec bash <(curl -s "$REPO_URL/scripts/menus/menu_Helper_Scripts.sh") ;;
7) exec bash <(curl -s "$REPO_URL/scripts/menus/network_menu.sh") ;;
8) exec bash <(curl -s "$REPO_URL/scripts/menus/utilities_menu.sh") ;;
9) exec bash <(curl -s "$REPO_URL/scripts/menus/config_menu.sh") ;;
1) exec bash "$LOCAL_SCRIPTS/menus/menu_post_install.sh" ;;
2) bash "$LOCAL_SCRIPTS/help_info_menu.sh" ;;
3) exec bash "$LOCAL_SCRIPTS/menus/hw_grafics_menu.sh" ;;
4) exec bash "$LOCAL_SCRIPTS/menus/create_vm_menu.sh" ;;
5) exec bash "$LOCAL_SCRIPTS/menus/storage_menu.sh" ;;
6) exec bash "$LOCAL_SCRIPTS/menus/menu_Helper_Scripts.sh" ;;
7) exec bash "$LOCAL_SCRIPTS/menus/network_menu.sh" ;;
8) exec bash "$LOCAL_SCRIPTS/menus/utilities_menu.sh" ;;
9) exec bash "$LOCAL_SCRIPTS/menus/config_menu.sh" ;;
0) clear; msg_ok "$(translate "Thank you for using ProxMenux. Goodbye!")"; rm -f "$TEMP_FILE"; exit 0 ;;
*) msg_warn "$(translate "Invalid option")"; sleep 2 ;;
esac
done
}
show_menu
+111 -55
View File
@@ -5,9 +5,9 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# Version : 1.1
# Last Updated: 04/06/2025
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.2
# Last Updated: 14/11/2025
# ==========================================================
# Description:
# This script provides a simple and efficient way to access and execute Proxmox VE scripts
@@ -20,7 +20,7 @@
# Configuration ============================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
@@ -52,7 +52,8 @@ while read -r id name; do
done < <(echo "$META_JSON" | jq -r '.categories[] | "\(.id)\t\(.name)"')
declare -A CATEGORY_COUNT
for id in $(echo "$CACHE_JSON" | jq -r '.[].categories[]'); do
for id in $(echo "$CACHE_JSON" | jq -r '
group_by(.slug) | map(.[0])[] | .categories[]'); do
((CATEGORY_COUNT[$id]++))
done
@@ -112,74 +113,121 @@ format_credentials() {
echo "$credentials_info"
}
run_script_by_slug() {
local slug="$1"
local script_info
script_info=$(echo "$CACHE_JSON" | jq -r --arg slug "$slug" '.[] | select(.slug == $slug) | @base64')
local -a script_infos
mapfile -t script_infos < <(echo "$CACHE_JSON" | jq -r --arg slug "$slug" '.[] | select(.slug == $slug) | @base64')
if [[ ${#script_infos[@]} -eq 0 ]]; then
dialog --title "Helper Scripts" --msgbox "Error: No script data found for slug: $slug" 8 60
return
fi
decode() {
echo "$1" | base64 --decode | jq -r "$2"
}
local name desc script_url notes
name=$(decode "$script_info" ".name")
desc=$(decode "$script_info" ".desc")
script_url=$(decode "$script_info" ".script_url")
notes=$(decode "$script_info" ".notes | join(\"\n\")")
local first="${script_infos[0]}"
local name desc notes
name=$(decode "$first" ".name")
desc=$(decode "$first" ".desc")
notes=$(decode "$first" ".notes | join(\"\n\")")
local notes_dialog=""
if [[ -n "$notes" ]]; then
while IFS= read -r line; do
[[ -z "$line" ]] && continue
notes_dialog+="$line\n"
done <<< "$notes"
notes_dialog="${notes_dialog%\\n}"
notes_dialog="${notes_dialog%\\n}"
fi
local credentials
credentials=$(format_credentials "$script_info")
credentials=$(format_credentials "$first")
local msg="\Zb\Z4Descripción:\Zn\n$desc"
[[ -n "$notes_dialog" ]] && msg+="\n\n\Zb\Z4Notes:\Zn\n$notes_dialog"
[[ -n "$credentials" ]] && msg+="\n\n\Zb\Z4Default Credentials:\Zn\n$credentials"
# Add separator before menu options
msg+="\n\n$(translate "Choose how to run the script:"):"
dialog --clear --colors --backtitle "ProxMenux" --title "$name" --yesno "$msg\n\nExecute this script?" 22 85
if [[ $? -eq 0 ]]; then
download_script "$script_url"
echo
echo
declare -a MENU_OPTS=()
local idx=0
for s in "${script_infos[@]}"; do
local os script_url script_url_mirror script_name
os=$(decode "$s" ".os // empty")
[[ -z "$os" ]] && os="$(translate "default")"
script_name=$(decode "$s" ".name")
script_url=$(decode "$s" ".script_url")
script_url_mirror=$(decode "$s" ".script_url_mirror // empty")
if [[ -n "$desc" || -n "$notes" || -n "$credentials" ]]; then
echo -e "$TAB\e[1;36mScript Information:\e[0m"
MENU_OPTS+=("${idx}_GH" "$os | $script_name | GitHub")
if [[ -n "$notes" ]]; then
echo -e "$TAB\e[1;33mNotes:\e[0m"
while IFS= read -r line; do
[[ -z "$line" ]] && continue
echo -e "$TAB$line"
done <<< "$notes"
echo
fi
if [[ -n "$credentials" ]]; then
echo -e "$TAB\e[1;32mDefault Credentials:\e[0m"
echo "$TAB$credentials"
echo
fi
if [[ -n "$script_url_mirror" ]]; then
MENU_OPTS+=("${idx}_MR" "$os | $script_name | Mirror")
fi
msg_success "Press Enter to return to the main menu..."
read -r
RETURN_TO_MAIN=true
fi
}
((idx++))
done
local choice
choice=$(dialog --clear --colors --backtitle "ProxMenux" \
--title "$name" \
--menu "$msg" 28 80 6 \
"${MENU_OPTS[@]}" 3>&1 1>&2 2>&3)
if [[ $? -ne 0 || -z "$choice" ]]; then
RETURN_TO_MAIN=false
return
fi
local sel_idx sel_src
IFS="_" read -r sel_idx sel_src <<< "$choice"
local selected="${script_infos[$sel_idx]}"
local gh_url mirror_url
gh_url=$(decode "$selected" ".script_url")
mirror_url=$(decode "$selected" ".script_url_mirror // empty")
if [[ "$sel_src" == "GH" ]]; then
download_script "$gh_url"
elif [[ "$sel_src" == "MR" ]]; then
if [[ -n "$mirror_url" ]]; then
download_script "$mirror_url"
else
dialog --title "Helper Scripts" --msgbox "$(translate "Mirror URL not available for this script.")" 8 60
RETURN_TO_MAIN=false
return
fi
fi
echo
echo
if [[ -n "$desc" || -n "$notes" || -n "$credentials" ]]; then
echo -e "$TAB\e[1;36mScript Information:\e[0m"
if [[ -n "$notes" ]]; then
echo -e "$TAB\e[1;33mNotes:\e[0m"
while IFS= read -r line; do
[[ -z "$line" ]] && continue
echo -e "$TAB$line"
done <<< "$notes"
echo
fi
if [[ -n "$credentials" ]]; then
echo -e "$TAB\e[1;32mDefault Credentials:\e[0m"
echo "$TAB$credentials"
echo
fi
fi
msg_success "Press Enter to return to the main menu..."
read -r
RETURN_TO_MAIN=true
}
search_and_filter_scripts() {
local search_term=""
@@ -204,7 +252,7 @@ search_and_filter_scripts() {
fi
local count
count=$(echo "$filtered_json" | jq length)
count=$(echo "$filtered_json" | jq 'group_by(.slug) | length')
if [[ $count -eq 0 ]]; then
dialog --msgbox "No scripts found for: '$search_term'\n\nTry a different search term." 8 50
@@ -226,7 +274,7 @@ search_and_filter_scripts() {
menu_items+=("$i" "$entry")
((i++))
done < <(echo "$filtered_json" | jq -r '
sort_by(.name)[] | [.slug, .name, .type] | @tsv')
group_by(.slug) | map(.[0]) | sort_by(.name)[] | [.slug, .name, .type] | @tsv')
menu_items+=("" "")
menu_items+=("new_search" "New Search")
@@ -256,7 +304,7 @@ search_and_filter_scripts() {
"show_all")
search_term=""
filtered_json="$CACHE_JSON"
count=$(echo "$filtered_json" | jq length)
count=$(echo "$filtered_json" | jq 'group_by(.slug) | length')
continue
;;
"back"|"")
@@ -290,10 +338,9 @@ while true; do
SELECTED=$(dialog --backtitle "ProxMenux" --title "Proxmox VE Helper-Scripts" --menu \
"Select a category or search for scripts:" 20 70 14 \
"${MENU_ITEMS[@]}" 3>&1 1>&2 2>&3) || {
dialog --clear --title "Proxmox VE Helper-Scripts" \
dialog --clear --title "ProxMenux" \
--msgbox "\n\n$(translate "Visit the website to discover more scripts, stay updated with the latest updates, and support the project:")\n\nhttps://community-scripts.github.io/ProxmoxVE" 15 70
#clear
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")
exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"
}
if [[ "$SELECTED" == "search" ]]; then
@@ -312,8 +359,17 @@ while true; do
entry="$padded_name $label"
SCRIPTS+=("$i" "$entry")
((i++))
done < <(echo "$CACHE_JSON" | jq -r --argjson id "$SELECTED" \
'[.[] | select(.categories | index($id)) | {slug, name, type}] | sort_by(.name)[] | [.slug, .name, .type] | @tsv')
done < <(echo "$CACHE_JSON" | jq -r --argjson id "$SELECTED" '
[
.[]
| select(.categories | index($id))
| {slug, name, type}
]
| group_by(.slug)
| map(.[0])
| sort_by(.name)[]
| [.slug, .name, .type]
| @tsv')
SCRIPT_INDEX=$(dialog --colors --backtitle "ProxMenux" --title "Scripts in ${CATEGORY_NAMES[$SELECTED]}" --menu \
"Choose a script to execute:" 20 70 14 \
+6 -6
View File
@@ -4,12 +4,12 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.2
# Last Updated: 06/07/2025
# ==========================================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
@@ -70,7 +70,7 @@ confirm_automated_script() {
clear
if [ $response -eq 0 ]; then
bash <(curl -s $REPO_URL/scripts/post_install/auto_post_install.sh)
bash "$LOCAL_SCRIPTS/post_install/auto_post_install.sh"
else
msg_warn "$(translate "Cancelled by user.")"
sleep 1
@@ -80,9 +80,9 @@ confirm_automated_script() {
# ==========================================================
declare -a PROXMENUX_SCRIPTS=(
"Customizable post-installation script|ProxMenux|bash <(curl -s $REPO_URL/scripts/post_install/customizable_post_install.sh)"
"Customizable post-installation script|ProxMenux|bash \"$LOCAL_SCRIPTS/post_install/customizable_post_install.sh\""
"Automated post-installation script|ProxMenux|confirm_automated_script"
"Uninstall optimizations|ProxMenux|bash <(curl -s $REPO_URL/scripts/post_install/uninstall-tools.sh)"
"Uninstall optimizations|ProxMenux|bash \"$LOCAL_SCRIPTS/post_install/uninstall-tools.sh\""
)
@@ -168,7 +168,7 @@ show_menu() {
if [ $exit_status -ne 0 ] || [ "$script_selection" = "0" ]; then
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")
exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"
fi
+3 -3
View File
@@ -4,7 +4,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.1
# Last Updated: 08/07/2025
# ==========================================================
@@ -16,7 +16,7 @@
# Special thanks to @Andres_Eduardo_Rojas_Moya for contributing the persistent
# network naming function and for the original idea.
# Configuration ============================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
@@ -1082,7 +1082,7 @@ show_menu() {
if [ $exit_status -ne 0 ] || [ "$script_selection" = "0" ]; then
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")
exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"
fi
+13 -13
View File
@@ -10,7 +10,7 @@
# ==========================================================
# Configuration
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
@@ -43,7 +43,7 @@ while true; do
"h" "$(translate "Help & Info (commands)")" \
"0" "$(translate "Return to Main Menu")" \
2>&1 >/dev/tty
) || { exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh"); }
) || { exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"; }
case "$OPTION" in
@@ -52,37 +52,37 @@ while true; do
;;
1)
bash <(curl -s "$REPO_URL/scripts/share/nfs_host.sh")
bash "$LOCAL_SCRIPTS/share/nfs_host.sh"
;;
2)
bash <(curl -s "$REPO_URL/scripts/share/samba_host.sh")
bash "$LOCAL_SCRIPTS/share/samba_host.sh"
;;
3)
bash <(curl -s "$REPO_URL/scripts/share/local-shared-manager.sh")
bash "$LOCAL_SCRIPTS/share/local-shared-manager.sh"
;;
4)
bash <(curl -s "$REPO_URL/scripts/share/lxc-mount-manager_minimal.sh")
bash "$LOCAL_SCRIPTS/share/lxc-mount-manager_minimal.sh"
;;
5)
bash <(curl -s "$REPO_URL/scripts/share/nfs_client.sh")
bash "$LOCAL_SCRIPTS/share/nfs_client.sh"
;;
6)
bash <(curl -s "$REPO_URL/scripts/share/samba_client.sh")
bash "$LOCAL_SCRIPTS/share/samba_client.sh"
;;
7)
bash <(curl -s "$REPO_URL/scripts/share/nfs_lxc_server.sh")
bash "$LOCAL_SCRIPTS/share/nfs_lxc_server.sh"
;;
8)
bash <(curl -s "$REPO_URL/scripts/share/samba_lxc_server.sh")
bash "$LOCAL_SCRIPTS/share/samba_lxc_server.sh"
;;
h)
bash <(curl -s "$REPO_URL/scripts/share/commands_share.sh")
bash "$LOCAL_SCRIPTS/share/commands_share.sh"
;;
0)
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")
exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"
;;
*)
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")
exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"
;;
esac
done
+11 -12
View File
@@ -5,14 +5,14 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 28/01/2025
# ==========================================================
# Configuration ============================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
@@ -39,38 +39,37 @@ while true; do
case $OPTION in
1)
msg_info2 "$(translate "Running script: Add Disk Passthrough to a VM")..."
bash <(curl -s "$REPO_URL/scripts/storage/disk-passthrough.sh")
bash "$LOCAL_SCRIPTS/storage/disk-passthrough.sh"
;;
2)
msg_info2 "$(translate "Running script: Add Disk Passthrough to a CT")..."
bash <(curl -s "$REPO_URL/scripts/storage/disk-passthrough_ct.sh")
bash "$LOCAL_SCRIPTS/storage/disk-passthrough_ct.sh"
;;
3)
msg_info2 "$(translate "Running script: Import Disk Image to a VM")..."
bash <(curl -s "$REPO_URL/scripts/storage/import-disk-image.sh")
bash "$LOCAL_SCRIPTS/storage/import-disk-image.sh"
;;
4)
msg_info2 "$(translate "Running script: Mount point to CT")..."
bash <(curl -s "$REPO_URL/scripts/storage/mount-point-to-ct.sh")
bash "$LOCAL_SCRIPTS/storage/mount-point-to-ct.sh"
;;
5)
msg_info2 "$(translate "Running script: Mount disk on HOST")..."
bash <(curl -s "$REPO_URL/scripts/storage/mount-disk-on-host.sh")
bash "$LOCAL_SCRIPTS/storage/mount-disk-on-host.sh"
;;
6)
msg_info2 "$(translate "Running script: Unmount disk from HOST")..."
bash <(curl -s "$REPO_URL/scripts/storage/unmount-disk-from-host.sh")
bash "$LOCAL_SCRIPTS/storage/unmount-disk-from-host.sh"
;;
7)
msg_info2 "$(translate "Running script: Format disk")..."
bash <(curl -s "$REPO_URL/scripts/storage/format-disk.sh")
bash "$LOCAL_SCRIPTS/storage/format-disk.sh"
;;
8)
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")
exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"
;;
*)
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")
exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"
;;
esac
done
+7 -7
View File
@@ -5,14 +5,14 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.1
# Last Updated: 15/04/2025
# ==========================================================
# Configuration ============================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
@@ -36,19 +36,19 @@ while true; do
case $OPTION in
1)
bash <(curl -s "$REPO_URL/scripts/storage/disk-passthrough.sh")
bash "$LOCAL_SCRIPTS/storage/disk-passthrough.sh"
;;
2)
bash <(curl -s "$REPO_URL/scripts/storage/disk-passthrough_ct.sh")
bash "$LOCAL_SCRIPTS/storage/disk-passthrough_ct.sh"
;;
3)
bash <(curl -s "$REPO_URL/scripts/storage/import-disk-image.sh")
bash "$LOCAL_SCRIPTS/storage/import-disk-image.sh"
;;
4)
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")
exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"
;;
*)
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")
exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"
;;
esac
done
+10 -9
View File
@@ -5,14 +5,14 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 02/07/2025
# ==========================================================
# Configuration ============================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
@@ -36,13 +36,13 @@ initialize_cache
case $OPTION in
1)
bash <(curl -s "$REPO_URL/scripts/utilities/uup_dump_iso_creator.sh")
bash "$LOCAL_SCRIPTS/utilities/uup_dump_iso_creator.sh"
if [ $? -ne 0 ]; then
return
fi
;;
2)
bash <(curl -s "$REPO_URL/scripts/utilities/system_utils.sh")
bash "$LOCAL_SCRIPTS/utilities/system_utils.sh"
if [ $? -ne 0 ]; then
return
fi
@@ -64,19 +64,20 @@ initialize_cache
dialog_result=$?
if [[ $dialog_result -eq 0 ]]; then
bash <(curl -s "$REPO_URL/scripts/utilities/proxmox_update.sh")
bash "$LOCAL_SCRIPTS/utilities/proxmox_update.sh"
if [ $? -ne 0 ]; then
return
fi
fi
;;
4)
bash <(curl -s "$REPO_URL/scripts/utilities/upgrade_pve8_to_pve9.sh")
bash "$LOCAL_SCRIPTS/utilities/upgrade_pve8_to_pve9.sh"
if [ $? -ne 0 ]; then
return
fi
;;
5) exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh") ;;
*) exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh") ;;
5) exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh" ;;
*) exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh" ;;
esac
done
done

Some files were not shown because too many files have changed in this diff Show More