1035 Commits

Author SHA1 Message Date
MacRimi 62b200c5d9 Update CHANGELOG.md 2025-10-31 23:27:11 +01:00
MacRimi c2ed772f34 Update coral TPU pve9 2025-10-31 22:55:13 +01:00
MacRimi bbce6d4ad0 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-10-31 22:45:01 +01:00
MacRimi 45f6a0ec02 Update Install ProxMenux 2025-10-31 22:44:59 +01:00
github-actions[bot] 6b681278e0 Update AppImage build (2025-10-31 20:13:11) 2025-10-31 20:13:11 +00:00
MacRimi 2281ff06c7 Update GitHub Actions workflow permissions
Added permissions for write access to contents.
2025-10-31 21:10:47 +01:00
MacRimi 572a81fd4e Enhance workflow to include SHA256 checksum generation
Added steps to generate SHA256 checksum and upload AppImage.
2025-10-31 21:04:28 +01:00
MacRimi 5fd7df69fd Update update-pve9_2.sh 2025-10-31 20:47:46 +01:00
MacRimi 3401c6305e Update remove-banner-pve-v3.sh 2025-10-31 20:25:55 +01:00
MacRimi 269f9ac52c Update update-pve9_2.sh 2025-10-31 20:20:24 +01:00
MacRimi 4712171d43 Updsate Post Install 2025-10-31 20:16:42 +01:00
MacRimi 632c7b91f4 Update proxmox_update.sh 2025-10-31 20:09:52 +01:00
MacRimi f72dd79dff Update remove-banner-pve-v3.sh 2025-10-31 20:07:57 +01:00
MacRimi 4d109c0481 Update Post Install 2025-10-31 20:03:34 +01:00
MacRimi c046b77223 Update auto_post_install.sh 2025-10-31 18:44:30 +01:00
MacRimi 56ba3b5e5f Update auto_post_install.sh 2025-10-31 18:39:11 +01:00
MacRimi fa99247cb7 Update update-pve.sh 2025-10-31 18:35:49 +01:00
MacRimi 653fd37c08 Update update-pve.sh 2025-10-31 18:31:02 +01:00
MacRimi 9d053beafb Update Post Install menu 2025-10-31 18:28:11 +01:00
MacRimi f33c451a19 Update Post Install 2025-10-31 17:57:09 +01:00
MacRimi 9543148887 remove subscription banner V3 2025-10-31 17:49:28 +01:00
MacRimi 029ee4ed2f Update onboarding-carousel.tsx 2025-10-31 17:02:09 +01:00
MacRimi 688e826e9d Update onboarding-carousel.tsx 2025-10-30 23:30:58 +01:00
MacRimi deea0c54d4 Updarte AppImage 2025-10-30 23:14:00 +01:00
MacRimi 028f62aa9c Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-10-30 21:08:31 +01:00
MacRimi d3aef9c1d1 Update AppImage 2025-10-30 21:08:08 +01:00
ProxMenuxBot 72edce511a Update helpers_cache.json 2025-10-30 12:27:00 +00:00
MacRimi 37fade8f7a Update AppImage 2025-10-29 22:16:40 +01:00
MacRimi 0d2aa6738c Create ProxMenux-rc2.AppImage 2025-10-29 21:39:35 +01:00
MacRimi e3e0e5cba8 Create Heriberto.AppImage 2025-10-29 21:33:09 +01:00
MacRimi e40de189c3 Update flask_server.py 2025-10-29 21:28:23 +01:00
MacRimi c9c99f7b2a Create ProxMenux-rc.AppImage 2025-10-29 21:07:28 +01:00
MacRimi e6400918c8 Update AppImage 2025-10-29 20:57:06 +01:00
MacRimi d08557ad0e Update flask_server.py 2025-10-29 20:42:11 +01:00
MacRimi ba84dce7a7 Update build_appimage.sh 2025-10-29 20:29:43 +01:00
MacRimi 7044725bf1 Update AppImage 2025-10-29 20:27:39 +01:00
MacRimi 1812966fe6 Update AppImage 2025-10-29 20:22:55 +01:00
MacRimi a9bac25c9b Update proxmox-dashboard.tsx 2025-10-29 20:14:30 +01:00
MacRimi 5c967f11f0 Update AppImage 2025-10-29 20:03:17 +01:00
MacRimi 1b5f080495 Update system-logs.tsx 2025-10-29 19:45:32 +01:00
MacRimi 50a27fa3f6 Update virtual-machines.tsx 2025-10-29 19:22:53 +01:00
MacRimi f8ed53c1b9 Update virtual-machines.tsx 2025-10-29 19:04:01 +01:00
MacRimi 2163830a54 Update virtual-machines.tsx 2025-10-29 18:41:01 +01:00
MacRimi cae5e3b99f Update virtual-machines.tsx 2025-10-29 18:27:00 +01:00
MacRimi cc0a7941ea Update AppImage 2025-10-29 18:14:09 +01:00
MacRimi 18901c0e2d Update flask_server.py 2025-10-29 17:47:53 +01:00
ProxMenuxBot d4ea239185 Update helpers_cache.json 2025-10-29 12:29:04 +00:00
MacRimi 813c6aab13 Update AppImage 2025-10-28 23:18:32 +01:00
MacRimi f606e131a7 Update AppImage 2025-10-28 23:07:22 +01:00
MacRimi ccefa61b3d Update virtual-machines.tsx 2025-10-28 23:01:51 +01:00
MacRimi 606cae411f Update virtual-machines.tsx 2025-10-28 22:51:54 +01:00
MacRimi 901e4012cc Update AppImage 2025-10-28 22:45:15 +01:00
MacRimi d03b667194 Update AppImage 2025-10-28 22:12:57 +01:00
MacRimi d30954167e Update AppImage 2025-10-28 21:44:39 +01:00
MacRimi 0ee514ea15 Update flask_server.py 2025-10-28 19:59:37 +01:00
MacRimi 7e60792be8 Update flask_server.py 2025-10-28 19:38:56 +01:00
MacRimi 244a325394 Update flask_server.py 2025-10-28 19:07:08 +01:00
MacRimi 53df16a7ca Update flask_server.py 2025-10-28 18:48:33 +01:00
MacRimi 420576da09 Update flask_server.py 2025-10-28 18:45:31 +01:00
MacRimi d5a9d8ffdb Update flask_server.py 2025-10-28 18:40:11 +01:00
MacRimi 1873ad1a02 Update flask_server.py 2025-10-28 18:28:37 +01:00
MacRimi 9dec238f41 Update flask_server.py 2025-10-28 17:53:06 +01:00
MacRimi b93a018dc1 Update flask_server.py 2025-10-28 17:47:37 +01:00
ProxMenuxBot 6b1d5bf7db Update helpers_cache.json 2025-10-28 12:26:47 +00:00
ProxMenuxBot 4580866281 Update helpers_cache.json 2025-10-28 01:00:36 +00:00
ProxMenuxBot 5b743772ac Update helpers_cache.json 2025-10-27 18:19:45 +00:00
MacRimi 0ecf08e8e6 Update flask_server.py 2025-10-27 00:18:23 +01:00
MacRimi 7ca53d30b2 Update flask_server.py 2025-10-27 00:15:26 +01:00
MacRimi 18a0ba1981 Update flask_server.py 2025-10-27 00:03:26 +01:00
MacRimi 11a35ed589 Update flask_server.py 2025-10-26 23:58:02 +01:00
MacRimi eb5aa12d7f Update flask_server.py 2025-10-26 23:33:17 +01:00
MacRimi 1065b67073 Update flask_server.py 2025-10-26 23:17:18 +01:00
MacRimi 0d7b278003 Update flask_server.py 2025-10-26 22:58:56 +01:00
MacRimi 05093a9d49 Update AppImage 2025-10-26 22:53:13 +01:00
MacRimi 87f9b2b72c Update AppImage 2025-10-26 22:30:14 +01:00
MacRimi 2d4833d199 Update AppImage 2025-10-26 22:11:02 +01:00
MacRimi 610e08e690 Update AppImage 2025-10-26 21:42:12 +01:00
MacRimi 1192224c15 Update proxmox-dashboard.tsx 2025-10-26 21:30:48 +01:00
MacRimi 4c3a9928e7 Update metrics-dialog.tsx 2025-10-26 21:11:55 +01:00
MacRimi 433a4359e6 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-10-26 21:00:02 +01:00
MacRimi f1854b5120 Update AppImage 2025-10-26 20:59:59 +01:00
ProxMenuxBot 6961b0c2f5 Update helpers_cache.json 2025-10-26 18:17:36 +00:00
MacRimi 8b26f30e37 Create ProxMenux-beta7.AppImage 2025-10-26 18:28:54 +01:00
MacRimi 071724949f Update AppImage 2025-10-26 18:03:09 +01:00
MacRimi 28898aa1db Update network-metrics.tsx 2025-10-26 17:39:19 +01:00
MacRimi 11fae19e33 Update AppImage 2025-10-26 16:23:46 +01:00
MacRimi 13b9dd0262 Update network-metrics.tsx 2025-10-26 16:17:23 +01:00
MacRimi b47520c938 Update flask_server.py 2025-10-26 15:39:52 +01:00
MacRimi f6869b9e1c Update AppImage 2025-10-26 15:36:53 +01:00
MacRimi 96046a5d1f Update AppImage 2025-10-26 15:18:54 +01:00
MacRimi 524d0b278b Update network-metrics.tsx 2025-10-26 14:44:41 +01:00
MacRimi 6e2348eb06 Update AppImage 2025-10-26 14:31:10 +01:00
MacRimi e4b57e6ca3 Updae AppImage 2025-10-26 14:28:35 +01:00
MacRimi 9640e558cd Update AppImage 2025-10-26 14:25:23 +01:00
MacRimi 07b13d1374 Update network-traffic-chart.tsx 2025-10-26 14:17:22 +01:00
MacRimi 7e4389abd9 Update network-traffic-chart.tsx 2025-10-26 14:08:42 +01:00
MacRimi 0f424e7f0d Update network-traffic-chart.tsx 2025-10-26 13:31:14 +01:00
MacRimi 455e5735ff Update network-traffic-chart.tsx 2025-10-26 12:44:18 +01:00
MacRimi 6577d2ae3c Update AppImage 2025-10-26 12:32:40 +01:00
MacRimi 56ed543dfb Update AppImage 2025-10-26 12:17:22 +01:00
MacRimi 5549e3a398 Update network-metrics.tsx 2025-10-26 12:00:42 +01:00
MacRimi 2ff7c111af Update network-metrics.tsx 2025-10-26 11:49:13 +01:00
MacRimi a7af072ca7 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-10-26 11:41:20 +01:00
MacRimi a4a455f31e Update network-metrics.tsx 2025-10-26 11:41:18 +01:00
ProxMenuxBot 99d55b4314 Update helpers_cache.json 2025-10-26 01:05:33 +00:00
MacRimi 811e2155a6 Update flask_server.py 2025-10-26 01:46:12 +02:00
MacRimi ab7a49351d Update network-metrics.tsx 2025-10-26 00:34:05 +02:00
MacRimi efdfba0575 Update AppImage 2025-10-26 00:21:33 +02:00
MacRimi af9b5f6ca4 Update network-metrics.tsx 2025-10-25 23:44:54 +02:00
MacRimi 65144b9a3d Update AppImage 2025-10-25 23:39:23 +02:00
MacRimi 621f57d702 Update AppImage 2025-10-25 23:33:18 +02:00
MacRimi a0444fbeee Update node-metrics-charts.tsx 2025-10-25 22:57:21 +02:00
MacRimi cff81eea14 Update AppImage 2025-10-25 22:43:17 +02:00
MacRimi 5738c90721 Update network-metrics.tsx 2025-10-25 22:31:13 +02:00
MacRimi a229231c0c Update AppImage 2025-10-25 22:10:08 +02:00
MacRimi 6bf5bd97b5 Update network-metrics.tsx 2025-10-25 21:47:04 +02:00
MacRimi 35c50a7c60 Update network-metrics.tsx 2025-10-25 21:36:46 +02:00
MacRimi 042e6584eb Update network-metrics.tsx 2025-10-25 21:19:43 +02:00
MacRimi bcce1b7ea8 Update network-traffic-chart.tsx 2025-10-25 21:04:21 +02:00
MacRimi 73181f9e33 Update network-metrics.tsx 2025-10-25 20:55:24 +02:00
MacRimi b0a7b6c7cd Update AppImage 2025-10-25 18:47:24 +02:00
MacRimi 09744818dc Update AppImage 2025-10-25 18:09:48 +02:00
MacRimi f93b3109b9 Update uninstall-tools.sh 2025-10-25 17:45:57 +02:00
MacRimi 48d4836f0a Update auto_post_install.sh 2025-10-25 17:33:05 +02:00
MacRimi 5d4f70e943 Update auto_post_install.sh 2025-10-25 17:22:52 +02:00
ProxMenuxBot 9e05197a9a Update helpers_cache.json 2025-10-25 12:22:51 +00:00
MacRimi 11671e884d Update auto_post_install.sh 2025-10-25 11:44:00 +02:00
MacRimi dcce818678 Update post Install 2025-10-25 11:28:07 +02:00
MacRimi f6c23bc9a0 Update virtual-machines.tsx 2025-10-24 23:35:08 +02:00
MacRimi 15eca895fb Update virtual-machines.tsx 2025-10-24 23:28:10 +02:00
MacRimi d5d5dd7855 Update hardware.tsx 2025-10-24 23:15:53 +02:00
MacRimi c79f5fd8a5 Update hardware.tsx 2025-10-24 23:11:24 +02:00
MacRimi 409e40f3b7 Update hardware.tsx 2025-10-24 23:04:03 +02:00
MacRimi 67a83cb164 Update hardware.tsx 2025-10-24 22:57:06 +02:00
MacRimi 908bdc7c86 Update hardware.tsx 2025-10-24 22:50:03 +02:00
MacRimi b1c2bd3d64 Update hardware.tsx 2025-10-24 22:42:08 +02:00
MacRimi ffd317aff0 Update hardware.tsx 2025-10-24 22:34:29 +02:00
MacRimi 84a10afea1 Update network-metrics.tsx 2025-10-24 22:00:16 +02:00
MacRimi 32036ef64d Update AppImage 2025-10-24 21:54:34 +02:00
MacRimi a8b8036311 Update network-metrics.tsx 2025-10-24 21:40:19 +02:00
MacRimi b813716f7c Update network-metrics.tsx 2025-10-24 21:24:57 +02:00
MacRimi 7bd6061a59 Update network-metrics.tsx 2025-10-24 21:06:45 +02:00
MacRimi 7682a6e708 Update AppImage 2025-10-24 20:55:45 +02:00
MacRimi fe9c592107 Update network-metrics.tsx 2025-10-24 20:31:56 +02:00
MacRimi 9ff24dc446 Update network-metrics.tsx 2025-10-24 20:22:24 +02:00
MacRimi 3036711fb4 Update network-metrics.tsx 2025-10-24 20:08:33 +02:00
MacRimi 53363f293b Update network-metrics.tsx 2025-10-24 19:58:04 +02:00
MacRimi e9791984ee Update AppImage 2025-10-24 19:30:10 +02:00
MacRimi ddca96a60e Update AppImage 2025-10-24 19:20:37 +02:00
MacRimi be3607dd4d Update AppImage 2025-10-24 18:56:39 +02:00
MacRimi 6000a7a60f Update proxmox-dashboard.tsx 2025-10-24 18:39:00 +02:00
MacRimi cc64c9f9d8 Update install_coral_pve9.sh 2025-10-24 18:23:09 +02:00
MacRimi 5c699d956c Delete 0001-fix-apex-group-and-udev-rules.patch 2025-10-24 18:17:46 +02:00
MacRimi e1757e5ac5 Update 0001-fix-apex-group-and-udev-rules.patch 2025-10-24 18:11:33 +02:00
MacRimi 31167234be Update 0001-fix-apex-group-and-udev-rules.patch 2025-10-24 18:05:27 +02:00
MacRimi 807638ca04 Update coral TPU 2025-10-24 18:00:26 +02:00
MacRimi ff6b78252c Create 0001-fix-apex-group-and-udev-rules.patch .sh 2025-10-24 17:58:37 +02:00
MacRimi 93bdcaab7f Update auto_post_install.sh 2025-10-24 17:26:32 +02:00
MacRimi d06c580bbc Update AppImage 2025-10-24 17:17:14 +02:00
MacRimi a5b32b356c Create ProxMenux-beta6.AppImage 2025-10-23 22:44:55 +02:00
MacRimi d117e666fd Update proxmox-dashboard.tsx 2025-10-23 22:40:00 +02:00
MacRimi 09da94b2ab Update proxmox-dashboard.tsx 2025-10-23 22:32:43 +02:00
MacRimi 9ebf5919a2 Update proxmox-dashboard.tsx 2025-10-23 22:25:59 +02:00
MacRimi 9c46452a4d Update proxmox-dashboard.tsx 2025-10-23 22:16:02 +02:00
MacRimi b34536491b Update proxmox-dashboard.tsx 2025-10-23 22:10:21 +02:00
MacRimi a7c1e240c1 Update proxmox-dashboard.tsx 2025-10-23 22:00:44 +02:00
MacRimi 44618d3d73 Update proxmox-dashboard.tsx 2025-10-23 21:54:22 +02:00
MacRimi 1f92af64f0 Update proxmox-dashboard.tsx 2025-10-23 21:41:55 +02:00
MacRimi 5bcd081e88 Update proxmox-dashboard.tsx 2025-10-23 21:36:02 +02:00
MacRimi b141622e75 Update proxmox-dashboard.tsx 2025-10-23 21:31:21 +02:00
MacRimi f96bdee71d Update proxmox-dashboard.tsx 2025-10-23 21:28:00 +02:00
MacRimi 0af70d3298 Update proxmox-dashboard.tsx 2025-10-23 21:20:56 +02:00
MacRimi e044c59627 Update proxmox-dashboard.tsx 2025-10-23 21:13:17 +02:00
MacRimi cce1c902e5 Update proxmox-dashboard.tsx 2025-10-23 21:09:01 +02:00
MacRimi d845474644 Update proxmox-dashboard.tsx 2025-10-23 21:02:56 +02:00
MacRimi cd67aba2ad Update proxmox-dashboard.tsx 2025-10-23 20:56:11 +02:00
MacRimi 1e47162357 Update proxmox-dashboard.tsx 2025-10-23 20:48:52 +02:00
MacRimi 230400846f Update proxmox-dashboard.tsx 2025-10-23 20:35:58 +02:00
MacRimi ebb29ad04b Update proxmox-dashboard.tsx 2025-10-23 20:29:39 +02:00
MacRimi 2cd603357d Update proxmox-dashboard.tsx 2025-10-23 20:22:12 +02:00
MacRimi bee26838e1 Update proxmox-dashboard.tsx 2025-10-23 20:12:56 +02:00
MacRimi 5b0572879d Update proxmox-dashboard.tsx 2025-10-23 20:04:56 +02:00
MacRimi 6e86275dce Update proxmox-dashboard.tsx 2025-10-23 19:57:11 +02:00
MacRimi 03a007b9b6 Update AppImage 2025-10-23 19:47:26 +02:00
MacRimi dadb215ce0 Update AppImage 2025-10-23 19:39:24 +02:00
MacRimi a1e3e12c6b Update globals.css 2025-10-23 19:34:30 +02:00
MacRimi 4274c817d3 Update AppImage 2025-10-23 19:29:19 +02:00
MacRimi 5abedc15dc Update proxmox-dashboard.tsx 2025-10-23 18:41:37 +02:00
MacRimi 947c9639e8 Update proxmox-dashboard.tsx 2025-10-23 18:31:44 +02:00
MacRimi c4cdf4a834 Update AppImage 2025-10-23 18:22:00 +02:00
MacRimi 44b9bfee68 Update system-overview.tsx 2025-10-23 17:56:56 +02:00
MacRimi ac9d43892b Update system-overview.tsx 2025-10-23 17:33:48 +02:00
MacRimi 9f62f8eff9 Update AppImage 2025-10-23 17:21:48 +02:00
MacRimi 13dd400795 Update hardware.tsx 2025-10-23 15:31:25 +02:00
MacRimi 37343a4114 Update hardware.tsx 2025-10-23 14:58:46 +02:00
MacRimi 3df6a4048a Update hardware.tsx 2025-10-23 14:38:17 +02:00
MacRimi c9fb87b571 Update ProxMenux-beta5.AppImage 2025-10-23 13:27:36 +02:00
MacRimi 44ca1d507d Update metrics-dialog.tsx 2025-10-23 12:55:31 +02:00
MacRimi 30b236548a Update metrics-dialog.tsx 2025-10-23 12:47:28 +02:00
MacRimi 21b7d1c3fb Update virtual-machines.tsx 2025-10-23 12:36:48 +02:00
MacRimi 8d5ea66ecc Update network-metrics.tsx 2025-10-23 12:25:17 +02:00
MacRimi b5ed10689d Update node-metrics-charts.tsx 2025-10-23 12:13:22 +02:00
MacRimi a55cdfd7fa Update node-metrics-charts.tsx 2025-10-23 11:56:51 +02:00
MacRimi 39a4c10ac9 Update node-metrics-charts.tsx 2025-10-23 11:38:03 +02:00
MacRimi c542cd4d7d Update node-metrics-charts.tsx 2025-10-23 11:00:36 +02:00
MacRimi 01de338a65 Update AppImage 2025-10-23 09:59:27 +02:00
MacRimi f23f7b1983 Updata AppImage 2025-10-23 09:32:46 +02:00
MacRimi a349ab62ec Update node-metrics-charts.tsx 2025-10-22 20:02:32 +02:00
MacRimi e620010f10 Update AppImage 2025-10-22 19:50:26 +02:00
MacRimi 70509355de Uodate AppImage 2025-10-22 19:38:04 +02:00
MacRimi f25654ead7 Updte AppImage 2025-10-22 19:25:04 +02:00
MacRimi f1741d4dac Update AppImage 2025-10-22 19:07:18 +02:00
MacRimi f3245d092b Update virtual-machines.tsx 2025-10-22 18:49:41 +02:00
MacRimi a039a8600e Update virtual-machines.tsx 2025-10-22 18:38:04 +02:00
MacRimi ee56f4a7a2 Update virtual-machines.tsx 2025-10-22 18:27:27 +02:00
MacRimi c40b6ca7f4 Update AppImage 2025-10-22 18:17:57 +02:00
MacRimi 2c0e1e498b Update globals.css 2025-10-22 18:10:05 +02:00
MacRimi 0262ea31eb Update AppImage 2025-10-22 18:05:33 +02:00
MacRimi 4b671d7fb0 Update virtual-machines.tsx 2025-10-22 17:50:36 +02:00
MacRimi d8f9419eb9 Update virtual-machines.tsx 2025-10-22 17:34:46 +02:00
MacRimi aa65bab486 Update flask_server.py 2025-10-22 17:19:42 +02:00
MacRimi 849c3967fd Update virtual-machines.tsx 2025-10-22 17:03:27 +02:00
MacRimi 83562cf7d8 Update virtual-machines.tsx 2025-10-22 16:49:15 +02:00
MacRimi 2631a44410 Update AppImage 2025-10-22 16:15:49 +02:00
MacRimi ddfea43b79 Update virtual-machines.tsx 2025-10-22 12:50:53 +02:00
MacRimi 68f19ffa5f Update virtual-machines.tsx 2025-10-22 12:37:52 +02:00
MacRimi 8800d42c32 Update virtual-machines.tsx 2025-10-22 12:19:03 +02:00
MacRimi 416a8a7cb2 Update virtual-machines.tsx 2025-10-22 12:03:51 +02:00
MacRimi 7088f249a5 Update virtual-machines.tsx 2025-10-22 11:45:06 +02:00
MacRimi a2301cc980 Update virtual-machines.tsx 2025-10-22 11:24:56 +02:00
MacRimi af545404e8 Update virtual-machines.tsx 2025-10-22 11:09:46 +02:00
MacRimi 2e96f19476 Update virtual-machines.tsx 2025-10-22 10:59:20 +02:00
MacRimi e3f26b7f75 Update virtual-machines.tsx 2025-10-22 10:27:14 +02:00
MacRimi f45c98a6a7 Update AppImage 2025-10-22 09:28:46 +02:00
ProxMenuxBot a565a3c909 Update helpers_cache.json 2025-10-22 01:02:42 +00:00
MacRimi b5e3dd6c06 Update AppImage 2025-10-21 20:47:04 +02:00
MacRimi 928f592d9c Update virtual-machines.tsx 2025-10-21 20:31:15 +02:00
MacRimi 8ee8edcd36 Update AppImage 2025-10-21 20:12:00 +02:00
MacRimi 1e128348e5 Update AppImage 2025-10-21 19:50:17 +02:00
MacRimi 6ef5655b7d Update virtual-machines.tsx 2025-10-21 19:16:44 +02:00
MacRimi f6ba5329ce Update AppImage 2025-10-21 19:05:38 +02:00
MacRimi 93cef0d580 Update update-pve.sh 2025-10-21 18:56:33 +02:00
MacRimi 797b088cc8 aupdate AppImage 2025-10-21 18:43:56 +02:00
MacRimi 6d23d3510f Update AppImage 2025-10-21 18:04:35 +02:00
MacRimi f2d7d0af43 Update AppImage 2025-10-21 17:57:05 +02:00
MacRimi e55c0461db Update virtual-machines.tsx 2025-10-21 17:45:12 +02:00
MacRimi 8eca511a53 Uodate AppImage 2025-10-21 17:33:53 +02:00
MacRimi f20e46dee0 Update AppImage 2025-10-21 17:20:16 +02:00
MacRimi b79f22f4fe Add log directories for pveproxy with permissions
Create directories for pveproxy logs and set permissions
2025-10-21 14:26:37 +02:00
MacRimi 3287dc77e2 Update auto_post_install.sh 2025-10-21 14:24:41 +02:00
MacRimi 78a08b35e7 Update auto_post_install.sh 2025-10-21 14:06:10 +02:00
MacRimi e86196999a Update auto_post_install.sh 2025-10-21 13:57:32 +02:00
MacRimi 4d50339041 Update journald configuration in auto_post_install.sh 2025-10-21 13:56:37 +02:00
MacRimi edc5a2c0f2 Enhance Log2RAM installation script
Refactor Log2RAM installation and configuration script to improve error handling, cleanup previous installations, and adjust systemd-journald limits based on Log2RAM size.
2025-10-21 09:29:58 +02:00
MacRimi 598b88b1f0 Update virtual-machines.tsx 2025-10-20 23:48:07 +02:00
MacRimi c22b9f8ff5 Update AppImage 2025-10-20 23:30:18 +02:00
MacRimi 3c654ab495 Update virtual-machines.tsx 2025-10-20 23:24:38 +02:00
MacRimi 2f0fabea7a Update virtual-machines.tsx 2025-10-20 23:16:56 +02:00
MacRimi 099b14efc3 Update virtual-machines.tsx 2025-10-20 23:02:52 +02:00
MacRimi ee42dee366 Update virtual-machines.tsx 2025-10-20 22:40:37 +02:00
MacRimi deae081cb3 Update AppImage 2025-10-20 22:15:08 +02:00
MacRimi 6479f14d3d Update AppImage 2025-10-20 20:56:54 +02:00
MacRimi 60707c3868 Update create VM 2025-10-20 20:22:31 +02:00
MacRimi e78d8e1ae6 Update metrics-dialog.tsx 2025-10-20 20:17:46 +02:00
MacRimi fee0d0aed9 Update metrics-dialog.tsx 2025-10-20 20:00:29 +02:00
MacRimi 178abc77ce Update metrics-dialog.tsx 2025-10-20 19:40:59 +02:00
MacRimi 7001f97d96 Update vm_creator.sh 2025-10-20 19:20:50 +02:00
MacRimi 55432e61ff Update metrics-dialog.tsx 2025-10-20 19:00:00 +02:00
MacRimi 4ee993ef3b Update metrics-dialog.tsx 2025-10-20 18:39:12 +02:00
MacRimi 64d471bb9b Update metrics-dialog.tsx 2025-10-20 17:30:39 +02:00
MacRimi 4dfcdcb0b2 Update metrics-dialog.tsx 2025-10-20 17:11:30 +02:00
MacRimi 8e69a84e7a Update metrics-dialog.tsx 2025-10-20 16:52:16 +02:00
MacRimi 8ec643d882 Update metrics-dialog.tsx 2025-10-20 16:43:07 +02:00
ProxMenuxBot f4d6192c80 Update helpers_cache.json 2025-10-20 12:28:47 +00:00
MacRimi c0aa2b85fc Update metrics-dialog.tsx 2025-10-19 17:53:39 +02:00
MacRimi 61a376fb6d Update virtual-machines.tsx 2025-10-19 17:46:47 +02:00
MacRimi 8786cb5180 Update AppImage 2025-10-19 17:40:13 +02:00
MacRimi c305ef1360 Update AppImage 2025-10-19 17:29:23 +02:00
MacRimi f662ce0b7a Update AppImage 2025-10-19 17:16:35 +02:00
MacRimi 71af9345a5 Update AppImage 2025-10-19 16:51:52 +02:00
MacRimi a819a19c77 Update virtual-machines.tsx 2025-10-19 16:19:47 +02:00
MacRimi c65fad06b7 Update virtual-machines.tsx 2025-10-19 16:06:19 +02:00
MacRimi 9e6e1931b1 Update customizable_post_install.sh 2025-10-19 09:48:38 +02:00
MacRimi 3b22273f5a Update virtual-machines.tsx 2025-10-18 18:57:14 +02:00
MacRimi fc5ff1782b Update virtual-machines.tsx 2025-10-18 18:48:01 +02:00
MacRimi 4d3b3d984d Update virtual-machines.tsx 2025-10-18 18:37:22 +02:00
MacRimi 514976561f Update hardware.tsx 2025-10-18 18:32:13 +02:00
MacRimi fb4998d21b Update auto_post_install.sh 2025-10-18 18:24:05 +02:00
MacRimi 0feec978d3 Update system-logs.tsx 2025-10-18 18:18:57 +02:00
MacRimi 5f1c39aba5 Update system-logs.tsx 2025-10-18 18:04:05 +02:00
MacRimi 17973619de Update system-logs.tsx 2025-10-18 17:54:06 +02:00
MacRimi 478d7a2d2d Update system-logs.tsx 2025-10-18 17:45:13 +02:00
MacRimi f2af0be1e1 Update system-logs.tsx 2025-10-18 17:36:08 +02:00
MacRimi d0725f5098 Update system-logs.tsx 2025-10-18 17:18:41 +02:00
MacRimi 646d614d94 Update update-pve.sh 2025-10-18 17:12:11 +02:00
MacRimi 4c337ef5e9 Update system-logs.tsx 2025-10-18 17:02:16 +02:00
MacRimi 2b633b8566 Update system-logs.tsx 2025-10-18 16:47:04 +02:00
MacRimi 6b16454217 Update system-logs.tsx 2025-10-18 16:36:31 +02:00
MacRimi b7086deeac Update zimaos.sh 2025-10-18 16:16:48 +02:00
MacRimi f021afb6a4 Update AppImage and ZimaOS 2025-10-18 16:11:28 +02:00
ProxMenuxBot 99622bd3d6 Update helpers_cache.json 2025-10-18 12:23:38 +00:00
MacRimi 50a76519ea Update system-logs.tsx 2025-10-18 12:39:26 +02:00
MacRimi 1e806054ab Update system-logs.tsx 2025-10-18 12:26:27 +02:00
MacRimi 89d7f335fc Update system-logs.tsx 2025-10-18 11:58:01 +02:00
MacRimi 7a664ec4ec Update system-logs.tsx 2025-10-18 11:41:49 +02:00
MacRimi a8a4d029f8 Update system-logs.tsx 2025-10-18 11:35:14 +02:00
MacRimi 501b5dce76 Update system-logs.tsx 2025-10-18 11:25:16 +02:00
MacRimi e9b3504370 Update system-logs.tsx 2025-10-18 11:13:01 +02:00
MacRimi 7b20c78e73 Update system-logs.tsx 2025-10-18 11:05:27 +02:00
MacRimi a343ce69aa Update system-logs.tsx 2025-10-18 10:58:08 +02:00
MacRimi d52ce400fb Update AppImage 2025-10-18 10:43:58 +02:00
MacRimi 0ee574eaaa Update system-logs.tsx 2025-10-18 10:34:34 +02:00
MacRimi 74c4392b6d Update AppImage 2025-10-18 10:28:04 +02:00
MacRimi d844c330e9 Update system-logs.tsx 2025-10-18 10:11:43 +02:00
MacRimi b50cb78fa6 Update system-logs.tsx 2025-10-18 09:56:53 +02:00
MacRimi 26c138f42c Update AppImage 2025-10-17 20:32:17 +02:00
MacRimi 0ec7e65926 Update proxmox-dashboard.tsx 2025-10-17 20:27:02 +02:00
MacRimi 3588cc4c03 Update proxmox-dashboard.tsx 2025-10-17 20:19:37 +02:00
MacRimi da8c7749c8 Update proxmox-dashboard.tsx 2025-10-17 20:09:26 +02:00
MacRimi 79fe999e77 Update proxmox-dashboard.tsx 2025-10-17 20:01:10 +02:00
MacRimi 439c65ad6d Update AppImage 2025-10-17 19:51:41 +02:00
MacRimi 81b3aa5ac1 Update system-logs.tsx 2025-10-17 19:44:19 +02:00
MacRimi c4beb9ae4d Update hardware.tsx 2025-10-17 19:34:35 +02:00
MacRimi 9d286d8378 Update network-metrics.tsx 2025-10-17 19:24:14 +02:00
MacRimi 19e7a43fe3 Update network-metrics.tsx 2025-10-17 19:16:55 +02:00
MacRimi ab59e2deac Update AppImage 2025-10-17 19:06:15 +02:00
MacRimi 043f22e6ec Update AppImage 2025-10-17 18:52:18 +02:00
MacRimi 4abb6af31e Update AppImagen 2025-10-17 18:30:18 +02:00
MacRimi a17ba4a81f Update virtual-machines.tsx 2025-10-17 18:15:46 +02:00
MacRimi bc8a6847e3 Update virtual-machines.tsx 2025-10-17 18:10:12 +02:00
MacRimi 18f97f9df2 Update AppImage 2025-10-17 18:03:01 +02:00
MacRimi 4a204d8d89 Update AppImage 2025-10-17 17:53:06 +02:00
MacRimi 062c6c2364 Update AppImage 2025-10-17 17:38:13 +02:00
MacRimi 477716ef67 Update virtual-machines.tsx 2025-10-17 17:22:10 +02:00
MacRimi ef973df7c9 Update flask_server.py 2025-10-17 17:04:55 +02:00
MacRimi c40bb6a4d5 Create rafa.AppImage 2025-10-16 21:29:45 +02:00
MacRimi 20e942dccd Update storage-overview.tsx 2025-10-16 21:19:03 +02:00
MacRimi 598cbc4d11 Update AppImage 2025-10-16 19:57:55 +02:00
MacRimi 70a3d5af07 Update flask_server.py 2025-10-16 19:34:45 +02:00
MacRimi 9cabb1afbd Update hardware.tsx 2025-10-16 19:23:41 +02:00
MacRimi 4267224f59 Create ProxMenux-beta5.AppImage 2025-10-16 09:41:04 +02:00
MacRimi 02d910f53c Update flask_server.py 2025-10-16 09:31:35 +02:00
MacRimi e81b7b5b9f Update AppImage 2025-10-16 09:07:39 +02:00
ProxMenuxBot 17e0a8eec1 Update helpers_cache.json 2025-10-16 01:00:35 +00:00
MacRimi eb322c9d41 Create ProxMenux-beta4.AppImage 2025-10-15 20:51:42 +02:00
MacRimi 751af92c21 Update proxmox-dashboard.tsx 2025-10-15 20:42:51 +02:00
MacRimi fb4962a41a Update proxmox-dashboard.tsx 2025-10-15 20:35:10 +02:00
MacRimi daf6598599 Update AppImage 2025-10-15 20:27:30 +02:00
MacRimi e1e4f71f3a Update virtual-machines.tsx 2025-10-15 20:13:01 +02:00
MacRimi a2fa7ec9c4 Update virtual-machines.tsx 2025-10-15 19:51:24 +02:00
MacRimi 5cd37b74b4 Update flask_server.py 2025-10-15 19:22:16 +02:00
MacRimi beed7e83f2 Update storage-overview.tsx 2025-10-15 19:06:33 +02:00
MacRimi a7726edca6 Update storage-overview.tsx 2025-10-15 18:56:02 +02:00
MacRimi eed0c21c41 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-10-15 18:41:21 +02:00
MacRimi 094a43157e Update storage-overview.tsx 2025-10-15 18:41:03 +02:00
MacRimi 4033958bf5 Merge pull request #37 from MrCaringi/main
🌶️ Contributors rocks! 🌶️
2025-10-15 18:23:37 +02:00
MacRimi f18784ecc1 Update flask_server.py 2025-10-15 18:22:04 +02:00
JFC 418494b7f3 Merge pull request #1 from MrCaringi/MrCaringi-contributors
Update README.md
2025-10-15 10:20:16 -06:00
JFC 343753ee2a Update README.md 2025-10-15 10:18:43 -06:00
MacRimi 1ceda066c4 Update hardware.tsx 2025-10-15 18:08:49 +02:00
MacRimi 3c13dea55f Update hardware.tsx 2025-10-15 17:54:04 +02:00
MacRimi 6ced2cbf7c Update flask_server.py 2025-10-15 17:24:06 +02:00
MacRimi b41f16a736 Update storage-overview.tsx 2025-10-15 17:04:10 +02:00
MacRimi 42fa02a887 Create ProxMenux-beta3.AppImage 2025-10-15 00:09:51 +02:00
MacRimi 2ce87cdac0 Update AppImage 2025-10-15 00:03:56 +02:00
MacRimi e4864d3871 Update AppImage 2025-10-14 23:57:46 +02:00
MacRimi b6e0052013 Update storage-overview.tsx 2025-10-14 23:42:00 +02:00
MacRimi 9ef83a59c7 Update flask_server.py 2025-10-14 23:21:09 +02:00
MacRimi 325724ff85 Update flask_server.py 2025-10-14 23:16:56 +02:00
MacRimi 998bfa0656 Update flask_server.py 2025-10-14 22:47:13 +02:00
MacRimi e476df5e7d Update flask_server.py 2025-10-14 22:36:50 +02:00
MacRimi 83a3601cdb Update flask_server.py 2025-10-14 22:28:28 +02:00
MacRimi 996dcc4b23 Update AppImage 2025-10-14 22:14:48 +02:00
MacRimi 04304f8283 Update storage-overview.tsx 2025-10-14 21:31:15 +02:00
MacRimi 3418f73390 Update storage-overview.tsx 2025-10-14 21:22:21 +02:00
MacRimi b07b6c8960 Update storage-overview.tsx 2025-10-14 21:11:34 +02:00
MacRimi 4f2cf37d73 Update storage-overview.tsx 2025-10-14 20:54:41 +02:00
MacRimi 408e017f2f Update virtual-machines.tsx 2025-10-14 20:31:53 +02:00
MacRimi 62b42266e8 Update virtual-machines.tsx 2025-10-14 20:22:55 +02:00
MacRimi 80e9e23965 Update virtual-machines.tsx 2025-10-14 20:04:18 +02:00
MacRimi c73154aeb1 Update virtual-machines.tsx 2025-10-14 19:48:57 +02:00
MacRimi 66c4786ec2 Update virtual-machines.tsx 2025-10-14 19:35:25 +02:00
MacRimi 143f5a2085 Update hardware.tsx 2025-10-14 19:00:24 +02:00
MacRimi 699c7df798 Update AppImage 2025-10-14 18:51:39 +02:00
MacRimi d3de7b95aa Update flask_server.py 2025-10-14 18:09:48 +02:00
MacRimi 2dc6f76da9 Update hardware.tsx 2025-10-14 17:38:26 +02:00
MacRimi 63ccf6b553 Update hardware.tsx 2025-10-14 17:23:59 +02:00
MacRimi f49ffe3cb0 Update AppImage 2025-10-14 15:34:19 +02:00
MacRimi c1b578350d Update flask_server.py 2025-10-14 15:19:18 +02:00
MacRimi 48e4af41ae Update flask_server.py 2025-10-14 14:28:08 +02:00
MacRimi 5e915f9c40 Update flask_server.py 2025-10-14 13:56:37 +02:00
MacRimi 70b5f91f82 Update AppImage 2025-10-14 13:37:48 +02:00
MacRimi 792df08c78 Update hardware.tsx 2025-10-14 12:58:53 +02:00
MacRimi 032b5d3580 Update AppImage 2025-10-14 12:40:27 +02:00
MacRimi 8f93e43bb3 Update AppImage 2025-10-14 11:37:30 +02:00
MacRimi 04f95e648b Update AppImage 2025-10-14 11:08:47 +02:00
MacRimi 511b8eb407 Update storage-metrics.tsx 2025-10-14 10:47:30 +02:00
MacRimi a6e6dd255d Update storage-metrics.tsx 2025-10-14 10:25:18 +02:00
MacRimi c3c53d4056 Update storage-metrics.tsx 2025-10-14 09:33:03 +02:00
MacRimi 2f700d9a4c Update AppImage 2025-10-14 09:15:44 +02:00
MacRimi e4da9f5afe Update storage-metrics.tsx 2025-10-14 09:06:17 +02:00
MacRimi 3f919813f2 Update AppImage 2025-10-14 08:54:56 +02:00
MacRimi 4063ffc163 Update storage-metrics.tsx 2025-10-14 00:20:15 +02:00
MacRimi 0d08b89853 Update storage-metrics.tsx 2025-10-14 00:13:10 +02:00
MacRimi 6c9da364d0 Update storage-metrics.tsx 2025-10-14 00:03:22 +02:00
MacRimi c1614e8241 Update AppImage 2025-10-13 23:50:31 +02:00
MacRimi 75a458f2be Update AppImage 2025-10-13 19:04:38 +02:00
MacRimi 602291736f Update flask_server.py 2025-10-13 17:53:34 +02:00
MacRimi 9f2d15e590 Update AppImage 2025-10-13 17:40:08 +02:00
MacRimi 5e88201d47 Update flask_server.py 2025-10-13 17:03:39 +02:00
MacRimi 598b8bd1cd Update AppImage 2025-10-13 15:22:19 +02:00
MacRimi 9186a44860 Update AppImage 2025-10-13 15:06:03 +02:00
MacRimi 61e3dae708 Update AppImage 2025-10-12 21:19:01 +02:00
MacRimi 94d46299d0 Update proxmox-dashboard.tsx 2025-10-12 21:10:24 +02:00
MacRimi 7070c05f2f Update proxmox-dashboard.tsx 2025-10-12 21:00:42 +02:00
MacRimi 0b7038cc65 Update proxmox-dashboard.tsx 2025-10-12 20:47:58 +02:00
MacRimi 4882d04ece Update system-logs.tsx 2025-10-12 20:36:12 +02:00
MacRimi 4b2d34491e Update system-logs.tsx 2025-10-12 20:28:10 +02:00
MacRimi 79d8230821 Update system-logs.tsx 2025-10-12 20:20:32 +02:00
MacRimi c7e3305a76 Update system-logs.tsx 2025-10-12 20:12:55 +02:00
MacRimi 81feccf0d2 Update AppImage 2025-10-12 20:03:40 +02:00
MacRimi 9666bee006 Update AppImage 2025-10-12 19:51:03 +02:00
MacRimi 44d54057d0 Update hardware.tsx 2025-10-12 19:40:35 +02:00
MacRimi beb4251688 Update network-metrics.tsx 2025-10-12 19:32:17 +02:00
MacRimi 598395cd38 Update system-overview.tsx 2025-10-12 19:20:05 +02:00
MacRimi 0a6913f5d0 Update system-overview.tsx 2025-10-12 19:11:39 +02:00
MacRimi fa36458303 Update AppImage 2025-10-12 18:51:38 +02:00
MacRimi 3f96f88027 Update system-overview.tsx 2025-10-12 18:19:50 +02:00
MacRimi 131ab714ba Update system-overview.tsx 2025-10-12 18:05:35 +02:00
MacRimi 333d0c933a Update system-overview.tsx 2025-10-12 17:50:15 +02:00
MacRimi 2a5c0e05cc Update flask_server.py 2025-10-12 17:37:51 +02:00
MacRimi 4bda9da860 Update AppImage 2025-10-12 17:24:13 +02:00
MacRimi 4c579cf862 Update flask_server.py 2025-10-12 17:09:01 +02:00
MacRimi 1b74ce7ac0 Update flask_server.py 2025-10-12 16:32:29 +02:00
MacRimi 29e3625c7b Update flask_server.py 2025-10-12 16:09:38 +02:00
MacRimi a41b9381a1 Update system-logs.tsx 2025-10-12 16:00:52 +02:00
MacRimi b4980a968c Update system-logs.tsx 2025-10-12 15:39:31 +02:00
MacRimi ea91751217 Update system-logs.tsx 2025-10-12 14:51:07 +02:00
MacRimi 1ceffc3391 Update system-logs.tsx 2025-10-12 03:12:15 +02:00
MacRimi 3de31427a3 Update system-logs.tsx 2025-10-12 02:58:11 +02:00
MacRimi 4abb9c2ea6 Update system-logs.tsx 2025-10-12 02:44:49 +02:00
MacRimi e7bfbe77c2 Update system-logs.tsx 2025-10-12 02:04:42 +02:00
MacRimi 776282ed6b Update system-logs.tsx 2025-10-12 01:46:48 +02:00
MacRimi d1621684df Update flask_server.py 2025-10-12 01:26:58 +02:00
MacRimi ba183e71e1 Update AppImage 2025-10-12 01:09:33 +02:00
MacRimi aac34d4fad Update system-logs.tsx 2025-10-12 00:54:23 +02:00
MacRimi 8e28e4ecbf Update AppImage 2025-10-12 00:45:38 +02:00
MacRimi 48665aa1ad Update AppImage 2025-10-12 00:41:15 +02:00
MacRimi f34968bcf5 Update calendar.tsx 2025-10-12 00:35:47 +02:00
MacRimi 4a5c1ed582 Update AppImage 2025-10-11 19:43:15 +02:00
MacRimi 6d87ab08e2 Update AppImage 2025-10-11 19:35:04 +02:00
MacRimi 5ae18bf4f9 Update calendar.tsx 2025-10-11 19:24:20 +02:00
MacRimi 1bac12259d Update system-logs.tsx 2025-10-11 19:16:44 +02:00
MacRimi 434dc408c3 Update calendar.tsx 2025-10-11 19:09:24 +02:00
MacRimi 6601ee3b12 Update AppImage 2025-10-11 19:04:02 +02:00
MacRimi fde1731365 Update AppImage 2025-10-11 18:51:50 +02:00
MacRimi 7725952776 Update AppImage 2025-10-11 18:37:26 +02:00
MacRimi e18ee08b70 Update AppImge 2025-10-11 18:13:35 +02:00
MacRimi 5aaaeb426c Update AppImage 2025-10-11 17:55:25 +02:00
MacRimi 1f55a0cbd8 Update AppImage 2025-10-11 17:34:10 +02:00
MacRimi 4ad026b398 Update flask_server.py 2025-10-11 17:29:17 +02:00
MacRimi d36825da52 Update AppImage 2025-10-11 17:18:52 +02:00
MacRimi bf2715c2be Update AppImage 2025-10-11 16:51:27 +02:00
MacRimi 80953a0148 Update AppImage 2025-10-11 16:25:22 +02:00
MacRimi bb9a08d00d Update system-logs.tsx 2025-10-11 16:04:42 +02:00
MacRimi 3e8fa7cba7 Update system-logs.tsx 2025-10-11 12:06:59 +02:00
MacRimi da8b88b6b2 Update flask_server.py 2025-10-11 11:42:02 +02:00
MacRimi 4bd21a1ccb Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-10-11 11:30:47 +02:00
MacRimi 29f7586b93 Update AppImage 2025-10-11 11:30:45 +02:00
ProxMenuxBot 39b6b725f5 Update helpers_cache.json 2025-10-11 00:56:17 +00:00
MacRimi 631c68029a Update AppImage 2025-10-11 01:36:42 +02:00
MacRimi 93aefc127f Update storage-overview.tsx 2025-10-11 00:21:22 +02:00
MacRimi 19ac88b560 Update flask_server.py 2025-10-11 00:11:12 +02:00
MacRimi 99e775a283 Update hardware.tsx 2025-10-11 00:00:21 +02:00
MacRimi 7fc05b96a2 Update flask_server.py 2025-10-10 23:52:06 +02:00
MacRimi 9b811da43d Update hardware.tsx 2025-10-10 23:39:31 +02:00
MacRimi 4199b609b4 Updata AppImage 2025-10-10 23:19:29 +02:00
MacRimi 958e6d8519 Update flask_server.py 2025-10-10 23:06:04 +02:00
MacRimi 9dea22ab05 Update hardware.tsx 2025-10-10 22:52:22 +02:00
MacRimi 3dc7c6b36f Update flask_server.py 2025-10-10 22:43:02 +02:00
MacRimi 44e76f36b4 Update flask_server.py 2025-10-10 22:33:45 +02:00
MacRimi 010333a190 Update flask_server.py 2025-10-10 21:43:43 +02:00
MacRimi ba833a265a Update AppImage 2025-10-10 21:38:19 +02:00
MacRimi e0bf156272 Update hardware.tsx 2025-10-10 21:18:49 +02:00
MacRimi a654e21b27 Update flask_server.py 2025-10-10 21:06:00 +02:00
MacRimi de6f149e3b Update flask_server.py 2025-10-10 17:37:30 +02:00
MacRimi 29893b89b3 Update flask_server.py 2025-10-10 17:09:23 +02:00
MacRimi 7783e9ed20 Update flask_server.py 2025-10-10 16:52:43 +02:00
MacRimi d93d1ed48a Update AppImage 2025-10-10 16:17:55 +02:00
MacRimi 32c461e93b Update AppImage 2025-10-10 15:40:41 +02:00
MacRimi d88e6153c1 Update flask_server.py 2025-10-10 12:45:08 +02:00
MacRimi 7b980ae4d4 Update flask_server.py 2025-10-10 12:40:58 +02:00
MacRimi b249d37bab Update flask_server.py 2025-10-10 12:29:47 +02:00
MacRimi fa34e081cc Update hardware.tsx 2025-10-10 01:06:17 +02:00
MacRimi 9f795d7256 Update flask_server.py 2025-10-10 00:48:56 +02:00
MacRimi c8d7d6be43 Update hardware.tsx 2025-10-10 00:38:57 +02:00
MacRimi c31124eb14 Update hardware.tsx 2025-10-10 00:27:22 +02:00
MacRimi e999b7a8f8 Update hardware.tsx 2025-10-10 00:13:54 +02:00
MacRimi 49353a5ec5 Update hardware.tsx 2025-10-09 23:56:43 +02:00
MacRimi 229fbdd306 Update hardware.tsx 2025-10-09 23:46:26 +02:00
MacRimi 4562dd08dc Update hardware.tsx 2025-10-09 23:34:44 +02:00
MacRimi f24f4ea8f9 Update hardware.tsx 2025-10-09 23:14:47 +02:00
MacRimi 6338d38ab6 Update flask_server.py 2025-10-09 23:00:25 +02:00
MacRimi 527d93c6b4 Update flask_server.py 2025-10-09 22:48:33 +02:00
MacRimi 3f5f8d9f57 Update flask_server.py 2025-10-09 22:37:02 +02:00
MacRimi 245c913ba1 Update flask_server.py 2025-10-09 22:17:09 +02:00
MacRimi 7d3ef52f03 Update flask_server.py 2025-10-09 21:16:55 +02:00
MacRimi 6cd7556bc5 Update flask_server.py 2025-10-09 20:58:43 +02:00
MacRimi 123f0594a3 Update flask_server.py 2025-10-09 20:40:29 +02:00
MacRimi 652cebc7d0 Update flask_server.py 2025-10-09 20:26:21 +02:00
MacRimi a4cb9a8923 Update flask_server.py 2025-10-09 20:18:49 +02:00
MacRimi eb954fb10d Update flask_server.py 2025-10-09 19:52:44 +02:00
MacRimi 845eab6f53 Update flask_server.py 2025-10-09 19:38:54 +02:00
MacRimi 4fe20db497 Update AppImage 2025-10-09 19:30:12 +02:00
MacRimi c40d503f6e Update flask_server.py 2025-10-09 19:24:39 +02:00
MacRimi 1ea843bde4 Update flask_server.py 2025-10-09 19:21:32 +02:00
MacRimi 9ed5d70250 Update flask_server.py 2025-10-09 19:08:43 +02:00
MacRimi f6209b97e2 Update AppImage 2025-10-09 19:00:58 +02:00
MacRimi 5221ad6da7 Update flask_server.py 2025-10-09 18:46:16 +02:00
MacRimi cd0bded428 Update flask_server.py 2025-10-09 18:09:59 +02:00
MacRimi ff5fddf353 Update flask_server.py 2025-10-09 18:01:46 +02:00
MacRimi 28b29ed086 Update flask_server.py 2025-10-09 17:03:01 +02:00
MacRimi 765b2b1d69 Update flask_server.py 2025-10-09 16:46:07 +02:00
MacRimi 599a434faa Update flask_server.py 2025-10-09 16:23:19 +02:00
MacRimi a2abee986d Update flask_server.py 2025-10-09 16:06:42 +02:00
MacRimi d57c0712b0 Update flask_server.py 2025-10-09 15:44:17 +02:00
MacRimi 4166d78e87 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-10-09 15:35:43 +02:00
MacRimi 5322291402 Update flask_server.py 2025-10-09 15:35:40 +02:00
ProxMenuxBot dc2ffd758d Update helpers_cache.json 2025-10-09 12:26:29 +00:00
MacRimi b73cdb2e7d Update flask_server.py 2025-10-09 10:29:21 +02:00
MacRimi 3a83e5d519 Update flask_server.py 2025-10-09 10:00:06 +02:00
MacRimi 3c0cdcadc0 Update flask_server.py 2025-10-09 09:48:18 +02:00
MacRimi 0a23ef8b5d Update flask_server.py 2025-10-08 18:26:28 +02:00
MacRimi c8a38ac709 Update flask_server.py 2025-10-08 18:19:19 +02:00
MacRimi 31d15fadbe Update flask_server.py 2025-10-08 18:15:38 +02:00
MacRimi 3fc481e302 Update hardware.tsx 2025-10-08 12:06:24 +02:00
MacRimi 803605c318 Update AppImage 2025-10-08 11:43:45 +02:00
MacRimi 1d138e3b4b Update flask_server.py 2025-10-07 23:42:15 +02:00
MacRimi 5b6c5326b6 Update AppImage 2025-10-07 23:36:13 +02:00
MacRimi 60a1c303da Update hardware.tsx 2025-10-07 23:04:44 +02:00
MacRimi b9f32da7b8 Update flask_server.py 2025-10-07 22:38:49 +02:00
MacRimi fde0b1d8bf Update AppImage 2025-10-07 22:26:49 +02:00
MacRimi cf07004fcd Update flask_server.py 2025-10-07 21:59:57 +02:00
MacRimi b41b52df84 Update flask_server.py 2025-10-07 21:49:29 +02:00
MacRimi 9632dd170a Update hardware.tsx 2025-10-07 21:32:54 +02:00
MacRimi 9dc334dea9 Update flask_server.py 2025-10-07 21:15:01 +02:00
MacRimi 0741079450 Update flask_server.py 2025-10-07 21:00:03 +02:00
MacRimi 562df0f48f Update flask_server.py 2025-10-07 20:26:05 +02:00
MacRimi 3b87c078f4 Update flask_server.py 2025-10-07 20:15:42 +02:00
MacRimi fc091665ff Update flask_server.py 2025-10-07 19:59:18 +02:00
MacRimi 3f2842a9a3 Update flask_server.py 2025-10-07 18:26:37 +02:00
MacRimi f2418c81d7 Update flask_server.py 2025-10-07 18:22:15 +02:00
MacRimi 3c85797cc9 Update flask_server.py 2025-10-07 18:15:33 +02:00
MacRimi 9187bf0b83 Update flask_server.py 2025-10-07 17:53:57 +02:00
MacRimi 5a2381d9dd Update flask_server.py 2025-10-07 17:43:12 +02:00
MacRimi 5fb8bb0dac Update flask_server.py 2025-10-07 17:22:14 +02:00
MacRimi 0d55da18d4 Update hardware.tsx 2025-10-07 12:08:49 +02:00
MacRimi 73148d65bb Update hardware.tsx 2025-10-07 11:22:51 +02:00
MacRimi e81438e49f Update AppImage 2025-10-07 11:10:20 +02:00
MacRimi e247d8095e Update AppImage 2025-10-07 10:49:43 +02:00
MacRimi 46ddb36c79 Update hardware.tsx 2025-10-07 03:03:45 +02:00
MacRimi a87fee906f Update flask_server.py 2025-10-07 02:51:10 +02:00
MacRimi 888d94131e Update AppImage 2025-10-07 02:46:35 +02:00
MacRimi 63dd018756 Update flask_server.py 2025-10-07 02:29:07 +02:00
MacRimi db38571646 Update flask_server.py 2025-10-07 02:23:55 +02:00
MacRimi 8111d96a20 Update flask_server.py 2025-10-07 02:02:14 +02:00
MacRimi a1b5b7c03c Update hardware.tsx 2025-10-07 01:37:43 +02:00
MacRimi 91e95b1ef2 Update flask_server.py 2025-10-07 01:31:29 +02:00
MacRimi 1491f35f5e Update AppImage 2025-10-07 01:24:02 +02:00
MacRimi 9bb127dda7 Update flask_server.py 2025-10-07 01:10:54 +02:00
MacRimi c4348b0cb2 Update flask_server.py 2025-10-07 00:56:41 +02:00
MacRimi a3fc0c7f96 Update hardware.tsx 2025-10-07 00:50:17 +02:00
MacRimi c7387068cc Update hardware.tsx 2025-10-07 00:44:26 +02:00
MacRimi 658ce390e2 Update AppImage 2025-10-07 00:35:23 +02:00
MacRimi f0b6f66be6 Update AppImage 2025-10-07 00:24:35 +02:00
MacRimi 304812e14f Update flask_server.py 2025-10-06 23:57:02 +02:00
MacRimi 92d8a05393 Update AppImage 2025-10-06 23:51:35 +02:00
MacRimi d96d98b8f4 Update AppImage 2025-10-06 23:40:54 +02:00
MacRimi 0d059187ec Update hardware.tsx 2025-10-06 23:26:26 +02:00
MacRimi 1b73b0b861 Update hardware.tsx 2025-10-06 23:16:31 +02:00
MacRimi 29f8d6b981 Update AppImage 2025-10-06 22:58:54 +02:00
MacRimi 7826de9d29 Update flask_server.py 2025-10-06 22:44:24 +02:00
MacRimi 78c56e4f28 Update AppImage 2025-10-06 22:39:37 +02:00
MacRimi 3ef7736e85 Update hardware.tsx 2025-10-06 22:23:56 +02:00
MacRimi 73e6194551 Update AppImage 2025-10-06 22:19:42 +02:00
MacRimi 7ceed3dfbc Update flask_server.py 2025-10-06 19:15:17 +02:00
MacRimi 7a0c2dc261 Update AppImage 2025-10-06 19:08:21 +02:00
MacRimi 5807c4d97f Update flask_server.py 2025-10-06 19:00:36 +02:00
MacRimi a689607e98 Update AppImage 2025-10-06 18:52:58 +02:00
MacRimi b6b3e27408 Update AppImage 2025-10-06 18:29:00 +02:00
MacRimi ac30bd6e51 Update hardware.ts 2025-10-06 18:15:56 +02:00
MacRimi 174fc4f72b Update flask_server.py 2025-10-06 18:09:07 +02:00
MacRimi 047ec982f4 Update AppImage 2025-10-06 18:02:40 +02:00
MacRimi e427f37f0e Update AppImage 2025-10-06 17:40:42 +02:00
MacRimi 810ac1fcfa Update AppImage 2025-10-06 17:25:08 +02:00
MacRimi 5ee3cc6712 Update build_appimage.sh 2025-10-06 17:00:52 +02:00
MacRimi 5ad3d5697e Update build_appimage.sh 2025-10-06 16:54:12 +02:00
MacRimi 874ab093d5 Update AppImage 2025-10-06 16:40:14 +02:00
MacRimi fb668859b0 Update build_appimage.sh 2025-10-06 14:53:10 +02:00
MacRimi be7a2d7f41 Update AppImage 2025-10-06 14:17:14 +02:00
MacRimi 154b6b9f74 Update AppImage 2025-10-06 14:12:28 +02:00
MacRimi 23c91386dc Update AppImage 2025-10-06 13:48:02 +02:00
MacRimi 741b6ce0d9 Update AppImage 2025-10-06 13:06:27 +02:00
MacRimi 600c2f6061 Update AppImagen 2025-10-06 12:09:43 +02:00
MacRimi 359de2dbe0 Update build_appimage.sh 2025-10-06 11:06:19 +02:00
MacRimi 84eec4655a Update AppImage 2025-10-06 11:02:00 +02:00
MacRimi 7e8c69a02d Update hardware.tsx 2025-10-05 22:46:14 +02:00
MacRimi 730d47f2f7 Update AppImage 2025-10-05 22:40:38 +02:00
MacRimi 5afb74e606 Update hardware.tsx 2025-10-05 22:34:30 +02:00
MacRimi 8a21547668 Update hardware.tsx 2025-10-05 22:27:20 +02:00
MacRimi 3ee3044270 Update flask_server.py 2025-10-05 22:15:32 +02:00
MacRimi 782dc24eba Update AppImage 2025-10-05 22:10:24 +02:00
MacRimi 475b96178e Update AppImage 2025-10-05 21:59:44 +02:00
MacRimi 4beba53675 Update AppImage 2025-10-05 21:44:22 +02:00
MacRimi efb7cad993 Update flask_server.py 2025-10-05 21:24:15 +02:00
MacRimi 7b7705866d Update proxmox-dashboard.tsx 2025-10-05 21:15:51 +02:00
MacRimi b7e06d51ea Create sheet.tsx 2025-10-05 21:02:20 +02:00
MacRimi 95476276ac Update proxmox-dashboard.tsx 2025-10-05 20:58:15 +02:00
MacRimi 2347e10458 Update AppImage 2025-10-05 20:45:54 +02:00
MacRimi 85051f1340 Update virtual-machines.tsx 2025-10-05 20:30:47 +02:00
MacRimi 42c6e70ebe Update virtual-machines.tsx 2025-10-05 20:24:30 +02:00
MacRimi b9fe83e7a8 Update virtual-machines.tsx 2025-10-05 20:12:33 +02:00
MacRimi 841108623f Update virtual-machines.tsx 2025-10-05 17:27:10 +02:00
MacRimi 8ce221e41b Update virtual-machines.tsx 2025-10-05 17:18:26 +02:00
MacRimi f8c41ab39f Update virtual-machines.tsx 2025-10-05 17:10:28 +02:00
MacRimi 79e7fd175e Update virtual-machines.tsx 2025-10-05 17:01:50 +02:00
MacRimi fbcf755591 Update virtual-machines.tsx 2025-10-05 16:28:12 +02:00
MacRimi 6168a47e24 Update network-metrics.tsx 2025-10-05 16:15:45 +02:00
MacRimi d788114be3 Update AppImage 2025-10-05 16:05:54 +02:00
MacRimi 497814f80c Update AppImage 2025-10-05 15:54:24 +02:00
MacRimi 7297edf16f Update AppImage 2025-10-05 15:44:19 +02:00
MacRimi 714407eb46 Update virtual-machines.tsx 2025-10-05 15:38:29 +02:00
MacRimi dd3523ddd7 Update virtual-machines.tsx 2025-10-05 15:18:50 +02:00
MacRimi 7739de5db9 Update AppImage 2025-10-05 15:00:42 +02:00
MacRimi 99c08026ee Update network-metrics.tsx 2025-10-05 14:33:47 +02:00
MacRimi 19f7ea70f0 Update AppImage 2025-10-05 14:16:21 +02:00
MacRimi 49050c042d Update AppImage 2025-10-05 13:50:29 +02:00
MacRimi 18ccff5759 Update AppImage 2025-10-05 13:15:44 +02:00
MacRimi 9f6f646e77 Update virtual-machines.tsx 2025-10-05 12:56:06 +02:00
MacRimi b8c0d8ef79 Update AppImage 2025-10-05 12:48:34 +02:00
MacRimi 2ccd41bfb9 Update AppImage 2025-10-05 12:32:09 +02:00
MacRimi fa64b51d4a Update AppImage 2025-10-05 12:03:47 +02:00
MacRimi f5ac194008 Update AppImage 2025-10-05 11:48:32 +02:00
MacRimi 816cf0141b Update AppImage 2025-10-04 20:23:42 +02:00
ProxMenuxBot 7baabc6d2c Update helpers_cache.json 2025-10-04 18:16:43 +00:00
MacRimi 37d1c7338b Update flask_server.py 2025-10-04 20:14:57 +02:00
MacRimi 404ea9d838 Update AppImage 2025-10-04 20:06:47 +02:00
MacRimi 98c5c5827c Update AppImage 2025-10-04 19:58:12 +02:00
MacRimi 79525284b1 Update network-metrics.tsx 2025-10-04 19:53:12 +02:00
MacRimi c14ea7afdf Update AppImage 2025-10-04 19:45:37 +02:00
MacRimi c437753d64 Update package.json 2025-10-04 19:28:13 +02:00
MacRimi 441cc35e5a Update AppImage 2025-10-04 19:25:28 +02:00
MacRimi dc03144773 Update AppImage 2025-10-04 19:05:39 +02:00
MacRimi 992921b24c Update storage-overview.tsx 2025-10-04 18:53:31 +02:00
MacRimi 28f38dca46 Update storage-overview.tsx 2025-10-04 18:46:12 +02:00
MacRimi 53155ccef0 Update AppImage 2025-10-04 18:36:15 +02:00
MacRimi ba6f0a1aab Update AppImage 2025-10-04 18:23:45 +02:00
MacRimi 2d89d06bcb Update AppImage 2025-10-04 17:48:10 +02:00
MacRimi 54ff50ce68 Update AppImage 2025-10-04 17:34:07 +02:00
MacRimi 22aa8cdd6c Update flask_server.py 2025-10-04 17:03:39 +02:00
MacRimi 06b0195d74 Update customizable_post_install.sh 2025-10-04 16:31:38 +02:00
MacRimi a99b4ded7f Update uninstall-tools.sh 2025-10-04 16:22:17 +02:00
MacRimi 2405a0e778 Update uninstall-tools.sh 2025-10-04 16:20:30 +02:00
MacRimi 84544b1e84 Update auto_post_install.sh 2025-10-04 16:11:45 +02:00
MacRimi 95fce39502 Update auto_post_install.sh 2025-10-04 16:04:09 +02:00
MacRimi 99c5b26241 Update uninstall-tools.sh 2025-10-04 10:26:06 +02:00
ProxMenuxBot 6e07e49c84 Update helpers_cache.json 2025-10-03 18:18:02 +00:00
ProxMenuxBot 0bcfea9d20 Update helpers_cache.json 2025-10-03 12:25:37 +00:00
MacRimi 2658331fd2 Update AppImage 2025-10-02 23:41:31 +02:00
MacRimi 2ab49cc545 Update AppImage 2025-10-02 23:20:59 +02:00
MacRimi a39fe5ff3b Update flask_server.py 2025-10-02 23:02:17 +02:00
MacRimi 01578b4e34 Update flask_server.py 2025-10-02 22:38:06 +02:00
MacRimi 95718c889d Update AppImage 2025-10-02 22:29:24 +02:00
MacRimi 6279cc9ec1 Update AppImage 2025-10-02 19:51:53 +02:00
MacRimi f7fb9034ef Update system-overview.tsx 2025-10-02 19:34:53 +02:00
MacRimi 15f3af2020 Update AppImage 2025-10-02 18:28:36 +02:00
MacRimi 97288ed6ce Update system-overview.tsx 2025-10-02 18:11:00 +02:00
MacRimi 5e168c2561 Update system-overview.tsx 2025-10-02 17:49:40 +02:00
MacRimi 358b3f96ae Update globals.css 2025-10-02 17:41:23 +02:00
MacRimi c0d9c3808a Update globals.css 2025-10-02 17:28:23 +02:00
MacRimi 7404bb8e64 Update globals.css 2025-10-02 17:21:04 +02:00
MacRimi 93eccd7dcf Update globals.css 2025-10-02 17:17:21 +02:00
MacRimi 05d9d41860 Update AppImage 2025-10-02 17:10:27 +02:00
MacRimi c47c41548f Update globals.css 2025-10-02 16:57:12 +02:00
ProxMenuxBot 013d1980a3 Update helpers_cache.json 2025-10-01 18:18:31 +00:00
MacRimi df9f4a23b4 Update AppImage 2025-10-01 18:14:58 +02:00
MacRimi c41da47a48 Update AppImage 2025-10-01 18:08:31 +02:00
MacRimi e7214ad8df Update globals.css 2025-10-01 18:04:38 +02:00
MacRimi d6671de842 Update AppImage 2025-10-01 18:01:54 +02:00
MacRimi aad218db5d Update globals.css 2025-10-01 17:44:00 +02:00
MacRimi 724ba1e271 Update AppImge 2025-10-01 17:39:43 +02:00
MacRimi 97d554f638 update AppImage 2025-10-01 17:27:05 +02:00
MacRimi c5a7655d26 Update AppImage 2025-10-01 17:10:37 +02:00
MacRimi 403e896e3e Update proxmox-dashboard.tsx 2025-10-01 17:03:56 +02:00
MacRimi 1a15f43cad Update proxmox-dashboard.tsx 2025-10-01 16:53:37 +02:00
MacRimi 399b460c53 Update globals.css 2025-09-30 00:11:24 +02:00
MacRimi acc0362180 Update AppImage 2025-09-30 00:09:11 +02:00
MacRimi 00db93e03f Update AppImage 2025-09-29 23:56:33 +02:00
MacRimi d1997794c8 Update globals.css 2025-09-29 23:40:45 +02:00
MacRimi aa1ebe69f2 Update globals.css 2025-09-29 23:11:40 +02:00
MacRimi 4e7f5f56f1 Update AppImage 2025-09-29 22:59:10 +02:00
MacRimi 28cb7359ce Update system-overview.tsx 2025-09-29 22:38:25 +02:00
ProxMenuxBot 91c272d21c Update helpers_cache.json 2025-09-29 18:19:17 +00:00
MacRimi 3c00125e83 Update flask_server.py 2025-09-29 19:19:35 +02:00
MacRimi f359848a2f Update AppRun 2025-09-29 19:12:56 +02:00
MacRimi 989769e5e8 Update AppRun 2025-09-29 19:07:35 +02:00
MacRimi 0f2f1b6211 Update system-overview.tsx 2025-09-29 19:02:59 +02:00
MacRimi ffe8f4acc6 Update AppImage 2025-09-29 18:58:53 +02:00
MacRimi edb09777de Update package.json 2025-09-29 18:47:18 +02:00
MacRimi 5262c7863e Update AppImage 2025-09-29 18:43:14 +02:00
MacRimi 54256826fe Update AppImage 2025-09-29 18:37:32 +02:00
MacRimi 3d3c224b3a Update flask_server.py 2025-09-29 18:31:09 +02:00
MacRimi 049eccb872 Update AppImage 2025-09-29 18:27:09 +02:00
MacRimi 269828c79e Update AppImage 2025-09-29 18:16:01 +02:00
MacRimi b4e25ae66d Update AppImage 2025-09-29 18:07:30 +02:00
MacRimi b20dd74d23 Update AppImage 2025-09-29 17:57:00 +02:00
MacRimi bc3e2ec358 Update AppImage 2025-09-29 17:46:37 +02:00
MacRimi 6133a6d6d8 Update build_appimage.sh 2025-09-29 17:32:24 +02:00
MacRimi 46a16c04e6 Update AppImage 2025-09-29 17:21:59 +02:00
MacRimi 8469b3b26f Update AppImage 2025-09-29 17:05:42 +02:00
ProxMenuxBot 2ed04f57fe Update helpers_cache.json 2025-09-29 12:26:48 +00:00
MacRimi b19bac679a Update flask_server.py 2025-09-29 00:00:01 +02:00
MacRimi 3c33d5982c Update AppImage 2025-09-28 23:51:06 +02:00
MacRimi 5b934eeb87 Update AppImage 2025-09-28 23:25:58 +02:00
MacRimi 795d96f8d5 Update AppImage 2025-09-28 23:09:31 +02:00
MacRimi a8e7119b4a Update AppImage 2025-09-28 23:05:59 +02:00
MacRimi 38569ff7fc Update AppImage 2025-09-28 22:57:15 +02:00
MacRimi e404557d62 Update AppImage 2025-09-28 22:53:42 +02:00
MacRimi 96cbc75a5e Update build_appimage.sh 2025-09-28 21:26:25 +02:00
MacRimi c989af6cf0 Update build_appimage.sh 2025-09-28 21:18:52 +02:00
MacRimi 4eac9d03ea Update build_appimage.sh 2025-09-28 21:15:18 +02:00
MacRimi 6292009b0b Update AppImage 2025-09-28 21:08:58 +02:00
MacRimi 3272be967d Update build_appimage.sh 2025-09-28 21:01:48 +02:00
MacRimi 1c015da440 Update build_appimage.sh 2025-09-28 20:57:23 +02:00
MacRimi 0d047cc956 Update build_appimage.sh 2025-09-28 20:46:55 +02:00
MacRimi e682070b85 update AppImage 2025-09-28 20:28:35 +02:00
MacRimi 9f08694d9b Update Appimage 2025-09-28 20:25:55 +02:00
MacRimi 70f0db73e5 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-09-28 20:20:46 +02:00
MacRimi 9dc8f44379 Create tsconfig.json 2025-09-28 20:20:31 +02:00
MacRimi 59f7ccd723 Update build-appimage.yml 2025-09-28 20:15:56 +02:00
MacRimi 0710e95a6d Update package.json 2025-09-28 20:15:33 +02:00
MacRimi 4d1b5e3919 Update build-appimage.yml 2025-09-28 20:12:21 +02:00
MacRimi 0cc2cb92dd Update build-appimage.yml 2025-09-28 20:10:08 +02:00
MacRimi dba4d168f7 Update build-appimage.yml 2025-09-28 20:08:02 +02:00
MacRimi d87ac7843c Update build-appimage.yml 2025-09-28 20:07:12 +02:00
MacRimi 040535b004 Update build-appimage.yml 2025-09-28 20:04:39 +02:00
MacRimi c8acd2c0b1 Update build-appimage.yml 2025-09-28 19:56:03 +02:00
MacRimi d67fecea6e Update build-appimage.yml 2025-09-28 19:46:03 +02:00
MacRimi 61f80f9ee6 Update build-appimage.yml 2025-09-28 19:44:51 +02:00
MacRimi 9da8f9a5d1 Update build-appimage.yml 2025-09-28 19:43:05 +02:00
MacRimi f381468d5a Create build-appimage.yml 2025-09-28 19:41:13 +02:00
MacRimi 6ae97266e4 Create AppImage 2025-09-28 19:40:23 +02:00
MacRimi 66060f345c Create jd2_2.sh 2025-09-27 20:21:15 +02:00
MacRimi c61f568170 Update nfs_lxc_server.sh 2025-09-27 18:50:50 +02:00
MacRimi dcd108bda3 Update update-pve.sh 2025-09-27 18:28:56 +02:00
MacRimi 9d89f98987 Update customizable_post_install.sh 2025-09-27 18:26:23 +02:00
MacRimi ca7b959fce Update auto_post_install.sh 2025-09-27 18:25:25 +02:00
MacRimi 4a30793595 Update uninstall-tools.sh 2025-09-27 18:24:02 +02:00
MacRimi 35e2d53f0f update remove subscription banner PVE 9 2025-09-27 18:16:12 +02:00
MacRimi 503efa4572 Create remove-banner-pve9_2.sh 2025-09-27 17:42:18 +02:00
ProxMenuxBot b0c33d9dff Update helpers_cache.json 2025-09-27 00:56:57 +00:00
MacRimi 012b156b46 Update install_coral_pve9.sh 2025-09-26 00:17:32 +02:00
MacRimi 25d0d3bf59 Create install_coral_pve9.sh 2025-09-25 23:45:50 +02:00
ProxMenuxBot 0f1babc82b Update helpers_cache.json 2025-09-25 18:20:06 +00:00
ProxMenuxBot e2b93ea785 Update helpers_cache.json 2025-09-24 18:19:23 +00:00
ProxMenuxBot b1cedfa81e Update helpers_cache.json 2025-09-24 12:27:12 +00:00
ProxMenuxBot 701ee36f6a Update helpers_cache.json 2025-09-23 12:25:52 +00:00
ProxMenuxBot 4e5db86434 Update helpers_cache.json 2025-09-21 12:23:25 +00:00
ProxMenuxBot f45e9e657c Update helpers_cache.json 2025-09-19 18:18:42 +00:00
ProxMenuxBot 4936fcdb1e Update helpers_cache.json 2025-09-18 18:18:59 +00:00
ProxMenuxBot 374e05c422 Update helpers_cache.json 2025-09-18 12:25:39 +00:00
ProxMenuxBot 9c00798373 Update helpers_cache.json 2025-09-17 18:18:09 +00:00
ProxMenuxBot db82fce925 Update helpers_cache.json 2025-09-15 12:26:37 +00:00
ProxMenuxBot acaa28e476 Update helpers_cache.json 2025-09-13 00:54:55 +00:00
ProxMenuxBot f297ce5809 Update helpers_cache.json 2025-09-12 12:24:43 +00:00
ProxMenuxBot 3dc3fc5f67 Update helpers_cache.json 2025-09-12 00:57:45 +00:00
ProxMenuxBot 4884fc4418 Update helpers_cache.json 2025-09-11 18:15:27 +00:00
MacRimi adc17842ec Update README.md 2025-09-11 18:18:42 +02:00
MacRimi daa48b0b7c Update README.md 2025-09-11 18:17:16 +02:00
MacRimi 17c0362df3 Update cache.json 2025-09-10 20:29:19 +02:00
MacRimi 29b9a63fc9 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-09-10 20:28:00 +02:00
MacRimi 2a9fae160e Update cache.json 2025-09-10 20:27:58 +02:00
ProxMenuxBot 0c49a1e3bd Update helpers_cache.json 2025-09-10 18:18:53 +00:00
MacRimi e896c41be1 Update main_menu.sh 2025-09-10 19:14:42 +02:00
MacRimi 187250fa24 update text ProxMenux 2025-09-10 19:06:04 +02:00
MacRimi 9035b18584 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-09-10 18:58:12 +02:00
MacRimi 4534d78978 update text ProxMenux 2025-09-10 18:57:49 +02:00
MacRimi f4ab0e982c Update README.md 2025-09-10 18:53:41 +02:00
MacRimi 3e7c6629a6 update text ProxMenux 2025-09-10 18:47:55 +02:00
MacRimi 3ea17331fe Update install_proxmenux.sh 2025-09-10 18:24:14 +02:00
MacRimi 1057fcc271 Update install_proxmenux.sh 2025-09-10 18:19:13 +02:00
MacRimi 5a31c36097 update menu share 2025-09-10 18:05:06 +02:00
MacRimi 1677a69bba Update version.txt 2025-09-10 17:52:17 +02:00
MacRimi 315c49165d Update CHANGELOG.md 2025-09-10 17:50:53 +02:00
MacRimi aae70e7ec0 Update CHANGELOG.md 2025-09-10 17:47:37 +02:00
MacRimi 5cb9e13ca7 Update CHANGELOG.md 2025-09-10 17:37:51 +02:00
MacRimi 0187010f94 Create main-menu.png 2025-09-10 16:28:47 +02:00
ProxMenuxBot 2c2ed21e59 Update helpers_cache.json 2025-09-10 12:24:52 +00:00
MacRimi f8b2ccec40 Update commands_share.sh 2025-09-10 13:48:17 +02:00
MacRimi e858dc582d Update commands_share.sh 2025-09-10 13:47:13 +02:00
MacRimi dd737f4b46 Update commands_share.sh 2025-09-10 13:46:02 +02:00
MacRimi f0bc238b6d Update commands_share.sh 2025-09-10 13:35:16 +02:00
MacRimi af55424850 Update lxc-mount-manager_minimal.sh 2025-09-10 13:16:54 +02:00
MacRimi 902534baff update menu shared 2025-09-10 12:57:17 +02:00
MacRimi 6daa630040 Update nfs_host.sh 2025-09-10 12:53:09 +02:00
MacRimi 0b2b86673b Update lxc-mount-manager_minimal.sh 2025-09-10 12:31:49 +02:00
MacRimi 6aa5b58208 Update share_menu.sh 2025-09-10 12:17:03 +02:00
MacRimi 4430201cd2 Update lxc-mount-manager_minimal.sh 2025-09-10 12:16:25 +02:00
MacRimi 7c7963a83e Create lxc-mount-manager_minimal.sh 2025-09-10 11:10:02 +02:00
ProxMenuxBot e2202cd2d8 Update helpers_cache.json 2025-09-09 18:17:13 +00:00
MacRimi a931be83bc Update share-common.func 2025-09-09 19:20:15 +02:00
MacRimi 7350bea345 Update auto_post_install.sh 2025-09-09 19:16:13 +02:00
MacRimi 9b1e39dbb4 Update update-pve.sh 2025-09-09 19:14:27 +02:00
MacRimi 15cd118845 Update auto_post_install.sh 2025-09-09 19:06:33 +02:00
MacRimi d58dff047c Update auto_post_install.sh 2025-09-08 19:24:49 +02:00
MacRimi a2f83c896c Update common-functions.sh 2025-09-08 19:14:41 +02:00
MacRimi 6ef77c731c Update guia.md 2025-09-08 17:44:17 +02:00
MacRimi 29b0f61958 Update guia.md 2025-09-08 17:43:39 +02:00
MacRimi e944b2ecdd Update guia.md 2025-09-08 17:24:23 +02:00
MacRimi 41819c46a3 Update guia.md 2025-09-08 17:21:58 +02:00
MacRimi 13f391a6f0 Update guia.md 2025-09-08 17:14:52 +02:00
MacRimi 85a3d44f2c Update guia.md 2025-09-08 17:05:26 +02:00
MacRimi 0792392058 Update guia.md 2025-09-08 16:42:46 +02:00
MacRimi ff5083ada0 Update guia.md 2025-09-08 16:20:10 +02:00
MacRimi 62841677bc Update guia.md 2025-09-08 16:06:50 +02:00
MacRimi 1761cf53a2 Update guia.md 2025-09-08 15:27:32 +02:00
MacRimi a771efc5fa Update guia.md 2025-09-08 15:08:02 +02:00
MacRimi ed049da76a Update guia.md 2025-09-08 15:03:10 +02:00
MacRimi 5d1d357a2e Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-09-08 15:01:02 +02:00
MacRimi 30d0706a1c Create guia.md 2025-09-08 15:01:00 +02:00
ProxMenuxBot e9667e1266 Update helpers_cache.json 2025-09-08 12:27:18 +00:00
MacRimi 73109483e7 Update share-common.func 2025-09-08 11:29:02 +02:00
MacRimi a9c1acf204 Update lxc-mount-manager.sh 2025-09-08 11:15:52 +02:00
MacRimi 81c4f5814c Update samba_host.sh 2025-09-08 10:40:59 +02:00
MacRimi c595f6d781 Update nfs_host.sh 2025-09-08 10:36:57 +02:00
MacRimi 24bb6b1d3d Update lxc-mount-manager.sh 2025-09-07 19:10:23 +02:00
MacRimi 49eeb6020d Update install_proxmenux.sh 2025-09-07 17:24:29 +02:00
MacRimi 7c272bd2a2 Update install_proxmenux.sh 2025-09-07 17:22:47 +02:00
MacRimi cfbd865937 Update share-common.func 2025-09-07 09:29:43 +02:00
MacRimi fe472f33ef Update lxc-mount-manager.sh 2025-09-07 09:28:09 +02:00
MacRimi 8d6b3d650f update menu share 2025-09-07 09:19:53 +02:00
MacRimi 3b0d5b5eb7 Update nfs_lxc_server.sh 2025-09-07 09:16:22 +02:00
MacRimi 875e8a99bd Update samba_client.sh 2025-09-07 09:06:47 +02:00
MacRimi 6c19d81844 Update samba_client.sh 2025-09-07 09:05:23 +02:00
MacRimi ba535a931f Update nfs_client.sh 2025-09-07 09:04:13 +02:00
MacRimi 45dca5218d Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-09-07 09:00:39 +02:00
MacRimi da3cb9971b Update share-common.func 2025-09-07 09:00:37 +02:00
ProxMenuxBot b39270dc1e Update helpers_cache.json 2025-09-07 01:03:30 +00:00
MacRimi ae8a7d0de9 Update commands_share.sh 2025-09-06 23:59:23 +02:00
MacRimi 2d501415bf Update commands_share.sh 2025-09-06 23:50:57 +02:00
MacRimi da639ccaac Update commands_share.sh 2025-09-06 23:35:46 +02:00
MacRimi a352770e2d Update share_menu.sh 2025-09-06 23:20:59 +02:00
MacRimi e3e1899466 update menu share 2025-09-06 23:18:11 +02:00
MacRimi e67288e623 Update lxc-mount-manager.sh 2025-09-06 22:55:28 +02:00
MacRimi 4019e49b07 Update share-common.func 2025-09-06 22:51:57 +02:00
MacRimi cd8711f3bc Update lxc-mount-manager.sh 2025-09-06 22:50:30 +02:00
MacRimi 0d119379de Update lxc-mount-manager.sh 2025-09-06 22:46:52 +02:00
MacRimi aa2b6ff112 Update lxc-mount-manager.sh 2025-09-06 22:44:09 +02:00
MacRimi 3482f7dc98 Update share-common.func 2025-09-06 22:42:17 +02:00
MacRimi 16c321f114 Update lxc-mount-manager.sh 2025-09-06 22:33:19 +02:00
MacRimi a81e7f3c44 Update lxc-mount-manager.sh 2025-09-06 22:25:50 +02:00
MacRimi d7cc001521 Update lxc-mount-manager.sh 2025-09-06 22:20:34 +02:00
MacRimi eb11962231 Update share-common.func 2025-09-06 22:19:52 +02:00
MacRimi 9f73b8f159 Update lxc-mount-manager.sh 2025-09-06 22:17:08 +02:00
MacRimi 873a4abe24 Update share-common.func 2025-09-06 22:16:40 +02:00
MacRimi 56bc584f5e Update lxc-mount-manager.sh 2025-09-06 22:10:01 +02:00
MacRimi 2a9f2f3c2e Update lxc-mount-manager.sh 2025-09-06 22:09:11 +02:00
MacRimi ee719cdd39 Update share-common.func 2025-09-06 22:06:02 +02:00
MacRimi a571b57b30 Update share-common.func 2025-09-06 21:48:39 +02:00
MacRimi 5ee7a23bea Update share-common.func 2025-09-06 21:47:20 +02:00
MacRimi fe159ea195 Update share-common.func 2025-09-06 21:42:30 +02:00
MacRimi 8fcdf6176b Update share-common.func 2025-09-06 21:31:03 +02:00
MacRimi 715166bbca Update share-common.func 2025-09-06 21:22:40 +02:00
MacRimi 1d58072c70 Update share-common.func 2025-09-06 21:21:39 +02:00
MacRimi d667cde699 Update lxc-mount-manager.sh 2025-09-06 21:19:55 +02:00
MacRimi 4cd8889c38 Update share-common.func 2025-09-06 21:14:54 +02:00
MacRimi 93896f6fb7 Update share-common.func 2025-09-06 21:07:12 +02:00
MacRimi 3b3f0387bb Update share-common.func 2025-09-06 21:05:32 +02:00
MacRimi 2875c9af95 Update lxc-mount-manager.sh 2025-09-06 20:59:03 +02:00
MacRimi 93ef1bfccc update share menu 2025-09-06 20:57:04 +02:00
MacRimi a886af1d87 Update samba_client.sh 2025-09-06 20:48:37 +02:00
MacRimi d731ff3ae6 Update samba_host.sh 2025-09-06 20:40:45 +02:00
MacRimi d44864637d Update nfs_host_auto.sh 2025-09-06 20:37:23 +02:00
MacRimi 674ee34ec6 Update nfs_host_auto.sh 2025-09-06 20:30:04 +02:00
MacRimi a93eeda243 Update share-common.func 2025-09-06 19:56:24 +02:00
MacRimi 80fd92e2a1 Update share-common.func 2025-09-06 19:55:01 +02:00
MacRimi d4ff2da473 Update share-common.func 2025-09-06 19:16:15 +02:00
MacRimi 9b7b271580 update menu shared 2025-09-06 19:13:52 +02:00
MacRimi e1b340966a Update share-common.func 2025-09-06 18:56:10 +02:00
MacRimi 7ec4c331af Update nfs_client.sh 2025-09-06 18:39:35 +02:00
MacRimi 3102d596ee Update nfs_lxc_server.sh 2025-09-06 18:37:28 +02:00
MacRimi af56dc546e Update nfs_host_auto.sh 2025-09-06 18:26:13 +02:00
MacRimi 15d47499fa Update nfs_host_auto.sh 2025-09-06 18:22:02 +02:00
MacRimi 53a34d0470 update nfs menu 2025-09-06 18:20:20 +02:00
MacRimi 3ee675cefe Update nfs_lxc_server.sh 2025-09-06 18:08:48 +02:00
MacRimi d98c7bdc03 Update share_menu.sh 2025-09-06 16:48:07 +02:00
MacRimi bb4f1ebed6 Update share_menu.sh 2025-09-06 16:44:58 +02:00
MacRimi c8f73ea23b Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-09-06 16:41:55 +02:00
MacRimi 8292b12787 Create nfs_lxc_server.sh 2025-09-06 16:41:54 +02:00
ProxMenuxBot 0f518e3c35 Update helpers_cache.json 2025-09-06 12:22:12 +00:00
MacRimi 1c2f67d43d Update lxc-mount-manager.sh 2025-09-06 11:55:23 +02:00
MacRimi a5560a3123 Update share-common.func 2025-09-06 11:50:48 +02:00
MacRimi 1332096360 Update share-common.func 2025-09-06 11:39:17 +02:00
MacRimi 80381a6375 Update share-common.func 2025-09-06 11:32:07 +02:00
MacRimi acf92bd005 Update share-common.func 2025-09-06 11:30:23 +02:00
MacRimi da4f8a3a19 Update share-common.func 2025-09-06 11:11:19 +02:00
MacRimi 3a332192e3 Update share-common.func 2025-09-06 11:08:23 +02:00
MacRimi 1fdb1d87cc Update share-common.func 2025-09-06 11:01:35 +02:00
MacRimi b99aa55d7a Update share-common.func 2025-09-06 10:46:22 +02:00
MacRimi de20da2dad Update lxc-mount-manager.sh 2025-09-06 10:10:37 +02:00
MacRimi 9444f0a68b Update nfs_host_auto.sh 2025-09-06 08:51:41 +02:00
MacRimi 48fd223a28 Update samba.sh 2025-09-04 20:38:11 +02:00
MacRimi 0845efe419 Update samba_client.sh 2025-09-04 20:37:18 +02:00
MacRimi 57b7ba91bc Update share_menu.sh 2025-09-04 20:34:43 +02:00
MacRimi 97af8a4892 Update share_menu.sh 2025-09-04 20:31:33 +02:00
MacRimi d6f237e289 Update share_menu.sh 2025-09-04 20:30:36 +02:00
MacRimi aba7109b35 Update samba_host.sh 2025-09-04 20:28:16 +02:00
MacRimi d3ec71052e Update samba_client.sh 2025-09-04 20:26:51 +02:00
MacRimi 1be63f396b Update nfs_client.sh 2025-09-04 20:26:26 +02:00
MacRimi 9308742146 Update samba_client.sh 2025-09-04 20:23:00 +02:00
MacRimi b32241082d Update share_menu.sh 2025-09-04 20:12:24 +02:00
MacRimi 1f8504d685 update shared 2025-09-04 20:04:08 +02:00
MacRimi 97c5c48150 Update nfs_host.sh 2025-09-03 23:13:21 +02:00
MacRimi afe84dc46a Update nfs_client.sh 2025-09-03 23:10:01 +02:00
MacRimi ffafd42f03 Update nfs.sh 2025-09-03 23:06:47 +02:00
MacRimi 7dca715c91 Update nfs.sh 2025-09-03 23:04:46 +02:00
MacRimi 7695e1d8dd Update nfs.sh 2025-09-03 22:55:45 +02:00
MacRimi 84b86d1db7 Update nfs.sh 2025-09-03 22:14:29 +02:00
MacRimi bae3ef6460 Update nfs.sh 2025-09-03 22:06:34 +02:00
MacRimi 97c6ec8875 Update share-common.func 2025-09-03 16:47:03 +02:00
MacRimi d33128dc26 Update share_menu.sh 2025-09-03 12:27:59 +02:00
MacRimi 10bdecabb6 Update share_menu.sh 2025-09-03 12:25:35 +02:00
MacRimi de88f530c8 Update share_menu.sh 2025-09-03 12:23:54 +02:00
MacRimi fb511b7596 Update share_menu.sh 2025-09-03 12:22:49 +02:00
MacRimi 322665ce91 Update share_menu.sh 2025-09-03 12:21:21 +02:00
MacRimi baeca1fcfb Update share-common.func 2025-09-03 11:35:38 +02:00
MacRimi 095b98c36a Update share-common.func 2025-09-03 11:28:37 +02:00
MacRimi 29bb7e7608 Update share-common.func 2025-09-03 11:16:34 +02:00
MacRimi e3d137efba Update share-common.func 2025-09-02 22:56:46 +02:00
MacRimi 207e915393 Update share-common.func 2025-09-02 22:45:33 +02:00
MacRimi 614e629a2b Update share-common.func 2025-09-02 22:44:42 +02:00
MacRimi f35de5c749 Update share-common.func 2025-09-02 21:34:13 +02:00
MacRimi c1623bd4df Update share-common.func 2025-09-02 21:23:57 +02:00
ProxMenuxBot 8690da5017 Update helpers_cache.json 2025-09-02 18:17:01 +00:00
MacRimi 696adcdc24 Update share-common.func 2025-09-02 18:48:57 +02:00
MacRimi 2756bd06c1 Update share-common.func 2025-09-02 18:48:20 +02:00
MacRimi 4893f6ea00 Update lxc-mount-manager.sh 2025-09-02 18:45:53 +02:00
MacRimi 35a7348197 Update lxc-mount-manager.sh 2025-09-02 18:45:04 +02:00
MacRimi cdd6333d0a Update share_menu.sh 2025-09-02 18:43:27 +02:00
MacRimi 54399b5b5d Update share-common.func 2025-09-02 18:25:39 +02:00
MacRimi f6b192cc1e Update lxc-mount-manager.sh 2025-09-02 18:22:34 +02:00
MacRimi cd231b90d8 Update share-common.func 2025-09-02 17:21:09 +02:00
ProxMenuxBot 87fe788358 Update helpers_cache.json 2025-09-02 12:26:27 +00:00
MacRimi 3e9bd21ea8 update share menu 2025-09-02 00:02:16 +02:00
MacRimi b6d4029797 Update local-shared-manager.sh 2025-09-01 19:10:46 +02:00
MacRimi ec65e96148 Update share_menu.sh 2025-09-01 19:08:48 +02:00
MacRimi 926f1f971f update share menu 2025-09-01 19:06:52 +02:00
MacRimi 5d69fad73f Update share_menu.sh 2025-09-01 18:45:14 +02:00
MacRimi a796761023 Update share-common.func 2025-09-01 17:39:45 +02:00
MacRimi 5d1338e485 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-09-01 17:21:41 +02:00
MacRimi ce25a167f1 Update auto_post_install.sh 2025-09-01 17:21:39 +02:00
MacRimi 1c44969580 Update share_menu.sh 2025-09-01 14:43:46 +02:00
MacRimi b6e04e3ede Update share-common.func 2025-09-01 14:30:16 +02:00
ProxMenuxBot 84c26be703 Update helpers_cache.json 2025-09-01 12:26:47 +00:00
MacRimi d201160722 Update share-common.func 2025-09-01 14:08:10 +02:00
MacRimi e112361b43 Update share-common.func 2025-09-01 13:53:54 +02:00
MacRimi 3e69795c9d Update lxc-mount-manager.sh 2025-09-01 13:33:48 +02:00
MacRimi b11baf2e5d Update auto_post_install.sh 2025-09-01 12:42:46 +02:00
MacRimi 233770b553 Update auto_post_install.sh 2025-09-01 12:41:26 +02:00
MacRimi 187db73798 Update zimaos.sh 2025-09-01 12:20:45 +02:00
MacRimi 0e3fc6f682 Update zimaos.sh 2025-09-01 12:15:30 +02:00
MacRimi d11e3a4ac4 Update auto_post_install.sh 2025-08-31 23:45:37 +02:00
MacRimi d3b4ca3e66 Update customizable_post_install.sh 2025-08-31 21:46:28 +02:00
MacRimi f37fbbfb8b Update menu share 2025-08-30 18:56:49 +02:00
MacRimi 52b7aac424 Update share-common.func 2025-08-30 18:05:02 +02:00
MacRimi d42f3f8f0c Update share-common.func 2025-08-30 18:02:20 +02:00
MacRimi 91b5c7c9bc Update share-common.func 2025-08-30 17:51:45 +02:00
MacRimi 48feebc092 Update share-common.func 2025-08-30 16:59:46 +02:00
MacRimi 14e2d66d96 Update share-common.func 2025-08-30 11:43:02 +02:00
MacRimi 10d844a195 Update share-common.func 2025-08-30 11:36:52 +02:00
MacRimi bbf91ae5d6 Update main_menu.sh 2025-08-30 09:40:42 +02:00
ProxMenuxBot cb82eda49a Update helpers_cache.json 2025-08-30 00:57:37 +00:00
MacRimi bc1dbb1c27 Update help_info_menu.sh 2025-08-30 00:17:51 +02:00
ProxMenuxBot 9496a7f1ce Update helpers_cache.json 2025-08-28 18:19:20 +00:00
ProxMenuxBot 7241fa31b4 Update helpers_cache.json 2025-08-28 12:26:37 +00:00
MacRimi fed7216436 Update share-common.func 2025-08-27 18:15:26 +02:00
MacRimi ffe7d7c4c6 Create group_manager.sh 2025-08-27 10:54:03 +02:00
MacRimi f430ac8d6c Update upgrade_pve8_to_pve9.sh 2025-08-26 19:40:38 +02:00
MacRimi 70dfd7c9a3 Update upgrade_pve8_to_pve9.sh 2025-08-26 19:25:59 +02:00
MacRimi ed3140932b Update upgrade_pve8_to_pve9.sh 2025-08-26 19:24:41 +02:00
MacRimi 3cd2bd6ce8 Update share-common.func 2025-08-26 17:46:48 +02:00
MacRimi 982bf45fc4 Update share-common.func 2025-08-26 17:37:09 +02:00
MacRimi aaba8569fc Update share-common.func 2025-08-26 17:19:11 +02:00
MacRimi 4111e15eb9 Update share-common.func 2025-08-26 17:15:47 +02:00
MacRimi 2012478f26 Update share-common.func 2025-08-26 17:05:17 +02:00
MacRimi 88869d3239 Update share-common.func 2025-08-26 17:02:24 +02:00
ProxMenuxBot f3c2549b18 Update helpers_cache.json 2025-08-26 12:28:24 +00:00
MacRimi 57e3b839d0 Update share-common.func 2025-08-26 13:56:00 +02:00
MacRimi faf3f43413 Create share-common.func 2025-08-26 13:26:22 +02:00
ProxMenuxBot 52e5bb3386 Update helpers_cache.json 2025-08-26 01:02:34 +00:00
ProxMenuxBot 89405f6670 Update helpers_cache.json 2025-08-25 18:20:02 +00:00
ProxMenuxBot 73111c4139 Update helpers_cache.json 2025-08-25 12:27:14 +00:00
ProxMenuxBot 04e9c5db8c Update helpers_cache.json 2025-08-24 12:24:09 +00:00
MacRimi 69278902de Update customizable_post_install.sh 2025-08-24 10:55:25 +02:00
MacRimi efa95b0858 Update customizable_post_install.sh 2025-08-24 10:29:00 +02:00
MacRimi 660128cd5c Update customizable_post_install.sh 2025-08-24 10:28:14 +02:00
MacRimi ef1e052e47 Update customizable_post_install.sh 2025-08-24 10:06:20 +02:00
ProxMenuxBot 0b346bc343 Update helpers_cache.json 2025-08-24 06:19:28 +00:00
MacRimi 2272eaf833 Update lxc-unprivileged-to-privileged.sh 2025-08-22 19:08:03 +02:00
MacRimi 4adee98bce new menu lxc 2025-08-22 19:05:36 +02:00
MacRimi cbdb2c0705 Rename lxc-manual-guide.sh to lxc-manual-guide.sh 2025-08-22 19:04:10 +02:00
MacRimi 4f438aabbf update manual lxc guide 2025-08-22 19:03:08 +02:00
MacRimi b6ccc06963 Update lxc_menu.sh 2025-08-22 18:58:10 +02:00
MacRimi 5b89a15bfc menu lxc 2025-08-22 18:57:00 +02:00
MacRimi 5596ae551d Update storage_menu.sh 2025-08-22 18:34:12 +02:00
MacRimi 1360df592a Create backup_host4.sh 2025-08-22 18:17:01 +02:00
MacRimi 13684ff83c Update share_menu.sh 2025-08-22 18:10:34 +02:00
MacRimi ae88f7870e Update share_menu.sh 2025-08-22 18:08:20 +02:00
MacRimi 810b6da60c Share menu 2025-08-22 18:05:14 +02:00
ProxMenuxBot 7bdf3e08f9 Update helpers_cache.json 2025-08-21 18:19:22 +00:00
MacRimi fdad2a087f Update zimaos.sh 2025-08-21 19:39:50 +02:00
MacRimi c437a8c426 Update zimaos.sh 2025-08-21 19:36:28 +02:00
MacRimi ef861e6d1d Update zimaos.sh 2025-08-21 19:02:58 +02:00
MacRimi 928a008688 Update zimaos.sh 2025-08-21 18:59:54 +02:00
MacRimi 638a124adb Update zimaos.sh 2025-08-21 18:31:32 +02:00
MacRimi c2a63ae9bb Update zimaos.sh 2025-08-21 18:30:43 +02:00
MacRimi 28cf31e6e7 Update zimaos.sh 2025-08-21 18:27:30 +02:00
MacRimi 3cf416167d Update select_nas_iso.sh 2025-08-21 18:22:57 +02:00
MacRimi ebf03923a0 Update select_nas_iso.sh 2025-08-21 18:14:58 +02:00
MacRimi 82797d2421 Update select_nas_iso.sh 2025-08-21 18:05:45 +02:00
MacRimi 52b6be946c Create zimaos.sh 2025-08-21 18:00:58 +02:00
MacRimi dc46724d7b Update select_linux_iso.sh 2025-08-20 23:02:38 +02:00
MacRimi ed7d43b6a9 Update select_linux_iso.sh 2025-08-20 22:58:14 +02:00
MacRimi 6f3fc51278 Update system_utils.sh 2025-08-20 22:40:42 +02:00
MacRimi a446acc282 Update customizable_post_install.sh 2025-08-20 22:39:28 +02:00
150 changed files with 35466 additions and 790 deletions
+86
View File
@@ -0,0 +1,86 @@
name: Build ProxMenux Monitor AppImage
on:
push:
branches: [ main ]
paths: [ 'AppImage/**' ]
pull_request:
branches: [ main ]
paths: [ 'AppImage/**' ]
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
BIN
View File
Binary file not shown.
@@ -0,0 +1 @@
e896eb10de4bf990d31c1d8357289f64cbce481921647f2be53efb850d0b73b2 ProxMenux-1.0.0.AppImage
+41
View File
@@ -0,0 +1,41 @@
# ProxMenux Monitor
A modern, responsive dashboard for monitoring Proxmox VE systems built with Next.js and React.
## 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
- **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
## Technology Stack
- **Frontend**: Next.js 15, React 19, TypeScript
- **Styling**: Tailwind CSS 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
## Onboarding Images
To customize the onboarding experience, place your screenshot images in `public/images/onboarding/`:
- `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
**Recommended image specifications:**
- Format: PNG or JPG
- Size: 1200x800px or similar 3:2 aspect ratio
- Quality: High-quality screenshots with representative data
The onboarding carousel will automatically show on first visit and can be dismissed or marked as "Don't show again".
+9
View File
@@ -0,0 +1,9 @@
import { ProxmoxDashboard } from "../../components/proxmox-dashboard"
export default function DashboardPage() {
return (
<main className="min-h-screen bg-background">
<ProxmoxDashboard />
</main>
)
}
+146
View File
@@ -0,0 +1,146 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* ===================== */
/* Light Mode (default) */
/* ===================== */
:root {
--background: oklch(1 0 0); /* blanco */
--foreground: oklch(0.145 0 0); /* casi negro */
--card: oklch(1 0 0);
--card-foreground: var(--foreground);
--popover: var(--card);
--popover-foreground: var(--foreground);
--primary: oklch(0.205 0 0); /* gris oscuro */
--primary-foreground: oklch(0.985 0 0); /* blanco */
--secondary: oklch(0.97 0 0);
--secondary-foreground: var(--primary);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0); /* gris medio */
--accent: oklch(0.97 0 0);
--accent-foreground: var(--primary);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.145 0 0);
--border: oklch(0.922 0 0);
--input: var(--border);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: var(--foreground);
--sidebar-primary: var(--primary);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: var(--primary);
--sidebar-border: var(--border);
--sidebar-ring: var(--ring);
}
/* ===================== */
/* Dark Mode (gris) */
/* ===================== */
.dark {
--background: oklch(0.22 0 0); /* gris oscuro */
--foreground: oklch(0.97 0 0); /* blanco/gris claro */
--card: oklch(0.24 0 0);
--card-foreground: var(--foreground);
--popover: var(--card);
--popover-foreground: var(--foreground);
--primary: oklch(0.83 0 0); /* casi blanco */
--primary-foreground: var(--background);
--secondary: oklch(0.28 0 0);
--secondary-foreground: oklch(0.92 0 0);
--muted: oklch(0.26 0 0);
--muted-foreground: oklch(0.72 0 0);
--accent: oklch(0.28 0 0);
--accent-foreground: var(--primary);
--destructive: oklch(0.53 0.25 27);
--destructive-foreground: oklch(0.9 0 0);
--border: oklch(0.34 0 0);
--input: var(--border);
--ring: oklch(0.55 0 0);
--chart-1: oklch(0.60 0.20 255);
--chart-2: oklch(0.70 0.16 165);
--chart-3: oklch(0.76 0.19 70);
--chart-4: oklch(0.63 0.25 305);
--chart-5: oklch(0.66 0.24 20);
--sidebar: oklch(0.24 0 0);
--sidebar-foreground: var(--foreground);
--sidebar-primary: var(--chart-1);
--sidebar-primary-foreground: var(--foreground);
--sidebar-accent: oklch(0.28 0 0);
--sidebar-accent-foreground: var(--foreground);
--sidebar-border: var(--border);
--sidebar-ring: var(--ring);
}
/* ===================== */
/* Base layer */
/* ===================== */
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
/* Foco accesible */
:is(button,[role="button"],a,input,select,textarea,[tabindex]:not([tabindex="-1"])):focus {
@apply outline-none;
}
:is(button,[role="button"],a,input,select,textarea,[tabindex]:not([tabindex="-1"])):focus-visible {
@apply ring-2;
--tw-ring-color: var(--ring);
--tw-ring-opacity: 0.5; /* equivalente al /50 */
}
}
/* ===================== */
/* Ajustes para Charts */
/* ===================== */
@layer components {
/* Recharts axis */
.recharts-cartesian-axis-tick tspan {
fill: var(--muted-foreground);
}
.recharts-cartesian-axis-line,
.recharts-cartesian-grid line {
stroke: var(--border);
}
/* Chart.js axis */
.chartjs-render-monitor text {
fill: var(--muted-foreground);
}
.chartjs-render-monitor line {
stroke: var(--border);
}
}
+5
View File
@@ -0,0 +1,5 @@
import Hardware from "@/components/hardware"
export default function HardwarePage() {
return <Hardware />
}
+46
View File
@@ -0,0 +1,46 @@
import type React from "react"
import type { Metadata } from "next"
import { GeistSans } from "geist/font/sans"
import { GeistMono } from "geist/font/mono"
import { ThemeProvider } from "../components/theme-provider"
import { Suspense } from "react"
import "./globals.css"
export const metadata: Metadata = {
title: "ProxMenux Monitor",
description: "Proxmox System Dashboard and Monitor",
generator: "v0.app",
manifest: "/manifest.json",
icons: {
icon: [
{ url: "/favicon.ico", sizes: "any" },
{ url: "/icon.svg", type: "image/svg+xml" },
{ url: "/icon.png", type: "image/png", sizes: "32x32" },
],
shortcut: "/favicon.ico",
apple: [{ url: "/apple-touch-icon.png", sizes: "180x180", type: "image/png" }],
},
viewport: "width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no",
themeColor: [
{ media: "(prefers-color-scheme: light)", color: "#ffffff" },
{ media: "(prefers-color-scheme: dark)", color: "#2b2f36" },
],
}
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html lang="en" suppressHydrationWarning>
<body className={`${GeistSans.variable} ${GeistMono.variable} antialiased bg-background text-foreground`}>
<Suspense fallback={<div>Loading...</div>}>
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem disableTransitionOnChange>
{children}
</ThemeProvider>
</Suspense>
</body>
</html>
)
}
+7
View File
@@ -0,0 +1,7 @@
"use client"
import { ProxmoxDashboard } from "../components/proxmox-dashboard"
export default function Home() {
return <ProxmoxDashboard />
}
+114
View File
@@ -0,0 +1,114 @@
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card"
import { Cpu } from "@/components/icons/cpu" // Added import for Cpu
import type { PCIDevice } from "../types/hardware" // Fixed import to use relative path instead of alias
import { Progress } from "@/components/ui/progress"
function GPUCard({ device }: { device: PCIDevice }) {
const hasMonitoring = device.gpu_temperature !== undefined || device.gpu_utilization !== undefined
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Cpu className="h-5 w-5" />
{device.device}
</CardTitle>
<CardDescription>{device.vendor}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<div className="text-muted-foreground">Slot</div>
<div className="font-medium">{device.slot}</div>
</div>
{device.driver && (
<div>
<div className="text-muted-foreground">Driver</div>
<div className="font-medium">{device.driver}</div>
</div>
)}
{device.gpu_driver_version && (
<div>
<div className="text-muted-foreground">Driver Version</div>
<div className="font-medium">{device.gpu_driver_version}</div>
</div>
)}
{device.gpu_memory && (
<div>
<div className="text-muted-foreground">Memory</div>
<div className="font-medium">{device.gpu_memory}</div>
</div>
)}
{device.gpu_compute_capability && (
<div>
<div className="text-muted-foreground">Compute Capability</div>
<div className="font-medium">{device.gpu_compute_capability}</div>
</div>
)}
</div>
{hasMonitoring && (
<div className="space-y-3 pt-4 border-t">
<h4 className="text-sm font-semibold">Real-time Monitoring</h4>
{device.gpu_temperature !== undefined && (
<div className="space-y-1">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Temperature</span>
<span className="font-medium">{device.gpu_temperature}°C</span>
</div>
<Progress value={(device.gpu_temperature / 100) * 100} className="h-2" />
</div>
)}
{device.gpu_utilization !== undefined && (
<div className="space-y-1">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">GPU Utilization</span>
<span className="font-medium">{device.gpu_utilization}%</span>
</div>
<Progress value={device.gpu_utilization} className="h-2" />
</div>
)}
{device.gpu_memory_used && device.gpu_memory_total && (
<div className="space-y-1">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Memory Usage</span>
<span className="font-medium">
{device.gpu_memory_used} / {device.gpu_memory_total}
</span>
</div>
<Progress
value={(Number.parseInt(device.gpu_memory_used) / Number.parseInt(device.gpu_memory_total)) * 100}
className="h-2"
/>
</div>
)}
{device.gpu_power_draw && (
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Power Draw</span>
<span className="font-medium">{device.gpu_power_draw}</span>
</div>
)}
{device.gpu_clock_speed && (
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">GPU Clock</span>
<span className="font-medium">{device.gpu_clock_speed}</span>
</div>
)}
{device.gpu_memory_clock && (
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Memory Clock</span>
<span className="font-medium">{device.gpu_memory_clock}</span>
</div>
)}
</div>
)}
</CardContent>
</Card>
)
}
File diff suppressed because it is too large Load Diff
+499
View File
@@ -0,0 +1,499 @@
"use client"
import { useState, useEffect } from "react"
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"
interface MetricsViewProps {
vmid: number
vmName: string
vmType: "qemu" | "lxc"
onBack: () => void
}
const TIMEFRAME_OPTIONS = [
{ value: "hour", label: "1 Hour" },
{ value: "day", label: "24 Hours" },
{ value: "week", label: "7 Days" },
{ value: "month", label: "30 Days" },
{ value: "year", label: "1 Year" },
]
const CustomCPUTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<div className="bg-gray-900/95 backdrop-blur-sm border border-gray-700 rounded-lg p-3 shadow-xl">
<p className="text-sm font-semibold text-white mb-2">{label}</p>
<div className="space-y-1.5">
{payload.map((entry: any, index: number) => (
<div key={index} className="flex items-center gap-2">
<div className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ backgroundColor: entry.color }} />
<span className="text-xs text-gray-300 min-w-[60px]">{entry.name}:</span>
<span className="text-sm font-semibold text-white">{entry.value}</span>
</div>
))}
</div>
</div>
)
}
return null
}
const CustomMemoryTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<div className="bg-gray-900/95 backdrop-blur-sm border border-gray-700 rounded-lg p-3 shadow-xl">
<p className="text-sm font-semibold text-white mb-2">{label}</p>
<div className="space-y-1.5">
{payload.map((entry: any, index: number) => (
<div key={index} className="flex items-center gap-2">
<div className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ backgroundColor: entry.color }} />
<span className="text-xs text-gray-300 min-w-[60px]">{entry.name}:</span>
<span className="text-sm font-semibold text-white">{entry.value} GB</span>
</div>
))}
</div>
</div>
)
}
return null
}
const CustomDiskTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<div className="bg-gray-900/95 backdrop-blur-sm border border-gray-700 rounded-lg p-3 shadow-xl">
<p className="text-sm font-semibold text-white mb-2">{label}</p>
<div className="space-y-1.5">
{payload.map((entry: any, index: number) => (
<div key={index} className="flex items-center gap-2">
<div className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ backgroundColor: entry.color }} />
<span className="text-xs text-gray-300 min-w-[60px]">{entry.name}:</span>
<span className="text-sm font-semibold text-white">{entry.value} MB</span>
</div>
))}
</div>
</div>
)
}
return null
}
const CustomNetworkTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<div className="bg-gray-900/95 backdrop-blur-sm border border-gray-700 rounded-lg p-3 shadow-xl">
<p className="text-sm font-semibold text-white mb-2">{label}</p>
<div className="space-y-1.5">
{payload.map((entry: any, index: number) => (
<div key={index} className="flex items-center gap-2">
<div className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ backgroundColor: entry.color }} />
<span className="text-xs text-gray-300 min-w-[60px]">{entry.name}:</span>
<span className="text-sm font-semibold text-white">{entry.value} MB</span>
</div>
))}
</div>
</div>
)
}
return null
}
export function MetricsView({ vmid, vmName, vmType, onBack }: MetricsViewProps) {
const [timeframe, setTimeframe] = useState("week")
const [data, setData] = useState<any[]>([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [hiddenDiskLines, setHiddenDiskLines] = useState<string[]>([])
const [hiddenNetworkLines, setHiddenNetworkLines] = useState<string[]>([])
useEffect(() => {
fetchMetrics()
}, [vmid, timeframe])
const fetchMetrics = async () => {
setLoading(true)
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 transformedData = result.data.map((item: any) => {
const date = new Date(item.time * 1000)
let timeLabel = ""
if (timeframe === "hour") {
timeLabel = date.toLocaleString("en-US", {
hour: "2-digit",
minute: "2-digit",
hour12: false,
})
} else if (timeframe === "day") {
timeLabel = date.toLocaleString("en-US", {
hour: "2-digit",
minute: "2-digit",
hour12: false,
})
} else if (timeframe === "week") {
timeLabel = date.toLocaleString("en-US", {
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false,
})
} else if (timeframe === "month") {
timeLabel = date.toLocaleString("en-US", {
month: "short",
day: "numeric",
})
} else {
timeLabel = date.toLocaleString("en-US", {
month: "short",
year: "numeric",
})
}
return {
time: timeLabel,
timestamp: item.time,
cpu: item.cpu ? Number((item.cpu * 100).toFixed(2)) : 0,
memory: item.mem ? Number(((item.mem / item.maxmem) * 100).toFixed(2)) : 0,
memoryGB: item.mem ? Number((item.mem / 1024 / 1024 / 1024).toFixed(2)) : 0,
maxMemoryGB: item.maxmem ? Number((item.maxmem / 1024 / 1024 / 1024).toFixed(2)) : 0,
netin: item.netin ? Number((item.netin / 1024 / 1024).toFixed(2)) : 0,
netout: item.netout ? Number((item.netout / 1024 / 1024).toFixed(2)) : 0,
diskread: item.diskread ? Number((item.diskread / 1024 / 1024).toFixed(2)) : 0,
diskwrite: item.diskwrite ? Number((item.diskwrite / 1024 / 1024).toFixed(2)) : 0,
}
})
setData(transformedData)
} catch (err: any) {
setError(err.message || "Error loading metrics")
} finally {
setLoading(false)
}
}
const formatXAxisTick = (tick: any) => {
return tick
}
const renderAllCharts = () => {
if (loading) {
return (
<div className="flex items-center justify-center h-[400px]">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
)
}
if (error) {
return (
<div className="flex items-center justify-center h-[400px]">
<p className="text-red-500">{error}</p>
</div>
)
}
if (data.length === 0) {
return (
<div className="flex items-center justify-center h-[400px]">
<p className="text-muted-foreground">No data available</p>
</div>
)
}
const tickInterval = Math.ceil(data.length / 8)
return (
<div className="space-y-8">
{/* CPU Chart */}
<div>
<h3 className="text-lg font-semibold mb-4">CPU Usage</h3>
<ResponsiveContainer width="100%" height={300}>
<AreaChart data={data} margin={{ bottom: 80 }}>
<CartesianGrid strokeDasharray="3 3" stroke="currentColor" className="text-border" />
<XAxis
dataKey="time"
stroke="currentColor"
className="text-foreground"
tick={{ fill: "currentColor" }}
angle={-45}
textAnchor="end"
height={60}
interval={tickInterval}
tickFormatter={formatXAxisTick}
/>
<YAxis
stroke="currentColor"
className="text-foreground"
tick={{ fill: "currentColor" }}
label={{ value: "%", angle: -90, position: "insideLeft", fill: "currentColor" }}
domain={[0, "dataMax"]}
/>
<Tooltip content={<CustomCPUTooltip />} />
<Area
type="monotone"
dataKey="cpu"
stroke="#3b82f6"
strokeWidth={2}
fill="#3b82f6"
fillOpacity={0.3}
name="CPU %"
/>
</AreaChart>
</ResponsiveContainer>
</div>
{/* Memory Chart */}
<div>
<h3 className="text-lg font-semibold mb-4">Memory Usage</h3>
<ResponsiveContainer width="100%" height={300}>
<AreaChart data={data} margin={{ bottom: 80 }}>
<CartesianGrid strokeDasharray="3 3" stroke="currentColor" className="text-border" />
<XAxis
dataKey="time"
stroke="currentColor"
className="text-foreground"
tick={{ fill: "currentColor" }}
angle={-45}
textAnchor="end"
height={60}
interval={tickInterval}
tickFormatter={formatXAxisTick}
/>
<YAxis
stroke="currentColor"
className="text-foreground"
tick={{ fill: "currentColor" }}
label={{ value: "GB", angle: -90, position: "insideLeft", fill: "currentColor" }}
domain={[0, "dataMax"]}
/>
<Tooltip content={<CustomMemoryTooltip />} />
<Area
type="monotone"
dataKey="memoryGB"
stroke="#10b981"
fill="#10b981"
fillOpacity={0.3}
strokeWidth={2}
name="Memory GB"
/>
</AreaChart>
</ResponsiveContainer>
</div>
{/* Disk I/O Chart */}
<div>
<h3 className="text-lg font-semibold mb-4">Disk I/O</h3>
<ResponsiveContainer width="100%" height={300}>
<AreaChart data={data} margin={{ bottom: 80 }}>
<CartesianGrid strokeDasharray="3 3" stroke="currentColor" className="text-border" />
<XAxis
dataKey="time"
stroke="currentColor"
className="text-foreground"
tick={{ fill: "currentColor" }}
angle={-45}
textAnchor="end"
height={60}
interval={tickInterval}
tickFormatter={formatXAxisTick}
/>
<YAxis
stroke="currentColor"
className="text-foreground"
tick={{ fill: "currentColor" }}
label={{ value: "MB", angle: -90, position: "insideLeft", fill: "currentColor" }}
domain={[0, "dataMax"]}
/>
<Tooltip content={<CustomDiskTooltip />} />
<Legend content={renderDiskLegend} verticalAlign="top" />
<Area
type="monotone"
dataKey="diskread"
stroke="#10b981"
fill="#10b981"
fillOpacity={0.3}
strokeWidth={2}
name="Read"
hide={hiddenDiskLines.includes("diskread")}
/>
<Area
type="monotone"
dataKey="diskwrite"
stroke="#3b82f6"
fill="#3b82f6"
fillOpacity={0.3}
strokeWidth={2}
name="Write"
hide={hiddenDiskLines.includes("diskwrite")}
/>
</AreaChart>
</ResponsiveContainer>
</div>
{/* Network I/O Chart */}
<div>
<h3 className="text-lg font-semibold mb-4">Network I/O</h3>
<ResponsiveContainer width="100%" height={300}>
<AreaChart data={data} margin={{ bottom: 80 }}>
<CartesianGrid strokeDasharray="3 3" stroke="currentColor" className="text-border" />
<XAxis
dataKey="time"
stroke="currentColor"
className="text-foreground"
tick={{ fill: "currentColor" }}
angle={-45}
textAnchor="end"
height={60}
interval={tickInterval}
tickFormatter={formatXAxisTick}
/>
<YAxis
stroke="currentColor"
className="text-foreground"
tick={{ fill: "currentColor" }}
label={{ value: "MB", angle: -90, position: "insideLeft", fill: "currentColor" }}
domain={[0, "dataMax"]}
/>
<Tooltip content={<CustomNetworkTooltip />} />
<Legend content={renderNetworkLegend} verticalAlign="top" />
<Area
type="monotone"
dataKey="netin"
stroke="#10b981"
fill="#10b981"
fillOpacity={0.3}
strokeWidth={2}
name="Download"
hide={hiddenNetworkLines.includes("netin")}
/>
<Area
type="monotone"
dataKey="netout"
stroke="#3b82f6"
fill="#3b82f6"
fillOpacity={0.3}
strokeWidth={2}
name="Upload"
hide={hiddenNetworkLines.includes("netout")}
/>
</AreaChart>
</ResponsiveContainer>
</div>
</div>
)
}
const handleDiskLegendClick = (dataKey: string) => {
setHiddenDiskLines((prev) => {
if (prev.includes(dataKey)) {
return prev.filter((key) => key !== dataKey)
} else {
return [...prev, dataKey]
}
})
}
const handleNetworkLegendClick = (dataKey: string) => {
setHiddenNetworkLines((prev) => {
if (prev.includes(dataKey)) {
return prev.filter((key) => key !== dataKey)
} else {
return [...prev, dataKey]
}
})
}
const renderDiskLegend = (props: any) => {
const { payload } = props
return (
<div className="flex justify-center gap-6 pb-2">
{payload.map((entry: any) => (
<button
key={entry.dataKey}
onClick={() => handleDiskLegendClick(entry.dataKey)}
className={`flex items-center gap-2 cursor-pointer transition-opacity hover:opacity-100 ${
hiddenDiskLines.includes(entry.dataKey) ? "opacity-40" : "opacity-100"
}`}
>
<span className="w-3 h-3 rounded-full" style={{ backgroundColor: entry.color }} />
<span className="text-sm">{entry.value}</span>
</button>
))}
</div>
)
}
const renderNetworkLegend = (props: any) => {
const { payload } = props
return (
<div className="flex justify-center gap-6 pb-2">
{payload.map((entry: any) => (
<button
key={entry.dataKey}
onClick={() => handleNetworkLegendClick(entry.dataKey)}
className={`flex items-center gap-2 cursor-pointer transition-opacity hover:opacity-100 ${
hiddenNetworkLines.includes(entry.dataKey) ? "opacity-40" : "opacity-100"
}`}
>
<span className="w-3 h-3 rounded-full" style={{ backgroundColor: entry.color }} />
<span className="text-sm">{entry.value}</span>
</button>
))}
</div>
)
}
return (
<div className="flex flex-col h-full max-h-[90vh]">
{/* Fixed Header */}
<div className="p-6 pb-4 border-b shrink-0">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Button variant="ghost" size="icon" onClick={onBack}>
<ArrowLeft className="h-5 w-5" />
</Button>
<div>
<h2 className="text-xl font-semibold">Metrics - {vmName}</h2>
<p className="text-sm text-muted-foreground mt-1">
VMID: {vmid} Type: {vmType.toUpperCase()}
</p>
</div>
</div>
<Select value={timeframe} onValueChange={setTimeframe}>
<SelectTrigger className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
{TIMEFRAME_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* Scrollable Content with all charts */}
<div className="flex-1 overflow-y-auto p-6 min-h-0">{renderAllCharts()}</div>
</div>
)
}
+251
View File
@@ -0,0 +1,251 @@
"use client"
import { Card, CardContent } from "./ui/card"
import { Badge } from "./ui/badge"
import { Wifi, Zap } from "lucide-react"
import { useState, useEffect } from "react"
interface NetworkCardProps {
interface_: {
name: string
type: string
status: string
speed: number
duplex?: string
mtu?: number
mac_address: string | null
addresses: Array<{
ip: string
netmask: string
}>
bytes_sent?: number
bytes_recv?: number
bridge_physical_interface?: string
bridge_bond_slaves?: string[]
vmid?: number
vm_name?: string
vm_type?: string
}
timeframe: "hour" | "day" | "week" | "month" | "year"
onClick?: () => void
}
const getInterfaceTypeBadge = (type: string) => {
switch (type) {
case "physical":
return { color: "bg-blue-500/10 text-blue-500 border-blue-500/20", label: "Physical" }
case "bridge":
return { color: "bg-green-500/10 text-green-500 border-green-500/20", label: "Bridge" }
case "bond":
return { color: "bg-purple-500/10 text-purple-500 border-purple-500/20", label: "Bond" }
case "vlan":
return { color: "bg-cyan-500/10 text-cyan-500 border-cyan-500/20", label: "VLAN" }
case "vm_lxc":
return { color: "bg-orange-500/10 text-orange-500 border-orange-500/20", label: "Virtual" }
case "virtual":
return { color: "bg-orange-500/10 text-orange-500 border-orange-500/20", label: "Virtual" }
default:
return { color: "bg-gray-500/10 text-gray-500 border-gray-500/20", label: "Unknown" }
}
}
const getVMTypeBadge = (vmType: string | undefined) => {
if (vmType === "lxc") {
return { color: "bg-cyan-500/10 text-cyan-500 border-cyan-500/20", label: "LXC" }
} else if (vmType === "vm") {
return { color: "bg-purple-500/10 text-purple-500 border-purple-500/20", label: "VM" }
}
return { color: "bg-gray-500/10 text-gray-500 border-gray-500/20", label: "Unknown" }
}
const formatBytes = (bytes: number | undefined): string => {
if (!bytes || bytes === 0) return "0 B"
const k = 1024
const sizes = ["B", "KB", "MB", "GB", "TB"]
const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`
}
const formatSpeed = (speed: number): string => {
if (speed === 0) return "N/A"
if (speed >= 1000) return `${(speed / 1000).toFixed(1)} Gbps`
return `${speed} Mbps`
}
const formatStorage = (bytes: number): string => {
if (bytes === 0) return "0 B"
const k = 1024
const sizes = ["B", "KB", "MB", "GB", "TB", "PB"]
const i = Math.floor(Math.log(bytes) / Math.log(k))
const value = bytes / Math.pow(k, i)
const decimals = value >= 10 ? 1 : 2
return `${value.toFixed(decimals)} ${sizes[i]}`
}
export function NetworkCard({ interface_, timeframe, onClick }: NetworkCardProps) {
const typeBadge = getInterfaceTypeBadge(interface_.type)
const vmTypeBadge = interface_.vm_type ? getVMTypeBadge(interface_.vm_type) : null
const [trafficData, setTrafficData] = useState<{ received: number; sent: number }>({
received: 0,
sent: 0,
})
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),
})
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))
setTrafficData({
received: receivedGB,
sent: sentGB,
})
}
} 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)
}
}, [interface_.name, interface_.status, interface_.vm_type, timeframe])
const getTimeframeLabel = () => {
switch (timeframe) {
case "hour":
return "Last Hour"
case "day":
return "Last 24 Hours"
case "week":
return "Last 7 Days"
case "month":
return "Last 30 Days"
case "year":
return "Last Year"
default:
return "Last 24 Hours"
}
}
return (
<Card className="bg-card border-border hover:bg-white/5 transition-colors cursor-pointer" onClick={onClick}>
<CardContent className="p-4">
<div className="flex flex-col gap-3">
{/* First row: Icon, Name, Type Badge, Status */}
<div className="flex items-center gap-3 flex-wrap">
<Wifi className="h-5 w-5 text-muted-foreground flex-shrink-0" />
<div className="flex items-center gap-2 min-w-0 flex-1 flex-wrap">
<div className="font-medium text-foreground">{interface_.name}</div>
{vmTypeBadge ? (
<Badge variant="outline" className={vmTypeBadge.color}>
{vmTypeBadge.label}
</Badge>
) : (
<Badge variant="outline" className={typeBadge.color}>
{typeBadge.label}
</Badge>
)}
{interface_.vm_name && (
<div className="text-sm text-muted-foreground truncate"> {interface_.vm_name}</div>
)}
{interface_.type === "bridge" && interface_.bridge_physical_interface && (
<div className="text-sm text-blue-500 font-medium flex items-center gap-1 flex-wrap break-all">
{interface_.bridge_physical_interface}
</div>
)}
</div>
<Badge
variant="outline"
className={
interface_.status === "up"
? "bg-green-500/10 text-green-500 border-green-500/20"
: "bg-red-500/10 text-red-500 border-red-500/20"
}
>
{interface_.status.toUpperCase()}
</Badge>
</div>
{/* Second row: Details - Responsive layout */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div>
<div className="text-muted-foreground text-xs">
{interface_.type === "vm_lxc" ? "VMID" : "IP Address"}
</div>
<div className="font-medium text-foreground font-mono text-sm truncate">
{interface_.type === "vm_lxc"
? (interface_.vmid ?? "N/A")
: interface_.addresses.length > 0
? interface_.addresses[0].ip
: "N/A"}
</div>
</div>
<div>
<div className="text-muted-foreground text-xs">Speed</div>
<div className="font-medium text-foreground flex items-center gap-1 text-xs">
<Zap className="h-3 w-3" />
{formatSpeed(interface_.speed)}
</div>
</div>
<div className="col-span-2 md:col-span-1">
<div className="text-muted-foreground text-xs">{getTimeframeLabel()}</div>
<div className="font-medium text-foreground text-xs">
{interface_.status.toLowerCase() === "up" && interface_.vm_type !== "vm" ? (
<>
<span className="text-green-500"> {formatStorage(trafficData.received * 1024 * 1024 * 1024)}</span>
{" / "}
<span className="text-blue-500"> {formatStorage(trafficData.sent * 1024 * 1024 * 1024)}</span>
</>
) : (
<>
<span className="text-green-500"> {formatBytes(interface_.bytes_recv)}</span>
{" / "}
<span className="text-blue-500"> {formatBytes(interface_.bytes_sent)}</span>
</>
)}
</div>
</div>
{interface_.mac_address && (
<div className="col-span-2 md:col-span-1">
<div className="text-muted-foreground text-xs">MAC</div>
<div className="font-medium text-foreground font-mono text-xs truncate">{interface_.mac_address}</div>
</div>
)}
</div>
</div>
</CardContent>
</Card>
)
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,285 @@
"use client"
import { useState, useEffect } from "react"
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts"
import { Loader2 } from "lucide-react"
interface NetworkMetricsData {
time: string
timestamp: number
netIn: number
netOut: number
}
interface NetworkTrafficChartProps {
timeframe: string
interfaceName?: string
onTotalsCalculated?: (totals: { received: number; sent: number }) => void
refreshInterval?: number // En milisegundos, por defecto 60000 (60 segundos)
}
const CustomNetworkTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<div className="bg-gray-900/95 backdrop-blur-sm border border-gray-700 rounded-lg p-3 shadow-xl">
<p className="text-sm font-semibold text-white mb-2">{label}</p>
<div className="space-y-1.5">
{payload.map((entry: any, index: number) => (
<div key={index} className="flex items-center gap-2">
<div className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ backgroundColor: entry.color }} />
<span className="text-xs text-gray-300 min-w-[60px]">{entry.name}:</span>
<span className="text-sm font-semibold text-white">{entry.value.toFixed(3)} GB</span>
</div>
))}
</div>
</div>
)
}
return null
}
export function NetworkTrafficChart({
timeframe,
interfaceName,
onTotalsCalculated,
refreshInterval = 60000,
}: NetworkTrafficChartProps) {
const [data, setData] = useState<NetworkMetricsData[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [isInitialLoad, setIsInitialLoad] = useState(true)
const [visibleLines, setVisibleLines] = useState({
netIn: true,
netOut: true,
})
useEffect(() => {
setIsInitialLoad(true)
fetchMetrics()
}, [timeframe, interfaceName])
useEffect(() => {
if (refreshInterval > 0) {
const interval = setInterval(() => {
fetchMetrics()
}, refreshInterval)
return () => clearInterval(interval)
}
}, [timeframe, interfaceName, refreshInterval])
const fetchMetrics = async () => {
if (isInitialLoad) {
setLoading(true)
}
setError(null)
try {
const baseUrl =
typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : ""
const apiUrl = interfaceName
? `${baseUrl}/api/network/${interfaceName}/metrics?timeframe=${timeframe}`
: `${baseUrl}/api/node/metrics?timeframe=${timeframe}`
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()
if (!result.data || !Array.isArray(result.data)) {
throw new Error("Invalid data format received from server")
}
if (result.data.length === 0) {
setData([])
setLoading(false)
return
}
const transformedData = result.data.map((item: any, index: number) => {
const date = new Date(item.time * 1000)
let timeLabel = ""
if (timeframe === "hour") {
timeLabel = date.toLocaleString("en-US", {
hour: "2-digit",
minute: "2-digit",
hour12: false,
})
} else if (timeframe === "day") {
timeLabel = date.toLocaleString("en-US", {
hour: "2-digit",
minute: "2-digit",
hour12: false,
})
} else if (timeframe === "week") {
timeLabel = date.toLocaleString("en-US", {
month: "short",
day: "numeric",
hour: "2-digit",
hour12: false,
})
} else if (timeframe === "year") {
timeLabel = date.toLocaleString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
})
} else {
timeLabel = date.toLocaleString("en-US", {
month: "short",
day: "numeric",
})
}
let intervalSeconds = 60
if (index > 0) {
intervalSeconds = item.time - result.data[index - 1].time
}
const netInBytes = (item.netin || 0) * intervalSeconds
const netOutBytes = (item.netout || 0) * intervalSeconds
return {
time: timeLabel,
timestamp: item.time,
netIn: Number((netInBytes / 1024 / 1024 / 1024).toFixed(4)),
netOut: Number((netOutBytes / 1024 / 1024 / 1024).toFixed(4)),
}
})
setData(transformedData)
const totalReceived = transformedData.reduce((sum: number, item: NetworkMetricsData) => sum + item.netIn, 0)
const totalSent = transformedData.reduce((sum: number, item: NetworkMetricsData) => sum + item.netOut, 0)
if (onTotalsCalculated) {
onTotalsCalculated({ received: totalReceived, sent: totalSent })
}
if (isInitialLoad) {
setIsInitialLoad(false)
}
} catch (err: any) {
console.error("[v0] Error fetching network metrics:", err)
setError(err.message || "Error loading metrics")
} finally {
setLoading(false)
}
}
const tickInterval = Math.ceil(data.length / 8)
const handleLegendClick = (dataKey: string) => {
setVisibleLines((prev) => ({
...prev,
[dataKey as keyof typeof prev]: !prev[dataKey as keyof typeof prev],
}))
}
const renderLegend = (props: any) => {
const { payload } = props
return (
<div className="flex justify-center gap-4 pb-2 flex-wrap">
{payload.map((entry: any, index: number) => {
const isVisible = visibleLines[entry.dataKey as keyof typeof visibleLines]
return (
<div
key={`legend-${index}`}
className="flex items-center gap-2 cursor-pointer hover:opacity-80 transition-opacity"
onClick={() => handleLegendClick(entry.dataKey)}
style={{ opacity: isVisible ? 1 : 0.4 }}
>
<div className="w-3 h-3 rounded-sm" style={{ backgroundColor: entry.color }} />
<span className="text-sm text-foreground">{entry.value}</span>
</div>
)
})}
</div>
)
}
if (loading && isInitialLoad) {
return (
<div className="flex items-center justify-center h-[300px]">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
)
}
if (error) {
return (
<div className="flex flex-col items-center justify-center h-[300px] gap-2">
<p className="text-muted-foreground text-sm">Network metrics not available yet</p>
<p className="text-xs text-red-500">{error}</p>
</div>
)
}
if (data.length === 0) {
return (
<div className="flex items-center justify-center h-[300px]">
<p className="text-muted-foreground text-sm">No network metrics available</p>
</div>
)
}
return (
<ResponsiveContainer width="100%" height={300}>
<AreaChart data={data} margin={{ bottom: 80 }}>
<CartesianGrid strokeDasharray="3 3" stroke="currentColor" className="text-border" />
<XAxis
dataKey="time"
stroke="currentColor"
className="text-foreground"
tick={{ fill: "currentColor", fontSize: 12 }}
angle={-45}
textAnchor="end"
height={60}
interval={tickInterval}
/>
<YAxis
stroke="currentColor"
className="text-foreground"
tick={{ fill: "currentColor", fontSize: 12 }}
label={{ value: "GB", angle: -90, position: "insideLeft", fill: "currentColor" }}
domain={[0, "auto"]}
/>
<Tooltip content={<CustomNetworkTooltip />} />
<Legend verticalAlign="top" height={36} content={renderLegend} />
<Area
type="monotone"
dataKey="netIn"
stroke="#10b981"
strokeWidth={2}
fill="#10b981"
fillOpacity={0.3}
name="Received"
hide={!visibleLines.netIn}
isAnimationActive={true}
animationDuration={300}
animationEasing="ease-in-out"
/>
<Area
type="monotone"
dataKey="netOut"
stroke="#3b82f6"
strokeWidth={2}
fill="#3b82f6"
fillOpacity={0.3}
name="Sent"
hide={!visibleLines.netOut}
isAnimationActive={true}
animationDuration={300}
animationEasing="ease-in-out"
/>
</AreaChart>
</ResponsiveContainer>
)
}
+465
View File
@@ -0,0 +1,465 @@
"use client"
import { useState, useEffect } from "react"
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"
const TIMEFRAME_OPTIONS = [
{ value: "hour", label: "1 Hour" },
{ value: "day", label: "24 Hours" },
{ value: "week", label: "7 Days" },
{ value: "month", label: "30 Days" },
]
interface NodeMetricsData {
time: string
timestamp: number
cpu: number
load: number
memoryTotal: number
memoryUsed: number
memoryFree: number
memoryZfsArc: number
}
const CustomCpuTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<div className="bg-gray-900/95 backdrop-blur-sm border border-gray-700 rounded-lg p-3 shadow-xl">
<p className="text-sm font-semibold text-white mb-2">{label}</p>
<div className="space-y-1.5">
{payload.map((entry: any, index: number) => (
<div key={index} className="flex items-center gap-2">
<div className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ backgroundColor: entry.color }} />
<span className="text-xs text-gray-300 min-w-[60px]">{entry.name}:</span>
<span className="text-sm font-semibold text-white">{entry.value}</span>
</div>
))}
</div>
</div>
)
}
return null
}
const CustomMemoryTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<div className="bg-gray-900/95 backdrop-blur-sm border border-gray-700 rounded-lg p-3 shadow-xl">
<p className="text-sm font-semibold text-white mb-2">{label}</p>
<div className="space-y-1.5">
{payload.map((entry: any, index: number) => (
<div key={index} className="flex items-center gap-2">
<div className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ backgroundColor: entry.color }} />
<span className="text-xs text-gray-300 min-w-[60px]">{entry.name}:</span>
<span className="text-sm font-semibold text-white">{entry.value} GB</span>
</div>
))}
</div>
</div>
)
}
return null
}
export function NodeMetricsCharts() {
const [timeframe, setTimeframe] = useState("day")
const [data, setData] = useState<NodeMetricsData[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [visibleLines, setVisibleLines] = useState({
cpu: { cpu: true, load: true },
memory: { memoryTotal: true, memoryUsed: true, memoryZfsArc: true, memoryFree: true },
})
useEffect(() => {
console.log("[v0] NodeMetricsCharts component mounted")
fetchMetrics()
}, [timeframe])
const fetchMetrics = async () => {
console.log("[v0] fetchMetrics called with timeframe:", timeframe)
setLoading(true)
setError(null)
try {
const baseUrl =
typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : ""
const apiUrl = `${baseUrl}/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)
if (!result.data || !Array.isArray(result.data)) {
console.error("[v0] Invalid data format - data is not an array:", result)
throw new Error("Invalid data format received from server")
}
if (result.data.length === 0) {
console.warn("[v0] No data points received")
setData([])
setLoading(false)
return
}
console.log("[v0] First data point sample:", result.data[0])
console.log("[v0] First data point loadavg field:", result.data[0]?.loadavg)
console.log("[v0] loadavg type:", typeof result.data[0]?.loadavg)
console.log("[v0] loadavg is array:", Array.isArray(result.data[0]?.loadavg))
if (result.data[0]?.loadavg) {
console.log("[v0] loadavg length:", result.data[0].loadavg.length)
console.log("[v0] loadavg[0]:", result.data[0].loadavg[0])
}
const transformedData = result.data.map((item: any) => {
const date = new Date(item.time * 1000)
let timeLabel = ""
if (timeframe === "hour") {
timeLabel = date.toLocaleString("en-US", {
hour: "2-digit",
minute: "2-digit",
hour12: false,
})
} else if (timeframe === "day") {
timeLabel = date.toLocaleString("en-US", {
hour: "2-digit",
minute: "2-digit",
hour12: false,
})
} else if (timeframe === "week") {
timeLabel = date.toLocaleString("en-US", {
month: "short",
day: "numeric",
hour: "2-digit",
hour12: false,
})
} else {
timeLabel = date.toLocaleString("en-US", {
month: "short",
day: "numeric",
})
}
return {
time: timeLabel,
timestamp: item.time,
cpu: item.cpu ? Number((item.cpu * 100).toFixed(2)) : 0,
load: item.loadavg
? typeof item.loadavg === "number"
? Number(item.loadavg.toFixed(2))
: Array.isArray(item.loadavg) && item.loadavg.length > 0
? Number(item.loadavg[0].toFixed(2))
: 0
: 0,
memoryTotal: item.memtotal ? Number((item.memtotal / 1024 / 1024 / 1024).toFixed(2)) : 0,
memoryUsed: item.memused ? Number((item.memused / 1024 / 1024 / 1024).toFixed(2)) : 0,
memoryFree: item.memfree ? Number((item.memfree / 1024 / 1024 / 1024).toFixed(2)) : 0,
memoryZfsArc: item.zfsarc ? Number((item.zfsarc / 1024 / 1024 / 1024).toFixed(2)) : 0,
}
})
setData(transformedData)
} catch (err: any) {
console.error("[v0] Error fetching node metrics:", err)
console.error("[v0] Error message:", err.message)
console.error("[v0] Error stack:", err.stack)
setError(err.message || "Error loading metrics")
} finally {
console.log("[v0] fetchMetrics finally block - setting loading to false")
setLoading(false)
}
}
const tickInterval = Math.ceil(data.length / 8)
const handleLegendClick = (chartType: "cpu" | "memory", dataKey: string) => {
setVisibleLines((prev) => ({
...prev,
[chartType]: {
...prev[chartType],
[dataKey as keyof (typeof prev)[typeof chartType]]:
!prev[chartType][dataKey as keyof (typeof prev)[typeof chartType]],
},
}))
}
const renderLegend = (chartType: "cpu" | "memory") => (props: any) => {
const { payload } = props
return (
<div className="flex justify-center gap-4 pb-2 flex-wrap">
{payload.map((entry: any, index: number) => {
const isVisible = visibleLines[chartType][entry.dataKey as keyof (typeof visibleLines)[typeof chartType]]
return (
<div
key={`legend-${index}`}
className="flex items-center gap-2 cursor-pointer hover:opacity-80 transition-opacity"
onClick={() => handleLegendClick(chartType, entry.dataKey)}
style={{ opacity: isVisible ? 1 : 0.4 }}
>
<div className="w-3 h-3 rounded-sm" style={{ backgroundColor: entry.color }} />
<span className="text-sm text-foreground">{entry.value}</span>
</div>
)
})}
</div>
)
}
console.log("[v0] Render state - loading:", loading, "error:", error, "data length:", data.length)
if (loading) {
console.log("[v0] Rendering loading state")
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card className="bg-card border-border">
<CardContent className="p-6">
<div className="flex items-center justify-center h-[300px]">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
</CardContent>
</Card>
<Card className="bg-card border-border">
<CardContent className="p-6">
<div className="flex items-center justify-center h-[300px]">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
</CardContent>
</Card>
</div>
)
}
if (error) {
console.log("[v0] Rendering error state:", error)
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card className="bg-card border-border">
<CardContent className="p-6">
<div className="flex flex-col items-center justify-center h-[300px] gap-2">
<p className="text-muted-foreground text-sm">Metrics data not available yet</p>
<p className="text-xs text-red-500">{error}</p>
</div>
</CardContent>
</Card>
<Card className="bg-card border-border">
<CardContent className="p-6">
<div className="flex flex-col items-center justify-center h-[300px] gap-2">
<p className="text-muted-foreground text-sm">Metrics data not available yet</p>
<p className="text-xs text-red-500">{error}</p>
</div>
</CardContent>
</Card>
</div>
)
}
if (data.length === 0) {
console.log("[v0] Rendering no data state")
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card className="bg-card border-border">
<CardContent className="p-6">
<div className="flex items-center justify-center h-[300px]">
<p className="text-muted-foreground text-sm">No metrics data available</p>
</div>
</CardContent>
</Card>
<Card className="bg-card border-border">
<CardContent className="p-6">
<div className="flex items-center justify-center h-[300px]">
<p className="text-muted-foreground text-sm">No metrics data available</p>
</div>
</CardContent>
</Card>
</div>
)
}
console.log("[v0] Rendering charts with", data.length, "data points")
return (
<div className="space-y-6">
{/* Timeframe Selector */}
<div className="flex justify-end">
<Select value={timeframe} onValueChange={setTimeframe}>
<SelectTrigger className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
{TIMEFRAME_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Charts Grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* CPU Usage + Load Average Chart */}
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground flex items-center">
<TrendingUp className="h-5 w-5 mr-2" />
CPU Usage & Load Average
</CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<AreaChart data={data} margin={{ bottom: 60, left: 30, right: 10 }}>
<CartesianGrid strokeDasharray="3 3" stroke="currentColor" className="text-border" />
<XAxis
dataKey="time"
stroke="currentColor"
className="text-foreground"
tick={{ fill: "currentColor", fontSize: 12 }}
angle={-45}
textAnchor="end"
height={60}
interval={tickInterval}
/>
<YAxis
yAxisId="left"
stroke="currentColor"
className="text-foreground"
tick={{ fill: "currentColor", fontSize: 12 }}
label={{ value: "CPU %", angle: -90, position: "insideLeft", fill: "currentColor" }}
domain={[0, "dataMax"]}
/>
<YAxis
yAxisId="right"
orientation="right"
stroke="currentColor"
className="text-foreground"
tick={{ fill: "currentColor", fontSize: 12 }}
label={{ value: "Load", angle: 90, position: "insideRight", fill: "currentColor" }}
domain={[0, "dataMax"]}
/>
<Tooltip content={<CustomCpuTooltip />} />
<Legend verticalAlign="top" height={36} content={renderLegend("cpu")} />
<Area
yAxisId="left"
type="monotone"
dataKey="cpu"
stroke="#3b82f6"
strokeWidth={2}
fill="#3b82f6"
fillOpacity={0.3}
name="CPU %"
hide={!visibleLines.cpu.cpu}
/>
<Area
yAxisId="right"
type="monotone"
dataKey="load"
stroke="#10b981"
strokeWidth={2}
fill="#10b981"
fillOpacity={0.3}
name="Load Avg"
hide={!visibleLines.cpu.load}
/>
</AreaChart>
</ResponsiveContainer>
</CardContent>
</Card>
{/* Memory Usage Chart */}
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground flex items-center">
<MemoryStick className="h-5 w-5 mr-2" />
Memory Usage
</CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<AreaChart data={data} margin={{ bottom: 60, left: 30, right: 10 }}>
<CartesianGrid strokeDasharray="3 3" stroke="currentColor" className="text-border" />
<XAxis
dataKey="time"
stroke="currentColor"
className="text-foreground"
tick={{ fill: "currentColor", fontSize: 12 }}
angle={-45}
textAnchor="end"
height={60}
interval={tickInterval}
/>
<YAxis
stroke="currentColor"
className="text-foreground"
tick={{ fill: "currentColor", fontSize: 12 }}
label={{ value: "GB", angle: -90, position: "insideLeft", fill: "currentColor" }}
domain={[0, "dataMax"]}
/>
<Tooltip content={<CustomMemoryTooltip />} />
<Legend verticalAlign="top" height={36} content={renderLegend("memory")} />
<Area
type="monotone"
dataKey="memoryTotal"
stroke="#3b82f6"
strokeWidth={2}
fill="#3b82f6"
fillOpacity={0.1}
name="Total"
hide={!visibleLines.memory.memoryTotal}
/>
<Area
type="monotone"
dataKey="memoryUsed"
stroke="#10b981"
strokeWidth={2}
fill="#10b981"
fillOpacity={0.3}
name="Used"
hide={!visibleLines.memory.memoryUsed}
/>
<Area
type="monotone"
dataKey="memoryZfsArc"
stroke="#f59e0b"
strokeWidth={2}
fill="#f59e0b"
fillOpacity={0.3}
name="ZFS ARC"
hide={!visibleLines.memory.memoryZfsArc}
/>
<Area
type="monotone"
dataKey="memoryFree"
stroke="#06b6d4"
strokeWidth={2}
fill="#06b6d4"
fillOpacity={0.3}
name="Available"
hide={!visibleLines.memory.memoryFree}
/>
</AreaChart>
</ResponsiveContainer>
</CardContent>
</Card>
</div>
</div>
)
}
+274
View File
@@ -0,0 +1,274 @@
"use client"
import type React from "react"
import { useState, useEffect } from "react"
import { Button } from "./ui/button"
import { Dialog, DialogContent } from "./ui/dialog"
import {
ChevronLeft,
ChevronRight,
X,
Sparkles,
LayoutDashboard,
HardDrive,
Network,
Box,
Cpu,
FileText,
Rocket,
} from "lucide-react"
import Image from "next/image"
interface OnboardingSlide {
id: number
title: string
description: string
image?: string
icon: React.ReactNode
gradient: string
}
const slides: OnboardingSlide[] = [
{
id: 0,
title: "Welcome to ProxMenux Monitor!",
description:
"Your new monitoring tool for Proxmox. Discover all the features that will help you manage and supervise your infrastructure efficiently.",
icon: <Sparkles className="h-16 w-16" />,
gradient: "from-blue-500 via-purple-500 to-pink-500",
},
{
id: 1,
title: "System Overview",
description:
"Monitor your server's status in real-time: CPU, memory, temperature, system load and more. Everything in an intuitive and easy-to-understand dashboard.",
image: "/images/onboarding/imagen1.png",
icon: <LayoutDashboard className="h-12 w-12" />,
gradient: "from-blue-500 to-cyan-500",
},
{
id: 2,
title: "Storage Management",
description:
"Visualize the status of all your disks and volumes. Detailed information on capacity, usage, SMART health, temperature and performance of each storage device.",
image: "/images/onboarding/imagen2.png",
icon: <HardDrive className="h-12 w-12" />,
gradient: "from-cyan-500 to-teal-500",
},
{
id: 3,
title: "Network Metrics",
description:
"Monitor network traffic in real-time. Bandwidth statistics, active interfaces, transfer speeds and historical usage graphs.",
image: "/images/onboarding/imagen3.png",
icon: <Network className="h-12 w-12" />,
gradient: "from-teal-500 to-green-500",
},
{
id: 4,
title: "Virtual Machines & Containers",
description:
"Manage all your VMs and LXC containers from one place. Status, allocated resources, current usage and quick controls for each virtual machine.",
image: "/images/onboarding/imagen4.png",
icon: <Box className="h-12 w-12" />,
gradient: "from-green-500 to-emerald-500",
},
{
id: 5,
title: "Hardware Information",
description:
"Complete details of your server hardware: CPU, RAM, GPU, disks, network, UPS and more. Technical specifications, models, serial numbers and status of each component.",
image: "/images/onboarding/imagen5.png",
icon: <Cpu className="h-12 w-12" />,
gradient: "from-emerald-500 to-blue-500",
},
{
id: 6,
title: "System Logs",
description:
"Access system logs in real-time. Filter by event type, search for specific errors and keep complete track of your server activity. Download the displayed logs for further analysis.",
image: "/images/onboarding/imagen6.png",
icon: <FileText className="h-12 w-12" />,
gradient: "from-blue-500 to-indigo-500",
},
{
id: 7,
title: "Ready for the Future!",
description:
"ProxMenux Monitor is prepared to receive updates and improvements that will be added gradually, improving the user experience and being able to execute ProxMenux functions from the web panel.",
icon: <Rocket className="h-16 w-16" />,
gradient: "from-indigo-500 via-purple-500 to-pink-500",
},
]
export function OnboardingCarousel() {
const [open, setOpen] = useState(false)
const [currentSlide, setCurrentSlide] = useState(0)
const [direction, setDirection] = useState<"next" | "prev">("next")
useEffect(() => {
const hasSeenOnboarding = localStorage.getItem("proxmenux-onboarding-seen")
if (!hasSeenOnboarding) {
setOpen(true)
}
}, [])
const handleNext = () => {
if (currentSlide < slides.length - 1) {
setDirection("next")
setCurrentSlide(currentSlide + 1)
} else {
setOpen(false)
}
}
const handlePrev = () => {
if (currentSlide > 0) {
setDirection("prev")
setCurrentSlide(currentSlide - 1)
}
}
const handleSkip = () => {
setOpen(false)
}
const handleDontShowAgain = () => {
localStorage.setItem("proxmenux-onboarding-seen", "true")
setOpen(false)
}
const handleDotClick = (index: number) => {
setDirection(index > currentSlide ? "next" : "prev")
setCurrentSlide(index)
}
const slide = slides[currentSlide]
return (
<Dialog open={open} onOpenChange={setOpen}>
<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}
>
<X className="h-4 w-4" />
</Button>
<div
className={`relative h-48 md:h-64 bg-gradient-to-br ${slide.gradient} flex items-center justify-center overflow-hidden`}
>
<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">
<Image
src={slide.image || "/placeholder.svg"}
alt={slide.title}
width={600}
height={400}
className="rounded-lg shadow-2xl object-cover max-h-36 md:max-h-48"
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")
}
}}
/>
<div className="fallback-icon hidden">{slide.icon}</div>
</div>
) : (
<div className="animate-pulse">{slide.icon}</div>
)}
</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="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">
{slide.description}
</p>
</div>
{/* Progress dots */}
<div className="flex items-center justify-center gap-2 py-2 md:py-4">
{slides.map((_, index) => (
<button
key={index}
onClick={() => handleDotClick(index)}
className={`transition-all duration-300 rounded-full ${
index === currentSlide
? "w-8 h-2.5 bg-blue-500 shadow-lg shadow-blue-500/50"
: "w-2.5 h-2.5 bg-muted-foreground/60 hover:bg-muted-foreground/80 border border-muted-foreground/40"
}`}
aria-label={`Go to slide ${index + 1}`}
/>
))}
</div>
<div className="flex flex-col sm:flex-row items-center justify-between gap-3 md:gap-4">
<Button
variant="ghost"
onClick={handlePrev}
disabled={currentSlide === 0}
className="gap-2 w-full sm:w-auto"
>
<ChevronLeft className="h-4 w-4" />
Previous
</Button>
<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">
Skip
</Button>
<Button onClick={handleNext} className="gap-2 bg-blue-500 hover:bg-blue-600 flex-1 sm:flex-none">
Next
<ChevronRight className="h-4 w-4" />
</Button>
</>
) : (
<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"
>
Get Started!
<Sparkles className="h-4 w-4" />
</Button>
)}
</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>
</div>
</DialogContent>
</Dialog>
)
}
+544
View File
@@ -0,0 +1,544 @@
"use client"
import { useState, useEffect, useMemo, useCallback } from "react"
import { Badge } from "./ui/badge"
import { Button } from "./ui/button"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs"
import { SystemOverview } from "./system-overview"
import { StorageOverview } from "./storage-overview"
import { NetworkMetrics } from "./network-metrics"
import { VirtualMachines } from "./virtual-machines"
import Hardware from "./hardware"
import { SystemLogs } from "./system-logs"
import { OnboardingCarousel } from "./onboarding-carousel"
import {
RefreshCw,
AlertTriangle,
CheckCircle,
XCircle,
Server,
Menu,
LayoutDashboard,
HardDrive,
NetworkIcon,
Box,
Cpu,
FileText,
} from "lucide-react"
import Image from "next/image"
import { ThemeToggle } from "./theme-toggle"
import { Sheet, SheetContent, SheetTrigger } from "./ui/sheet"
interface SystemStatus {
status: "healthy" | "warning" | "critical"
uptime: string
lastUpdate: string
serverName: string
nodeId: string
}
interface FlaskSystemData {
hostname: string
node_id: string
uptime: string
cpu_usage: number
memory_usage: number
temperature: number
load_average: number[]
}
export function ProxmoxDashboard() {
const [systemStatus, setSystemStatus] = useState<SystemStatus>({
status: "healthy",
uptime: "Loading...",
lastUpdate: new Date().toLocaleTimeString(),
serverName: "Loading...",
nodeId: "Loading...",
})
const [isRefreshing, setIsRefreshing] = useState(false)
const [isServerConnected, setIsServerConnected] = useState(true)
const [componentKey, setComponentKey] = useState(0)
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
const [activeTab, setActiveTab] = useState("overview")
const [showNavigation, setShowNavigation] = useState(true)
const [lastScrollY, setLastScrollY] = useState(0)
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)
if (!response.ok) {
throw new Error(`Server responded with status: ${response.status}`)
}
const data: FlaskSystemData = await response.json()
console.log("[v0] System data received:", data)
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"
}
setSystemStatus({
status,
uptime: data.uptime,
lastUpdate: new Date().toLocaleTimeString(),
serverName: data.hostname,
nodeId: data.node_id,
})
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) => ({
...prev,
status: "critical",
serverName: "Server Offline",
nodeId: "Server Offline",
uptime: "N/A",
lastUpdate: new Date().toLocaleTimeString(),
}))
}
}, [])
useEffect(() => {
fetchSystemData()
const interval = setInterval(fetchSystemData, 10000)
return () => clearInterval(interval)
}, [fetchSystemData])
useEffect(() => {
if (
systemStatus.serverName &&
systemStatus.serverName !== "Loading..." &&
systemStatus.serverName !== "Server Offline"
) {
document.title = `${systemStatus.serverName} - ProxMenux Monitor`
} else {
document.title = "ProxMenux Monitor"
}
}, [systemStatus.serverName])
useEffect(() => {
let hideTimeout: ReturnType<typeof setTimeout> | null = null
let lastPosition = window.scrollY
const handleScroll = () => {
const currentScrollY = window.scrollY
const delta = currentScrollY - lastPosition
if (currentScrollY < 50) {
setShowNavigation(true)
} else if (delta > 2) {
if (hideTimeout) clearTimeout(hideTimeout)
hideTimeout = setTimeout(() => setShowNavigation(false), 20)
} else if (delta < -2) {
if (hideTimeout) clearTimeout(hideTimeout)
setShowNavigation(true)
}
lastPosition = currentScrollY
}
window.addEventListener("scroll", handleScroll, { passive: true })
return () => {
window.removeEventListener("scroll", handleScroll)
if (hideTimeout) clearTimeout(hideTimeout)
}
}, [])
const refreshData = async () => {
setIsRefreshing(true)
await fetchSystemData()
setComponentKey((prev) => prev + 1)
await new Promise((resolve) => setTimeout(resolve, 500))
setIsRefreshing(false)
}
const statusIcon = useMemo(() => {
switch (systemStatus.status) {
case "healthy":
return <CheckCircle className="h-4 w-4 text-green-500" />
case "warning":
return <AlertTriangle className="h-4 w-4 text-yellow-500" />
case "critical":
return <XCircle className="h-4 w-4 text-red-500" />
}
}, [systemStatus.status])
const statusColor = useMemo(() => {
switch (systemStatus.status) {
case "healthy":
return "bg-green-500/10 text-green-500 border-green-500/20"
case "warning":
return "bg-yellow-500/10 text-yellow-500 border-yellow-500/20"
case "critical":
return "bg-red-500/10 text-red-500 border-red-500/20"
}
}, [systemStatus.status])
const getActiveTabLabel = () => {
switch (activeTab) {
case "overview":
return "Overview"
case "storage":
return "Storage"
case "network":
return "Network"
case "vms":
return "VMs & LXCs"
case "hardware":
return "Hardware"
case "logs":
return "System Logs"
default:
return "Navigation Menu"
}
}
return (
<div className="min-h-screen bg-background">
<OnboardingCarousel />
{!isServerConnected && (
<div className="bg-red-500/10 border-b border-red-500/20 px-6 py-3">
<div className="container mx-auto">
<div className="flex items-center space-x-2 text-red-500 mb-2">
<XCircle className="h-5 w-5" />
<span className="font-medium">ProxMenux Server Connection Failed</span>
</div>
<div className="text-sm text-red-500/80 space-y-1 ml-7">
<p> Check that the monitor.service is running correctly.</p>
<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>
</p>
</div>
</div>
</div>
)}
<header className="border-b border-border bg-card sticky top-0 z-50 shadow-sm">
<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">
{/* Logo and Title */}
<div className="flex items-center space-x-2 md:space-x-3 min-w-0">
<div className="w-16 h-16 md:w-10 md:h-10 relative flex items-center justify-center bg-primary/10 flex-shrink-0">
<Image
src="/images/proxmenux-logo.png"
alt="ProxMenux Logo"
width={64}
height={64}
className="object-contain md:w-10 md:h-10"
priority
onError={(e) => {
console.log("[v0] Logo failed to load, using fallback icon")
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-8 w-8 md:h-6 md:w-6 text-primary absolute fallback-icon hidden" />
</div>
<div className="min-w-0">
<h1 className="text-lg md:text-xl font-semibold text-foreground truncate">ProxMenux Monitor</h1>
<p className="text-xs md:text-sm text-muted-foreground">Proxmox System Dashboard</p>
<div className="lg:hidden flex items-center gap-1 text-xs text-muted-foreground mt-0.5">
<Server className="h-3 w-3" />
<span className="truncate">Node: {systemStatus.serverName}</span>
</div>
</div>
</div>
{/* Desktop Actions */}
<div className="hidden lg:flex items-center space-x-4">
<div className="flex items-center space-x-2">
<Server className="h-4 w-4 text-muted-foreground" />
<div className="text-sm">
<div className="font-medium text-foreground">Node: {systemStatus.serverName}</div>
</div>
</div>
<Badge variant="outline" className={statusColor}>
{statusIcon}
<span className="ml-1 capitalize">{systemStatus.status}</span>
</Badge>
<div className="text-sm text-muted-foreground whitespace-nowrap">Uptime: {systemStatus.uptime}</div>
<Button
variant="outline"
size="sm"
onClick={refreshData}
disabled={isRefreshing}
className="border-border/50 bg-transparent hover:bg-secondary"
>
<RefreshCw className={`h-4 w-4 mr-2 ${isRefreshing ? "animate-spin" : ""}`} />
Refresh
</Button>
<ThemeToggle />
</div>
{/* Mobile Actions */}
<div className="flex lg:hidden items-center gap-2">
<Badge variant="outline" className={`${statusColor} text-xs px-2`}>
{statusIcon}
<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">
<RefreshCw className={`h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`} />
</Button>
<ThemeToggle />
</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>
</div>
</div>
</header>
<div
className={`sticky z-40 bg-background
top-[120px] md:top-[76px]
transition-all duration-700 ease-[cubic-bezier(0.4,0,0.2,1)]
${showNavigation ? "translate-y-0 opacity-100" : "-translate-y-[120%] opacity-0 pointer-events-none"}
`}
>
<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">
<TabsTrigger
value="overview"
className="data-[state=active]:bg-blue-500 data-[state=active]:text-white data-[state=active]:rounded-md"
>
Overview
</TabsTrigger>
<TabsTrigger
value="storage"
className="data-[state=active]:bg-blue-500 data-[state=active]:text-white data-[state=active]:rounded-md"
>
Storage
</TabsTrigger>
<TabsTrigger
value="network"
className="data-[state=active]:bg-blue-500 data-[state=active]:text-white data-[state=active]:rounded-md"
>
Network
</TabsTrigger>
<TabsTrigger
value="vms"
className="data-[state=active]:bg-blue-500 data-[state=active]:text-white data-[state=active]:rounded-md"
>
VMs & LXCs
</TabsTrigger>
<TabsTrigger
value="hardware"
className="data-[state=active]:bg-blue-500 data-[state=active]:text-white data-[state=active]:rounded-md"
>
Hardware
</TabsTrigger>
<TabsTrigger
value="logs"
className="data-[state=active]:bg-blue-500 data-[state=active]:text-white data-[state=active]:rounded-md"
>
System Logs
</TabsTrigger>
</TabsList>
<Sheet open={mobileMenuOpen} onOpenChange={setMobileMenuOpen}>
<div className="md:hidden">
<SheetTrigger asChild>
<Button
variant="outline"
className={`w-full justify-between border-border ${
activeTab ? "bg-blue-500/10 text-blue-500" : "bg-card"
}`}
>
<span>{getActiveTabLabel()}</span>
<Menu className="h-4 w-4" />
</Button>
</SheetTrigger>
</div>
<SheetContent side="top" className="bg-card border-border">
<div className="flex flex-col gap-2 mt-4">
<Button
variant="ghost"
onClick={() => {
setActiveTab("overview")
setMobileMenuOpen(false)
}}
className={`w-full justify-start gap-3 ${
activeTab === "overview"
? "bg-blue-500/10 text-blue-500 border-l-4 border-blue-500 rounded-l-none"
: ""
}`}
>
<LayoutDashboard className="h-5 w-5" />
<span>Overview</span>
</Button>
<Button
variant="ghost"
onClick={() => {
setActiveTab("storage")
setMobileMenuOpen(false)
}}
className={`w-full justify-start gap-3 ${
activeTab === "storage"
? "bg-blue-500/10 text-blue-500 border-l-4 border-blue-500 rounded-l-none"
: ""
}`}
>
<HardDrive className="h-5 w-5" />
<span>Storage</span>
</Button>
<Button
variant="ghost"
onClick={() => {
setActiveTab("network")
setMobileMenuOpen(false)
}}
className={`w-full justify-start gap-3 ${
activeTab === "network"
? "bg-blue-500/10 text-blue-500 border-l-4 border-blue-500 rounded-l-none"
: ""
}`}
>
<NetworkIcon className="h-5 w-5" />
<span>Network</span>
</Button>
<Button
variant="ghost"
onClick={() => {
setActiveTab("vms")
setMobileMenuOpen(false)
}}
className={`w-full justify-start gap-3 ${
activeTab === "vms"
? "bg-blue-500/10 text-blue-500 border-l-4 border-blue-500 rounded-l-none"
: ""
}`}
>
<Box className="h-5 w-5" />
<span>VMs & LXCs</span>
</Button>
<Button
variant="ghost"
onClick={() => {
setActiveTab("hardware")
setMobileMenuOpen(false)
}}
className={`w-full justify-start gap-3 ${
activeTab === "hardware"
? "bg-blue-500/10 text-blue-500 border-l-4 border-blue-500 rounded-l-none"
: ""
}`}
>
<Cpu className="h-5 w-5" />
<span>Hardware</span>
</Button>
<Button
variant="ghost"
onClick={() => {
setActiveTab("logs")
setMobileMenuOpen(false)
}}
className={`w-full justify-start gap-3 ${
activeTab === "logs"
? "bg-blue-500/10 text-blue-500 border-l-4 border-blue-500 rounded-l-none"
: ""
}`}
>
<FileText className="h-5 w-5" />
<span>System Logs</span>
</Button>
</div>
</SheetContent>
</Sheet>
</Tabs>
</div>
</div>
<div className="container mx-auto px-4 md:px-6 py-4 md:py-6">
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-4 md:space-y-6">
<TabsContent value="overview" className="space-y-4 md:space-y-6 mt-0">
<SystemOverview key={`overview-${componentKey}`} />
</TabsContent>
<TabsContent value="storage" className="space-y-4 md:space-y-6 mt-0">
<StorageOverview key={`storage-${componentKey}`} />
</TabsContent>
<TabsContent value="network" className="space-y-4 md:space-y-6 mt-0">
<NetworkMetrics key={`network-${componentKey}`} />
</TabsContent>
<TabsContent value="vms" className="space-y-4 md:space-y-6 mt-0">
<VirtualMachines key={`vms-${componentKey}`} />
</TabsContent>
<TabsContent value="hardware" className="space-y-4 md:space-y-6 mt-0">
<Hardware key={`hardware-${componentKey}`} />
</TabsContent>
<TabsContent value="logs" className="space-y-4 md:space-y-6 mt-0">
<SystemLogs key={`logs-${componentKey}`} />
</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>
<a
href="https://ko-fi.com/macrimi"
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 hover:text-blue-600 hover:underline transition-colors"
>
Support and contribute to the project
</a>
</p>
</footer>
</div>
</div>
)
}
+10
View File
@@ -0,0 +1,10 @@
import { LayoutDashboard, HardDrive, Network, Server, Cpu, FileText } from "path-to-icons"
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: "System Logs", href: "/logs", icon: FileText },
]
+237
View File
@@ -0,0 +1,237 @@
"use client"
import { useState, useEffect } from "react"
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"
interface StorageData {
total: number
used: number
available: number
disks: DiskInfo[]
}
interface DiskInfo {
name: string
mountpoint: string
fstype: string
total: number
used: number
available: number
usage_percent: number
health: string
temperature: number
}
const fetchStorageData = async (): Promise<StorageData | null> => {
try {
console.log("[v0] Fetching storage data from Flask server...")
const response = await fetch("/api/storage", {
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()
console.log("[v0] Successfully fetched storage data from Flask:", data)
return data
} catch (error) {
console.error("[v0] Failed to fetch storage data from Flask server:", error)
return null
}
}
export function StorageMetrics() {
const [storageData, setStorageData] = useState<StorageData | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
const fetchData = async () => {
setLoading(true)
setError(null)
const result = await fetchStorageData()
if (!result) {
setError("Flask server not available. Please ensure the server is running.")
} else {
setStorageData(result)
}
setLoading(false)
}
fetchData()
const interval = setInterval(fetchData, 60000)
return () => clearInterval(interval)
}, [])
if (loading) {
return (
<div className="space-y-6">
<div className="text-center py-8">
<div className="text-lg font-medium text-foreground mb-2">Loading storage data...</div>
</div>
</div>
)
}
if (error || !storageData) {
return (
<div className="space-y-6">
<Card className="bg-red-500/10 border-red-500/20">
<CardContent className="p-6">
<div className="flex items-center gap-3 text-red-600">
<AlertCircle className="h-6 w-6" />
<div>
<div className="font-semibold text-lg mb-1">Flask Server Not Available</div>
<div className="text-sm">
{error || "Unable to connect to the Flask server. Please ensure the server is running and try again."}
</div>
</div>
</div>
</CardContent>
</Card>
</div>
)
}
const usagePercent = storageData.total > 0 ? (storageData.used / storageData.total) * 100 : 0
return (
<div className="space-y-6">
{/* Storage Overview 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">
<CardTitle className="text-sm font-medium text-muted-foreground">Total Storage</CardTitle>
<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>
<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
</p>
</CardContent>
</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">Used Storage</CardTitle>
<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>
<Progress value={usagePercent} className="mt-2" />
<p className="text-xs text-muted-foreground mt-2">{usagePercent.toFixed(1)}% of total space</p>
</CardContent>
</Card>
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground flex items-center">
<Archive className="h-5 w-5 mr-2" />
Available
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-xl lg:text-2xl font-bold text-foreground">{storageData.available.toFixed(1)} GB</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
</Badge>
</div>
<p className="text-xs text-muted-foreground mt-2">Available space</p>
</CardContent>
</Card>
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground flex items-center">
<Activity className="h-5 w-5 mr-2" />
Disks
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-xl lg:text-2xl font-bold text-foreground">{storageData.disks.length}</div>
<div className="flex items-center space-x-2 mt-2">
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20">
{storageData.disks.filter((d) => d.health === "healthy").length} Healthy
</Badge>
</div>
<p className="text-xs text-muted-foreground mt-2">Storage devices</p>
</CardContent>
</Card>
</div>
{/* Disk Details */}
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground flex items-center">
<Database className="h-5 w-5 mr-2" />
Storage Devices
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{storageData.disks.map((disk, index) => (
<div
key={index}
className="flex items-center justify-between p-4 rounded-lg border border-border bg-card/50"
>
<div className="flex items-center space-x-4">
<HardDrive className="h-5 w-5 text-muted-foreground" />
<div>
<div className="font-medium text-foreground">{disk.name}</div>
<div className="text-sm text-muted-foreground">
{disk.fstype} {disk.mountpoint}
</div>
</div>
</div>
<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
</div>
<Progress value={disk.usage_percent} className="w-24 mt-1" />
</div>
<div className="text-center">
<div className="text-sm text-muted-foreground">Temp</div>
<div className="text-sm font-medium text-foreground">{disk.temperature}°C</div>
</div>
<Badge
variant="outline"
className={
disk.health === "healthy"
? "bg-green-500/10 text-green-500 border-green-500/20"
: "bg-yellow-500/10 text-yellow-500 border-yellow-500/20"
}
>
{disk.health === "healthy" ? (
<CheckCircle className="h-3 w-3 mr-1" />
) : (
<AlertTriangle className="h-3 w-3 mr-1" />
)}
{disk.health}
</Badge>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
)
}
+919
View File
@@ -0,0 +1,919 @@
"use client"
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 { Badge } from "@/components/ui/badge"
import { Progress } from "@/components/ui/progress"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
interface DiskInfo {
name: string
size?: number // Changed from string to number (KB) for formatMemory()
size_formatted?: string // Added formatted size string for display
temperature: number
health: string
power_on_hours?: number
smart_status?: string
model?: string
serial?: string
mountpoint?: string
fstype?: string
total?: number
used?: number
available?: number
usage_percent?: number
reallocated_sectors?: number
pending_sectors?: number
crc_errors?: number
rotation_rate?: number
power_cycles?: number
percentage_used?: number // NVMe: Percentage Used (0-100)
media_wearout_indicator?: number // SSD: Media Wearout Indicator
wear_leveling_count?: number // SSD: Wear Leveling Count
total_lbas_written?: number // SSD/NVMe: Total LBAs Written (GB)
ssd_life_left?: number // SSD: SSD Life Left percentage
}
interface ZFSPool {
name: string
size: string
allocated: string
free: string
health: string
}
interface StorageData {
total: number
used: number
available: number
disks: DiskInfo[]
zfs_pools: ZFSPool[]
disk_count: number
healthy_disks: number
warning_disks: number
critical_disks: number
error?: string
}
interface ProxmoxStorage {
name: string
type: string
status: string
total: number
used: number
available: number
percent: number
}
interface ProxmoxStorageData {
storage: ProxmoxStorage[]
error?: string
}
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`
}
}
export function StorageOverview() {
const [storageData, setStorageData] = useState<StorageData | null>(null)
const [proxmoxStorage, setProxmoxStorage] = useState<ProxmoxStorageData | null>(null)
const [loading, setLoading] = useState(true)
const [selectedDisk, setSelectedDisk] = useState<DiskInfo | null>(null)
const [detailsOpen, setDetailsOpen] = useState(false)
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 = 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) {
console.error("Error fetching storage data:", error)
} finally {
setLoading(false)
}
}
useEffect(() => {
fetchStorageData()
const interval = setInterval(fetchStorageData, 60000)
return () => clearInterval(interval)
}, [])
const getHealthIcon = (health: string) => {
switch (health.toLowerCase()) {
case "healthy":
case "passed":
case "online":
return <CheckCircle2 className="h-5 w-5 text-green-500" />
case "warning":
return <AlertTriangle className="h-5 w-5 text-yellow-500" />
case "critical":
case "failed":
case "degraded":
return <XCircle className="h-5 w-5 text-red-500" />
default:
return <AlertTriangle className="h-5 w-5 text-gray-500" />
}
}
const getHealthBadge = (health: string) => {
switch (health.toLowerCase()) {
case "healthy":
case "passed":
case "online":
return <Badge className="bg-green-500/10 text-green-500 border-green-500/20">Healthy</Badge>
case "warning":
return <Badge className="bg-yellow-500/10 text-yellow-500 border-yellow-500/20">Warning</Badge>
case "critical":
case "failed":
case "degraded":
return <Badge className="bg-red-500/10 text-red-500 border-red-500/20">Critical</Badge>
default:
return <Badge className="bg-gray-500/10 text-gray-500 border-gray-500/20">Unknown</Badge>
}
}
const getTempColor = (temp: number, diskName?: string, rotationRate?: number) => {
if (temp === 0) return "text-gray-500"
// Determinar el tipo de disco
let diskType = "HDD" // Por defecto
if (diskName) {
if (diskName.startsWith("nvme")) {
diskType = "NVMe"
} else if (!rotationRate || rotationRate === 0) {
diskType = "SSD"
}
}
// Aplicar rangos de temperatura según el tipo
switch (diskType) {
case "NVMe":
// NVMe: ≤70°C verde, 71-80°C amarillo, >80°C rojo
if (temp <= 70) return "text-green-500"
if (temp <= 80) return "text-yellow-500"
return "text-red-500"
case "SSD":
// SSD: ≤59°C verde, 60-70°C amarillo, >70°C rojo
if (temp <= 59) return "text-green-500"
if (temp <= 70) return "text-yellow-500"
return "text-red-500"
case "HDD":
default:
// HDD: ≤45°C verde, 46-55°C amarillo, >55°C rojo
if (temp <= 45) return "text-green-500"
if (temp <= 55) return "text-yellow-500"
return "text-red-500"
}
}
const formatHours = (hours: number) => {
if (hours === 0) return "N/A"
const years = Math.floor(hours / 8760)
const days = Math.floor((hours % 8760) / 24)
if (years > 0) {
return `${years}y ${days}d`
}
return `${days}d`
}
const formatRotationRate = (rpm: number | undefined) => {
if (!rpm || rpm === 0) return "SSD"
return `${rpm.toLocaleString()} RPM`
}
const getDiskType = (diskName: string, rotationRate: number | undefined): string => {
if (diskName.startsWith("nvme")) {
return "NVMe"
}
if (!rotationRate || rotationRate === 0) {
return "SSD"
}
return "HDD"
}
const getDiskTypeBadge = (diskName: string, rotationRate: number | undefined) => {
const diskType = getDiskType(diskName, rotationRate)
const badgeStyles: Record<string, { className: string; label: string }> = {
NVMe: {
className: "bg-purple-500/10 text-purple-500 border-purple-500/20",
label: "NVMe",
},
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 handleDiskClick = (disk: DiskInfo) => {
setSelectedDisk(disk)
setDetailsOpen(true)
}
const getStorageTypeBadge = (type: string) => {
const typeColors: Record<string, string> = {
pbs: "bg-purple-500/10 text-purple-500 border-purple-500/20",
dir: "bg-blue-500/10 text-blue-500 border-blue-500/20",
lvmthin: "bg-cyan-500/10 text-cyan-500 border-cyan-500/20",
zfspool: "bg-green-500/10 text-green-500 border-green-500/20",
nfs: "bg-orange-500/10 text-orange-500 border-orange-500/20",
cifs: "bg-yellow-500/10 text-yellow-500 border-yellow-500/20",
}
return typeColors[type.toLowerCase()] || "bg-gray-500/10 text-gray-500 border-gray-500/20"
}
const getStatusIcon = (status: string) => {
switch (status.toLowerCase()) {
case "active":
case "online":
return <CheckCircle2 className="h-5 w-5 text-green-500" />
case "inactive":
case "offline":
return <Square className="h-5 w-5 text-gray-500" />
case "error":
case "failed":
return <AlertTriangle className="h-5 w-5 text-red-500" />
default:
return <CheckCircle2 className="h-5 w-5 text-gray-500" />
}
}
const getWearIndicator = (disk: DiskInfo): { value: number; label: string } | null => {
const diskType = getDiskType(disk.name, disk.rotation_rate)
if (diskType === "NVMe" && disk.percentage_used !== undefined && disk.percentage_used !== null) {
return { value: disk.percentage_used, label: "Percentage Used" }
}
if (diskType === "SSD") {
// Prioridad: Media Wearout Indicator > Wear Leveling Count > SSD Life Left
if (disk.media_wearout_indicator !== undefined && disk.media_wearout_indicator !== null) {
return { value: disk.media_wearout_indicator, label: "Media Wearout" }
}
if (disk.wear_leveling_count !== undefined && disk.wear_leveling_count !== null) {
return { value: disk.wear_leveling_count, label: "Wear Level" }
}
if (disk.ssd_life_left !== undefined && disk.ssd_life_left !== null) {
return { value: 100 - disk.ssd_life_left, label: "Life Used" }
}
}
return null
}
const getWearColor = (wearPercent: number): string => {
if (wearPercent <= 50) return "text-green-500"
if (wearPercent <= 80) return "text-yellow-500"
return "text-red-500"
}
const getEstimatedLifeRemaining = (disk: DiskInfo): string | null => {
const wearIndicator = getWearIndicator(disk)
if (!wearIndicator || !disk.power_on_hours || disk.power_on_hours === 0) {
return null
}
const wearPercent = wearIndicator.value
const hoursUsed = disk.power_on_hours
// Si el desgaste es 0, no podemos calcular
if (wearPercent === 0) {
return "N/A"
}
// Calcular horas totales estimadas: hoursUsed / (wearPercent / 100)
const totalEstimatedHours = hoursUsed / (wearPercent / 100)
const remainingHours = totalEstimatedHours - hoursUsed
// Convertir a años
const remainingYears = remainingHours / 8760 // 8760 horas en un año
if (remainingYears < 1) {
const remainingMonths = Math.round(remainingYears * 12)
return `~${remainingMonths} months`
}
return `~${remainingYears.toFixed(1)} years`
}
const getDiskHealthBreakdown = () => {
if (!storageData || !storageData.disks) {
return { normal: 0, warning: 0, critical: 0 }
}
let normal = 0
let warning = 0
let critical = 0
storageData.disks.forEach((disk) => {
if (disk.temperature === 0) {
// Si no hay temperatura, considerarlo normal
normal++
return
}
const diskType = getDiskType(disk.name, disk.rotation_rate)
switch (diskType) {
case "NVMe":
if (disk.temperature <= 70) normal++
else if (disk.temperature <= 80) warning++
else critical++
break
case "SSD":
if (disk.temperature <= 59) normal++
else if (disk.temperature <= 70) warning++
else critical++
break
case "HDD":
default:
if (disk.temperature <= 45) normal++
else if (disk.temperature <= 55) warning++
else critical++
break
}
})
return { normal, warning, critical }
}
const getDiskTypesBreakdown = () => {
if (!storageData || !storageData.disks) {
return { nvme: 0, ssd: 0, hdd: 0 }
}
let nvme = 0
let ssd = 0
let hdd = 0
storageData.disks.forEach((disk) => {
const diskType = getDiskType(disk.name, disk.rotation_rate)
if (diskType === "NVMe") nvme++
else if (diskType === "SSD") ssd++
else if (diskType === "HDD") hdd++
})
return { nvme, ssd, hdd }
}
const getWearProgressColor = (wearPercent: number): string => {
if (wearPercent < 70) return "[&>div]:bg-blue-500"
if (wearPercent < 85) return "[&>div]:bg-yellow-500"
return "[&>div]:bg-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 usagePercent =
storageData && storageData.total > 0 ? ((totalProxmoxUsed / (storageData.total * 1024)) * 100).toFixed(2) : "0.00"
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-muted-foreground">Loading storage information...</div>
</div>
)
}
if (!storageData || storageData.error) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-red-500">Error loading storage data: {storageData?.error || "Unknown error"}</div>
</div>
)
}
return (
<div className="space-y-6">
{/* Storage Summary */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 lg:gap-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Storage</CardTitle>
<HardDrive className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-xl lg:text-2xl font-bold">{storageData.total.toFixed(1)} TB</div>
<p className="text-xs text-muted-foreground mt-1">{storageData.disk_count} physical disks</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Used Storage</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>
</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" />
</CardHeader>
<CardContent>
<div className="text-xl lg:text-2xl font-bold">{storageData.disk_count} disks</div>
<p className="text-xs mt-1">
<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>
</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>
<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>
</CardContent>
</Card>
</div>
{proxmoxStorage && proxmoxStorage.storage && proxmoxStorage.storage.length > 0 && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Database className="h-5 w-5" />
Proxmox Storage
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{proxmoxStorage.storage
.filter((storage) => storage && storage.name && storage.total > 0)
.sort((a, b) => a.name.localeCompare(b.name))
.map((storage) => (
<div key={storage.name} className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
{/* Desktop: Icon + Name + Badge tipo alineados horizontalmente */}
<div className="hidden md:flex items-center gap-3">
<Database className="h-5 w-5 text-muted-foreground" />
<h3 className="font-semibold text-lg">{storage.name}</h3>
<Badge className={getStorageTypeBadge(storage.type)}>{storage.type}</Badge>
</div>
<div className="flex md:hidden items-center gap-2 flex-1">
<Database className="h-5 w-5 text-muted-foreground flex-shrink-0" />
<Badge className={getStorageTypeBadge(storage.type)}>{storage.type}</Badge>
<h3 className="font-semibold text-base flex-1 min-w-0 truncate">{storage.name}</h3>
{getStatusIcon(storage.status)}
</div>
{/* Desktop: Badge active + Porcentaje */}
<div className="hidden md:flex items-center gap-2">
<Badge
className={
storage.status === "active"
? "bg-green-500/10 text-green-500 border-green-500/20"
: "bg-gray-500/10 text-gray-500 border-gray-500/20"
}
>
{storage.status}
</Badge>
<span className="text-sm font-medium">{storage.percent}%</span>
</div>
</div>
<div className="space-y-2">
<Progress
value={storage.percent}
className={`h-2 ${
storage.percent > 90
? "[&>div]:bg-red-500"
: storage.percent > 75
? "[&>div]:bg-yellow-500"
: "[&>div]:bg-blue-500"
}`}
/>
<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>
</div>
<div>
<p className="text-muted-foreground">Used</p>
<p
className={`font-medium ${
storage.percent > 90
? "text-red-400"
: storage.percent > 75
? "text-yellow-400"
: "text-blue-400"
}`}
>
{storage.used.toLocaleString()} GB
</p>
</div>
<div>
<p className="text-muted-foreground">Available</p>
<p className="font-medium text-green-400">{storage.available.toLocaleString()} GB</p>
</div>
</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
)}
{/* ZFS Pools */}
{storageData.zfs_pools && storageData.zfs_pools.length > 0 && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Database className="h-5 w-5" />
ZFS Pools
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{storageData.zfs_pools.map((pool) => (
<div key={pool.name} className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-3">
<h3 className="font-semibold text-lg">{pool.name}</h3>
{getHealthBadge(pool.health)}
</div>
{getHealthIcon(pool.health)}
</div>
<div className="grid grid-cols-3 gap-4 text-sm">
<div>
<p className="text-sm text-muted-foreground">Size</p>
<p className="font-medium">{pool.size}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Allocated</p>
<p className="font-medium">{pool.allocated}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Free</p>
<p className="font-medium">{pool.free}</p>
</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
)}
{/* Physical Disks */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<HardDrive className="h-5 w-5" />
Physical Disks & SMART Status
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{storageData.disks.map((disk) => (
<div key={disk.name}>
<div
className="sm:hidden border border-white/10 rounded-lg p-4 cursor-pointer bg-white/5 transition-colors"
onClick={() => handleDiskClick(disk)}
>
<div className="space-y-2 mb-3">
{/* Row 1: Device name and type badge */}
<div className="flex items-center gap-2">
<HardDrive className="h-5 w-5 text-muted-foreground flex-shrink-0" />
<h3 className="font-semibold">/dev/{disk.name}</h3>
<Badge className={getDiskTypeBadge(disk.name, disk.rotation_rate).className}>
{getDiskTypeBadge(disk.name, disk.rotation_rate).label}
</Badge>
</div>
{/* Row 2: Model, temperature, and health status */}
<div className="flex items-center justify-between gap-3 pl-7">
{disk.model && disk.model !== "Unknown" && (
<p className="text-sm text-muted-foreground truncate flex-1 min-w-0">{disk.model}</p>
)}
<div className="flex items-center gap-3 flex-shrink-0">
{disk.temperature > 0 && (
<div className="flex items-center gap-1">
<Thermometer
className={`h-4 w-4 ${getTempColor(disk.temperature, disk.name, disk.rotation_rate)}`}
/>
<span
className={`text-sm font-medium ${getTempColor(disk.temperature, disk.name, disk.rotation_rate)}`}
>
{disk.temperature}°C
</span>
</div>
)}
{getHealthBadge(disk.health)}
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4 text-sm">
{disk.size_formatted && (
<div>
<p className="text-sm text-muted-foreground">Size</p>
<p className="font-medium">{disk.size_formatted}</p>
</div>
)}
{disk.smart_status && disk.smart_status !== "unknown" && (
<div>
<p className="text-sm text-muted-foreground">SMART Status</p>
<p className="font-medium capitalize">{disk.smart_status}</p>
</div>
)}
{disk.power_on_hours !== undefined && disk.power_on_hours > 0 && (
<div>
<p className="text-sm text-muted-foreground">Power On Time</p>
<p className="font-medium">{formatHours(disk.power_on_hours)}</p>
</div>
)}
{disk.serial && disk.serial !== "Unknown" && (
<div>
<p className="text-sm text-muted-foreground">Serial</p>
<p className="font-medium text-xs">{disk.serial}</p>
</div>
)}
</div>
</div>
<div
className="hidden sm:block border border-white/10 rounded-lg p-4 cursor-pointer bg-card hover:bg-white/5 transition-colors"
onClick={() => handleDiskClick(disk)}
>
<div className="space-y-2 mb-3">
{/* Row 1: Device name and type badge */}
<div className="flex items-center gap-2">
<HardDrive className="h-5 w-5 text-muted-foreground flex-shrink-0" />
<h3 className="font-semibold">/dev/{disk.name}</h3>
<Badge className={getDiskTypeBadge(disk.name, disk.rotation_rate).className}>
{getDiskTypeBadge(disk.name, disk.rotation_rate).label}
</Badge>
</div>
{/* Row 2: Model, temperature, and health status */}
<div className="flex items-center justify-between gap-3 pl-7">
{disk.model && disk.model !== "Unknown" && (
<p className="text-sm text-muted-foreground truncate flex-1 min-w-0">{disk.model}</p>
)}
<div className="flex items-center gap-3 flex-shrink-0">
{disk.temperature > 0 && (
<div className="flex items-center gap-1">
<Thermometer
className={`h-4 w-4 ${getTempColor(disk.temperature, disk.name, disk.rotation_rate)}`}
/>
<span
className={`text-sm font-medium ${getTempColor(disk.temperature, disk.name, disk.rotation_rate)}`}
>
{disk.temperature}°C
</span>
</div>
)}
{getHealthBadge(disk.health)}
</div>
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
{disk.size_formatted && (
<div>
<p className="text-sm text-muted-foreground">Size</p>
<p className="font-medium">{disk.size_formatted}</p>
</div>
)}
{disk.smart_status && disk.smart_status !== "unknown" && (
<div>
<p className="text-sm text-muted-foreground">SMART Status</p>
<p className="font-medium capitalize">{disk.smart_status}</p>
</div>
)}
{disk.power_on_hours !== undefined && disk.power_on_hours > 0 && (
<div>
<p className="text-sm text-muted-foreground">Power On Time</p>
<p className="font-medium">{formatHours(disk.power_on_hours)}</p>
</div>
)}
{disk.serial && disk.serial !== "Unknown" && (
<div>
<p className="text-sm text-muted-foreground">Serial</p>
<p className="font-medium text-xs">{disk.serial}</p>
</div>
)}
</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
{/* Disk Details Dialog */}
<Dialog open={detailsOpen} onOpenChange={setDetailsOpen}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<HardDrive className="h-5 w-5" />
Disk Details: /dev/{selectedDisk?.name}
</DialogTitle>
<DialogDescription>Complete SMART information and health status</DialogDescription>
</DialogHeader>
{selectedDisk && (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-muted-foreground">Model</p>
<p className="font-medium">{selectedDisk.model}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Serial Number</p>
<p className="font-medium">{selectedDisk.serial}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Capacity</p>
<p className="font-medium">{selectedDisk.size_formatted}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Health Status</p>
<div className="mt-1">{getHealthBadge(selectedDisk.health)}</div>
</div>
</div>
{/* Wear & Lifetime Section */}
{getWearIndicator(selectedDisk) && (
<div className="border-t pt-4">
<h4 className="font-semibold mb-3">Wear & Lifetime</h4>
<div className="space-y-3">
<div>
<div className="flex items-center justify-between mb-2">
<p className="text-sm text-muted-foreground">{getWearIndicator(selectedDisk)!.label}</p>
<p className={`font-medium ${getWearColor(getWearIndicator(selectedDisk)!.value)}`}>
{getWearIndicator(selectedDisk)!.value}%
</p>
</div>
<Progress
value={getWearIndicator(selectedDisk)!.value}
className={`h-2 ${getWearProgressColor(getWearIndicator(selectedDisk)!.value)}`}
/>
</div>
{getEstimatedLifeRemaining(selectedDisk) && (
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-muted-foreground">Estimated Life Remaining</p>
<p className="font-medium">{getEstimatedLifeRemaining(selectedDisk)}</p>
</div>
{selectedDisk.total_lbas_written && selectedDisk.total_lbas_written > 0 && (
<div>
<p className="text-sm text-muted-foreground">Total Data Written</p>
<p className="font-medium">
{selectedDisk.total_lbas_written >= 1024
? `${(selectedDisk.total_lbas_written / 1024).toFixed(2)} TB`
: `${selectedDisk.total_lbas_written.toFixed(2)} GB`}
</p>
</div>
)}
</div>
)}
</div>
</div>
)}
<div className="border-t pt-4">
<h4 className="font-semibold mb-3">SMART Attributes</h4>
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-muted-foreground">Temperature</p>
<p
className={`font-medium ${getTempColor(selectedDisk.temperature, selectedDisk.name, selectedDisk.rotation_rate)}`}
>
{selectedDisk.temperature > 0 ? `${selectedDisk.temperature}°C` : "N/A"}
</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Power On Hours</p>
<p className="font-medium">
{selectedDisk.power_on_hours && selectedDisk.power_on_hours > 0
? `${selectedDisk.power_on_hours.toLocaleString()}h (${formatHours(selectedDisk.power_on_hours)})`
: "N/A"}
</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Rotation Rate</p>
<p className="font-medium">{formatRotationRate(selectedDisk.rotation_rate)}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Power Cycles</p>
<p className="font-medium">
{selectedDisk.power_cycles && selectedDisk.power_cycles > 0
? selectedDisk.power_cycles.toLocaleString()
: "N/A"}
</p>
</div>
<div>
<p className="text-sm text-muted-foreground">SMART Status</p>
<p className="font-medium capitalize">{selectedDisk.smart_status}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Reallocated Sectors</p>
<p
className={`font-medium ${selectedDisk.reallocated_sectors && selectedDisk.reallocated_sectors > 0 ? "text-yellow-500" : ""}`}
>
{selectedDisk.reallocated_sectors ?? 0}
</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Pending Sectors</p>
<p
className={`font-medium ${selectedDisk.pending_sectors && selectedDisk.pending_sectors > 0 ? "text-yellow-500" : ""}`}
>
{selectedDisk.pending_sectors ?? 0}
</p>
</div>
<div>
<p className="text-sm text-muted-foreground">CRC Errors</p>
<p
className={`font-medium ${selectedDisk.crc_errors && selectedDisk.crc_errors > 0 ? "text-yellow-500" : ""}`}
>
{selectedDisk.crc_errors ?? 0}
</p>
</div>
</div>
</div>
</div>
)}
</DialogContent>
</Dialog>
</div>
)
}
File diff suppressed because it is too large Load Diff
+816
View File
@@ -0,0 +1,816 @@
"use client"
import { useState, useEffect } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
import { Progress } from "./ui/progress"
import { Badge } from "./ui/badge"
import { Cpu, MemoryStick, Thermometer, Server, Zap, AlertCircle, HardDrive, Network } from "lucide-react"
import { NodeMetricsCharts } from "./node-metrics-charts"
import { NetworkTrafficChart } from "./network-traffic-chart"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
interface SystemData {
cpu_usage: number
memory_usage: number
memory_total: number
memory_used: number
temperature: number
uptime: string
load_average: number[]
hostname: string
node_id: string
timestamp: string
cpu_cores?: number
cpu_threads?: number
proxmox_version?: string
kernel_version?: string
available_updates?: number
}
interface VMData {
vmid: number
name: string
status: string
cpu: number
mem: number
maxmem: number
disk: number
maxdisk: number
uptime: number
type?: string
}
interface StorageData {
total: number
used: number
available: number
disk_count: number
disks: Array<{
name: string
mountpoint: string
total: number
used: number
available: number
usage_percent: number
}>
}
interface NetworkData {
interfaces: Array<{
name: string
status: string
addresses: Array<{ ip: string; netmask: string }>
}>
traffic: {
bytes_sent: number
bytes_recv: number
packets_sent: number
packets_recv: number
}
physical_active_count?: number
physical_total_count?: number
bridge_active_count?: number
bridge_total_count?: number
physical_interfaces?: Array<{
name: string
status: string
addresses: Array<{ ip: string; netmask: string }>
}>
bridge_interfaces?: Array<{
name: string
status: string
addresses: Array<{ ip: string; netmask: string }>
}>
}
interface ProxmoxStorageData {
storage: Array<{
name: string
type: string
status: string
total: number
used: number
available: number
percent: number
}>
}
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()
return data
} catch (error) {
console.error("[v0] Failed to fetch system data:", error)
return 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()
return Array.isArray(data) ? data : data.vms || []
} catch (error) {
console.error("[v0] Failed to fetch VM data:", error)
return []
}
}
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()
return data
} catch (error) {
console.log("[v0] Storage data unavailable:", error instanceof Error ? error.message : "Unknown error")
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()
return data
} catch (error) {
console.log("[v0] Network data unavailable:", error instanceof Error ? error.message : "Unknown error")
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()
return data
} catch (error) {
console.log("[v0] Proxmox storage data unavailable:", error instanceof Error ? error.message : "Unknown error")
return null
}
}
export function SystemOverview() {
const [systemData, setSystemData] = useState<SystemData | null>(null)
const [vmData, setVmData] = useState<VMData[]>([])
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 [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 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)
}
}
fetchData()
const systemInterval = setInterval(() => {
fetchSystemData().then((data) => {
if (data) setSystemData(data)
})
}, 10000)
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) {
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">
{[...Array(4)].map((_, i) => (
<Card key={i} className="bg-card border-border animate-pulse">
<CardContent className="p-6">
<div className="h-4 bg-muted rounded w-1/2 mb-4"></div>
<div className="h-8 bg-muted rounded w-3/4 mb-2"></div>
<div className="h-2 bg-muted rounded w-full mb-2"></div>
<div className="h-3 bg-muted rounded w-2/3"></div>
</CardContent>
</Card>
))}
</div>
</div>
)
}
if (error || !systemData) {
return (
<div className="space-y-6">
<Card className="bg-red-500/10 border-red-500/20">
<CardContent className="p-6">
<div className="flex items-center gap-3 text-red-600">
<AlertCircle className="h-6 w-6" />
<div>
<div className="font-semibold text-lg mb-1">Flask Server Not Available</div>
<div className="text-sm">
{error || "Unable to connect to the Flask server. Please ensure the server is running and try again."}
</div>
</div>
</div>
</CardContent>
</Card>
</div>
)
}
const vmStats = {
total: vmData.length,
running: vmData.filter((vm) => vm.status === "running").length,
stopped: vmData.filter((vm) => vm.status === "stopped").length,
lxc: vmData.filter((vm) => vm.type === "lxc").length,
vms: vmData.filter((vm) => vm.type === "qemu" || !vm.type).length,
}
const getTemperatureStatus = (temp: number) => {
if (temp === 0) return { status: "N/A", color: "bg-gray-500/10 text-gray-500 border-gray-500/20" }
if (temp < 60) return { status: "Normal", color: "bg-green-500/10 text-green-500 border-green-500/20" }
if (temp < 75) return { status: "Warm", color: "bg-yellow-500/10 text-yellow-500 border-yellow-500/20" }
return { status: "Hot", color: "bg-red-500/10 text-red-500 border-red-500/20" }
}
const formatUptime = (seconds: number) => {
if (!seconds || seconds === 0) return "Stopped"
const days = Math.floor(seconds / 86400)
const hours = Math.floor((seconds % 86400) / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
if (days > 0) return `${days}d ${hours}h`
if (hours > 0) return `${hours}h ${minutes}m`
return `${minutes}m`
}
const formatBytes = (bytes: number) => {
return (bytes / 1024 ** 3).toFixed(2)
}
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(2)} TB`
}
}
const tempStatus = getTemperatureStatus(systemData.temperature)
const localStorage = proxmoxStorageData?.storage.find((s) => s.name === "local")
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",
)
const vmLxcStorageTotal = vmLxcStorages?.reduce((acc, s) => acc + s.total, 0) || 0
const vmLxcStorageUsed = vmLxcStorages?.reduce((acc, s) => acc + s.used, 0) || 0
const vmLxcStorageAvailable = vmLxcStorages?.reduce((acc, s) => acc + s.available, 0) || 0
const vmLxcStoragePercent = vmLxcStorageTotal > 0 ? (vmLxcStorageUsed / vmLxcStorageTotal) * 100 : 0
const getLoadStatus = (load: number, cores: number) => {
if (load < cores) {
return { status: "Normal", color: "bg-green-500/10 text-green-500 border-green-500/20" }
} else if (load < cores * 1.5) {
return { status: "Moderate", color: "bg-yellow-500/10 text-yellow-500 border-yellow-500/20" }
} else {
return { status: "High", color: "bg-red-500/10 text-red-500 border-red-500/20" }
}
}
const systemAlerts = []
if (systemData.available_updates && systemData.available_updates > 0) {
systemAlerts.push({
type: "warning",
message: `${systemData.available_updates} updates available`,
})
}
if (vmStats.stopped > 0) {
systemAlerts.push({
type: "info",
message: `${vmStats.stopped} VM${vmStats.stopped > 1 ? "s" : ""} stopped`,
})
}
if (systemData.temperature > 75) {
systemAlerts.push({
type: "warning",
message: "High temperature detected",
})
}
if (localStorage && localStorage.percent > 90) {
systemAlerts.push({
type: "warning",
message: "System storage almost full",
})
}
const loadStatus = getLoadStatus(systemData.load_average[0], systemData.cpu_cores || 8)
const getTimeframeLabel = (timeframe: string): string => {
switch (timeframe) {
case "hour":
return "1h"
case "day":
return "24h"
case "week":
return "7d"
case "month":
return "30d"
case "year":
return "1y"
default:
return timeframe
}
}
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">
<CardTitle className="text-sm font-medium text-muted-foreground">CPU Usage</CardTitle>
<Cpu className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-xl lg:text-2xl font-bold text-foreground">{systemData.cpu_usage}%</div>
<Progress value={systemData.cpu_usage} className="mt-2 [&>div]:bg-blue-500" />
<p className="text-xs text-muted-foreground mt-2">Real-time usage</p>
</CardContent>
</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">Memory Usage</CardTitle>
<MemoryStick className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-xl lg:text-2xl font-bold text-foreground">{systemData.memory_used.toFixed(1)} GB</div>
<Progress value={systemData.memory_usage} className="mt-2 [&>div]:bg-blue-500" />
<p className="text-xs text-muted-foreground mt-2">
<span className="text-green-500 font-medium">{systemData.memory_usage.toFixed(1)}%</span> of{" "}
{systemData.memory_total} GB
</p>
</CardContent>
</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">Temperature</CardTitle>
<Thermometer className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-xl lg:text-2xl font-bold text-foreground">
{systemData.temperature === 0 ? "N/A" : `${systemData.temperature}°C`}
</div>
<div className="flex items-center mt-2">
<Badge variant="outline" className={tempStatus.color}>
{tempStatus.status}
</Badge>
</div>
<p className="text-xs text-muted-foreground mt-2">
{systemData.temperature === 0 ? "No sensor available" : "Live temperature reading"}
</p>
</CardContent>
</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>
<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>
</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">
<HardDrive className="h-5 w-5 mr-2" />
Storage Overview
</CardTitle>
</CardHeader>
<CardContent>
{storageData ? (
<div className="space-y-4">
<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>
<span className="text-lg font-semibold text-foreground">{storageData.total} TB</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Physical Disks:</span>
<span className="text-sm font-semibold text-foreground">
{storageData.disk_count} disk{storageData.disk_count !== 1 ? "s" : ""}
</span>
</div>
</div>
{vmLxcStorages && vmLxcStorages.length > 0 ? (
<div className="space-y-2 pb-3 border-b border-border">
<div className="text-xs font-medium text-muted-foreground mb-2">VM/LXC Storage</div>
<div className="flex justify-between items-center">
<span className="text-xs text-muted-foreground">Used:</span>
<span className="text-sm font-semibold text-foreground">{formatStorage(vmLxcStorageUsed)}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-xs text-muted-foreground">Available:</span>
<span className="text-sm font-semibold text-green-500">
{formatStorage(vmLxcStorageAvailable)}
</span>
</div>
<Progress value={vmLxcStoragePercent} className="mt-2 [&>div]:bg-blue-500" />
<div className="flex justify-between items-center mt-1">
<span className="text-xs text-muted-foreground">
{formatStorage(vmLxcStorageUsed)} / {formatStorage(vmLxcStorageTotal)}
</span>
<span className="text-xs text-muted-foreground">{vmLxcStoragePercent.toFixed(1)}%</span>
</div>
{vmLxcStorages.length > 1 && (
<div className="text-xs text-muted-foreground mt-1">
{vmLxcStorages.length} storage volume{vmLxcStorages.length > 1 ? "s" : ""}
</div>
)}
</div>
) : (
<div className="space-y-2 pb-3 border-b border-border">
<div className="text-xs font-medium text-muted-foreground mb-2">VM/LXC Storage</div>
<div className="text-center py-4 text-muted-foreground text-sm">No VM/LXC storage configured</div>
</div>
)}
{localStorage && (
<div className="space-y-2">
<div className="text-xs font-medium text-muted-foreground mb-2">Local Storage (System)</div>
<div className="flex justify-between items-center">
<span className="text-xs text-muted-foreground">Used:</span>
<span className="text-sm font-semibold text-foreground">{formatStorage(localStorage.used)}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-xs text-muted-foreground">Available:</span>
<span className="text-sm font-semibold text-green-500">
{formatStorage(localStorage.available)}
</span>
</div>
<Progress value={localStorage.percent} className="mt-2 [&>div]:bg-purple-500" />
<div className="flex justify-between items-center mt-1">
<span className="text-xs text-muted-foreground">
{formatStorage(localStorage.used)} / {formatStorage(localStorage.total)}
</span>
<span className="text-xs text-muted-foreground">{localStorage.percent.toFixed(1)}%</span>
</div>
</div>
)}
</div>
) : (
<div className="text-center py-8 text-muted-foreground">Storage data not available</div>
)}
</CardContent>
</Card>
{/* Network Summary */}
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground flex items-center justify-between">
<div className="flex items-center">
<Network className="h-5 w-5 mr-2" />
Network Overview
</div>
<Select value={networkTimeframe} onValueChange={setNetworkTimeframe}>
<SelectTrigger className="w-28 h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="hour">1 Hour</SelectItem>
<SelectItem value="day">24 Hours</SelectItem>
<SelectItem value="week">7 Days</SelectItem>
<SelectItem value="month">30 Days</SelectItem>
<SelectItem value="year">1 Year</SelectItem>
</SelectContent>
</Select>
</CardTitle>
</CardHeader>
<CardContent>
{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>
<span className="text-lg font-semibold text-foreground">
{(networkData.physical_active_count || 0) + (networkData.bridge_active_count || 0)}
</span>
</div>
<div className="space-y-2">
{networkData.physical_interfaces && networkData.physical_interfaces.length > 0 && (
<div className="flex flex-wrap gap-2">
{networkData.physical_interfaces
.filter((iface) => iface.status === "up")
.map((iface) => (
<Badge
key={iface.name}
variant="outline"
className="bg-blue-500/10 text-blue-500 border-blue-500/20"
>
{iface.name}
</Badge>
))}
</div>
)}
{networkData.bridge_interfaces && networkData.bridge_interfaces.length > 0 && (
<div className="flex flex-wrap gap-2">
{networkData.bridge_interfaces
.filter((iface) => iface.status === "up")
.map((iface) => (
<Badge
key={iface.name}
variant="outline"
className="bg-green-500/10 text-green-500 border-green-500/20"
>
{iface.name}
</Badge>
))}
</div>
)}
</div>
<div className="pt-2 border-t border-border space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Received:</span>
<span className="text-lg font-semibold text-green-500 flex items-center gap-1">
{formatStorage(networkTotals.received)}
<span className="text-xs text-muted-foreground">({getTimeframeLabel(networkTimeframe)})</span>
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Sent:</span>
<span className="text-lg font-semibold text-blue-500 flex items-center gap-1">
{formatStorage(networkTotals.sent)}
<span className="text-xs text-muted-foreground">({getTimeframeLabel(networkTimeframe)})</span>
</span>
</div>
</div>
<div className="pt-3 border-t border-border">
<NetworkTrafficChart timeframe={networkTimeframe} onTotalsCalculated={setNetworkTotals} />
</div>
</div>
) : (
<div className="text-center py-8 text-muted-foreground">Network data not available</div>
)}
</CardContent>
</Card>
</div>
{/* System Information */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground flex items-center">
<Server className="h-5 w-5 mr-2" />
System Information
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex justify-between">
<span className="text-muted-foreground">Uptime:</span>
<span className="text-foreground">{systemData.uptime}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Proxmox Version:</span>
<span className="text-foreground">{systemData.proxmox_version || "N/A"}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Kernel:</span>
<span className="text-foreground font-mono text-sm">{systemData.kernel_version || "Linux"}</span>
</div>
{systemData.available_updates !== undefined && systemData.available_updates > 0 && (
<div className="flex justify-between">
<span className="text-muted-foreground">Available Updates:</span>
<Badge variant="outline" className="bg-yellow-500/10 text-yellow-500 border-yellow-500/20">
{systemData.available_updates} packages
</Badge>
</div>
)}
</CardContent>
</Card>
{/* System Health & Alerts */}
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground flex items-center">
<Zap className="h-5 w-5 mr-2" />
System Overview
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex justify-between items-center pb-3 border-b border-border">
<div className="flex flex-col">
<span className="text-sm text-muted-foreground">Load Average (1m):</span>
</div>
<div className="flex items-center gap-2">
<span className="text-lg font-semibold text-foreground font-mono">
{systemData.load_average[0].toFixed(2)}
</span>
<Badge variant="outline" className={loadStatus.color}>
{loadStatus.status}
</Badge>
</div>
</div>
<div className="flex justify-between items-center pb-3 border-b border-border">
<span className="text-sm text-muted-foreground">CPU Threads:</span>
<span className="text-lg font-semibold text-foreground">{systemData.cpu_threads || "N/A"}</span>
</div>
<div className="flex justify-between items-center pb-3 border-b border-border">
<span className="text-sm text-muted-foreground">Physical Disks:</span>
<span className="text-lg font-semibold text-foreground">{storageData?.disk_count || "N/A"}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Network Interfaces:</span>
<span className="text-lg font-semibold text-foreground">
{networkData?.physical_total_count || networkData?.physical_interfaces?.length || "N/A"}
</span>
</div>
</CardContent>
</Card>
</div>
</div>
)
}
+7
View File
@@ -0,0 +1,7 @@
"use client"
import { ThemeProvider as NextThemesProvider } from "next-themes"
import type { ThemeProviderProps } from "next-themes"
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}
+39
View File
@@ -0,0 +1,39 @@
"use client"
import { Moon, Sun } from "lucide-react"
import { useTheme } from "next-themes"
import { useEffect, useState } from "react"
import { Button } from "./ui/button"
export function ThemeToggle() {
const { theme, setTheme } = useTheme()
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
const handleThemeToggle = () => {
console.log("[v0] Current theme:", theme)
const newTheme = theme === "light" ? "dark" : "light"
console.log("[v0] Switching to theme:", newTheme)
setTheme(newTheme)
}
if (!mounted) {
return (
<Button variant="outline" size="sm" className="border-border bg-transparent w-9 h-9">
<Sun className="h-4 w-4" />
<span className="sr-only">Toggle theme</span>
</Button>
)
}
return (
<Button variant="outline" size="sm" onClick={handleThemeToggle} className="border-border bg-transparent w-9 h-9">
<Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
)
}
+28
View File
@@ -0,0 +1,28 @@
import type * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
)
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant }), className)} {...props} />
}
export { Badge, badgeVariants }
+46
View File
@@ -0,0 +1,46 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
},
)
Button.displayName = "Button"
export { Button, buttonVariants }
+42
View File
@@ -0,0 +1,42 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)} {...props} />
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
),
)
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h3 ref={ref} className={cn("text-2xl font-semibold leading-none tracking-tight", className)} {...props} />
),
)
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => (
<p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
),
)
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />,
)
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
),
)
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
+97
View File
@@ -0,0 +1,97 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"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,
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...props} />
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}
+22
View File
@@ -0,0 +1,22 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type, ...props }, ref) => {
return (
<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",
className,
)}
ref={ref}
{...props}
/>
)
})
Input.displayName = "Input"
export { Input }
+25
View File
@@ -0,0 +1,25 @@
"use client"
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import { cn } from "@/lib/utils"
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn("relative h-2 w-full overflow-hidden rounded-full bg-secondary", className)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
))
Progress.displayName = ProgressPrimitive.Root.displayName
export { Progress }
+40
View File
@@ -0,0 +1,40 @@
"use client"
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root ref={ref} className={cn("relative overflow-hidden", className)} {...props}>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">{children}</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" && "h-2.5 flex-col border-t border-t-transparent p-[1px]",
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
export { ScrollArea, ScrollBar }
+144
View File
@@ -0,0 +1,144 @@
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn("flex cursor-default items-center justify-center py-1", className)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn("flex cursor-default items-center justify-center py-1", className)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className,
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label ref={ref} className={cn("px-2 py-1.5 text-sm font-semibold", className)} {...props} />
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}
+109
View File
@@ -0,0 +1,109 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Sheet = DialogPrimitive.Root
const SheetTrigger = DialogPrimitive.Trigger
const SheetClose = DialogPrimitive.Close
const SheetPortal = DialogPrimitive.Portal
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
ref={ref}
/>
))
SheetOverlay.displayName = DialogPrimitive.Overlay.displayName
const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
},
},
defaultVariants: {
side: "right",
},
},
)
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<React.ElementRef<typeof DialogPrimitive.Content>, SheetContentProps>(
({ side = "right", className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<DialogPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</SheetPortal>
),
)
SheetContent.displayName = DialogPrimitive.Content.displayName
const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
)
SheetHeader.displayName = "SheetHeader"
const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
)
SheetFooter.displayName = "SheetFooter"
const SheetTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title ref={ref} className={cn("text-lg font-semibold text-foreground", className)} {...props} />
))
SheetTitle.displayName = DialogPrimitive.Title.displayName
const SheetDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
))
SheetDescription.displayName = DialogPrimitive.Description.displayName
export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}
+52
View File
@@ -0,0 +1,52 @@
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className,
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className,
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className,
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }
File diff suppressed because it is too large Load Diff
+6
View File
@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
+30
View File
@@ -0,0 +1,30 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export',
trailingSlash: true,
eslint: {
ignoreDuringBuilds: true,
},
typescript: {
ignoreBuildErrors: true,
},
images: {
unoptimized: true,
},
experimental: {
esmExternals: 'loose',
},
webpack: (config, { isServer }) => {
if (!isServer) {
config.resolve.fallback = {
...config.resolve.fallback,
fs: false,
net: false,
tls: false,
};
}
return config;
},
};
export default nextConfig;
+74
View File
@@ -0,0 +1,74 @@
{
"name": "proxmenux-monitor",
"version": "1.0.0",
"description": "Proxmox System Monitoring Dashboard",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"export": "next build"
},
"dependencies": {
"@hookform/resolvers": "^3.10.0",
"@radix-ui/react-accordion": "1.2.2",
"@radix-ui/react-alert-dialog": "1.1.4",
"@radix-ui/react-aspect-ratio": "1.1.1",
"@radix-ui/react-avatar": "1.1.2",
"@radix-ui/react-checkbox": "1.1.3",
"@radix-ui/react-collapsible": "1.1.2",
"@radix-ui/react-context-menu": "2.2.4",
"@radix-ui/react-dialog": "1.1.4",
"@radix-ui/react-dropdown-menu": "2.1.4",
"@radix-ui/react-hover-card": "1.1.4",
"@radix-ui/react-label": "2.1.1",
"@radix-ui/react-menubar": "1.1.4",
"@radix-ui/react-navigation-menu": "1.2.3",
"@radix-ui/react-popover": "1.1.4",
"@radix-ui/react-progress": "1.1.1",
"@radix-ui/react-radio-group": "1.2.2",
"@radix-ui/react-scroll-area": "1.2.2",
"@radix-ui/react-select": "2.1.4",
"@radix-ui/react-separator": "1.1.1",
"@radix-ui/react-slider": "1.2.2",
"@radix-ui/react-slot": "1.1.1",
"@radix-ui/react-switch": "1.1.2",
"@radix-ui/react-tabs": "1.1.2",
"@radix-ui/react-toast": "1.2.4",
"@radix-ui/react-toggle": "1.1.1",
"@radix-ui/react-toggle-group": "1.1.1",
"@radix-ui/react-tooltip": "1.1.6",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "1.0.4",
"date-fns": "4.1.0",
"embla-carousel-react": "8.5.1",
"geist": "^1.3.1",
"input-otp": "1.4.1",
"lucide-react": "^0.454.0",
"next": "15.1.6",
"next-themes": "^0.4.6",
"react": "^19",
"react-day-picker": "9.8.0",
"react-dom": "^19",
"react-hook-form": "^7.60.0",
"react-resizable-panels": "^2.1.7",
"recharts": "2.15.4",
"sonner": "^1.7.4",
"swr": "^2.2.5",
"tailwind-merge": "^3.3.1",
"tailwindcss-animate": "^1.0.7",
"vaul": "^0.9.9",
"zod": "3.25.67"
},
"devDependencies": {
"@types/node": "^22",
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "^10.4.20",
"postcss": "^8.5",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
}
+9
View File
@@ -0,0 +1,9 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
export default config;
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 63 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="#0d597f"><path d="M23.25 38.81v-6.745l-4.855 4.864c.474.333.968.635 1.48.906.463.243.87.434 1.303.58s.782.24 1.13.304.66.093.95.096m24.822-.562c.045.037.092.07.142.1a2.77 2.77 0 0 0 .385.203 2.93 2.93 0 0 0 .637.194c.296.06.598.088.9.087.3 0 .608-.03.955-.087a7.24 7.24 0 0 0 1.138-.301 9.96 9.96 0 0 0 1.32-.579c.52-.274 1.02-.58 1.503-.918l-3.685-3.6-12.21-12.258-5.356 5.356-7.23-7.455-18.14 17.935a13.82 13.82 0 0 0 1.5.918c.47.246.91.434 1.317.58a7.18 7.18 0 0 0 1.135.301 5.53 5.53 0 0 0 .955.087c.302.001.604-.028.9-.087a3.29 3.29 0 0 0 .637-.194 2.49 2.49 0 0 0 .385-.197l.145-.104 8.193-8.193 2.924-2.808 8.106 8.106 2.837 2.912a1.29 1.29 0 0 0 .145.101 2.52 2.52 0 0 0 .385.2c.206.085.42.15.637.194.255.052.556.087.903.087.3 0 .608-.03.955-.087a6.89 6.89 0 0 0 1.138-.301 9.95 9.95 0 0 0 1.32-.579c.52-.274 1.02-.58 1.503-.918l-6.508-6.37 1.2-1.2 5.63 5.63 3.283 3.254m-.07-33.96l15.998 27.714L48.003 59.71H15.996L-.002 31.997 15.996 4.283z"/><path d="M38.02 30.65l-4.262-4.256.304-.304 4.3 4.244z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+20
View File
@@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256" version="1.0">
<defs>
<linearGradient xlink:href="#a" id="d" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-.39377 0 0 .39375 978.34969 416.9815)" x1="541.33502" y1="104.50665" x2="606.91248" y2="303.14029"/>
<linearGradient gradientUnits="userSpaceOnUse" id="a" y2="129.3468" x2="112.49853" y1="6.1372099" x1="112.49854" gradientTransform="translate(287 -83)">
<stop offset="0" style="stop-color:#fff;stop-opacity:0"/>
<stop offset="1" style="stop-color:#fff;stop-opacity:.27450982"/>
</linearGradient>
<linearGradient id="b">
<stop style="stop-color:#00bdec" offset="0"/>
<stop style="stop-color:#40bfde" offset="1"/>
</linearGradient>
<linearGradient id="c">
<stop style="stop-color:#6e6e6e" offset="0"/>
<stop style="stop-color:#4d4d4d" offset="1"/>
</linearGradient>
</defs>
<path style="fill:#1793d1" d="M128 0c-11.39482 27.937051-18.31337 46.237163-31 73.34375 7.7785 8.245207 17.33826 17.811753 32.84375 28.65625-16.66992-6.859577-28.03357-13.728504-36.53125-20.875C77.076039 115.00489 51.621645 163.24639 0 256c40.562707-23.41756 72.007597-37.86167 101.3125-43.375-1.25376-5.40435-1.923505-11.27752-1.875-17.375l.03125-1.28125c.64379-25.99398 14.16934-45.98224 30.1875-44.625 16.01815 1.35723 28.48754 23.53727 27.84375 49.53125-.12127 4.89622-.6905 9.60082-1.65625 13.96875C184.83328 218.51691 215.98162 232.89667 256 256c-7.89193-14.52962-14.96051-27.61983-21.6875-40.09375-10.59609-8.21269-21.64301-18.89743-44.1875-30.46875 15.4958 4.02645 26.60184 8.6825 35.25 13.875C156.97985 71.972668 151.45422 55.040376 128 0z" transform="matrix(1 0 0 1 -.000002 4e-8)"/>
<path style="fill:#fff;fill-opacity:.16568047" d="M818.22607 548.55277c-41.18143-55.89508-50.72685-100.94481-53.14467-111.70015 21.96737 50.6686 21.81733 51.28995 53.14467 111.70015z" transform="matrix(1.34737 0 0 1.34737 -902.40019 -586.944907)"/>
<path style="fill:url(#d);fill-opacity:1" d="M765.09805 436.43495c-1.05641 2.59705-2.08559 5.1172-3.06152 7.51465-1.08115 2.65585-2.10928 5.19128-3.13111 7.677-1.02174 2.48575-2.03439 4.91156-3.03833 7.30591-1.00398 2.39446-2.01068 4.76169-3.03833 7.14355-1.02758 2.38177-2.06156 4.78845-3.15429 7.23633-1.09273 2.44796-2.23335 4.94504-3.43262 7.53784-1.19937 2.59282-2.45641 5.27815-3.80371 8.09448-.18662.39008-.41312.83402-.60303 1.22925 5.75521 6.09563 12.84133 13.14976 24.28345 21.15234-12.34021-5.07792-20.76511-10.15751-27.06665-15.44677-.32717.66791-.61387 1.26431-.95093 1.94824-.44365.90024-.97632 1.92315-1.43799 2.85278-.80967 1.66032-1.65574 3.36576-2.52807 5.12574-.33524.66652-.62948 1.24283-.97413 1.92504-5.50733 11.05265-12.33962 24.28304-21.12915 40.72754 24.09557-13.57581 50.08533-33.16242 97.29615-16.30493-2.36708-4.48319-4.54319-8.68756-6.58692-12.64038-2.0437-3.95294-3.94246-7.6555-5.70556-11.15601-1.76297-3.50043-3.39212-6.80069-4.917-9.92675-1.52486-3.12599-2.93832-6.0765-4.26757-8.90625-1.32934-2.8297-2.58106-5.55264-3.75733-8.16407-1.17634-2.6114-2.29708-5.11315-3.36304-7.58422-1.06607-2.4712-2.08657-4.89718-3.08471-7.30591-.99823-2.4088-1.97267-4.81178-2.94556-7.23633-.34772-.86638-.69553-1.7689-1.0437-2.64404-2.66339-6.25269-5.3982-12.73163-8.55835-20.15503z" transform="matrix(1.34737 0 0 1.34737 -902.40019 -586.944907)"/>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

+86
View File
@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 10.0, SVG Export Plug-In . SVG Version: 3.0.0 Build 77) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd" [
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
<!ENTITY ns_extend "http://ns.adobe.com/Extensibility/1.0/">
<!ENTITY ns_ai "http://ns.adobe.com/AdobeIllustrator/10.0/">
<!ENTITY ns_graphs "http://ns.adobe.com/Graphs/1.0/">
<!ENTITY ns_vars "http://ns.adobe.com/Variables/1.0/">
<!ENTITY ns_imrep "http://ns.adobe.com/ImageReplacement/1.0/">
<!ENTITY ns_sfw "http://ns.adobe.com/SaveForWeb/1.0/">
<!ENTITY ns_custom "http://ns.adobe.com/GenericCustomNamespace/1.0/">
<!ENTITY ns_adobe_xpath "http://ns.adobe.com/XPath/1.0/">
<!ENTITY ns_svg "http://www.w3.org/2000/svg">
<!ENTITY ns_xlink "http://www.w3.org/1999/xlink">
]>
<svg
xmlns:x="&ns_extend;" xmlns:i="&ns_ai;" xmlns:graph="&ns_graphs;" i:viewOrigin="262 450" i:rulerOrigin="0 0" i:pageBounds="0 792 612 0"
xmlns="&ns_svg;" xmlns:xlink="&ns_xlink;" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
width="87.041" height="108.445" viewBox="0 0 87.041 108.445" overflow="visible" enable-background="new 0 0 87.041 108.445"
xml:space="preserve">
<metadata>
<variableSets xmlns="&ns_vars;">
<variableSet varSetName="binding1" locked="none">
<variables></variables>
<v:sampleDataSets xmlns="&ns_custom;" xmlns:v="&ns_vars;"></v:sampleDataSets>
</variableSet>
</variableSets>
<sfw xmlns="&ns_sfw;">
<slices></slices>
<sliceSourceBounds y="341.555" x="262" width="87.041" height="108.445" bottomLeftOrigin="true"></sliceSourceBounds>
</sfw>
</metadata>
<g id="Layer_1" i:layer="yes" i:dimmedPercent="50" i:rgbTrio="#4F008000FFFF">
<g>
<path i:knockout="Off" fill="#A80030" d="M51.986,57.297c-1.797,0.025,0.34,0.926,2.686,1.287
c0.648-0.506,1.236-1.018,1.76-1.516C54.971,57.426,53.484,57.434,51.986,57.297"/>
<path i:knockout="Off" fill="#A80030" d="M61.631,54.893c1.07-1.477,1.85-3.094,2.125-4.766c-0.24,1.192-0.887,2.221-1.496,3.307
c-3.359,2.115-0.316-1.256-0.002-2.537C58.646,55.443,61.762,53.623,61.631,54.893"/>
<path i:knockout="Off" fill="#A80030" d="M65.191,45.629c0.217-3.236-0.637-2.213-0.924-0.978
C64.602,44.825,64.867,46.932,65.191,45.629"/>
<path i:knockout="Off" fill="#A80030" d="M45.172,1.399c0.959,0.172,2.072,0.304,1.916,0.533
C48.137,1.702,48.375,1.49,45.172,1.399"/>
<path i:knockout="Off" fill="#A80030" d="M47.088,1.932l-0.678,0.14l0.631-0.056L47.088,1.932"/>
<path i:knockout="Off" fill="#A80030" d="M76.992,46.856c0.107,2.906-0.85,4.316-1.713,6.812l-1.553,0.776
c-1.271,2.468,0.123,1.567-0.787,3.53c-1.984,1.764-6.021,5.52-7.313,5.863c-0.943-0.021,0.639-1.113,0.846-1.541
c-2.656,1.824-2.131,2.738-6.193,3.846l-0.119-0.264c-10.018,4.713-23.934-4.627-23.751-17.371
c-0.107,0.809-0.304,0.607-0.526,0.934c-0.517-6.557,3.028-13.143,9.007-15.832c5.848-2.895,12.704-1.707,16.893,2.197
c-2.301-3.014-6.881-6.209-12.309-5.91c-5.317,0.084-10.291,3.463-11.951,7.131c-2.724,1.715-3.04,6.611-4.227,7.507
C31.699,56.271,36.3,61.342,44.083,67.307c1.225,0.826,0.345,0.951,0.511,1.58c-2.586-1.211-4.954-3.039-6.901-5.277
c1.033,1.512,2.148,2.982,3.589,4.137c-2.438-0.826-5.695-5.908-6.646-6.115c4.203,7.525,17.052,13.197,23.78,10.383
c-3.113,0.115-7.068,0.064-10.566-1.229c-1.469-0.756-3.467-2.322-3.11-2.615c9.182,3.43,18.667,2.598,26.612-3.771
c2.021-1.574,4.229-4.252,4.867-4.289c-0.961,1.445,0.164,0.695-0.574,1.971c2.014-3.248-0.875-1.322,2.082-5.609l1.092,1.504
c-0.406-2.696,3.348-5.97,2.967-10.234c0.861-1.304,0.961,1.403,0.047,4.403c1.268-3.328,0.334-3.863,0.66-6.609
c0.352,0.923,0.814,1.904,1.051,2.878c-0.826-3.216,0.848-5.416,1.262-7.285c-0.408-0.181-1.275,1.422-1.473-2.377
c0.029-1.65,0.459-0.865,0.625-1.271c-0.324-0.186-1.174-1.451-1.691-3.877c0.375-0.57,1.002,1.478,1.512,1.562
c-0.328-1.929-0.893-3.4-0.916-4.88c-1.49-3.114-0.527,0.415-1.736-1.337c-1.586-4.947,1.316-1.148,1.512-3.396
c2.404,3.483,3.775,8.881,4.404,11.117c-0.48-2.726-1.256-5.367-2.203-7.922c0.73,0.307-1.176-5.609,0.949-1.691
c-2.27-8.352-9.715-16.156-16.564-19.818c0.838,0.767,1.896,1.73,1.516,1.881c-3.406-2.028-2.807-2.186-3.295-3.043
c-2.775-1.129-2.957,0.091-4.795,0.002c-5.23-2.774-6.238-2.479-11.051-4.217l0.219,1.023c-3.465-1.154-4.037,0.438-7.782,0.004
c-0.228-0.178,1.2-0.644,2.375-0.815c-3.35,0.442-3.193-0.66-6.471,0.122c0.808-0.567,1.662-0.942,2.524-1.424
c-2.732,0.166-6.522,1.59-5.352,0.295c-4.456,1.988-12.37,4.779-16.811,8.943l-0.14-0.933c-2.035,2.443-8.874,7.296-9.419,10.46
l-0.544,0.127c-1.059,1.793-1.744,3.825-2.584,5.67c-1.385,2.36-2.03,0.908-1.833,1.278c-2.724,5.523-4.077,10.164-5.246,13.97
c0.833,1.245,0.02,7.495,0.335,12.497c-1.368,24.704,17.338,48.69,37.785,54.228c2.997,1.072,7.454,1.031,11.245,1.141
c-4.473-1.279-5.051-0.678-9.408-2.197c-3.143-1.48-3.832-3.17-6.058-5.102l0.881,1.557c-4.366-1.545-2.539-1.912-6.091-3.037
l0.941-1.229c-1.415-0.107-3.748-2.385-4.386-3.646l-1.548,0.061c-1.86-2.295-2.851-3.949-2.779-5.23l-0.5,0.891
c-0.567-0.973-6.843-8.607-3.587-6.83c-0.605-0.553-1.409-0.9-2.281-2.484l0.663-0.758c-1.567-2.016-2.884-4.6-2.784-5.461
c0.836,1.129,1.416,1.34,1.99,1.533c-3.957-9.818-4.179-0.541-7.176-9.994l0.634-0.051c-0.486-0.732-0.781-1.527-1.172-2.307
l0.276-2.75C4.667,58.121,6.719,47.409,7.13,41.534c0.285-2.389,2.378-4.932,3.97-8.92l-0.97-0.167
c1.854-3.234,10.586-12.988,14.63-12.486c1.959-2.461-0.389-0.009-0.772-0.629c4.303-4.453,5.656-3.146,8.56-3.947
c3.132-1.859-2.688,0.725-1.203-0.709c5.414-1.383,3.837-3.144,10.9-3.846c0.745,0.424-1.729,0.655-2.35,1.205
c4.511-2.207,14.275-1.705,20.617,1.225c7.359,3.439,15.627,13.605,15.953,23.17l0.371,0.1
c-0.188,3.802,0.582,8.199-0.752,12.238L76.992,46.856"/>
<path i:knockout="Off" fill="#A80030" d="M32.372,59.764l-0.252,1.26c1.181,1.604,2.118,3.342,3.626,4.596
C34.661,63.502,33.855,62.627,32.372,59.764"/>
<path i:knockout="Off" fill="#A80030" d="M35.164,59.654c-0.625-0.691-0.995-1.523-1.409-2.352
c0.396,1.457,1.207,2.709,1.962,3.982L35.164,59.654"/>
<path i:knockout="Off" fill="#A80030" d="M84.568,48.916l-0.264,0.662c-0.484,3.438-1.529,6.84-3.131,9.994
C82.943,56.244,84.088,52.604,84.568,48.916"/>
<path i:knockout="Off" fill="#A80030" d="M45.527,0.537C46.742,0.092,48.514,0.293,49.803,0c-1.68,0.141-3.352,0.225-5.003,0.438
L45.527,0.537"/>
<path i:knockout="Off" fill="#A80030" d="M2.872,23.219c0.28,2.592-1.95,3.598,0.494,1.889
C4.676,22.157,2.854,24.293,2.872,23.219"/>
<path i:knockout="Off" fill="#A80030" d="M0,35.215c0.563-1.728,0.665-2.766,0.88-3.766C-0.676,33.438,0.164,33.862,0,35.215"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.7 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 32 32" preserveAspectRatio="xMidYMid"><path d="M32 16c0 8.836-7.164 16-16 16S0 24.836 0 16 7.164 0 16 0s16 7.164 16 16z" fill="#dd4814"/><path d="M5.12 13.864c-1.18 0-2.137.956-2.137 2.137s.956 2.136 2.137 2.136S7.257 17.18 7.257 16 6.3 13.864 5.12 13.864zm15.252 9.71c-1.022.6-1.372 1.896-.782 2.917s1.895 1.372 2.917.782 1.372-1.895.782-2.917-1.896-1.37-2.917-.782zM9.76 16a6.23 6.23 0 0 1 2.653-5.105L10.852 8.28a9.3 9.3 0 0 0-3.838 5.394C7.69 14.224 8.12 15.06 8.12 16s-.432 1.776-1.106 2.326c.577 2.237 1.968 4.146 3.838 5.395l1.562-2.616A6.23 6.23 0 0 1 9.761 16zM16 9.76a6.24 6.24 0 0 1 6.215 5.687l3.044-.045a9.25 9.25 0 0 0-2.757-6.019c-.812.307-1.75.26-2.56-.208a2.99 2.99 0 0 1-1.461-2.118C17.7 6.84 16.86 6.72 16 6.72c-1.477 0-2.873.347-4.113.96l1.484 2.66c.8-.372 1.69-.58 2.628-.58zm0 12.48c-.94 0-1.83-.21-2.628-.58l-1.484 2.66c1.24.614 2.636.96 4.113.96a9.28 9.28 0 0 0 2.479-.338c.14-.858.65-1.648 1.46-2.118s1.75-.514 2.56-.207a9.25 9.25 0 0 0 2.757-6.019l-3.045-.045A6.24 6.24 0 0 1 16 22.24zm4.372-13.813c1.022.6 2.328.24 2.917-.78s.24-2.328-.78-2.918-2.328-.24-2.918.783-.24 2.327.782 2.917z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,23 @@
# Onboarding Images
Place your screenshot images here with the following names:
- `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
## Image Guidelines
- **Format**: PNG or JPG
- **Recommended size**: 1200x800px or similar aspect ratio
- **Quality**: High quality screenshots showing the main features of each section
- **Content**: Capture the full section with representative data
## Notes
- The last slide (Future Updates) doesn't need an image as it uses an icon
- If an image fails to load, the component will show a fallback icon
- Images should be optimized for web (compressed but still high quality)
Binary file not shown.

After

Width:  |  Height:  |  Size: 404 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

+16
View File
@@ -0,0 +1,16 @@
{
"name": "ProxMenux Monitor",
"short_name": "ProxMenux",
"description": "Proxmox System Dashboard and Monitor",
"start_url": "/",
"display": "standalone",
"background_color": "#2b2f36",
"theme_color": "#2b2f36",
"icons": [
{
"src": "/images/proxmenux-logo.png",
"sizes": "256x256",
"type": "image/png"
}
]
}
+41
View File
@@ -0,0 +1,41 @@
#!/bin/bash
# ProxMenux Monitor AppImage Entry Point
# This script is executed when the AppImage is run
# Get the directory where this AppImage is mounted
APPDIR="$(dirname "$(readlink -f "${0}")")"
export PATH="${APPDIR}/usr/bin:${PATH}"
export LD_LIBRARY_PATH="${APPDIR}/usr/lib/x86_64-linux-gnu:${APPDIR}/usr/lib:${APPDIR}/lib/x86_64-linux-gnu:${APPDIR}/lib:${LD_LIBRARY_PATH}"
export PYTHONPATH="${APPDIR}/usr/lib/python3/dist-packages:${APPDIR}/usr/lib/python3/site-packages:${PYTHONPATH}"
# Change to the AppImage directory
cd "${APPDIR}"
# Check for translation argument
if [[ "$1" == "--translate" ]]; then
echo "🌐 Starting ProxMenux Translation Service..."
exec python3 "${APPDIR}/usr/bin/translate_cli.py" "${@:2}"
else
echo "🚀 Starting ProxMenux Monitor Dashboard..."
echo ""
echo "🔧 Hardware monitoring tools:"
[ -x "${APPDIR}/usr/bin/ipmitool" ] && echo " ✅ ipmitool available" || echo " ⚠️ ipmitool not available"
[ -x "${APPDIR}/usr/bin/sensors" ] && echo " ✅ sensors available" || echo " ⚠️ sensors not available"
[ -x "${APPDIR}/usr/bin/upsc" ] && echo " ✅ upsc available" || echo " ⚠️ upsc not available"
if [ -x "${APPDIR}/usr/bin/ipmitool" ]; then
if ldd "${APPDIR}/usr/bin/ipmitool" 2>/dev/null | grep -q "libfreeipmi.so.17 => not found"; then
echo " ⚠️ libfreeipmi.so.17 not found - ipmitool may not work"
elif ldd "${APPDIR}/usr/bin/ipmitool" 2>/dev/null | grep -q "libfreeipmi.so.17"; then
echo " ✅ libfreeipmi.so.17 loaded successfully"
fi
fi
echo ""
# Start the Flask server
exec python3 "${APPDIR}/usr/bin/flask_server.py"
fi
+490
View File
@@ -0,0 +1,490 @@
#!/bin/bash
# ProxMenux Monitor AppImage Builder
# This script creates a single AppImage with Flask server, Next.js dashboard, and translation support
set -e
WORK_DIR="/tmp/proxmenux_build"
APP_DIR="$WORK_DIR/ProxMenux.AppDir"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DIST_DIR="$SCRIPT_DIR/../dist"
APPIMAGE_ROOT="$SCRIPT_DIR/.."
VERSION=$(node -p "require('$APPIMAGE_ROOT/package.json').version")
APPIMAGE_NAME="ProxMenux-${VERSION}.AppImage"
echo "🚀 Building ProxMenux Monitor AppImage v${VERSION} with hardware monitoring tools..."
# Clean and create work directory
rm -rf "$WORK_DIR"
mkdir -p "$APP_DIR"
mkdir -p "$DIST_DIR"
# Download appimagetool if not exists
if [ ! -f "$WORK_DIR/appimagetool" ]; then
echo "📥 Downloading appimagetool..."
wget -q "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" -O "$WORK_DIR/appimagetool"
chmod +x "$WORK_DIR/appimagetool"
fi
# Create directory structure
mkdir -p "$APP_DIR/usr/bin"
mkdir -p "$APP_DIR/usr/lib/python3/dist-packages"
mkdir -p "$APP_DIR/usr/share/applications"
mkdir -p "$APP_DIR/usr/share/icons/hicolor/256x256/apps"
mkdir -p "$APP_DIR/web"
echo "🔨 Building Next.js application..."
cd "$APPIMAGE_ROOT"
if [ ! -f "package.json" ]; then
echo "❌ Error: package.json not found in AppImage directory"
exit 1
fi
# Install dependencies if node_modules doesn't exist
if [ ! -d "node_modules" ]; then
echo "📦 Installing dependencies..."
npm install
fi
echo "🏗️ Building Next.js static export..."
npm run export
echo "🔍 Checking export results..."
if [ -d "out" ]; then
echo "✅ Export directory found"
echo "📁 Contents of out directory:"
ls -la out/
if [ -f "out/index.html" ]; then
echo "✅ index.html found in out directory"
else
echo "❌ index.html NOT found in out directory"
echo "📁 Looking for HTML files:"
find out/ -name "*.html" -type f || echo "No HTML files found"
fi
else
echo "❌ Error: Next.js export failed - out directory not found"
echo "📁 Current directory contents:"
ls -la
echo "📁 Looking for any build outputs:"
find . -name "*.html" -type f 2>/dev/null || echo "No HTML files found anywhere"
exit 1
fi
# Return to script directory
cd "$SCRIPT_DIR"
# Copy Flask server
echo "📋 Copying Flask server..."
cp "$SCRIPT_DIR/flask_server.py" "$APP_DIR/usr/bin/"
echo "📋 Adding translation support..."
cat > "$APP_DIR/usr/bin/translate_cli.py" << 'PYEOF'
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
ProxMenux translate CLI
stdin JSON -> {"text":"...", "dest_lang":"es", "context":"...", "cache_file":"/usr/local/share/proxmenux/cache.json"}
stdout JSON -> {"success":true,"text":"..."} or {"success":false,"error":"..."}
"""
import sys, json, re
from pathlib import Path
# Ensure embedded site-packages are discoverable
HERE = Path(__file__).resolve().parents[2] # .../AppDir
DIST = HERE / "usr" / "lib" / "python3" / "dist-packages"
SITE = HERE / "usr" / "lib" / "python3" / "site-packages"
for p in (str(DIST), str(SITE)):
if p not in sys.path:
sys.path.insert(0, p)
# Python 3.13 compat: inline 'cgi' shim
try:
import cgi
except Exception:
import types, html
def _parse_header(value: str):
value = str(value or "")
parts = [p.strip() for p in value.split(";")]
if not parts:
return "", {}
key = parts[0].lower()
params = {}
for item in parts[1:]:
if not item:
continue
if "=" in item:
k, v = item.split("=", 1)
k = k.strip().lower()
v = v.strip().strip('"').strip("'")
params[k] = v
else:
params[item.strip().lower()] = ""
return key, params
cgi = types.SimpleNamespace(parse_header=_parse_header, escape=html.escape)
try:
from googletrans import Translator
except Exception as e:
print(json.dumps({"success": False, "error": f"ImportError: {e}"}))
sys.exit(0)
def load_json_stdin():
try:
return json.load(sys.stdin)
except Exception as e:
print(json.dumps({"success": False, "error": f"Invalid JSON input: {e}"}))
sys.exit(0)
def ensure_cache(path: Path):
try:
path.parent.mkdir(parents=True, exist_ok=True)
if not path.exists():
path.write_text("{}", encoding="utf-8")
json.loads(path.read_text(encoding="utf-8") or "{}")
except Exception:
path.write_text("{}", encoding="utf-8")
def read_cache(path: Path):
try:
return json.loads(path.read_text(encoding="utf-8") or "{}")
except Exception:
return {}
def write_cache(path: Path, cache: dict):
tmp = path.with_suffix(".tmp")
tmp.write_text(json.dumps(cache, ensure_ascii=False), encoding="utf-8")
tmp.replace(path)
def clean_translated(s: str) -> str:
s = re.sub(r'^.*?(Translate:|Traducir:|Traduire:|Übersetzen:|Tradurre:|Traduzir:|翻译:|翻訳:)', '', s, flags=re.IGNORECASE | re.DOTALL).strip()
s = re.sub(r'^.*?(Context:|Contexto:|Contexte:|Kontext:|Contesto:|上下文:|コンテキスト:).*?:', '', s, flags=re.IGNORECASE | re.DOTALL).strip()
return s.strip()
def main():
req = load_json_stdin()
text = req.get("text", "")
dest = req.get("dest_lang", "en") or "en"
context = req.get("context", "")
cache_file = Path(req.get("cache_file", "")) if req.get("cache_file") else None
if dest == "en":
print(json.dumps({"success": True, "text": text}))
return
cache = {}
if cache_file:
ensure_cache(cache_file)
cache = read_cache(cache_file)
if text in cache and (dest in cache[text] or "notranslate" in cache[text]):
found = cache[text].get(dest) or cache[text].get("notranslate")
print(json.dumps({"success": True, "text": found}))
return
try:
full = (context + " " + text).strip() if context else text
tr = Translator()
result = tr.translate(full, dest=dest).text
result = clean_translated(result)
if cache_file:
cache.setdefault(text, {})
cache[text][dest] = result
write_cache(cache_file, cache)
print(json.dumps({"success": True, "text": result}))
except Exception as e:
print(json.dumps({"success": False, "error": str(e)}))
if __name__ == "__main__":
main()
PYEOF
chmod +x "$APP_DIR/usr/bin/translate_cli.py"
# Copy Next.js build
echo "📋 Copying web dashboard..."
if [ -d "$APPIMAGE_ROOT/out" ]; then
mkdir -p "$APP_DIR/web"
echo "📁 Copying from $APPIMAGE_ROOT/out to $APP_DIR/web"
cp -r "$APPIMAGE_ROOT/out"/* "$APP_DIR/web/"
if [ -f "$APP_DIR/web/index.html" ]; then
echo "✅ index.html copied successfully to $APP_DIR/web/"
else
echo "❌ index.html NOT found after copying"
echo "📁 Contents of $APP_DIR/web:"
ls -la "$APP_DIR/web/" || echo "Directory is empty or doesn't exist"
fi
if [ -d "$APPIMAGE_ROOT/public" ]; then
cp -r "$APPIMAGE_ROOT/public"/* "$APP_DIR/web/" 2>/dev/null || true
fi
cp "$APPIMAGE_ROOT/package.json" "$APP_DIR/web/"
echo "✅ Next.js static export copied successfully"
else
echo "❌ Error: Next.js export not found even after building"
exit 1
fi
# Copy AppRun script
echo "📋 Copying AppRun script..."
if [ -f "$SCRIPT_DIR/AppRun" ]; then
cp "$SCRIPT_DIR/AppRun" "$APP_DIR/AppRun"
chmod +x "$APP_DIR/AppRun"
echo "✅ AppRun script copied successfully"
else
echo "❌ Error: AppRun script not found at $SCRIPT_DIR/AppRun"
exit 1
fi
# Create desktop file
cat > "$APP_DIR/proxmenux-monitor.desktop" << EOF
[Desktop Entry]
Type=Application
Name=ProxMenux Monitor
Comment=Proxmox System Monitoring Dashboard with Translation Support
Exec=AppRun
Icon=proxmenux-monitor
Categories=System;Monitor;
Terminal=false
StartupNotify=true
EOF
# Copy desktop file to applications directory
cp "$APP_DIR/proxmenux-monitor.desktop" "$APP_DIR/usr/share/applications/"
# Download and set icon
echo "🎨 Setting up icon..."
if [ -f "$APPIMAGE_ROOT/public/images/proxmenux-logo.png" ]; then
cp "$APPIMAGE_ROOT/public/images/proxmenux-logo.png" "$APP_DIR/proxmenux-monitor.png"
else
wget -q "https://raw.githubusercontent.com/MacRimi/ProxMenux/main/images/logo.png" -O "$APP_DIR/proxmenux-monitor.png" || {
echo "⚠️ Could not download logo, creating placeholder..."
convert -size 256x256 xc:blue -fill white -gravity center -pointsize 24 -annotate +0+0 "PM" "$APP_DIR/proxmenux-monitor.png" 2>/dev/null || {
echo "⚠️ ImageMagick not available, skipping icon creation"
}
}
fi
if [ -f "$APP_DIR/proxmenux-monitor.png" ]; then
cp "$APP_DIR/proxmenux-monitor.png" "$APP_DIR/usr/share/icons/hicolor/256x256/apps/"
fi
echo "📦 Installing Python dependencies..."
pip3 install --target "$APP_DIR/usr/lib/python3/dist-packages" \
flask \
flask-cors \
psutil \
requests \
googletrans==4.0.0-rc1 \
httpx==0.13.3 \
httpcore==0.9.1 \
beautifulsoup4
cat > "$APP_DIR/usr/lib/python3/dist-packages/cgi.py" << 'PYEOF'
from typing import Tuple, Dict
try:
from html import escape as _html_escape
except Exception:
def _html_escape(s, quote=True): return s
__all__ = ["parse_header", "escape"]
def escape(s, quote=True):
return _html_escape(s, quote=quote)
def parse_header(value: str) -> Tuple[str, Dict[str, str]]:
if not isinstance(value, str):
value = str(value or "")
parts = [p.strip() for p in value.split(";")]
if not parts:
return "", {}
key = parts[0].lower()
params: Dict[str, str] = {}
for item in parts[1:]:
if not item:
continue
if "=" in item:
k, v = item.split("=", 1)
k = k.strip().lower()
v = v.strip().strip('"').strip("'")
params[k] = v
else:
params[item.strip().lower()] = ""
return key, params
PYEOF
echo "🔧 Installing hardware monitoring tools..."
mkdir -p "$WORK_DIR/debs"
cd "$WORK_DIR/debs"
# ==============================================================
echo "📥 Downloading hardware monitoring tools (dynamic via APT)..."
dl_pkg() {
local out="$1"; shift
local pkg deb_file
for pkg in "$@"; do
echo " - trying: $pkg"
if apt-get download -y "$pkg" >/dev/null 2>&1; then
deb_file="$(ls -1 ${pkg}_*.deb 2>/dev/null | head -n1)"
if [ -n "$deb_file" ] && [ -f "$deb_file" ]; then
mv "$deb_file" "$out"
echo " ✅ downloaded: $pkg -> $out"
return 0
fi
fi
done
if command -v sudo >/dev/null 2>&1 && sudo -n true >/dev/null 2>&1; then
echo " ↻ retry with sudo apt-get update && download"
sudo apt-get update -qq || true
for pkg in "$@"; do
echo " - trying (sudo): $pkg"
if sudo apt-get download -y "$pkg" >/dev/null 2>&1; then
deb_file="$(ls -1 ${pkg}_*.deb 2>/dev/null | head -n1)"
if [ -n "$deb_file" ] && [ -f "$deb_file" ]; then
mv "$deb_file" "$out"
echo " ✅ downloaded (sudo): $pkg -> $out"
return 0
fi
fi
done
fi
echo " ⚠️ none of the candidates could be downloaded for $out"
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
for deb in *.deb; do
echo " -> $deb"
if file "$deb" | grep -q "Debian binary package"; then
dpkg-deb -x "$deb" "$APP_DIR" && extracted_count=$((extracted_count + 1))
else
echo " ⚠️ $deb is not a valid .deb, skipping"
fi
done
shopt -u nullglob
if [ $extracted_count -eq 0 ]; then
echo "⚠️ No packages extracted; hardware/GPU monitoring may be unavailable"
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"
cp -r "$APP_DIR/bin/"* "$APP_DIR/usr/bin/" 2>/dev/null || true
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 ' ')"
echo " missing: $missing"
case "$missing" in
libupsclient.so.6) need_pkg="libupsclient6" ;;
libupsclient.so.5) need_pkg="libupsclient5" ;;
libupsclient.so.4) need_pkg="libupsclient4" ;;
*) need_pkg="" ;;
esac
if [ -n "$need_pkg" ]; then
echo " downloading: $need_pkg"
dl_pkg "libupsclient_autofix.deb" "$need_pkg" || true
if [ -f "libupsclient_autofix.deb" ]; then
dpkg-deb -x "libupsclient_autofix.deb" "$APP_DIR"
echo " re-checking ldd for upsc..."
if ldd "$APP_DIR/usr/bin/upsc" | grep -q 'not found'; then
echo "❌ upsc still has unresolved libs:"
ldd "$APP_DIR/usr/bin/upsc" | grep 'not found' || true
exit 1
fi
else
echo "❌ could not download $need_pkg automatically"
exit 1
fi
else
echo "❌ unknown missing library for upsc: $missing"
exit 1
fi
fi
echo "✅ Sanity check OK (ipmitool/upsc ready; libfreeipmi present)"
# Info rápida
[ -x "$APP_DIR/usr/bin/sensors" ] && echo " • sensors: OK" || echo " • sensors: missing"
[ -x "$APP_DIR/usr/bin/ipmitool" ] && echo " • ipmitool: OK" || echo " • ipmitool: missing"
[ -x "$APP_DIR/usr/bin/upsc" ] && echo " • upsc: OK" || echo " • upsc: missing"
[ -x "$APP_DIR/usr/bin/nvidia-smi" ] && echo " • nvidia-smi: OK" || echo " • nvidia-smi: missing"
[ -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"
export NO_CLEANUP=1
export APPIMAGE_EXTRACT_AND_RUN=1
ARCH=x86_64 ./appimagetool --no-appstream --verbose "$APP_DIR" "$APPIMAGE_NAME"
# Move to dist directory
mv "$APPIMAGE_NAME" "$DIST_DIR/"
echo "✅ Unified AppImage created: $DIST_DIR/$APPIMAGE_NAME"
echo ""
echo "📋 Usage:"
echo " Dashboard: ./$APPIMAGE_NAME"
echo " Translation: ./$APPIMAGE_NAME --translate"
echo ""
echo "🚀 Installation:"
echo " sudo cp $DIST_DIR/$APPIMAGE_NAME /usr/local/bin/proxmenux-monitor"
echo " sudo chmod +x /usr/local/bin/proxmenux-monitor"
File diff suppressed because it is too large Load Diff
+369
View File
@@ -0,0 +1,369 @@
#!/usr/bin/env python3
import json
import subprocess
import re
import os
from typing import Dict, List, Any, Optional
def run_command(cmd: List[str]) -> str:
"""Run a command and return its output."""
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=5)
return result.stdout
except Exception:
return ""
def get_nvidia_gpu_info() -> List[Dict[str, Any]]:
"""Get detailed NVIDIA GPU information using nvidia-smi."""
gpus = []
# Check if nvidia-smi is available
if not os.path.exists('/usr/bin/nvidia-smi'):
return gpus
try:
# Query all GPU metrics at once
query_fields = [
'index',
'name',
'driver_version',
'memory.total',
'memory.used',
'memory.free',
'temperature.gpu',
'utilization.gpu',
'utilization.memory',
'power.draw',
'power.limit',
'clocks.current.graphics',
'clocks.current.memory',
'pcie.link.gen.current',
'pcie.link.width.current'
]
cmd = ['nvidia-smi', '--query-gpu=' + ','.join(query_fields), '--format=csv,noheader,nounits']
output = run_command(cmd)
if not output:
return gpus
for line in output.strip().split('\n'):
if not line:
continue
values = [v.strip() for v in line.split(',')]
if len(values) < len(query_fields):
continue
gpu_info = {
'index': values[0],
'name': values[1],
'driver_version': values[2],
'memory_total': f"{values[3]} MiB",
'memory_used': f"{values[4]} MiB",
'memory_free': f"{values[5]} MiB",
'temperature': values[6],
'utilization_gpu': values[7],
'utilization_memory': values[8],
'power_draw': f"{values[9]} W",
'power_limit': f"{values[10]} W",
'clock_graphics': f"{values[11]} MHz",
'clock_memory': f"{values[12]} MHz",
'pcie_gen': values[13],
'pcie_width': f"x{values[14]}"
}
# Get CUDA version if available
cuda_output = run_command(['nvidia-smi', '--query-gpu=compute_cap', '--format=csv,noheader', '-i', values[0]])
if cuda_output:
gpu_info['compute_capability'] = cuda_output.strip()
gpus.append(gpu_info)
except Exception as e:
print(f"Error getting NVIDIA GPU info: {e}", file=sys.stderr)
return gpus
def get_amd_gpu_info() -> List[Dict[str, Any]]:
"""Get AMD GPU information using rocm-smi."""
gpus = []
# Check if rocm-smi is available
if not os.path.exists('/opt/rocm/bin/rocm-smi'):
return gpus
try:
# Get basic GPU info
output = run_command(['/opt/rocm/bin/rocm-smi', '--showid', '--showtemp', '--showuse', '--showmeminfo', 'vram'])
if not output:
return gpus
# Parse rocm-smi output (format varies, this is a basic parser)
current_gpu = None
for line in output.split('\n'):
if 'GPU[' in line:
if current_gpu:
gpus.append(current_gpu)
current_gpu = {'index': line.split('[')[1].split(']')[0]}
elif current_gpu:
if 'Temperature' in line:
temp_match = re.search(r'(\d+\.?\d*)', line)
if temp_match:
current_gpu['temperature'] = temp_match.group(1)
elif 'GPU use' in line:
use_match = re.search(r'(\d+)%', line)
if use_match:
current_gpu['utilization_gpu'] = use_match.group(1)
elif 'VRAM' in line:
mem_match = re.search(r'(\d+)MB / (\d+)MB', line)
if mem_match:
current_gpu['memory_used'] = f"{mem_match.group(1)} MiB"
current_gpu['memory_total'] = f"{mem_match.group(2)} MiB"
if current_gpu:
gpus.append(current_gpu)
except Exception as e:
print(f"Error getting AMD GPU info: {e}", file=sys.stderr)
return gpus
def get_temperatures() -> List[Dict[str, Any]]:
"""Get temperature readings from sensors."""
temps = []
output = run_command(['sensors', '-A', '-u'])
current_adapter = None
current_sensor = None
for line in output.split('\n'):
line = line.strip()
if not line:
continue
if line.endswith(':') and not line.startswith(' '):
current_adapter = line[:-1]
elif '_input:' in line and current_adapter:
parts = line.split(':')
if len(parts) == 2:
sensor_name = parts[0].replace('_input', '').replace('_', ' ').title()
try:
temp_value = float(parts[1].strip())
temps.append({
'name': sensor_name,
'current': round(temp_value, 1),
'adapter': current_adapter
})
except ValueError:
pass
return temps
def get_fans() -> List[Dict[str, Any]]:
"""Get fan speed readings."""
fans = []
output = run_command(['sensors', '-A', '-u'])
current_adapter = None
for line in output.split('\n'):
line = line.strip()
if not line:
continue
if line.endswith(':') and not line.startswith(' '):
current_adapter = line[:-1]
elif 'fan' in line.lower() and '_input:' in line and current_adapter:
parts = line.split(':')
if len(parts) == 2:
fan_name = parts[0].replace('_input', '').replace('_', ' ').title()
try:
speed = float(parts[1].strip())
fans.append({
'name': fan_name,
'speed': int(speed),
'unit': 'RPM'
})
except ValueError:
pass
return fans
def get_network_cards() -> List[Dict[str, Any]]:
"""Get network interface information."""
cards = []
output = run_command(['ip', '-o', 'link', 'show'])
for line in output.split('\n'):
if not line or 'lo:' in line:
continue
parts = line.split()
if len(parts) >= 2:
name = parts[1].rstrip(':')
state = 'UP' if 'UP' in line else 'DOWN'
# Get interface type
iface_type = 'Unknown'
if 'ether' in line:
iface_type = 'Ethernet'
elif 'wlan' in name or 'wifi' in name:
iface_type = 'WiFi'
# Try to get speed
speed = None
speed_output = run_command(['ethtool', name])
speed_match = re.search(r'Speed: (\d+\w+)', speed_output)
if speed_match:
speed = speed_match.group(1)
cards.append({
'name': name,
'type': iface_type,
'status': state,
'speed': speed
})
return cards
def get_storage_devices() -> List[Dict[str, Any]]:
"""Get storage device information."""
devices = []
output = run_command(['lsblk', '-d', '-o', 'NAME,TYPE,SIZE,MODEL', '-n'])
for line in output.split('\n'):
if not line:
continue
parts = line.split(None, 3)
if len(parts) >= 3:
name = parts[0]
dev_type = parts[1]
size = parts[2]
model = parts[3] if len(parts) > 3 else 'Unknown'
if dev_type in ['disk', 'nvme']:
devices.append({
'name': name,
'type': dev_type,
'size': size,
'model': model.strip()
})
return devices
def get_pci_devices() -> List[Dict[str, Any]]:
"""Get PCI device information including GPUs."""
devices = []
output = run_command(['lspci', '-vmm'])
current_device = {}
for line in output.split('\n'):
line = line.strip()
if not line:
if current_device:
devices.append(current_device)
current_device = {}
continue
if ':' in line:
key, value = line.split(':', 1)
key = key.strip().lower().replace(' ', '_')
value = value.strip()
current_device[key] = value
if current_device:
devices.append(current_device)
# Enhance GPU devices with monitoring data
nvidia_gpus = get_nvidia_gpu_info()
amd_gpus = get_amd_gpu_info()
nvidia_idx = 0
amd_idx = 0
for device in devices:
# Check if it's a GPU
device_class = device.get('class', '').lower()
vendor = device.get('vendor', '').lower()
if 'vga' in device_class or 'display' in device_class or '3d' in device_class:
device['type'] = 'GPU'
# Add NVIDIA GPU monitoring data
if 'nvidia' in vendor and nvidia_idx < len(nvidia_gpus):
gpu_data = nvidia_gpus[nvidia_idx]
device['gpu_memory'] = gpu_data.get('memory_total')
device['gpu_driver_version'] = gpu_data.get('driver_version')
device['gpu_compute_capability'] = gpu_data.get('compute_capability')
device['gpu_power_draw'] = gpu_data.get('power_draw')
device['gpu_temperature'] = float(gpu_data.get('temperature', 0))
device['gpu_utilization'] = float(gpu_data.get('utilization_gpu', 0))
device['gpu_memory_used'] = gpu_data.get('memory_used')
device['gpu_memory_total'] = gpu_data.get('memory_total')
device['gpu_clock_speed'] = gpu_data.get('clock_graphics')
device['gpu_memory_clock'] = gpu_data.get('clock_memory')
nvidia_idx += 1
# Add AMD GPU monitoring data
elif 'amd' in vendor and amd_idx < len(amd_gpus):
gpu_data = amd_gpus[amd_idx]
device['gpu_temperature'] = float(gpu_data.get('temperature', 0))
device['gpu_utilization'] = float(gpu_data.get('utilization_gpu', 0))
device['gpu_memory_used'] = gpu_data.get('memory_used')
device['gpu_memory_total'] = gpu_data.get('memory_total')
amd_idx += 1
elif 'network' in device_class or 'ethernet' in device_class:
device['type'] = 'Network'
elif 'storage' in device_class or 'sata' in device_class or 'nvme' in device_class:
device['type'] = 'Storage'
else:
device['type'] = 'Other'
return devices
def get_power_info() -> Optional[Dict[str, Any]]:
"""Get power consumption information if available."""
# Try to get system power from RAPL (Running Average Power Limit)
rapl_path = '/sys/class/powercap/intel-rapl/intel-rapl:0/energy_uj'
if os.path.exists(rapl_path):
try:
with open(rapl_path, 'r') as f:
energy_uj = int(f.read().strip())
# This is cumulative energy, would need to track over time for watts
# For now, just indicate power monitoring is available
return {
'name': 'System Power',
'watts': 0, # Would need time-based calculation
'adapter': 'RAPL'
}
except Exception:
pass
return None
def main():
"""Main function to gather all hardware information."""
data = {
'temperatures': get_temperatures(),
'fans': get_fans(),
'network_cards': get_network_cards(),
'storage_devices': get_storage_devices(),
'pci_devices': get_pci_devices(),
}
power_info = get_power_info()
if power_info:
data['power_meter'] = power_info
print(json.dumps(data, indent=2))
if __name__ == '__main__':
import sys
main()
+65
View File
@@ -0,0 +1,65 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./app/**/*.{ts,tsx,js,jsx}",
"./components/**/*.{ts,tsx,js,jsx}",
"./pages/**/*.{ts,tsx,js,jsx}",
"./src/**/*.{ts,tsx,js,jsx}",
],
darkMode: "class",
theme: {
extend: {
colors: {
background: "var(--background)",
foreground: "var(--foreground)",
card: "var(--card)",
"card-foreground": "var(--card-foreground)",
popover: "var(--popover)",
"popover-foreground": "var(--popover-foreground)",
primary: "var(--primary)",
"primary-foreground": "var(--primary-foreground)",
secondary: "var(--secondary)",
"secondary-foreground": "var(--secondary-foreground)",
muted: "var(--muted)",
"muted-foreground": "var(--muted-foreground)",
accent: "var(--accent)",
"accent-foreground": "var(--accent-foreground)",
destructive: "var(--destructive)",
"destructive-foreground": "var(--destructive-foreground)",
border: "var(--border)",
input: "var(--input)",
ring: "var(--ring)",
"chart-1": "var(--chart-1)",
"chart-2": "var(--chart-2)",
"chart-3": "var(--chart-3)",
"chart-4": "var(--chart-4)",
"chart-5": "var(--chart-5)",
sidebar: "var(--sidebar)",
"sidebar-foreground": "var(--sidebar-foreground)",
"sidebar-primary": "var(--sidebar-primary)",
"sidebar-primary-foreground": "var(--sidebar-primary-foreground)",
"sidebar-accent": "var(--sidebar-accent)",
"sidebar-accent-foreground": "var(--sidebar-accent-foreground)",
"sidebar-border": "var(--sidebar-border)",
"sidebar-ring": "var(--sidebar-ring)",
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
xl: "calc(var(--radius) + 4px)",
},
},
},
plugins: [require("tailwindcss-animate")],
};
+27
View File
@@ -0,0 +1,27 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "es6"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
+204
View File
@@ -0,0 +1,204 @@
export interface Temperature {
name: string
original_name?: string
current: number
high?: number
critical?: number
adapter?: string
}
export interface PowerMeter {
name: string
watts: number
adapter?: string
}
export interface NetworkInterface {
name: string
type: string
speed?: string
status?: string
}
export interface StorageDevice {
name: string
type: string
size?: string
model?: string
driver?: string
interface?: string
serial?: string
family?: string
firmware?: string
rotation_rate?: number | string
form_factor?: string
sata_version?: string
}
export interface PCIDevice {
slot: string
type: string
device: string
vendor: string
class: string
driver?: string
kernel_module?: string
irq?: string
memory_address?: string
link_speed?: string
capabilities?: string[]
gpu_memory?: string
gpu_driver_version?: string
gpu_cuda_version?: string
gpu_compute_capability?: string
gpu_power_draw?: string
gpu_temperature?: number
gpu_utilization?: number
gpu_memory_used?: string
gpu_memory_total?: string
gpu_clock_speed?: string
gpu_memory_clock?: string
}
export interface Fan {
name: string
original_name?: string
speed: number
unit: string
adapter?: string
}
export interface PowerSupply {
name: string
watts: number
status?: string
}
export interface UPS {
name: string
host?: string
is_remote?: boolean
connection_type?: string
status: string
model?: string
manufacturer?: string
serial?: string
device_type?: string
firmware?: string
driver?: string
battery_charge?: string
battery_charge_raw?: number
battery_voltage?: string
battery_date?: string
time_left?: string
time_left_seconds?: number
load_percent?: string
load_percent_raw?: number
input_voltage?: string
input_frequency?: string
output_voltage?: string
output_frequency?: string
real_power?: string
apparent_power?: string
[key: string]: any
}
export interface GPU {
slot: string
name: string
vendor: string
type: string
pci_class?: string
pci_driver?: string
pci_kernel_module?: string
driver_version?: string
memory_total?: string
memory_used?: string
memory_free?: string
temperature?: number
power_draw?: string
power_limit?: string
utilization_gpu?: number
utilization_memory?: number
clock_graphics?: string
clock_memory?: string
engine_render?: number
engine_blitter?: number
engine_video?: number
engine_video_enhance?: number
pcie_gen?: string
pcie_width?: string
fan_speed?: number
fan_unit?: string
processes?: Array<{
pid: string
name: string
memory: string
}>
has_monitoring_tool?: boolean
note?: string
}
export interface DiskHardwareInfo {
type?: string
driver?: string
interface?: string
model?: string
serial?: string
family?: string
firmware?: string
rotation_rate?: string
form_factor?: string
sata_version?: string
}
export interface NetworkHardwareInfo {
driver?: string
kernel_modules?: string
subsystem?: string
max_link_speed?: string
max_link_width?: string
current_link_speed?: string
current_link_width?: string
interface_name?: string
interface_speed?: string
mac_address?: string
}
export interface HardwareData {
cpu?: {
model?: string
cores_per_socket?: number
sockets?: number
total_threads?: number
l3_cache?: string
virtualization?: string
}
motherboard?: {
manufacturer?: string
model?: string
bios?: {
vendor?: string
version?: string
date?: string
}
}
memory_modules?: Array<{
slot: string
size?: string
type?: string
speed?: string
manufacturer?: string
}>
temperatures?: Temperature[]
power_meter?: PowerMeter
network_cards?: NetworkInterface[]
storage_devices?: StorageDevice[]
pci_devices?: PCIDevice[]
gpus?: GPU[]
fans?: Fan[]
power_supplies?: PowerSupply[]
ups?: UPS | UPS[]
}
export const fetcher = (url: string) => fetch(url).then((res) => res.json())
+101
View File
@@ -1,3 +1,104 @@
## 2025-09-04
### New version v1.1.7
### Added
- **ProxMenux Monitor**
Your new monitoring tool for Proxmox. Discover all the features that will help you manage and supervise your infrastructure efficiently.
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)
- **New Banner Removal Method**
A new function to disable the Proxmox subscription message with improved safety:
- Creates a full backup before modifying any files
- Shows a clear warning that breaking changes may occur with future GUI updates
- If the GUI fails to load, the user can revert changes via SSH from the post-install menu using the **"Uninstall Options → Restore Banner"** tool
Special thanks to **@eryonki** for providing the improved method.
---
### Improved
- **CORAL TPU Installer Updated for PVE 9**
The CORAL TPU driver installer now supports both **Proxmox VE 8 and VE 9**, ensuring compatibility with the latest kernels and udev rules.
- **Log2RAM Installation & Integration**
- Log2RAM installation is now idempotent and can be safely run multiple times.
- Automatically adjusts `journald` configuration to align with the size and behavior of Log2RAM.
- Ensures journaling is correctly tuned to avoid overflows or RAM exhaustion on low-memory systems.
- **Network Optimization Function (LXC + NFS)**
Improved to prevent “martian source” warnings in setups where **LXC containers share storage with VMs** over NFS within the same server.
- **APT Upgrade Progress**
When running full system upgrades via ProxMenux, a **real-time progress bar** is now displayed, giving the user clear visibility into the update process.
---
### Fixed
- Other small improvements and fixes to optimize runtime performance and eliminate minor bugs.
## 2025-01-10
### New version v1.1.6
![Shared Resources Menu](https://macrimi.github.io/ProxMenux/share/main-menu.png)
### Added
- **New Menu: Mount and Share Manager**
Introduced a comprehensive new menu for managing shared resources between Proxmox host and LXC containers:
**Host Configuration Options:**
- **Configure NFS Shared on Host** - Add, view, and remove NFS shared resources on the Proxmox server with automatic export management
- **Configure Samba Shared on Host** - Add, view, and remove Samba/CIFS shared resources on the Proxmox server with share configuration
- **Configure Local Shared on Host** - Create and manage local shared directories with proper permissions on the Proxmox host
**LXC Integration Options:**
- **Configure LXC Mount Points (Host ↔ Container)** - **Core feature** that enables mounting host directories into LXC containers with automatic permission handling. Includes the ability to **view existing mount points** for each container in a clear, organized way and **remove mount points** with proper verification that the process completed successfully. Especially optimized for **unprivileged containers** where UID/GID mapping is critical.
- **Configure NFS Client in LXC** - Set up NFS client inside privileged containers
- **Configure Samba Client in LXC** - Set up Samba client inside privileged containers
- **Configure NFS Server in LXC** - Install NFS server inside privileged containers
- **Configure Samba Server in LXC** - Install Samba server inside privileged containers
**Documentation & Support:**
- **Help & Info (commands)** - Comprehensive guides with step-by-step manual instructions for all sharing scenarios
The entire system is built around the **LXC Mount Points** functionality, which automatically detects filesystem types, handles permission mapping between host and container users, and provides seamless integration for both privileged and unprivileged containers.
---
### Improved
- **Log2RAM Auto-Detection Enhancement**
In the automatic post-install script, the Log2RAM installation function now prompts the user when automatic disk ssd/m2 detection fails.
This ensures Log2RAM can still be installed on systems where automatic disk detection doesn't work properly.
---
### Fixed
- **Proxmox Update Repository Verification**
Fixed an issue in the Proxmox update function where empty repository source files would cause errors during conflict verification. The function now properly handles empty `/etc/apt/sources.list.d/` files without throwing false warnings.
Thanks to **@JF_Car** for reporting this issue.
---
### Acknowledgments
Special thanks to **@JF_Car**, **@ghosthvj**, and **@jonatanc** for their testing, valuable feedback, and suggestions that helped refine the shared resources functionality and improve the overall user experience.
## 2025-08-20
### New version v1.1.5
+13 -1
View File
@@ -1,6 +1,6 @@
<div align="center">
<img src="https://github.com/MacRimi/ProxMenux/blob/main/images/main.png"
alt="ProxMenu Logo"
alt="ProxMenux Logo"
style="max-width: 100%; height: auto;" >
</div>
@@ -70,6 +70,12 @@ Then, follow the on-screen options to manage your Proxmox server efficiently.
## ⭐ Support the Project!
If you find **ProxMenux** useful, consider giving it a ⭐ on GitHub to help others discover it!
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=MacRimi/ProxMenux&type=Date)](https://www.star-history.com/#MacRimi/ProxMenux&Date)
<div style="display: flex; justify-content: center; align-items: center;">
<a href="https://ko-fi.com/G2G313ECAN" target="_blank" style="display: flex; align-items: center; text-decoration: none;">
<img src="https://raw.githubusercontent.com/MacRimi/HWEncoderX/main/images/kofi.png" alt="Support me on Ko-fi" style="width:140px; margin-right:40px;"/>
@@ -78,4 +84,10 @@ If you find **ProxMenux** useful, consider giving it a ⭐ on GitHub to help oth
Support the project on Ko-fi!
## Contributors
<a href="https://github.com/MacRimi/ProxMenux/graphs/contributors">
<img src="https://contrib.rocks/image?repo=MacRimi/ProxMenux" />
</a>
[contrib.rocks](https://contrib.rocks).
+143 -23
View File
@@ -1,7 +1,7 @@
#!/bin/bash
# ==========================================================
# ProxMenu - A menu-driven script for Proxmox VE management
# ProxMenux - A menu-driven script for Proxmox VE management
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
@@ -41,11 +41,16 @@ BASE_DIR="/usr/local/share/proxmenux"
CONFIG_FILE="$BASE_DIR/config.json"
CACHE_FILE="$BASE_DIR/cache.json"
UTILS_FILE="$BASE_DIR/utils.sh"
#EMERGENCY_FILE="$BASE_DIR/emergency_repair.sh"
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_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
@@ -101,17 +106,17 @@ check_existing_installation() {
fi
}
uninstall_proxmenu() {
uninstall_proxmenux() {
local install_type="$1"
local force_clean="$2"
if [ "$force_clean" != "force" ]; then
if ! whiptail --title "Uninstall ProxMenu" --yesno "Are you sure you want to uninstall ProxMenu?" 10 60; then
if ! whiptail --title "Uninstall ProxMenux" --yesno "Are you sure you want to uninstall ProxMenux?" 10 60; then
return 1
fi
fi
echo "Uninstalling ProxMenu..."
echo "Uninstalling ProxMenux..."
if [ -f "$VENV_PATH/bin/activate" ]; then
echo "Removing googletrans and virtual environment..."
@@ -151,7 +156,7 @@ uninstall_proxmenu() {
sed -i '/This system is optimised by: ProxMenux/d' /etc/motd
fi
echo "ProxMenu has been uninstalled."
echo "ProxMenux has been uninstalled."
return 0
}
@@ -168,7 +173,7 @@ handle_installation_change() {
if whiptail --title "Installation Type Change" \
--yesno "Switch from Translation to Normal Version?\n\nThis will remove translation components." 10 60; then
echo "Preparing for installation type change..."
uninstall_proxmenu "translation" "force" >/dev/null 2>&1
uninstall_proxmenux "translation" "force" >/dev/null 2>&1
return 0
else
return 1
@@ -193,7 +198,7 @@ update_config() {
local status="$2"
local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
local tracked_components=("dialog" "curl" "jq" "python3" "python3-venv" "python3-pip" "virtual_environment" "pip" "googletrans")
local tracked_components=("dialog" "curl" "jq" "python3" "python3-venv" "python3-pip" "virtual_environment" "pip" "googletrans" "proxmenux_monitor")
if [[ " ${tracked_components[@]} " =~ " ${component} " ]]; then
mkdir -p "$(dirname "$CONFIG_FILE")"
@@ -221,7 +226,7 @@ show_progress() {
local total="$2"
local message="$3"
echo -e "\n${BOLD}${BL}${TAB}Installing ProxMenu: Step $step of $total${CL}"
echo -e "\n${BOLD}${BL}${TAB}Installing ProxMenux: Step $step of $total${CL}"
echo
msg_info2 "$message"
}
@@ -274,7 +279,7 @@ show_installation_confirmation() {
case "$install_type" in
"1")
if whiptail --title "ProxMenux - Normal Version Installation" \
--yesno "ProxMenux Normal Version will install:\n\n• dialog (interactive menus) - Official Debian package\n• curl (file downloads) - Official Debian package\n• jq (JSON processing) - Official Debian package\n• ProxMenux core files (/usr/local/share/proxmenux)\n\nThis is a lightweight installation with minimal dependencies.\n\nProceed with installation?" 18 70; then
--yesno "ProxMenux Normal Version will install:\n\n• dialog (interactive menus) - Official Debian package\n• curl (file downloads) - Official Debian package\n• jq (JSON processing) - Official Debian package\n• ProxMenux core files (/usr/local/share/proxmenux)\n• ProxMenux Monitor (Web dashboard on port 8008)\n\nThis is a lightweight installation with minimal dependencies.\n\nProceed with installation?" 20 70; then
return 0
else
return 1
@@ -282,7 +287,7 @@ show_installation_confirmation() {
;;
"2")
if whiptail --title "ProxMenux - Translation Version Installation" \
--yesno "ProxMenux Translation Version will install:\n\n• dialog (interactive menus)\n• curl (file downloads)\n• jq (JSON processing)\n• python3 + python3-venv + python3-pip\n• Google Translate library (googletrans)\n• Virtual environment (/opt/googletrans-env)\n• Translation cache system\n• ProxMenux core files\n\nThis version requires more dependencies for translation support.\n\nProceed with installation?" 18 70; then
--yesno "ProxMenux Translation Version will install:\n\n• dialog (interactive menus)\n• curl (file downloads)\n• jq (JSON processing)\n• python3 + python3-venv + python3-pip\n• Google Translate library (googletrans)\n• Virtual environment (/opt/googletrans-env)\n• Translation cache system\n• ProxMenux core files\n• ProxMenux Monitor (Web dashboard on port 8008)\n\nThis version requires more dependencies for translation support.\n\nProceed with installation?" 20 70; then
return 0
else
return 1
@@ -291,9 +296,112 @@ show_installation_confirmation() {
esac
}
get_server_ip() {
local ip
# Try to get the primary IP address
ip=$(ip route get 1.1.1.1 2>/dev/null | grep -oP 'src \K\S+')
if [ -z "$ip" ]; then
# Fallback: get first non-loopback IP
ip=$(hostname -I | awk '{print $1}')
fi
if [ -z "$ip" ]; then
# Last resort: use localhost
ip="localhost"
fi
echo "$ip"
}
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."
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
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}')
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"
fi
# Make executable
chmod +x "$MONITOR_INSTALL_PATH"
# Show single success message at the end
msg_ok "ProxMenux Monitor installed and activated successfully."
return 0
}
create_monitor_service() {
msg_info "Creating ProxMenux Monitor service..."
cat > "$MONITOR_SERVICE_FILE" << EOF
[Unit]
Description=ProxMenux Monitor - Web Dashboard
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=$BASE_DIR
ExecStart=$MONITOR_INSTALL_PATH
Restart=on-failure
RestartSec=10
Environment="PORT=$MONITOR_PORT"
[Install]
WantedBy=multi-user.target
EOF
# 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
# 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"
return 1
fi
}
####################################################
install_normal_version() {
local total_steps=3
local total_steps=4 # Increased from 3 to 4 for monitor installation
local current_step=1
show_progress $current_step $total_steps "Installing basic dependencies"
@@ -350,7 +458,6 @@ install_normal_version() {
FILES=(
"$UTILS_FILE $REPO_URL/scripts/utils.sh"
# "$EMERGENCY_FILE $REPO_URL/scripts/emergency_repair.sh"
"$INSTALL_DIR/$MENU_SCRIPT $REPO_URL/$MENU_SCRIPT"
"$LOCAL_VERSION_FILE $REPO_URL/version.txt"
)
@@ -368,12 +475,18 @@ install_normal_version() {
done
chmod +x "$INSTALL_DIR/$MENU_SCRIPT"
# chmod +x "$EMERGENCY_FILE"
((current_step++))
show_progress $current_step $total_steps "Installing ProxMenux Monitor"
if install_proxmenux_monitor; then
create_monitor_service
fi
}
####################################################
install_translation_version() {
local total_steps=4
local total_steps=5 # Increased from 4 to 5 for monitor installation
local current_step=1
show_progress $current_step $total_steps "Language selection"
@@ -470,7 +583,6 @@ install_translation_version() {
FILES=(
"$CACHE_FILE $REPO_URL/json/cache.json"
"$UTILS_FILE $REPO_URL/scripts/utils.sh"
# "$EMERGENCY_FILE $REPO_URL/scripts/emergency_repair.sh"
"$INSTALL_DIR/$MENU_SCRIPT $REPO_URL/$MENU_SCRIPT"
"$LOCAL_VERSION_FILE $REPO_URL/version.txt"
)
@@ -491,7 +603,13 @@ install_translation_version() {
done
chmod +x "$INSTALL_DIR/$MENU_SCRIPT"
#chmod +x "$EMERGENCY_FILE"
((current_step++))
show_progress $current_step $total_steps "Installing ProxMenux Monitor"
if install_proxmenux_monitor; then
create_monitor_service
fi
}
####################################################
@@ -518,9 +636,6 @@ show_installation_options() {
esac
fi
if [[ "$pve_version" -ge 9 ]]; then
INSTALL_TYPE=$(whiptail --backtitle "ProxMenux" --title "$menu_title" --menu "\n$menu_text" 14 70 2 \
"1" "Normal Version (English only)" 3>&1 1>&2 2>&3)
@@ -541,8 +656,6 @@ show_installation_options() {
exit 1
fi
fi
if [ -z "$INSTALL_TYPE" ]; then
show_proxmenux_logo
@@ -587,6 +700,13 @@ install_proxmenu() {
esac
msg_title "$(translate "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
fi
echo -ne "${GN}"
type_text "$(translate "To run ProxMenux, simply execute this command in the console or terminal:")"
echo -e "${YWB} menu${CL}"
@@ -599,4 +719,4 @@ if [ "$(id -u)" -ne 0 ]; then
fi
cleanup_corrupted_files
install_proxmenu
install_proxmenu
+104 -2
View File
@@ -202,7 +202,7 @@
"it": "Giapponese",
"pt": "Japonês"
},
"Thank you for using ProxMenu. Goodbye!": {
"Thank you for using ProxMenux. Goodbye!": {
"es": "Gracias por usar ProxMenu. ¡Adiós!",
"fr": "Merci d'avoir utilisé ProxMenu. Au revoir!",
"de": "Danke für die Nutzung von ProxMenu. Auf Wiedersehen!",
@@ -900,7 +900,11 @@
"it": "Trova il tuo dispositivo usando https://finds.synology.com"
},
"Help and Info Commands": {
"es": "Comandos de ayuda e información"
"es": "Comandos de ayuda e información",
"fr": "Aide et Informations (commandes)",
"de": "Hilfe & Informationen (Befehle)",
"it": "Aiuto e Informazioni (comandi)",
"pt": "Ajuda e Informações (comandos)"
},
"Create VM from template or script": {
"es": "Crear VM a partir de plantilla o script",
@@ -2566,5 +2570,103 @@
"de": "Notfallwiederherstellung:",
"it": "Ripristino di emergenza:",
"pt": "Recuperação de emergência:"
},
"Mount and Share Manager": {
"es": "Montajes y Recursos Compartidos",
"fr": "Gestionnaire de Montage et Partage",
"de": "Mount- und Share-Manager",
"it": "Gestore di Mount e Condivisioni",
"pt": "Gerenciador de Montagem e Compartilhamento"
},
"HOST": {
"es": "HOST",
"fr": "HÔTE",
"de": "HOST",
"it": "HOST",
"pt": "HOST"
},
"Configure NFS shared on Host": {
"es": "Configurar recursos NFS compartidos en el Host",
"fr": "Configurer les partages NFS sur l'hôte",
"de": "NFS-Freigaben auf Host konfigurieren",
"it": "Configurare condivisioni NFS su Host",
"pt": "Configurar compartilhamentos NFS no Host"
},
"Configure Samba shared on Host": {
"es": "Configurar recursos Samba compartidos en el Host",
"fr": "Configurer les partages Samba sur l'hôte",
"de": "Samba-Freigaben auf Host konfigurieren",
"it": "Configurare condivisioni Samba su Host",
"pt": "Configurar compartilhamentos Samba no Host"
},
"Configure Local Shared on Host": {
"es": "Configurar directorios locales compartidos en el Host",
"fr": "Configurer les répertoires locaux partagés sur l'hôte",
"de": "Lokale geteilte Verzeichnisse auf Host konfigurieren",
"it": "Configurare directory locali condivise su Host",
"pt": "Configurar diretórios locais compartilhados no Host"
},
"LXC": {
"es": "LXC",
"fr": "LXC",
"de": "LXC",
"it": "LXC",
"pt": "LXC"
},
"Configure LXC Mount Points (Host ↔ Container)": {
"es": "Configurar puntos de montaje LXC (Host ↔ LXC)",
"fr": "Configurer les points de montage LXC (Hôte ↔ LXC)",
"de": "LXC-Mount-Punkte konfigurieren (Host ↔ LXC)",
"it": "Configurare punti di mount LXC (Host ↔ LXC)",
"pt": "Configurar pontos de montagem LXC (Host ↔ LXC)"
},
"Configure NFS Client in LXC (only privileged)": {
"es": "Configurar cliente NFS en LXC (solo privilegiados)",
"fr": "Configurer le client NFS dans LXC (privilégiés uniquement)",
"de": "NFS-Client in LXC konfigurieren (nur privilegiert)",
"it": "Configurare client NFS in LXC (solo privilegiati)",
"pt": "Configurar cliente NFS em LXC (apenas privilegiados)"
},
"Configure Samba Client in LXC (only privileged)": {
"es": "Configurar cliente Samba en LXC (solo privilegiados)",
"fr": "Configurer le client Samba dans LXC (privilégiés uniquement)",
"de": "Samba-Client in LXC konfigurieren (nur privilegiert)",
"it": "Configurare client Samba in LXC (solo privilegiati)",
"pt": "Configurar cliente Samba em LXC (apenas privilegiados)"
},
"Configure NFS Server in LXC (only privileged)": {
"es": "Configurar servidor NFS en LXC (solo privilegiados)",
"fr": "Configurer le serveur NFS dans LXC (privilégiés uniquement)",
"de": "NFS-Server in LXC konfigurieren (nur privilegiert)",
"it": "Configurare server NFS in LXC (solo privilegiati)",
"pt": "Configurar servidor NFS em LXC (apenas privilegiados)"
},
"configure Samba Server in LXC (only privileged)": {
"es": "Configurar servidor Samba en LXC (solo privilegiados)",
"fr": "Configurer le serveur Samba dans LXC (privilégiés uniquement)",
"de": "Samba-Server in LXC konfigurieren (nur privilegiert)",
"it": "Configurare server Samba in LXC (solo privilegiati)",
"pt": "Configurar servidor Samba em LXC (apenas privilegiados)"
},
"Help & Info (commands)": {
"es": "Comandos de ayuda e información",
"fr": "Aide et Informations (commandes)",
"de": "Hilfe & Informationen (Befehle)",
"it": "Aiuto e Informazioni (comandi)",
"pt": "Ajuda e Informações (comandos)"
},
"English": {
"es": "Inglés",
"fr": "Anglais",
"de": "Englisch",
"it": "Inglese",
"pt": "Inglês"
},
"Language Change": {
"es": "Cambio de Idioma",
"fr": "Changement de Langue",
"de": "Sprachänderung",
"it": "Cambio Lingua",
"pt": "Mudança de Idioma"
}
}
+648 -125
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -1,7 +1,7 @@
#!/bin/bash
# ==========================================================
# ProxMenu - A menu-driven script for Proxmox VE management
# ProxMenux - A menu-driven script for Proxmox VE management
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
@@ -13,13 +13,13 @@
# This script serves as the main entry point for ProxMenux,
# a menu-driven tool designed for Proxmox VE management.
#
# - Displays the ProxMenu logo on startup.
# - Displays the ProxMenux logo on startup.
# - Loads necessary configurations and language settings.
# - Checks for available updates and installs them if confirmed.
# - Downloads and executes the latest main menu script.
#
# Key Features:
# - Ensures ProxMenu is always up-to-date by fetching the latest version.
# - Ensures ProxMenux is always up-to-date by fetching the latest version.
# - Uses whiptail for interactive menus and language selection.
# - Loads utility functions and translation support.
# - Maintains a cache system to improve performance.
@@ -67,7 +67,7 @@ check_updates() {
if whiptail --title "$(translate "Update Available")" \
--yesno "$(translate "New version available") ($REMOTE_VERSION)\n\n$(translate "Do you want to update now?")" \
10 60 --defaultno; then
msg_warn "$(translate "Starting ProxMenu update...")"
msg_warn "$(translate "Starting ProxMenux update...")"
if wget -qO "$INSTALL_SCRIPT" "$REPO_URL/install_proxmenux.sh"; then
chmod +x "$INSTALL_SCRIPT"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,7 +1,7 @@
#!/bin/bash
# ==========================================================
# ProxMenu - A menu-driven script for Proxmox VE management
# ProxMenux - A menu-driven script for Proxmox VE management
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
+1 -1
View File
@@ -1,7 +1,7 @@
#!/bin/bash
# ==========================================================
# ProxMenu - A menu-driven script for Proxmox VE management
# ProxMenux - A menu-driven script for Proxmox VE management
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
+1 -1
View File
@@ -1,7 +1,7 @@
#!/bin/bash
# ==========================================================
# ProxMenu - A menu-driven script for Proxmox VE management
# ProxMenux - A menu-driven script for Proxmox VE management
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
+1 -1
View File
@@ -1,6 +1,6 @@
#!/bin/bash
# ==========================================================
# ProxMenu - Network Management and Repair Tool
# ProxMenux - Network Management and Repair Tool
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
+3
View File
@@ -92,6 +92,9 @@ cleanup_duplicate_repos_pve9() {
local cleaned_count=0
declare -A seen_repos
if [ ! -s "$sources_file" ]; then
return 0
fi
while IFS= read -r line || [[ -n "$line" ]]; do
if [[ "$line" =~ ^[[:space:]]*# ]] || [[ -z "$line" ]]; then
+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
+257
View File
@@ -0,0 +1,257 @@
#!/bin/bash
# ==========================================================
# Remove Subscription Banner - Proxmox VE 9.x (Clean Version)
# ==========================================================
set -euo pipefail
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
ensure_tools_json() {
[ -f "$TOOLS_JSON" ] || echo "{}" > "$TOOLS_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"
}
JS_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js"
MIN_JS_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.min.js"
GZ_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js.gz"
MOBILE_TPL="/usr/share/pve-yew-mobile-gui/index.html.tpl"
APT_HOOK="/etc/apt/apt.conf.d/no-nag-script"
PATCH_BIN="/usr/local/bin/pve-remove-nag.sh"
MARK_JS="PROXMENUX_NAG_REMOVED_v2"
MARK_MOBILE="<!-- PROXMENUX: MOBILE NAG PATCH v2 -->"
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_backup() {
local file="$1"
local backup_dir="$BASE_DIR/backups"
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
cp -a "$file" "$backup_file"
ls -t "$backup_dir"/"$(basename "$file")".backup.* 2>/dev/null | tail -n +6 | xargs -r rm -f 2>/dev/null || true
echo "$backup_file"
fi
}
# ----------------------------------------------------
create_patch_script() {
cat > "$PATCH_BIN" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
JS_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js"
MIN_JS_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.min.js"
GZ_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js.gz"
MOBILE_TPL="/usr/share/pve-yew-mobile-gui/index.html.tpl"
MARK_JS="PROXMENUX_NAG_REMOVED_v2"
MARK_MOBILE="<!-- PROXMENUX: MOBILE NAG PATCH v2 -->"
BASE_DIR="/usr/local/share/proxmenux"
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_web() {
[ -f "$JS_FILE" ] || return 0
grep -q "$MARK_JS" "$JS_FILE" && return 0
local backup_dir="$BASE_DIR/backups"
mkdir -p "$backup_dir"
local backup="$backup_dir/$(basename "$JS_FILE").backup.$(date +%Y%m%d_%H%M%S)"
cp -a "$JS_FILE" "$backup"
trap "cp -a '$backup' '$JS_FILE' 2>/dev/null || true" ERR
sed -i '1s|^|/* '"$MARK_JS"' */\n|' "$JS_FILE"
local patterns_found=0
if grep -q "res\.data\.status\.toLowerCase() !== 'active'" "$JS_FILE"; then
sed -i "s/res\.data\.status\.toLowerCase() !== 'active'/false/g" "$JS_FILE"
patterns_found=$((patterns_found + 1))
fi
if grep -q "subscriptionActive: ''" "$JS_FILE"; then
sed -i "s/subscriptionActive: ''/subscriptionActive: true/g" "$JS_FILE"
patterns_found=$((patterns_found + 1))
fi
if grep -q "title: gettext('No valid subscription')" "$JS_FILE"; then
sed -i "s/title: gettext('No valid subscription')/title: gettext('Community Edition')/g" "$JS_FILE"
patterns_found=$((patterns_found + 1))
fi
if grep -q "icon: Ext\.Msg\.WARNING" "$JS_FILE"; then
sed -i "s/icon: Ext\.Msg\.WARNING/icon: Ext.Msg.INFO/g" "$JS_FILE"
patterns_found=$((patterns_found + 1))
fi
if grep -q "subscription = !(" "$JS_FILE"; then
sed -i "s/subscription = !(/subscription = false \&\& (/g" "$JS_FILE"
patterns_found=$((patterns_found + 1))
fi
# Si nada coincidió (cambio upstream), restaura y sal limpio
if [ "${patterns_found:-0}" -eq 0 ]; then
cp -a "$backup" "$JS_FILE"
return 0
fi
# Verificación final
if ! verify_js_integrity "$JS_FILE"; then
cp -a "$backup" "$JS_FILE"
return 1
fi
# Limpiar artefactos/cachés
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
}
patch_mobile() {
[ -f "$MOBILE_TPL" ] || return 0
grep -q "$MARK_MOBILE" "$MOBILE_TPL" && return 0
local backup_dir="$BASE_DIR/backups"
mkdir -p "$backup_dir"
cp -a "$MOBILE_TPL" "$backup_dir/$(basename "$MOBILE_TPL").backup.$(date +%Y%m%d_%H%M%S)"
cat >> "$MOBILE_TPL" <<EOM
$MARK_MOBILE
<script>
(function() {
'use strict';
function removeSubscriptionElements() {
try {
const dialogs = document.querySelectorAll('dialog.pwt-outer-dialog');
dialogs.forEach(d => {
const text = (d.textContent || '').toLowerCase();
if (text.includes('subscription') || text.includes('no valid')) { d.remove(); }
});
const cards = document.querySelectorAll('.pwt-card.pwt-p-2.pwt-d-flex.pwt-interactive.pwt-justify-content-center');
cards.forEach(c => {
const text = (c.textContent || '').toLowerCase();
const hasButton = c.querySelector('button');
if (!hasButton && (text.includes('subscription') || text.includes('no valid'))) { c.remove(); }
});
const alerts = document.querySelectorAll('[class*="alert"], [class*="warning"], [class*="notice"]');
alerts.forEach(a => {
const text = (a.textContent || '').toLowerCase();
if (text.includes('subscription') || text.includes('no valid')) { a.remove(); }
});
} catch (e) { console.warn('Error removing subscription elements:', e); }
}
if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', removeSubscriptionElements); }
else { removeSubscriptionElements(); }
const observer = new MutationObserver(removeSubscriptionElements);
if (document.body) {
observer.observe(document.body, { childList: true, subtree: true });
const interval = setInterval(removeSubscriptionElements, 500);
setTimeout(() => { try { observer.disconnect(); clearInterval(interval); } catch(e){} }, 30000);
}
})();
</script>
EOM
}
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
}
find /var/cache/pve-manager/ -type f -delete 2>/dev/null || true
find /var/lib/pve-manager/ -type f -delete 2>/dev/null || true
}
main() {
patch_web || return 1
patch_mobile
reload_services
}
main
EOF
chmod 755 "$PATCH_BIN"
}
# ----------------------------------------------------
create_apt_hook() {
cat > "$APT_HOOK" <<'EOF'
/* ProxMenux: reapply nag patch after upgrades */
DPkg::Post-Invoke { "/usr/local/bin/pve-remove-nag.sh || true"; };
EOF
chmod 644 "$APT_HOOK"
apt-config dump >/dev/null 2>&1 || { msg_warn "APT hook syntax issue"; rm -f "$APT_HOOK"; }
}
remove_subscription_banner_pve9() {
local pve_version
pve_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+\.[0-9]+' | head -1 || true)
local pve_major="${pve_version%%.*}"
msg_info "$(translate "Detected Proxmox VE ${pve_version:-9.x} removing subscription banner")"
create_patch_script
create_apt_hook
if ! "$PATCH_BIN"; then
msg_error "$(translate "Error applying patches")"
return 1
fi
register_tool "subscription_banner" true
msg_ok "$(translate "Subscription banner removed successfully.")"
msg_ok "$(translate "Refresh your browser to see changes.")"
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
remove_subscription_banner_pve9
fi
+901
View File
@@ -0,0 +1,901 @@
#!/usr/bin/env bash
# ==========================================================
# ProxMenux - Global Share Functions (reusable)
# File: scripts/global/share_common.func
# ==========================================================
if [[ -n "${__PROXMENUX_SHARE_COMMON__}" ]]; then
return 0
fi
__PROXMENUX_SHARE_COMMON__=1
: "${PROXMENUX_DEFAULT_SHARE_GROUP:=sharedfiles}"
: "${PROXMENUX_SHARE_MAP_DB:=/usr/local/share/proxmenux/share-map.db}"
mkdir -p "$(dirname "$PROXMENUX_SHARE_MAP_DB")" 2>/dev/null || true
touch "$PROXMENUX_SHARE_MAP_DB" 2>/dev/null || true
pmx_share_map_get() {
local key="$1"
awk -F'=' -v k="$key" '$1==k {print $2}' "$PROXMENUX_SHARE_MAP_DB" 2>/dev/null | tail -n1
}
pmx_share_map_set() {
local key="$1" val="$2"
sed -i "\|^${key}=|d" "$PROXMENUX_SHARE_MAP_DB" 2>/dev/null || true
echo "${key}=${val}" >> "$PROXMENUX_SHARE_MAP_DB"
}
pmx_choose_or_create_group() {
local default_group="${1:-$PROXMENUX_DEFAULT_SHARE_GROUP}"
local choice group_name groups menu_args gid_min
gid_min="$(awk '/^\s*GID_MIN\s+[0-9]+/ {print $2}' /etc/login.defs 2>/dev/null | tail -n1)"
[[ -z "$gid_min" ]] && gid_min=1000
choice=$(whiptail --title "$(translate "Shared Group")" \
--menu "$(translate "Choose a group policy for this shared directory:")" 18 78 6 \
"1" "$(translate "Use default group:") $default_group $(translate "(recommended)")" \
"2" "$(translate "Create a new group for isolation")" \
"3" "$(translate "Select an existing group")" \
3>&1 1>&2 2>&3) || { echo ""; return 1; }
case "$choice" in
1)
pmx_ensure_host_group "$default_group" >/dev/null || { echo ""; return 1; }
echo "$default_group"
;;
2)
group_name=$(whiptail --inputbox "$(translate "Enter new group name:")" 10 70 "sharedfiles-project" \
--title "$(translate "New Group")" 3>&1 1>&2 2>&3) || { echo ""; return 1; }
if [[ -z "$group_name" ]]; then
msg_error "$(translate "Group name cannot be empty.")"
echo ""; return 1
fi
if ! [[ "$group_name" =~ ^[a-zA-Z_][a-zA-Z0-9_-]*$ ]]; then
msg_error "$(translate "Invalid group name. Use letters, digits, underscore or hyphen, and start with a letter or underscore.")"
echo ""; return 1
fi
pmx_ensure_host_group "$group_name" >/dev/null || { echo ""; return 1; }
echo "$group_name"
;;
3)
groups=$(getent group | awk -F: -v MIN="$gid_min" '
$3 >= MIN && $1 != "nogroup" && $1 !~ /^pve/ {print $0}
' | sort -t: -k1,1)
if [[ -z "$groups" ]]; then
whiptail --title "$(translate "Groups")" --msgbox "$(translate "No user groups found.")" 8 60
echo ""; return 1
fi
menu_args=()
while IFS=: read -r gname _ gid members; do
menu_args+=("$gname" "GID=$gid")
done <<< "$groups"
group_name=$(whiptail --title "$(translate "Existing Groups")" \
--menu "$(translate "Select an existing group:")" 20 70 12 \
"${menu_args[@]}" 3>&1 1>&2 2>&3) || { echo ""; return 1; }
pmx_ensure_host_group "$group_name" >/dev/null || { echo ""; return 1; }
echo "$group_name"
;;
*)
echo ""; return 1
;;
esac
}
pmx_ensure_host_group() {
local group_name="$1"
local suggested_gid="${2:-}"
local base_gid=101000
local new_gid gid
if getent group "$group_name" >/dev/null 2>&1; then
gid="$(getent group "$group_name" | cut -d: -f3)"
echo "$gid"
return 0
fi
if [[ -n "$suggested_gid" ]]; then
if getent group "$suggested_gid" >/dev/null 2>&1; then
msg_error "$(translate "GID already in use:") $suggested_gid"
echo ""
return 1
fi
if ! groupadd -g "$suggested_gid" "$group_name" >/dev/null 2>&1; then
msg_error "$(translate "Failed to create group:") $group_name"
echo ""
return 1
fi
msg_ok "$(translate "Group created:") $group_name"
else
new_gid="$base_gid"
while getent group "$new_gid" >/dev/null 2>&1; do
new_gid=$((new_gid+1))
done
if ! groupadd -g "$new_gid" "$group_name" >/dev/null 2>&1; then
msg_error "$(translate "Failed to create group:") $group_name"
echo ""
return 1
fi
msg_ok "$(translate "Group created:") $group_name"
fi
gid="$(getent group "$group_name" | cut -d: -f3)"
if [[ -z "$gid" ]]; then
msg_error "$(translate "Failed to resolve group GID for") $group_name"
echo ""
return 1
fi
echo "$gid"
return 0
}
pmx_prepare_host_shared_dir() {
local dir="$1" group_name="$2"
[[ -z "$dir" || -z "$group_name" ]] && { msg_error "$(translate "Internal error: missing arguments in pmx_prepare_host_shared_dir")"; return 1; }
if [[ ! -d "$dir" ]]; then
if mkdir -p "$dir" 2>/dev/null; then
msg_ok "$(translate "Created directory on host:") $dir"
else
msg_error "$(translate "Failed to create directory on host:") $dir"
return 1
fi
fi
chown -R root:"$group_name" "$dir" 2>/dev/null || true
chmod -R 2775 "$dir" 2>/dev/null || true
if command -v setfacl >/dev/null 2>&1; then
setfacl -R -m d:g:"$group_name":rwx -m d:o::rx -m g:"$group_name":rwx "$dir" 2>/dev/null || true
msg_ok "$(translate "Default ACLs applied for group inheritance.")"
fi
return 0
}
pmx_select_host_mount_point() {
local title="${1:-$(translate "Select Mount Point")}"
local default_path="${2:-/mnt/shared}"
local context="${3:-local}"
local choice folder_name result existing_dirs mount_point
while true; do
choice=$(whiptail --title "$title" --menu "$(translate "Where do you want the host folder?")" 16 76 3 \
"1" "$(translate "Create new folder in /mnt")" \
"2" "$(translate "Enter custom pathr")" 3>&1 1>&2 2>&3) || { echo ""; return 1; }
case "$choice" in
1)
folder_name=$(whiptail --inputbox "$(translate "Enter folder name for /mnt:")" 10 70 "$(basename "$default_path")" --title "$(translate "Folder Name")" 3>&1 1>&2 2>&3) || { echo ""; return 1; }
[[ -z "$folder_name" ]] && continue
mount_point="/mnt/$folder_name"
echo "$mount_point"; return 0
;;
2)
result=$(whiptail --inputbox "$(translate "Enter full path:")" 10 80 "$default_path" --title "$(translate "Custom Path")" 3>&1 1>&2 2>&3) || { echo ""; return 1; }
[[ -z "$result" ]] && continue
echo "$result"; return 0
;;
esac
done
}
select_host_directory_() {
local method choice result
method=$(whiptail --title "$(translate "Select Host Directory")" --menu "$(translate "How do you want to select the HOST folder to mount?")" 15 70 4 \
"mnt" "$(translate "Select from /mnt directories")" \
"manual" "$(translate "Enter path manually")" 3>&1 1>&2 2>&3) || return 1
case "$method" in
mnt|srv|media)
local base_path="/$method"
local host_dirs=("$base_path"/*)
local options=()
for dir in "${host_dirs[@]}"; do
if [[ -d "$dir" ]]; then
options+=("$dir" "$(basename "$dir")")
fi
done
if [[ ${#options[@]} -eq 0 ]]; then
msg_error "$(translate "No directories found in") $base_path"
return 1
fi
result=$(whiptail --title "$(translate "Select Host Folder")" \
--menu "$(translate "Select the folder to mount:")" 20 80 10 "${options[@]}" 3>&1 1>&2 2>&3)
;;
manual)
result=$(whiptail --title "$(translate "Enter Path")" \
--inputbox "$(translate "Enter the full path to the host folder:")" 10 70 "/mnt/" 3>&1 1>&2 2>&3)
;;
esac
if [[ -z "$result" ]]; then
return 1
fi
if [[ ! -d "$result" ]]; then
msg_error "$(translate "The selected path is not a valid directory:") $result"
return 1
fi
echo "$result"
}
select_host_directory__() {
local method result
method=$(whiptail --title "$(translate "Select Host Directory")" \
--menu "$(translate "How do you want to select the HOST folder to mount?")" 15 70 4 \
"mnt" "$(translate "Select from /mnt directories")" \
"manual" "$(translate "Enter path manually")" \
3>&1 1>&2 2>&3) || return 1
case "$method" in
mnt|srv|media)
local base_path="/$method"
local host_dirs=("$base_path"/*)
local options=()
for dir in "${host_dirs[@]}"; do
[[ -d "$dir" ]] && options+=("$dir" "$(basename "$dir")")
done
if [[ ${#options[@]} -eq 0 ]]; then
msg_error "$(translate "No directories found in") $base_path"
return 1
fi
result=$(whiptail --title "$(translate "Select Host Folder")" \
--menu "$(translate "Select the folder to mount:")" 20 80 10 \
"${options[@]}" 3>&1 1>&2 2>&3) || return 1
;;
manual)
result=$(whiptail --title "$(translate "Enter Path")" \
--inputbox "$(translate "Enter the full path to the host folder:")" \
10 70 "/mnt/" 3>&1 1>&2 2>&3) || return 1
;;
*)
return 1
;;
esac
[[ -z "$result" ]] && return 1
[[ ! -d "$result" ]] && {
msg_error "$(translate "The selected path is not a valid directory:") $result"
return 1
}
echo "$result"
}
select_host_directory() {
local method result
method=$(whiptail --title "$(translate "Select Host Directory")" \
--menu "$(translate "How do you want to select the HOST folder to mount?")" 15 70 4 \
"mnt" "$(translate "Select from /mnt directories")" \
"manual" "$(translate "Enter path manually")" \
3>&1 1>&2 2>&3) || return 1
case "$method" in
mnt|srv|media)
local base_path="/$method"
local host_dirs=("$base_path"/*)
local options=()
for dir in "${host_dirs[@]}"; do
[[ -d "$dir" ]] && options+=("$dir" "$(basename "$dir")")
done
if [[ ${#options[@]} -eq 0 ]]; then
msg_error "$(translate "No directories found in") $base_path"
return 1
fi
result=$(whiptail --title "$(translate "Select Host Folder")" \
--menu "$(translate "Select the folder to mount:")" 20 80 10 \
"${options[@]}" 3>&1 1>&2 2>&3) || return 1
;;
manual)
result=$(whiptail --title "$(translate "Enter Path")" \
--inputbox "$(translate "Enter the full path to the host folder:")" \
10 70 "/mnt/" 3>&1 1>&2 2>&3) || return 1
;;
*)
return 1
;;
esac
[[ -z "$result" ]] && return 1
[[ ! -d "$result" ]] && {
msg_error "$(translate "The selected path is not a valid directory:") $result"
return 1
}
echo "$result"
}
select_lxc_container() {
local ct_list ctid ct_status
ct_list=$(pct list | awk 'NR>1 {print $1, $2, $3}')
if [[ -z "$ct_list" ]]; then
dialog --title "$(translate "Error")" \
--msgbox "$(translate "No LXC containers available")" 8 50
return 1
fi
local options=()
while read -r id name status; do
if [[ -n "$id" ]]; then
options+=("$id" "$name ($status)")
fi
done <<< "$ct_list"
ctid=$(dialog --title "$(translate "Select LXC Container")" \
--menu "\n$(translate "Select container:")" 25 80 15 \
"${options[@]}" 3>&1 1>&2 2>&3)
if [[ -z "$ctid" ]]; then
return 1
fi
echo "$ctid"
return 0
}
select_container_mount_point_() {
local ctid="$1"
local host_dir="$2"
local choice mount_point existing_dirs options
while true; do
choice=$(whiptail --title "$(translate "Configure Mount Point inside LXC")" \
--menu "$(translate "Where to mount inside container?")" 18 70 5 \
"1" "$(translate "Create new directory in /mnt")" \
"2" "$(translate "Enter path manually")" \
"3" "$(translate "Cancel")" 3>&1 1>&2 2>&3) || return 1
case "$choice" in
1)
mount_point=$(whiptail --inputbox "$(translate "Enter folder name for /mnt:")" 10 60 "shared" 3>&1 1>&2 2>&3) || continue
[[ -z "$mount_point" ]] && continue
mount_point="/mnt/$mount_point"
pct exec "$ctid" -- mkdir -p "$mount_point" 2>/dev/null
;;
2)
mount_point=$(whiptail --inputbox "$(translate "Enter full path:")" 10 70 "/mnt/shared" 3>&1 1>&2 2>&3) || continue
[[ -z "$mount_point" ]] && continue
mount_point="/mnt/$mount_point"
pct exec "$ctid" -- mkdir -p "$mount_point" 2>/dev/null
;;
3)
return 1
;;
esac
if pct exec "$ctid" -- test -d "$mount_point" 2>/dev/null; then
echo "$mount_point"
return 0
else
whiptail --msgbox "$(translate "Could not create or access directory:") $mount_point" 8 70
continue
fi
done
}
select_container_mount_point() {
local ctid="$1"
local host_dir="$2"
local choice mount_point base_name
base_name=$(basename "$host_dir")
while true; do
choice=$(whiptail --title "$(translate "Configure Mount Point inside LXC")" \
--menu "$(translate "Where to mount inside container?")" 18 70 5 \
"1" "$(translate "Create new directory in /mnt")" \
"2" "$(translate "Enter path manually")" \
"3" "$(translate "Cancel")" 3>&1 1>&2 2>&3) || return 1
case "$choice" in
1)
mount_point=$(whiptail --inputbox "$(translate "Enter folder name for /mnt:")" \
10 60 "$base_name" 3>&1 1>&2 2>&3) || continue
[[ -z "$mount_point" ]] && continue
mount_point="/mnt/$mount_point"
pct exec "$ctid" -- mkdir -p "$mount_point" 2>/dev/null
;;
2)
mount_point=$(whiptail --inputbox "$(translate "Enter full path:")" \
10 70 "/mnt/$base_name" 3>&1 1>&2 2>&3) || continue
[[ -z "$mount_point" ]] && continue
pct exec "$ctid" -- mkdir -p "$mount_point" 2>/dev/null
;;
3)
return 1
;;
esac
if pct exec "$ctid" -- test -d "$mount_point" 2>/dev/null; then
echo "$mount_point"
return 0
else
whiptail --msgbox "$(translate "Could not create or access directory:") $mount_point" 8 70
continue
fi
done
}
# ==========================================================
# CLIENT MOUNT FUNCTIONS (NFS/SAMBA COMMON)
# ==========================================================
# Check if container is privileged (required for client mounts)
select_privileged_lxc() {
# === Select CT ===
local ct_list ctid ct_status conf unpriv
ct_list=$(pct list | awk 'NR>1 {print $1, $3}')
if [[ -z "$ct_list" ]]; then
dialog --backtitle "ProxMenux" --title "$(translate "Error")" \
--msgbox "$(translate "No CTs available in the system.")" 8 50
return 1
fi
ctid=$(dialog --backtitle "ProxMenux" --title "$(translate "Select CT")" \
--menu "$(translate "Select the CT to manage NFS/Samba client:")" 20 70 12 \
$ct_list 3>&1 1>&2 2>&3)
if [[ -z "$ctid" ]]; then
dialog --backtitle "ProxMenux" --title "$(translate "Error")" \
--msgbox "$(translate "No CT was selected.")" 8 50
return 1
fi
# === Start CT if not running ===
ct_status=$(pct status "$ctid" | awk '{print $2}')
if [[ "$ct_status" != "running" ]]; then
show_proxmenux_logo
echo -e
msg_info "$(translate "Starting CT") $ctid..."
pct start "$ctid"
sleep 2
if [[ "$(pct status "$ctid" | awk '{print $2}')" != "running" ]]; then
msg_error "$(translate "Failed to start the CT.")"
echo -e ""
msg_success "$(translate 'Press Enter to continue...')"
read -r
return 1
fi
msg_ok "$(translate "CT started successfully.")"
fi
# === Check privileged/unprivileged ===
conf="/etc/pve/lxc/${ctid}.conf"
unpriv=$(awk '/^unprivileged:/ {print $2}' "$conf" 2>/dev/null)
if [[ "$unpriv" == "1" ]]; then
dialog --backtitle "ProxMenux" --title "$(translate "Privileged Container Required")" \
--msgbox "\n$(translate "Network share mounting (NFS/Samba) requires a PRIVILEGED container.")\n\n$(translate "Selected container") $ctid $(translate "is UNPRIVILEGED.")\n\n$(translate "For unprivileged containers, use instead:")\n • $(translate "Configure LXC mount points")\n • $(translate "Mount shares on HOST first")\n • $(translate "Then bind-mount to container")" 15 75
exit 1
fi
# Export CTID if all good
echo "$ctid"
CTID="$ctid"
return 0
}
# Common mount point selection for containers
pmx_select_container_mount_point() {
local ctid="$1"
local share_name="${2:-shared}"
while true; do
local choice=$(whiptail --title "$(translate "Select Mount Point")" --menu "$(translate "Where do you want to mount inside container?")" 15 70 3 \
"existing" "$(translate "Select from existing folders in /mnt")" \
"new" "$(translate "Create new folder in /mnt")" \
"custom" "$(translate "Enter custom path")" 3>&1 1>&2 2>&3)
case "$choice" in
existing)
local existing_dirs=$(pct exec "$ctid" -- find /mnt -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort)
if [[ -z "$existing_dirs" ]]; then
whiptail --title "$(translate "No Folders")" --msgbox "$(translate "No folders found in /mnt. Please create a new folder.")" 8 60
continue
fi
local options=()
while IFS= read -r dir; do
if [[ -n "$dir" ]]; then
local name=$(basename "$dir")
if pct exec "$ctid" -- [ "$(ls -A "$dir" 2>/dev/null | wc -l)" -eq 0 ]; then
local status="$(translate "Empty")"
else
local status="$(translate "Contains files")"
fi
options+=("$dir" "$name ($status)")
fi
done <<< "$existing_dirs"
local mount_point=$(whiptail --title "$(translate "Select Existing Folder")" --menu "$(translate "Choose a folder to mount:")" 20 80 10 "${options[@]}" 3>&1 1>&2 2>&3)
if [[ -n "$mount_point" ]]; then
if pct exec "$ctid" -- [ "$(ls -A "$mount_point" 2>/dev/null | wc -l)" -gt 0 ]; then
local file_count=$(pct exec "$ctid" -- ls -A "$mount_point" 2>/dev/null | wc -l || true)
if ! whiptail --yesno "$(translate "WARNING: The selected directory is not empty!")\n\n$(translate "Directory:"): $mount_point\n$(translate "Contains:"): $file_count $(translate "files/folders")\n\n$(translate "Mounting here will hide existing files until unmounted.")\n\n$(translate "Do you want to continue?")" 14 70 --title "$(translate "Directory Not Empty")"; then
continue
fi
fi
echo "$mount_point"
return 0
fi
;;
new)
local folder_name=$(whiptail --inputbox "$(translate "Enter new folder name:")" 10 60 "$share_name" --title "$(translate "New Folder in /mnt")" 3>&1 1>&2 2>&3)
if [[ -n "$folder_name" ]]; then
local mount_point="/mnt/$folder_name"
echo "$mount_point"
return 0
fi
;;
custom)
local mount_point=$(whiptail --inputbox "$(translate "Enter full path for mount point:")" 10 70 "/mnt/${share_name}" --title "$(translate "Custom Path")" 3>&1 1>&2 2>&3)
if [[ -n "$mount_point" ]]; then
echo "$mount_point"
return 0
fi
;;
*)
return 1
;;
esac
done
}
# Common server discovery function
pmx_discover_network_servers() {
local service_type="$1" # "NFS" or "Samba"
local port="$2" # "2049" for NFS, "139,445" for Samba
local host_ip=$(hostname -I | awk '{print $1}')
local network=$(echo "$host_ip" | cut -d. -f1-3).0/24
# Install nmap if needed
if ! which nmap >/dev/null 2>&1; then
apt-get install -y nmap &>/dev/null
fi
local servers
if [[ "$service_type" == "Samba" ]]; then
servers=$(nmap -p 139,445 --open "$network" 2>/dev/null | grep -B 4 -E "(139|445)/tcp open" | grep "Nmap scan report" | awk '{print $5}' | sort -u || true)
else
servers=$(nmap -p 2049 --open "$network" 2>/dev/null | grep -B 4 "2049/tcp open" | grep "Nmap scan report" | awk '{print $5}' | sort -u || true)
fi
if [[ -z "$servers" ]]; then
whiptail --title "$(translate "No Servers Found")" --msgbox "$(translate "No") $service_type $(translate "servers found on the network.")\n\n$(translate "You can add servers manually.")" 10 60
return 1
fi
local options=()
while IFS= read -r server; do
if [[ -n "$server" ]]; then
if [[ "$service_type" == "Samba" ]]; then
# Try to get NetBIOS name for Samba
local nb_name=$(nmblookup -A "$server" 2>/dev/null | awk '/<00> -.*B <ACTIVE>/ {print $1; exit}')
if [[ -z "$nb_name" || "$nb_name" == "$server" || "$nb_name" == "address" || "$nb_name" == "-" ]]; then
nb_name="Unknown"
fi
options+=("$server" "$nb_name ($server)")
else
# For NFS, show export count
local exports_count=$(showmount -e "$server" 2>/dev/null | tail -n +2 | wc -l || echo "0")
options+=("$server" "NFS Server ($exports_count exports)")
fi
fi
done <<< "$servers"
if [[ ${#options[@]} -eq 0 ]]; then
whiptail --title "$(translate "No Valid Servers")" --msgbox "$(translate "No accessible") $service_type $(translate "servers found.")" 8 50
return 1
fi
local selected_server=$(whiptail --title "$(translate "Select") $service_type $(translate "Server")" --menu "$(translate "Choose a server:")" 20 80 10 "${options[@]}" 3>&1 1>&2 2>&3)
if [[ -n "$selected_server" ]]; then
echo "$selected_server"
return 0
else
return 1
fi
}
# Common server selection function
pmx_select_server() {
local service_type="$1" # "NFS" or "Samba"
local port="$2" # "2049" for NFS, "139,445" for Samba
local method=$(whiptail --title "$(translate "$service_type Server Selection")" --menu "$(translate "How do you want to select the") $service_type $(translate "server?")" 15 70 3 \
"auto" "$(translate "Auto-discover servers on network")" \
"manual" "$(translate "Enter server IP/hostname manually")" \
"recent" "$(translate "Select from recent servers")" 3>&1 1>&2 2>&3)
local result_code=$?
if [[ $result_code -ne 0 ]]; then
return 1
fi
case "$method" in
auto)
local discovered_server
discovered_server=$(pmx_discover_network_servers "$service_type" "$port")
local discover_result=$?
if [[ $discover_result -eq 0 && -n "$discovered_server" ]]; then
echo "$discovered_server"
return 0
else
return 1
fi
;;
manual)
local server=$(whiptail --inputbox "$(translate "Enter") $service_type $(translate "server IP or hostname:")" 10 60 --title "$(translate "$service_type Server")" 3>&1 1>&2 2>&3)
local input_result=$?
if [[ $input_result -eq 0 && -n "$server" ]]; then
echo "$server"
return 0
else
return 1
fi
;;
recent)
local fs_type
if [[ "$service_type" == "NFS" ]]; then
fs_type="nfs"
else
fs_type="cifs"
fi
# Fix the recent servers detection for NFS
local recent
if [[ "$service_type" == "NFS" ]]; then
recent=$(grep "$fs_type" /etc/fstab 2>/dev/null | awk '{print $1}' | cut -d: -f1 | sort -u || true)
else
recent=$(grep "$fs_type" /etc/fstab 2>/dev/null | awk '{print $1}' | cut -d/ -f3 | sort -u || true)
fi
if [[ -z "$recent" ]]; then
whiptail --title "$(translate "No Recent Servers")" --msgbox "\n$(translate "No recent") $service_type $(translate "servers found.")" 8 50
return 1
fi
local options=()
while IFS= read -r server; do
[[ -n "$server" ]] && options+=("$server" "$(translate "Recent") $service_type $(translate "server")")
done <<< "$recent"
local selected_server=$(whiptail --title "$(translate "Recent") $service_type $(translate "Servers")" --menu "$(translate "Choose a recent server:")" 20 70 10 "${options[@]}" 3>&1 1>&2 2>&3)
local select_result=$?
if [[ $select_result -eq 0 && -n "$selected_server" ]]; then
echo "$selected_server"
return 0
else
return 1
fi
;;
*)
return 1
;;
esac
}
# Common mount options configuration
pmx_configure_mount_options() {
local service_type="$1" # "NFS" or "CIFS"
local mount_type
if [[ "$service_type" == "NFS" ]]; then
mount_type=$(whiptail --title "$(translate "Mount Options")" --menu "$(translate "Select mount configuration:")" 15 70 4 \
"default" "$(translate "Default options")" \
"readonly" "$(translate "Read-only mount")" \
"performance" "$(translate "Performance optimized")" \
"custom" "$(translate "Custom options")" 3>&1 1>&2 2>&3)
case "$mount_type" in
default)
echo "rw,hard,intr,rsize=8192,wsize=8192,timeo=14"
;;
readonly)
echo "ro,hard,intr,rsize=8192,timeo=14"
;;
performance)
echo "rw,hard,intr,rsize=1048576,wsize=1048576,timeo=14,retrans=2"
;;
custom)
local options=$(whiptail --inputbox "$(translate "Enter custom mount options:")" 10 70 "rw,hard,intr" --title "$(translate "Custom Options")" 3>&1 1>&2 2>&3)
echo "${options:-rw,hard,intr}"
;;
*)
echo "rw,hard,intr,rsize=8192,wsize=8192,timeo=14"
;;
esac
else
# CIFS options
mount_type=$(whiptail --title "$(translate "Mount Options")" --menu "$(translate "Select mount configuration:")" 15 70 4 \
"default" "$(translate "Default options")" \
"readonly" "$(translate "Read-only mount")" \
"performance" "$(translate "Performance optimized")" \
"custom" "$(translate "Custom options")" 3>&1 1>&2 2>&3)
case "$mount_type" in
default)
echo "rw,file_mode=0664,dir_mode=0775,iocharset=utf8"
;;
readonly)
echo "ro,file_mode=0444,dir_mode=0555,iocharset=utf8"
;;
performance)
echo "rw,file_mode=0664,dir_mode=0775,iocharset=utf8,cache=strict,rsize=1048576,wsize=1048576"
;;
custom)
local options=$(whiptail --inputbox "$(translate "Enter custom mount options:")" 10 70 "rw,file_mode=0664,dir_mode=0775" --title "$(translate "Custom Options")" 3>&1 1>&2 2>&3)
echo "${options:-rw,file_mode=0664,dir_mode=0775}"
;;
*)
echo "rw,file_mode=0664,dir_mode=0775,iocharset=utf8"
;;
esac
fi
}
# Common permanent mount question
pmx_ask_permanent_mount() {
if whiptail --yesno "$(translate "Do you want to make this mount permanent?")\n\n$(translate "This will add the mount to /etc/fstab so it persists after reboot.")" 10 70 --title "$(translate "Permanent Mount")"; then
echo "true"
else
echo "false"
fi
}
+12 -10
View File
@@ -35,6 +35,7 @@ download_common_functions() {
}
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
@@ -48,8 +49,8 @@ update_pve9() {
download_common_functions
msg_info2 "$(translate "Detected: Proxmox VE 9.x (Current: $OS_CODENAME, Target: $TARGET_CODENAME)")"
echo
msg_info2 "$(translate "Detected: Proxmox VE $pve_version (Current: $OS_CODENAME, Target: $TARGET_CODENAME)")"
echo -e
local available_space=$(df /var/cache/apt/archives | awk 'NR==2 {print int($4/1024)}')
if [ "$available_space" -lt 1024 ]; then
@@ -137,13 +138,13 @@ EOF
Types: deb
URIs: http://deb.debian.org/debian/
Suites: ${TARGET_CODENAME} ${TARGET_CODENAME}-updates
Components: main contrib non-free-firmware
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-firmware
Components: main contrib non-free non-free-firmware
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
EOF
@@ -158,8 +159,6 @@ EOF
msg_ok "$(translate "Non-free firmware warnings disabled")"
fi
cleanup_duplicate_repos
update_output=$(apt-get update 2>&1)
update_exit_code=$?
@@ -240,14 +239,16 @@ EOF
return 0
fi
msg_info "$(translate "Removing conflicting utilities...")"
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 "Conflicting utilities removed")"
msg_ok "$(translate "Old time services removed successfully")"
else
msg_warn "$(translate "Some conflicting utilities may not have been removed")"
msg_warn "$(translate "Some old time services could not be removed (not installed)")"
fi
msg_info "$(translate "Updating packages...")"
apt-get install pv -y > /dev/null 2>&1
msg_ok "$(translate "Packages updated successfully")"
@@ -313,7 +314,7 @@ EOF
lvm_repair_check
cleanup_duplicate_repos
msg_info "$(translate "Performing system cleanup...")"
#msg_info "$(translate "Performing system cleanup...")"
apt-get -y autoremove > /dev/null 2>&1 || true
apt-get -y autoclean > /dev/null 2>&1 || true
msg_ok "$(translate "Cleanup finished")"
@@ -330,6 +331,7 @@ EOF
echo -e "${TAB}${GN}🖥️ $(translate "Proxmox VE")${CL}: ${BL}$target_version (Debian $OS_CODENAME)${CL}"
msg_ok "$(translate "Proxmox VE 9.x configuration completed.")"
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
+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
+173
View File
@@ -0,0 +1,173 @@
#!/bin/bash
# ProxMenux - Coral TPU Installer (PVE 9.x)
# =========================================
# Author : MacRimi
# License : MIT
# Version : 1.3 (PVE9, silent build)
# Last Updated: 25/09/2025
# =========================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
LOG_FILE="/tmp/coral_install.log"
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
load_language
initialize_cache
ensure_apex_group_and_udev() {
msg_info "Ensuring apex group and udev rules..."
if ! getent group apex >/dev/null; then
groupadd --system apex || true
msg_ok "System group 'apex' created"
else
msg_ok "System group 'apex' already exists"
fi
cat >/etc/udev/rules.d/99-coral-apex.rules <<'EOF'
# Coral / Google APEX TPU (M.2 / PCIe)
# Assign group "apex" and safe permissions to device nodes
KERNEL=="apex_*", GROUP="apex", MODE="0660"
SUBSYSTEM=="apex", GROUP="apex", MODE="0660"
EOF
if [[ -f /usr/lib/udev/rules.d/60-gasket-dkms.rules ]]; then
sed -i 's/GROUP="[^"]*"/GROUP="apex"/g' /usr/lib/udev/rules.d/60-gasket-dkms.rules || true
fi
udevadm control --reload-rules
udevadm trigger --subsystem-match=apex || true
msg_ok "apex group and udev rules are in place"
if ls -l /dev/apex_* 2>/dev/null | grep -q ' apex '; then
msg_ok "Coral TPU device nodes detected with correct group (apex)"
else
msg_warn "apex device node not found yet; a reboot may be required"
fi
}
pre_install_prompt() {
if ! dialog --title "$(translate 'Coral TPU Installation')" --yesno \
"\n$(translate 'Installing Coral TPU drivers requires rebooting the server after installation. Do you want to proceed?')" 10 70; then
exit 0
fi
}
install_coral_host() {
show_proxmenux_logo
: >"$LOG_FILE"
msg_info "$(translate 'Installing build dependencies...')"
apt-get update -qq >>"$LOG_FILE" 2>&1
apt-get install -y git devscripts dh-dkms dkms proxmox-headers-$(uname -r) >>"$LOG_FILE" 2>&1
if [[ $? -ne 0 ]]; then msg_error "$(translate 'Error installing build dependencies. Check /tmp/coral_install.log')"; exit 1; fi
msg_ok "$(translate 'Build dependencies installed.')"
cd /tmp || exit 1
rm -rf gasket-driver >>"$LOG_FILE" 2>&1
msg_info "$(translate 'Cloning Google Coral driver repository...')"
git clone https://github.com/google/gasket-driver.git >>"$LOG_FILE" 2>&1
if [[ $? -ne 0 ]]; then msg_error "$(translate 'Could not clone the repository. Check /tmp/coral_install.log')"; exit 1; fi
msg_ok "$(translate 'Repository cloned successfully.')"
cd /tmp/gasket-driver || exit 1
msg_info "$(translate 'Patching source for kernel compatibility...')"
sed -i 's/\.llseek = no_llseek/\.llseek = noop_llseek/' src/gasket_core.c
sed -i 's/^MODULE_IMPORT_NS(DMA_BUF);/MODULE_IMPORT_NS("DMA_BUF");/' src/gasket_page_table.c
sed -i "s/\(linux-headers-686-pae | linux-headers-amd64 | linux-headers-generic | linux-headers\)/\1 | proxmox-headers-$(uname -r) | pve-headers-$(uname -r)/" debian/control
if [[ $? -ne 0 ]]; then msg_error "$(translate 'Patching failed. Check /tmp/coral_install.log')"; exit 1; fi
msg_ok "$(translate 'Source patched successfully.')"
msg_info "$(translate 'Building DKMS package...')"
debuild -us -uc -tc -b >>"$LOG_FILE" 2>&1
if [[ $? -ne 0 ]]; then msg_error "$(translate 'Failed to build DKMS package. Check /tmp/coral_install.log')"; exit 1; fi
msg_ok "$(translate 'DKMS package built successfully.')"
msg_info "$(translate 'Installing DKMS package...')"
dpkg -i ../gasket-dkms_*.deb >>"$LOG_FILE" 2>&1 || true
if ! dpkg -s gasket-dkms >/dev/null 2>&1; then
msg_error "$(translate 'Failed to install DKMS package. Check /tmp/coral_install.log')"; exit 1
fi
msg_ok "$(translate 'DKMS package installed.')"
msg_info "$(translate 'Compiling Coral TPU drivers for current kernel...')"
dkms remove -m gasket -v 1.0 -k "$(uname -r)" >>"$LOG_FILE" 2>&1 || true
dkms add -m gasket -v 1.0 >>"$LOG_FILE" 2>&1 || true
dkms build -m gasket -v 1.0 -k "$(uname -r)" >>"$LOG_FILE" 2>&1
if [[ $? -ne 0 ]]; then
sed -n '1,200p' /var/lib/dkms/gasket/1.0/build/make.log >>"$LOG_FILE" 2>&1 || true
msg_error "$(translate 'DKMS build failed. Check /tmp/coral_install.log')"; exit 1
fi
dkms install -m gasket -v 1.0 -k "$(uname -r)" >>"$LOG_FILE" 2>&1
if [[ $? -ne 0 ]]; then msg_error "$(translate 'DKMS install failed. Check /tmp/coral_install.log')"; exit 1; fi
msg_ok "$(translate 'Drivers compiled and installed via DKMS.')"
ensure_apex_group_and_udev
msg_info "$(translate 'Loading modules...')"
modprobe gasket >>"$LOG_FILE" 2>&1 || true
modprobe apex >>"$LOG_FILE" 2>&1 || true
if lsmod | grep -q '\bapex\b'; then
msg_ok "$(translate 'Modules loaded.')"
msg_success "$(translate 'Coral TPU drivers installed and loaded successfully.')"
else
msg_warn "$(translate 'Installation finished but drivers are not loaded. Please check dmesg and /tmp/coral_install.log')"
fi
echo "---- dmesg | grep -i apex (last lines) ----" >>"$LOG_FILE"
dmesg | grep -i apex | tail -n 20 >>"$LOG_FILE" 2>&1
}
restart_prompt() {
if whiptail --title "$(translate 'Coral TPU Installation')" --yesno \
"$(translate 'The installation requires a server restart to apply changes. Do you want to restart now?')" 10 70; then
msg_warn "$(translate 'Restarting the server...')"
reboot
else
msg_success "$(translate 'Completed. Press Enter to return to menu...')"
read -r
fi
}
pre_install_prompt
install_coral_host
restart_prompt
+3 -4
View File
@@ -1,7 +1,7 @@
#!/bin/bash
# ==========================================================
# ProxMenu - A menu-driven script for Proxmox VE management
# ProxMenux - A menu-driven script for Proxmox VE management
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
@@ -197,13 +197,12 @@ show_vm_ct_commands() {
echo -e "\n${YELLOW}$(translate 'Listing relevant CT users and their mapped UID/GID on host...')${NC}\n"
# Obtener el shift de UID del CT (por defecto 100000 si no está configurado)
UID_SHIFT=$(grep "^lxc.idmap" /etc/pve/lxc/"$id".conf | grep 'u 0' | awk '{print $5}')
UID_SHIFT=${UID_SHIFT:-100000}
# Obtener todos los usuarios y filtrar solo root o UID >= 1000
pct exec "$id" -- getent passwd | while IFS=: read -r username _ uid gid _ home _; do
if [ "$uid" -eq 0 ] || [ "$uid" -ge 1000 ]; then
if [ "$uid" -eq 0 ] || [ "$uid" -eq 65534 ] || [ "$uid" -ge 30 ]; then
real_uid=$((UID_SHIFT + uid))
real_gid=$((UID_SHIFT + gid))
echo -e "${GREEN}$(translate 'User')${NC}: $username"
+1 -1
View File
@@ -1,7 +1,7 @@
#!/bin/bash
# ==========================================================
# ProxMenu - A menu-driven script for Proxmox VE management
# ProxMenux - A menu-driven script for Proxmox VE management
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
+1 -1
View File
@@ -1,7 +1,7 @@
#!/bin/bash
# ==========================================================
# ProxMenu - A menu-driven script for Proxmox VE management
# ProxMenux - A menu-driven script for Proxmox VE management
# ==========================================================
# Author : MacRimi
# Revision : @Blaspt (USB passthrough via udev rule with persistent /dev/coral)
+1 -1
View File
@@ -1,7 +1,7 @@
#!/bin/bash
# ProxMenu - A menu-driven script for Proxmox VE management
# ProxMenux - A menu-driven script for Proxmox VE management
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
+157
View File
@@ -0,0 +1,157 @@
#!/bin/bash
# Script para instalar JDownloader en un contenedor LXC desde el host Proxmox
# Autor: MacRimi
# Mostrar lista de CTs
CT_LIST=$(pct list | awk 'NR>1 {print $1, $3}')
if [ -z "$CT_LIST" ]; then
whiptail --title "Error" --msgbox "No hay contenedores LXC disponibles en el sistema." 8 50
exit 1
fi
# Seleccionar CT
CTID=$(whiptail --title "Instalación de JDownloader" --menu "Selecciona el contenedor donde instalar JDownloader:" 20 60 10 $CT_LIST 3>&1 1>&2 2>&3)
if [ -z "$CTID" ]; then
whiptail --title "Cancelado" --msgbox "No se ha seleccionado ningún contenedor." 8 40
exit 1
fi
# Solicitar email
EMAIL=$(whiptail --title "Cuenta My JDownloader" --inputbox "Introduce tu correo electrónico para vincular JDownloader:" 10 60 3>&1 1>&2 2>&3)
if [ -z "$EMAIL" ]; then
whiptail --title "Error" --msgbox "No se ha introducido ningún correo." 8 40
exit 1
fi
# Solicitar contraseña con confirmación
while true; do
PASSWORD=$(whiptail --title "Cuenta My JDownloader" --passwordbox "Introduce tu contraseña de My JDownloader:" 10 60 3>&1 1>&2 2>&3)
[ -z "$PASSWORD" ] && whiptail --title "Error" --msgbox "No se ha introducido ninguna contraseña." 8 40 && exit 1
CONFIRM=$(whiptail --title "Confirmación de contraseña" --passwordbox "Repite tu contraseña para confirmar:" 10 60 3>&1 1>&2 2>&3)
[ "$PASSWORD" = "$CONFIRM" ] && break
whiptail --title "Error" --msgbox "Las contraseñas no coinciden. Intenta de nuevo." 8 50
done
# Confirmación final
whiptail --title "Confirmar datos" --yesno "¿Deseas continuar con los siguientes datos?\n\nCorreo: $EMAIL\nContraseña: (oculta)\n\nEsta información se usará para vincular el contenedor con tu cuenta de My.JDownloader." 14 60
[ $? -ne 0 ] && whiptail --title "Cancelado" --msgbox "Instalación cancelada por el usuario." 8 40 && exit 1
clear
echo "🔍 Detectando sistema operativo dentro del CT $CTID..."
OS_ID=$(pct exec "$CTID" -- awk -F= '/^ID=/{gsub("\"",""); print $2}' /etc/os-release)
echo "Sistema detectado: $OS_ID"
echo "🧰 Preparando entorno..."
case "$OS_ID" in
debian)
# Repositorio adicional para Java 8
pct exec "$CTID" -- wget -q http://www.mirbsd.org/~tg/Debs/sources.txt/wtf-bookworm.sources
pct exec "$CTID" -- mv wtf-bookworm.sources /etc/apt/sources.list.d/
pct exec "$CTID" -- apt update -y
pct exec "$CTID" -- apt install -y openjdk-8-jdk wget
JAVA_PATH="/usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java"
;;
ubuntu)
pct exec "$CTID" -- apt update -y
pct exec "$CTID" -- apt install -y openjdk-8-jdk wget
JAVA_PATH="/usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java"
;;
alpine)
pct exec "$CTID" -- apk update
pct exec "$CTID" -- apk add openjdk8 wget
JAVA_PATH="/usr/lib/jvm/java-1.8-openjdk/bin/java"
;;
*)
echo "❌ Sistema operativo no soportado: $OS_ID"
exit 1
;;
esac
# Crear carpeta de instalación
pct exec "$CTID" -- mkdir -p /opt/jdownloader
pct exec "$CTID" -- bash -lc '
set -e
mkdir -p /opt/jdownloader
cd /opt/jdownloader
if [ ! -f JDownloader.jar ]; then
if ls JDownloader.jar.backup.* >/dev/null 2>&1; then
cp -a "$(ls -t JDownloader.jar.backup.* | head -1)" JDownloader.jar
else
curl -fSLo JDownloader.jar https://installer.jdownloader.org/JDownloader.jar
fi
fi
chown root:root JDownloader.jar
chmod 0644 JDownloader.jar
'
# Crear archivo de configuración JSON para My JDownloader
pct exec "$CTID" -- bash -c "mkdir -p /opt/jdownloader/cfg && cat > /opt/jdownloader/cfg/org.jdownloader.api.myjdownloader.MyJDownloaderSettings.json" <<EOF
{
"email" : "$EMAIL",
"password" : "$PASSWORD",
"enabled" : true
}
EOF
# Crear servicio según sistema
if [[ "$OS_ID" == "alpine" ]]; then
# Servicio OpenRC para Alpine
pct exec "$CTID" -- bash -c 'cat > /etc/init.d/jdownloader <<EOF
#!/sbin/openrc-run
command="/usr/bin/java"
command_args="-jar /opt/jdownloader/JDownloader.jar -norestart"
pidfile="/var/run/jdownloader.pid"
name="JDownloader"
depend() {
need net
}
EOF'
pct exec "$CTID" -- chmod +x /etc/init.d/jdownloader
pct exec "$CTID" -- rc-update add jdownloader default
pct exec "$CTID" -- rc-service jdownloader start
else
# Servicio systemd para Debian/Ubuntu
pct exec "$CTID" -- bash -lc 'cat > /etc/systemd/system/jdownloader.service <<'"'"'EOF'"'"'
[Unit]
Description=JDownloader
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=root
WorkingDirectory=/opt/jdownloader
ExecStartPre=/usr/bin/test -s /opt/jdownloader/JDownloader.jar
ExecStart=/usr/bin/java -jar /opt/jdownloader/JDownloader.jar -norestart
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable jdownloader
systemctl restart jdownloader
systemctl status jdownloader --no-pager || true
'
pct exec "$CTID" -- systemctl daemon-reexec
pct exec "$CTID" -- systemctl daemon-reload
pct exec "$CTID" -- systemctl enable jdownloader
pct exec "$CTID" -- systemctl start jdownloader
fi
pct exec "$CTID" -- reboot
echo -e "\n\033[1;32m✅ JDownloader se ha instalado correctamente en el CT $CTID y está funcionando como servicio.\033[0m"
echo -e "\n➡️ Accede a \033[1;34mhttps://my.jdownloader.org\033[0m con tu cuenta para gestionarlo.\n"
+284
View File
@@ -0,0 +1,284 @@
#!/bin/bash
# ==========================================================
# ProxMenux - Manual LXC Conversion Guide
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# Version : 1.0
# Last Updated: 19/08/2025
# ==========================================================
# 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"
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
load_language
initialize_cache
# ==========================================================
show_command() {
local step="$1"
local description="$2"
local command="$3"
local note="$4"
local command_extra="$5"
echo -e "${BGN}${step}.${CL} ${BL}${description}${CL}"
echo ""
echo -e "${TAB}${command}"
echo -e
[[ -n "$note" ]] && echo -e "${TAB}${DARK_GRAY}${note}${CL}"
[[ -n "$command_extra" ]] && echo -e "${TAB}${YW}${command_extra}${CL}"
echo ""
}
show_privileged_to_unprivileged_guide() {
clear
show_proxmenux_logo
msg_title "$(translate "Manual Guide: Convert LXC Privileged to Unprivileged")"
echo -e "${TAB}${BL}------------------------------------------------------------------------${CL}"
echo -e
echo -e "${TAB}${BGN}$(translate "Source:")${CL} ${BL}https://forum.proxmox.com/threads/converting-between-privileged-and-unprivileged-containers.97243/${CL}"
echo -e
echo -e
echo -e "${TAB}${BOLD}$(translate "IMPORTANT PREREQUISITES:")${CL}"
echo -e
echo -e "${TAB}${BGN}$(translate "Container must be stopped before conversion")${CL}"
echo -e "${TAB}${BGN}$(translate "Create a backup of your container before proceeding")${CL}"
echo -e "${TAB}${BGN}$(translate "This process changes file ownership inside the container")${CL}"
echo -e "${TAB}${BGN}$(translate "Process may take several minutes depending on container size")${CL}"
echo -e "${TAB}${BGN}$(translate "Works with LVM, ZFS, and BTRFS storage types")${CL}"
echo -e
echo -e "${TAB}${BL}------------------------------------------------------------------------${CL}"
echo -e
show_command "1" \
"$(translate "List all containers to identify the privileged one:")" \
"pct list" \
"$(translate "Look for containers without 'unprivileged: 1' in their config")"
show_command "2" \
"$(translate "Stop the container if it's running:")" \
"pct stop <container-id>" \
"$(translate "Replace <container-id> with your actual container ID")" \
"$(translate "Example: pct stop 114")"
show_command "3" \
"$(translate "Create a backup of the container configuration:")" \
"cp /etc/pve/lxc/<container-id>.conf /etc/pve/lxc/<container-id>.conf.bak" \
"$(translate "This creates a backup in case you need to revert changes")" \
"$(translate "Example: cp /etc/pve/lxc/114.conf /etc/pve/lxc/114.conf.bak")"
show_command "4" \
"$(translate "Get the container's storage information:")" \
"grep '^rootfs:' /etc/pve/lxc/<container-id>.conf" \
"$(translate "This shows the storage type and disk identifier")" \
"$(translate "Example output: rootfs: local-lvm:vm-114-disk-0,size=8G")"
show_command "5" \
"$(translate "Get the actual disk path:")" \
"pvesm path <storage-identifier>" \
"$(translate "Replace <storage-identifier> with the value from step 4")" \
"$(translate "Example: pvesm path local-lvm:vm-114-disk-0")"
echo -e "${TAB}${BOLD}$(translate "STEP 6: Choose commands based on your storage type")${CL}"
echo -e
echo -e "${TAB}${BGN}$(translate "If pvesm path returned a DIRECTORY (ZFS/BTRFS):")${CL}"
echo -e "${TAB}${YW}$(translate "Example: /rpool/data/subvol-114-disk-0")${CL}"
echo -e
show_command "6a" \
"$(translate "For ZFS/BTRFS - Set the mount path:")" \
"MOUNT_PATH=\"/rpool/data/subvol-<container-id>-disk-0\"" \
"$(translate "Replace with your actual path from step 5")" \
"$(translate "Example: MOUNT_PATH=\"/rpool/data/subvol-114-disk-0\"")"
echo -e "${TAB}${BGN}$(translate "If pvesm path returned a DEVICE (LVM):")${CL}"
echo -e "${TAB}${YW}$(translate "Example: /dev/pve/vm-114-disk-0")${CL}"
echo -e
show_command "6b" \
"$(translate "For LVM - Create mount directory and mount:")" \
"mkdir -p /tmp/lxc_convert_<container-id>\nmount -o loop /dev/path/to/disk /tmp/lxc_convert_<container-id>\nMOUNT_PATH=\"/tmp/lxc_convert_<container-id>\"" \
"$(translate "Replace paths with your actual values from step 5")" \
"$(translate "Example: mkdir -p /tmp/lxc_convert_114")"
show_command "7" \
"$(translate "Convert file ownership (this takes time):")" \
"find \"\$MOUNT_PATH\" -type f | while read file; do\n if [ -e \"\$file\" ]; then\n CURRENT_UID=\$(stat -c '%u' \"\$file\")\n CURRENT_GID=\$(stat -c '%g' \"\$file\")\n NEW_UID=\$((100000 + CURRENT_UID))\n NEW_GID=\$((100000 + CURRENT_GID))\n chown \"\$NEW_UID:\$NEW_GID\" \"\$file\"\n fi\ndone" \
"$(translate "This converts all file UIDs/GIDs by adding 100000")" \
"$(translate "Process may take several minutes for large containers")"
show_command "8" \
"$(translate "Convert directory ownership:")" \
"find \"\$MOUNT_PATH\" -type d | while read dir; do\n if [ -e \"\$dir\" ]; then\n CURRENT_UID=\$(stat -c '%u' \"\$dir\")\n CURRENT_GID=\$(stat -c '%g' \"\$dir\")\n NEW_UID=\$((100000 + CURRENT_UID))\n NEW_GID=\$((100000 + CURRENT_GID))\n chown \"\$NEW_UID:\$NEW_GID\" \"\$dir\"\n fi\ndone" \
"$(translate "This converts all directory UIDs/GIDs by adding 100000")"
echo -e "${TAB}${BOLD}$(translate "STEP 9: Cleanup (LVM only)")${CL}"
echo -e "${TAB}${YW}$(translate "Only run this if you used LVM (step 6b):")${CL}"
echo -e
show_command "9" \
"$(translate "Unmount and cleanup (LVM only):")" \
"umount /tmp/lxc_convert_<container-id>\nrmdir /tmp/lxc_convert_<container-id>" \
"$(translate "Only needed if you mounted the filesystem in step 6b")" \
"$(translate "Skip this step for ZFS/BTRFS")"
show_command "10" \
"$(translate "Add unprivileged flag to container configuration:")" \
"echo 'unprivileged: 1' >> /etc/pve/lxc/<container-id>.conf" \
"$(translate "This marks the container as unprivileged")"
show_command "11" \
"$(translate "Start the converted container:")" \
"pct start <container-id>" \
"$(translate "The container should now start as unprivileged")"
show_command "12" \
"$(translate "Verify the conversion:")" \
"pct config <container-id> | grep unprivileged" \
"$(translate "Should show 'unprivileged: 1'")"
echo -e "${TAB}${BL}------------------------------------------------------------------------${CL}"
echo -e
echo -e "${TAB}${BOLD}$(translate "STORAGE TYPE IDENTIFICATION:")${CL}"
echo -e
echo -e "${TAB}${BGN}$(translate "LVM:")${CL} ${YW}pvesm path returns /dev/xxx (block device)${CL}"
echo -e "${TAB}${BGN}$(translate "ZFS:")${CL} ${YW}pvesm path returns /rpool/xxx (directory)${CL}"
echo -e "${TAB}${BGN}$(translate "BTRFS:")${CL} ${YW}pvesm path returns directory path${CL}"
echo -e
echo -e "${TAB}${BOLD}$(translate "TROUBLESHOOTING:")${CL}"
echo -e
echo -e "${TAB}${BGN}$(translate "If mount fails (LVM):")${CL} ${YW}Check that the container is stopped and disk path is correct${CL}"
echo -e "${TAB}${BGN}$(translate "If path not accessible (ZFS/BTRFS):")${CL} ${YW}Verify the dataset/subvolume exists and is mounted${CL}"
echo -e "${TAB}${BGN}$(translate "If container won't start:")${CL} ${YW}Check /var/log/pve/tasks/ for detailed error messages${CL}"
echo -e "${TAB}${BGN}$(translate "To revert changes:")${CL} ${YW}cp /etc/pve/lxc/<container-id>.conf.bak /etc/pve/lxc/<container-id>.conf${CL}"
echo -e
echo -e
msg_success "$(translate "Press Enter to return to menu...")"
echo -e
read -r
}
show_unprivileged_to_privileged_guide() {
clear
show_proxmenux_logo
msg_title "$(translate "Manual Guide: Convert LXC Unprivileged to Privileged")"
echo -e "${TAB}${BL}------------------------------------------------------------------------${CL}"
echo -e
echo -e "${TAB}${RD}$(translate "SECURITY WARNING:")${CL} ${YW}$(translate "Privileged containers have full root access to the host system!")${CL}"
echo -e "${TAB}${YW}$(translate "Only convert to privileged if absolutely necessary for your use case.")${CL}"
echo -e
echo -e
echo -e "${TAB}${BOLD}$(translate "IMPORTANT PREREQUISITES:")${CL}"
echo -e
echo -e "${TAB}${BGN}$(translate "Container must be stopped before conversion")${CL}"
echo -e "${TAB}${BGN}$(translate "Create a backup of your container before proceeding")${CL}"
echo -e "${TAB}${BGN}$(translate "Understand the security implications of privileged containers")${CL}"
echo -e "${TAB}${BGN}$(translate "This is a simple configuration change")${CL}"
echo -e
echo -e "${TAB}${BL}------------------------------------------------------------------------${CL}"
echo -e
show_command "1" \
"$(translate "List all containers to identify the unprivileged one:")" \
"pct list" \
"$(translate "Look for containers with 'unprivileged: 1' in their config")"
show_command "2" \
"$(translate "Check if container is unprivileged:")" \
"pct config <container-id> | grep unprivileged" \
"$(translate "Should show 'unprivileged: 1' if it's unprivileged")" \
"$(translate "Example: pct config 110 | grep unprivileged")"
show_command "3" \
"$(translate "Stop the container if it's running:")" \
"pct stop <container-id>" \
"$(translate "Replace <container-id> with your actual container ID")" \
"$(translate "Example: pct stop 110")"
show_command "4" \
"$(translate "Create a backup of the container configuration:")" \
"cp /etc/pve/lxc/<container-id>.conf /etc/pve/lxc/<container-id>.conf.bak" \
"$(translate "This creates a backup in case you need to revert changes")" \
"$(translate "Example: cp /etc/pve/lxc/110.conf /etc/pve/lxc/110.conf.bak")"
show_command "5" \
"$(translate "Remove the unprivileged flag from configuration:")" \
"sed -i '/^unprivileged: 1/d' /etc/pve/lxc/<container-id>.conf" \
"$(translate "This removes the 'unprivileged: 1' line from the config")" \
"$(translate "Example: sed -i '/^unprivileged: 1/d' /etc/pve/lxc/110.conf")"
show_command "6" \
"$(translate "Add explicit privileged flag (optional but recommended):")" \
"echo 'unprivileged: 0' >> /etc/pve/lxc/<container-id>.conf" \
"$(translate "This explicitly marks the container as privileged")"
show_command "7" \
"$(translate "Start the converted container:")" \
"pct start <container-id>" \
"$(translate "The container should now start as privileged")"
show_command "8" \
"$(translate "Verify the conversion:")" \
"pct config <container-id> | grep unprivileged" \
"$(translate "Should show 'unprivileged: 0' or no unprivileged line")"
echo -e "${TAB}${BL}------------------------------------------------------------------------${CL}"
echo -e
echo -e
echo -e "${TAB}${BOLD}$(translate "SECURITY CONSIDERATIONS:")${CL}"
echo -e
echo -e "${TAB}${RD}$(translate "Privileged containers can access host devices directly")${CL}"
echo -e "${TAB}${RD}$(translate "Root inside container = root on host system")${CL}"
echo -e "${TAB}${RD}$(translate "Use only when unprivileged containers cannot meet your needs")${CL}"
echo -e "${TAB}${RD}$(translate "Consider security implications for production environments")${CL}"
echo -e
echo -e
echo -e "${TAB}${BOLD}$(translate "TROUBLESHOOTING:")${CL}"
echo -e
echo -e "${TAB}${BGN}$(translate "If container won't start:")${CL} ${YW}Check /var/log/pve/tasks/ for detailed error messages${CL}"
echo -e "${TAB}${BGN}$(translate "To revert changes:")${CL} ${YW}cp /etc/pve/lxc/<container-id>.conf.bak /etc/pve/lxc/<container-id>.conf${CL}"
echo -e "${TAB}${BGN}$(translate "If config issues occur:")${CL} ${YW}Manually edit /etc/pve/lxc/<container-id>.conf${CL}"
echo -e
echo -e
echo -e
msg_success "$(translate "Press Enter to return to menu...")"
echo -e
read -r
}
show_lxc_conversion_manual_menu() {
while true; do
CHOICE=$(dialog --title "$(translate "LXC Conversion Manual Guides")" \
--menu "$(translate "Select conversion guide:")" 18 70 10 \
"1" "$(translate "Convert Privileged to Unprivileged")" \
"2" "$(translate "Convert Unprivileged to Privileged")" \
"3" "$(translate "Return to Main Menu")" \
3>&1 1>&2 2>&3)
case $CHOICE in
1) show_privileged_to_unprivileged_guide ;;
2) show_unprivileged_to_privileged_guide ;;
3) return ;;
*) return ;;
esac
done
}
# Main execution
show_lxc_conversion_manual_menu
@@ -232,6 +232,10 @@ convert_direct_method() {
fi
msg_ok "$(translate 'Direct conversion completed for container') $CONTAINER_ID"
echo -e
msg_success "Press Enter to continue..."
read -r
}
cleanup_and_finalize() {
@@ -258,7 +262,7 @@ main() {
msg_ok "$(translate 'Converted container ID:') $CONTAINER_ID"
msg_ok "$(translate 'LXC conversion from privileged to unprivileged completed successfully!')"
echo -e
msg_success "$(translate "Press Enter to continue")"
msg_success "$(translate "Press Enter to return to menu...")"
read -r
exit 0
}
@@ -0,0 +1,144 @@
#!/bin/bash
# ==========================================================
# ProxMenu - LXC Unprivileged to Privileged Converter
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# Version : 2.0
# Last Updated: 19/08/2025
# ==========================================================
# Description:
# This script converts an unprivileged LXC container to a privileged one
# by directly modifying the configuration file.
# WARNING: This reduces security. Use only when necessary.
# ==========================================================
# 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"
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
load_language
initialize_cache
# ==========================================================
select_unprivileged_container() {
CONTAINERS=$(pct list | awk 'NR>1 {print $1, $3}' | while read id name; do
if pct config "$id" | grep -q "^unprivileged: 1"; then
echo "$id" "$name"
fi
done | xargs -n2)
if [ -z "$CONTAINERS" ]; then
msg_error "$(translate 'No unprivileged containers available in Proxmox.')"
exit 1
fi
cleanup
CONTAINER_ID=$(whiptail --title "$(translate 'Select Unprivileged Container')" \
--menu "$(translate 'Select the unprivileged LXC container to convert:')" 20 70 10 $CONTAINERS 3>&1 1>&2 2>&3)
if [ -z "$CONTAINER_ID" ]; then
msg_error "$(translate 'No container selected. Exiting.')"
exit 1
fi
msg_ok "$(translate 'Unprivileged container selected:') $CONTAINER_ID"
}
show_backup_warning() {
if ! whiptail --title "$(translate 'Backup Recommendation')" \
--yes-button "$(translate 'Continue')" \
--no-button "$(translate 'Exit')" \
--yesno "$(translate 'It is recommended to create a backup before continuing.')" \
12 70; then
msg_info "$(translate 'Operation cancelled by user to create backup.')"
exit 0
fi
}
convert_to_privileged() {
CONF_FILE="/etc/pve/lxc/$CONTAINER_ID.conf"
CONTAINER_STATUS=$(pct status "$CONTAINER_ID" | awk '{print $2}')
if [ "$CONTAINER_STATUS" == "running" ]; then
msg_info "$(translate 'Stopping container') $CONTAINER_ID..."
pct shutdown "$CONTAINER_ID"
# Wait for container to stop
for i in {1..10}; do
sleep 1
if [ "$(pct status "$CONTAINER_ID" | awk '{print $2}')" != "running" ]; then
break
fi
done
# Verify container stopped
if [ "$(pct status "$CONTAINER_ID" | awk '{print $2}')" == "running" ]; then
msg_error "$(translate 'Failed to stop the container.')"
exit 1
fi
msg_ok "$(translate 'Container stopped.')"
else
msg_ok "$(translate 'Container is already stopped.')"
fi
msg_info "$(translate 'Creating backup of configuration file...')"
cp "$CONF_FILE" "$CONF_FILE.bak"
msg_ok "$(translate 'Configuration backup created:') $CONF_FILE.bak"
msg_info "$(translate 'Converting container to privileged...')"
sed -i '/^unprivileged: 1/d' "$CONF_FILE"
echo "unprivileged: 0" >> "$CONF_FILE"
msg_ok "$(translate 'Container successfully converted to privileged.')"
echo -e
msg_success "Press Enter to continue..."
read -r
}
finalize_conversion() {
if whiptail --yesno "$(translate 'Do you want to start the privileged container') $CONTAINER_ID $(translate 'now?')" 10 60; then
msg_info "$(translate 'Starting privileged container...')"
pct start "$CONTAINER_ID"
msg_ok "$(translate 'Privileged container') $CONTAINER_ID $(translate 'started successfully.')"
fi
}
main() {
show_proxmenux_logo
msg_title "$(translate "LXC Unprivileged to Privileged conversion")"
msg_info "$(translate 'Starting LXC Unprivileged to Privileged conversion process...')"
select_unprivileged_container
show_backup_warning
convert_to_privileged
finalize_conversion
msg_ok "$(translate 'LXC conversion from unprivileged to privileged completed successfully!')"
msg_ok "$(translate 'Converted container ID:') $CONTAINER_ID"
echo -e
msg_success "$(translate "Press Enter to return to menu...")"
read -r
exit 0
}
# Execute main function
main
+155 -42
View File
@@ -1,6 +1,6 @@
#!/bin/bash
# ==========================================================
# ProxMenu - A menu-driven script for Proxmox VE management
# ProxMenux - A menu-driven script for Proxmox VE management
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
@@ -19,6 +19,7 @@ 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"
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
@@ -33,12 +34,12 @@ detect_installation_type() {
local has_venv=false
local has_language=false
# Check if virtual environment exists
if [ -d "$VENV_PATH" ] && [ -f "$VENV_PATH/bin/activate" ]; then
has_venv=true
fi
# Check if language is configured
if [ -f "$CONFIG_FILE" ]; then
local current_language=$(jq -r '.language // empty' "$CONFIG_FILE" 2>/dev/null)
if [[ -n "$current_language" && "$current_language" != "null" && "$current_language" != "empty" ]]; then
@@ -53,6 +54,89 @@ detect_installation_type() {
fi
}
check_monitor_status() {
if systemctl list-unit-files | grep -q "$MONITOR_SERVICE"; then
if systemctl is-active --quiet "$MONITOR_SERVICE"; then
echo "active"
else
echo "inactive"
fi
else
echo "not_installed"
fi
}
toggle_monitor_service() {
local status=$(check_monitor_status)
if [ "$status" = "not_installed" ]; then
dialog --clear --backtitle "ProxMenux Configuration" \
--title "$(translate "ProxMenux Monitor")" \
--msgbox "\n\n$(translate "ProxMenux Monitor is not installed.")" 10 50
return
fi
if [ "$status" = "active" ]; then
if dialog --clear --backtitle "ProxMenux Configuration" \
--title "$(translate "Deactivate Monitor")" \
--yesno "\n$(translate "Do you want to deactivate ProxMenux Monitor?")" 8 60; then
systemctl stop "$MONITOR_SERVICE" 2>/dev/null
systemctl disable "$MONITOR_SERVICE" 2>/dev/null
dialog --clear --backtitle "ProxMenux Configuration" \
--title "$(translate "Monitor Deactivated")" \
--msgbox "\n\n$(translate "ProxMenux Monitor has been deactivated.")" 10 50
fi
else
if dialog --clear --backtitle "ProxMenux Configuration" \
--title "$(translate "Activate Monitor")" \
--yesno "\n$(translate "Do you want to activate ProxMenux Monitor?")" 8 60; then
systemctl enable "$MONITOR_SERVICE" 2>/dev/null
systemctl start "$MONITOR_SERVICE" 2>/dev/null
dialog --clear --backtitle "ProxMenux Configuration" \
--title "$(translate "Monitor Activated")" \
--msgbox "\n\n$(translate "ProxMenux Monitor has been activated.")" 10 50
fi
fi
}
show_monitor_status() {
clear
show_proxmenux_logo
msg_title "$(translate "ProxMenux Monitor Service Verification")"
echo ""
local status=$(check_monitor_status)
if [ "$status" = "not_installed" ]; then
msg_warn "$(translate "ProxMenux Monitor is not installed")"
echo ""
msg_info2 "$(translate "To install the monitor, reinstall ProxMenux with the latest version")"
else
msg_info2 "$(translate "Service Status"): $MONITOR_SERVICE"
echo ""
if [ "$status" = "active" ]; then
msg_ok "$(translate "Service is active and running")"
local server_ip=$(hostname -I | awk '{print $1}')
if [ -n "$server_ip" ]; then
echo -e "${TAB}${GN}🌐 $(translate "Monitor URL")${CL}: ${BL}http://${server_ip}:8008${CL}"
fi
else
msg_warn "$(translate "Service is inactive")"
fi
echo ""
msg_info2 "$(translate "Detailed service information"):"
echo ""
systemctl status "$MONITOR_SERVICE" --no-pager -l
fi
echo ""
msg_success "$(translate "Press Enter to continue...")"
read -r
}
# ==========================================================
show_config_menu() {
local install_type
@@ -62,39 +146,68 @@ show_config_menu() {
local menu_options=()
local option_actions=()
if [ "$install_type" = "translation" ]; then
menu_options+=("1" "$(translate "Change Language")")
option_actions[1]="change_language"
local monitor_status=$(check_monitor_status)
local option_num=1
if [ "$monitor_status" != "not_installed" ]; then
if [ "$monitor_status" = "active" ]; then
menu_options+=("$option_num" "$(translate "Deactivate ProxMenux Monitor")")
option_actions[$option_num]="toggle_monitor"
else
menu_options+=("$option_num" "$(translate "Activate ProxMenux Monitor")")
option_actions[$option_num]="toggle_monitor"
fi
((option_num++))
menu_options+=("2" "$(translate "Show Version Information")")
option_actions[2]="show_version_info"
menu_options+=("3" "$(translate "Uninstall ProxMenux")")
option_actions[3]="uninstall_proxmenu"
menu_options+=("4" "$(translate "Return to Main Menu")")
option_actions[4]="return_main"
else
menu_options+=("1" "Show Version Information")
option_actions[1]="show_version_info"
menu_options+=("2" "Uninstall ProxMenux")
option_actions[2]="uninstall_proxmenu"
menu_options+=("3" "Return to Main Menu")
option_actions[3]="return_main"
menu_options+=("$option_num" "$(translate "Show Monitor Service Status")")
option_actions[$option_num]="show_monitor_status"
((option_num++))
fi
# Build menu based on installation type
if [ "$install_type" = "translation" ]; then
menu_options+=("$option_num" "$(translate "Change Language")")
option_actions[$option_num]="change_language"
((option_num++))
menu_options+=("$option_num" "$(translate "Show Version Information")")
option_actions[$option_num]="show_version_info"
((option_num++))
menu_options+=("$option_num" "$(translate "Uninstall ProxMenux")")
option_actions[$option_num]="uninstall_proxmenu"
((option_num++))
menu_options+=("$option_num" "$(translate "Return to Main Menu")")
option_actions[$option_num]="return_main"
else
# Normal version (English only)
menu_options+=("$option_num" "Show Version Information")
option_actions[$option_num]="show_version_info"
((option_num++))
menu_options+=("$option_num" "Uninstall ProxMenux")
option_actions[$option_num]="uninstall_proxmenu"
((option_num++))
menu_options+=("$option_num" "Return to Main Menu")
option_actions[$option_num]="return_main"
fi
# Show menu
OPTION=$(dialog --clear --backtitle "ProxMenux Configuration" \
--title "$(translate "Configuration Menu")" \
--menu "$(translate "Select an option:")" 20 70 10 \
"${menu_options[@]}" 3>&1 1>&2 2>&3)
# Execute selected action
case "${option_actions[$OPTION]}" in
"toggle_monitor")
toggle_monitor_service
;;
"show_monitor_status")
show_monitor_status
;;
"change_language")
change_language
;;
@@ -131,7 +244,7 @@ change_language() {
return
fi
# Update language in config file
if [ -f "$CONFIG_FILE" ]; then
tmp=$(mktemp)
jq --arg lang "$new_language" '.language = $lang' "$CONFIG_FILE" > "$tmp" && mv "$tmp" "$CONFIG_FILE"
@@ -143,7 +256,7 @@ change_language() {
--title "$(translate "Language Change")" \
--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"
@@ -164,7 +277,7 @@ show_version_info() {
info_message+="$(translate "Current ProxMenux version:") $version\n\n"
# Show installation type
info_message+="$(translate "Installation type:")\n"
if [ "$install_type" = "translation" ]; then
info_message+="$(translate "Translation Version (Multi-language support)")\n"
@@ -197,13 +310,13 @@ show_version_info() {
info_message+="$(translate "No installation information available.")\n"
fi
info_message+="\n$(translate "ProxMenu files:")\n"
info_message+="\n$(translate "ProxMenux files:")\n"
[ -f "$INSTALL_DIR/$MENU_SCRIPT" ] && info_message+="$MENU_SCRIPT$INSTALL_DIR/$MENU_SCRIPT\n" || info_message+="$MENU_SCRIPT\n"
[ -f "$UTILS_FILE" ] && info_message+="✓ utils.sh → $UTILS_FILE\n" || info_message+="✗ utils.sh\n"
[ -f "$CONFIG_FILE" ] && info_message+="✓ config.json → $CONFIG_FILE\n" || info_message+="✗ config.json\n"
[ -f "$LOCAL_VERSION_FILE" ] && info_message+="✓ version.txt → $LOCAL_VERSION_FILE\n" || info_message+="✗ version.txt\n"
# Show translation-specific files
if [ "$install_type" = "translation" ]; then
[ -f "$CACHE_FILE" ] && info_message+="✓ cache.json → $CACHE_FILE\n" || info_message+="✗ cache.json\n"
@@ -222,7 +335,7 @@ show_version_info() {
info_message+="\n$(translate "Language:")\nEnglish (Fixed)\n"
fi
# Display information in a scrollable text box
tmpfile=$(mktemp)
echo -e "$info_message" > "$tmpfile"
dialog --clear --backtitle "ProxMenux Configuration" \
@@ -237,14 +350,14 @@ uninstall_proxmenu() {
install_type=$(detect_installation_type)
if ! dialog --clear --backtitle "ProxMenux Configuration" \
--title "Uninstall ProxMenu" \
--yesno "\n$(translate "Are you sure you want to uninstall ProxMenu?")" 8 60; then
--title "Uninstall ProxMenux" \
--yesno "\n$(translate "Are you sure you want to uninstall ProxMenux?")" 8 60; then
return
fi
local deps_to_remove=""
# Show different dependency options based on installation type
if [ "$install_type" = "translation" ]; then
deps_to_remove=$(dialog --clear --backtitle "ProxMenux Configuration" \
--title "Remove Dependencies" \
@@ -263,12 +376,12 @@ uninstall_proxmenu() {
3>&1 1>&2 2>&3)
fi
# Perform uninstallation with progress bar
(
echo "10" ; echo "Removing ProxMenu files..."
sleep 1
# Remove googletrans and virtual environment if exists
if [ -f "$VENV_PATH/bin/activate" ]; then
echo "30" ; echo "Removing googletrans and virtual environment..."
source "$VENV_PATH/bin/activate"
@@ -281,7 +394,7 @@ uninstall_proxmenu() {
rm -f "$INSTALL_DIR/$MENU_SCRIPT"
rm -rf "$BASE_DIR"
# Remove selected dependencies
if [ -n "$deps_to_remove" ]; then
echo "70" ; echo "Removing selected dependencies..."
read -r -a DEPS_ARRAY <<< "$(echo "$deps_to_remove" | tr -d '"')"
@@ -293,7 +406,7 @@ uninstall_proxmenu() {
fi
echo "90" ; echo "Restoring system files..."
# Restore .bashrc and motd
[ -f /root/.bashrc.bak ] && mv /root/.bashrc.bak /root/.bashrc
if [ -f /etc/motd.bak ]; then
mv /etc/motd.bak /etc/motd
@@ -308,7 +421,7 @@ uninstall_proxmenu() {
--title "Uninstalling ProxMenux" \
--gauge "Starting uninstallation..." 10 60 0
# Show completion message
local final_message="ProxMenux has been uninstalled successfully.\n\n"
if [ -n "$deps_to_remove" ]; then
final_message+="The following dependencies were removed:\n$deps_to_remove\n\n"
@@ -324,4 +437,4 @@ uninstall_proxmenu() {
# ==========================================================
# Main execution
show_config_menu
show_config_menu
+2 -2
View File
@@ -1,7 +1,7 @@
#!/bin/bash
# ==========================================================
# ProxMenu - A menu-driven script for Proxmox VE management
# ProxMenux - A menu-driven script for Proxmox VE management
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
@@ -47,7 +47,7 @@ initialize_cache
fi
;;
3)
bash <(curl -s "$REPO_URL/scripts/install_coral_pve.sh")
bash <(curl -s "$REPO_URL/scripts/gpu_tpu/install_coral_pve9.sh")
if [ $? -ne 0 ]; then
return
fi
+19 -18
View File
@@ -1,7 +1,7 @@
#!/bin/bash
# ==========================================================
# ProxMenu - LXC Conversion Management Menu
# ProxMenux - LXC Conversion Management Menu
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
@@ -35,37 +35,33 @@ show_main_menu() {
"1" "$(translate 'Convert Privileged to Unprivileged')" \
"2" "$(translate 'Convert Unprivileged to Privileged')" \
"3" "$(translate 'Show Container Privilege Status')" \
"4" "$(translate 'Exit')" 3>&1 1>&2 2>&3)
"4" "$(translate "Help & Info (commands)")" \
"5" "$(translate 'Exit')" 3>&1 1>&2 2>&3)
case $CHOICE in
1)
bash <(curl -fsSL "$REPO_URL/scripts/lcx/lxc-privileged-to-unprivileged.sh")
bash <(curl -s "$REPO_URL/scripts/lxc/lxc-privileged-to-unprivileged.sh")
;;
2)
convert_unprivileged_to_privileged
bash <(curl -s "$REPO_URL/scripts/lxc/lxc-unprivileged-to-privileged.sh")
;;
3)
show_container_status
;;
4)
exit 0
bash <(curl -s "$REPO_URL/scripts/lxc/lxc-conversion-manual-guide.sh")
;;
5)
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")
;;
*)
exit 0
;;
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")
esac
}
convert_unprivileged_to_privileged() {
bash <(curl -fsSL "$REPO_URL/scripts/lcx/lxc-privileged-to-unprivileged.sh")
}
show_container_status() {
show_proxmenux_logo
msg_info "$(translate 'Gathering container privilege information...')"
@@ -74,6 +70,7 @@ show_container_status() {
echo "$(translate 'LXC Container Privilege Status')" > "$TEMP_FILE"
echo "=================================" >> "$TEMP_FILE"
echo "" >> "$TEMP_FILE"
pct list | awk 'NR>1 {print $1, $3}' | while read id name; do
if pct config "$id" | grep -q "^unprivileged: 1"; then
@@ -87,17 +84,21 @@ show_container_status() {
printf "ID: %-4s | %-20s | %-12s | %s\n" "$id" "$name" "$status" "$running_status" >> "$TEMP_FILE"
done
echo "" >> "$TEMP_FILE"
echo "$(translate 'Legend:')" >> "$TEMP_FILE"
echo "$(translate 'Privileged: Full host access (less secure)')" >> "$TEMP_FILE"
echo "$(translate 'Unprivileged: Limited access (more secure)')" >> "$TEMP_FILE"
cleanup
dialog --backtitle "ProxMenux" --title "$(translate 'Container Status')" --textbox "$TEMP_FILE" 25 80
dialog --title "$(translate 'Container Status')" --textbox "$TEMP_FILE" 25 80
rm -f "$TEMP_FILE"
show_main_menu
show_main_menu
}
show_main_menu
show_main_menu
+14 -12
View File
@@ -1,7 +1,7 @@
#!/bin/bash
# ==========================================================
# ProxMenu - A menu-driven script for Proxmox VE management
# ProxMenux - A menu-driven script for Proxmox VE management
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
@@ -99,14 +99,15 @@ show_menu() {
--title "$(translate "$menu_title")" \
--menu "$(translate "Select an option:")" 20 70 10 \
1 "$(translate "Settings post-install Proxmox")" \
2 "$(translate "Help and Info Commands")" \
3 "$(translate "Hardware: GPUs and Coral-TPU")" \
4 "$(translate "Create VM from template or script")" \
5 "$(translate "Disk and Storage Manager")" \
2 "$(translate "Hardware: GPUs and Coral-TPU")" \
3 "$(translate "Create VM from template or script")" \
4 "$(translate "Disk and Storage Manager")" \
5 "$(translate "Mount and Share Manager")" \
6 "$(translate "Proxmox VE Helper Scripts")" \
7 "$(translate "Network Management")" \
8 "$(translate "Utilities and Tools")" \
9 "$(translate "Settings")" \
h "$(translate "Help and Info Commands")" \
s "$(translate "Settings")" \
0 "$(translate "Exit")" 2>"$TEMP_FILE"
local EXIT_STATUS=$?
@@ -122,15 +123,16 @@ show_menu() {
case $OPTION in
1) exec bash <(curl -s "$REPO_URL/scripts/menus/menu_post_install.sh") ;;
2) exec 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") ;;
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") ;;
9) exec bash <(curl -s "$REPO_URL/scripts/menus/config_menu.sh") ;;
0) clear; msg_ok "$(translate "Thank you for using ProxMenu. Goodbye!")"; rm -f "$TEMP_FILE"; exit 0 ;;
h) bash <(curl -s "$REPO_URL/scripts/help_info_menu.sh") ;;
s) exec bash <(curl -s "$REPO_URL/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
+2 -2
View File
@@ -1,7 +1,7 @@
#!/bin/bash
# ==========================================================
# ProxMenu - A menu-driven script for Proxmox VE management
# ProxMenux - A menu-driven script for Proxmox VE management
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
@@ -71,7 +71,7 @@ show_menu() {
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") ;;
0) clear; msg_ok "$(translate "Thank you for using ProxMenu. Goodbye!")"; rm -f "$TEMP_FILE"; exit 0 ;;
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
+1 -1
View File
@@ -1,7 +1,7 @@
#!/bin/bash
# ==========================================================
# ProxMenu - A menu-driven script for Proxmox VE management
# ProxMenux - A menu-driven script for Proxmox VE management
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
+1 -1
View File
@@ -1,6 +1,6 @@
#!/bin/bash
# ==========================================================
# ProxMenu - A menu-driven script for Proxmox VE management
# ProxMenux - A menu-driven script for Proxmox VE management
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
+1 -1
View File
@@ -1,6 +1,6 @@
#!/bin/bash
# ==========================================================
# ProxMenu - Network Management and Repair Tool
# ProxMenux - Network Management and Repair Tool
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi

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